From 8b4836fd6195175b556d34d7e1cc116f8517488f Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 22 Dec 2025 19:34:28 +0300 Subject: [PATCH] first commit --- app/(auth)/login/page.tsx | 172 ++++++++++++++ app/(home)/main/page.tsx | 280 +++++++++++++++++++++++ app/(home)/main/profile/page.tsx | 236 +++++++++++++++++++ app/(home)/main/progress/page.tsx | 286 ++++++++++++++++++++++++ app/(home)/main/reset-password/page.tsx | 137 ++++++++++++ app/globals.css | 127 +++++++++-- app/layout.tsx | 8 +- app/page.tsx | 208 ++++++++++++----- components.json | 22 ++ components/theme-provider.tsx | 11 + components/ui/button.tsx | 62 +++++ components/ui/card.tsx | 92 ++++++++ components/ui/input.tsx | 21 ++ components/ui/label.tsx | 24 ++ components/ui/progress.tsx | 31 +++ lib/utils.ts | 6 + package-lock.json | 195 +++++++++++++++- package.json | 11 +- 18 files changed, 1846 insertions(+), 83 deletions(-) create mode 100644 app/(auth)/login/page.tsx create mode 100644 app/(home)/main/page.tsx create mode 100644 app/(home)/main/profile/page.tsx create mode 100644 app/(home)/main/progress/page.tsx create mode 100644 app/(home)/main/reset-password/page.tsx create mode 100644 components.json create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/progress.tsx create mode 100644 lib/utils.ts diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 0000000..170a57b --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,172 @@ +"use client" + +import { useState } from "react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import Link from "next/link" +import { Eye, EyeOff } from "lucide-react" + +export default function LoginPage() { + const [showPassword, setShowPassword] = useState(false) + const [formData, setFormData] = useState({ + username: "", + password: "" + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + // Здесь будет логика авторизации + console.log("Login attempt:", formData) + } + + return ( +
+ {/* Background grid */} +
+ + {/* Glowing effects */} +
+
+ + + {/* Top accent line */} +
+ +
+ {/* Header */} +
+

+ Login +

+

+ Access your learning dashboard +

+
+ + {/* Login Form */} +
+
+ + setFormData({...formData, username: e.target.value})} + className="bg-[#0a0e1a] border-cyan-500/30 text-cyan-400 placeholder:text-cyan-400/30 font-mono focus:border-cyan-400 focus:ring-cyan-400/20" + /> +
+ +
+ +
+ setFormData({...formData, password: e.target.value})} + className="bg-[#0a0e1a] border-cyan-500/30 text-cyan-400 placeholder:text-cyan-400/30 font-mono focus:border-cyan-400 focus:ring-cyan-400/20 pr-10" + /> + +
+
+ +
+ + + Forgot password? + +
+ + +
+ + {/* Footer Links */} +
+
+
+
+
+
+ + OR + +
+
+ +

+ Don't have an account?{" "} + + Sign up + +

+ + + ← Back to home + +
+
+ + {/* Corner decorations */} +
+
+
+
+ +
+ ) +} \ No newline at end of file diff --git a/app/(home)/main/page.tsx b/app/(home)/main/page.tsx new file mode 100644 index 0000000..f1a436f --- /dev/null +++ b/app/(home)/main/page.tsx @@ -0,0 +1,280 @@ +"use client" + +import { useState } from "react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { + BookOpen, + Trophy, + Target, + TrendingUp, + Clock, + Award, + CheckCircle2, + PlayCircle +} from "lucide-react" + +export default function Home() { + const [activeTab, setActiveTab] = useState("main") + + // Mock data для демонстрации + const stats = { + coursesInProgress: 3, + completedCourses: 12, + totalPoints: 2450, + rank: "Advanced" + } + + const recentCourses = [ + { id: 1, title: "Advanced React Patterns", progress: 65, time: "2h 30m left" }, + { id: 2, title: "System Design", progress: 40, time: "5h 15m left" }, + { id: 3, title: "TypeScript Deep Dive", progress: 85, time: "45m left" } + ] + + const achievements = [ + { id: 1, title: "Fast Learner", icon: Trophy, unlocked: true }, + { id: 2, title: "Course Master", icon: Award, unlocked: true }, + { id: 3, title: "Consistency King", icon: Target, unlocked: false } + ] + + return ( +
+ {/* Header */} +
+ +
+ + {/* Main Content */} +
+ {/* Stats Grid */} +
+ +
+ + IN PROGRESS +
+
+ {stats.coursesInProgress} +
+

Active Courses

+
+ + +
+ + COMPLETED +
+
+ {stats.completedCourses} +
+

Finished

+
+ + +
+ + TOTAL +
+
+ {stats.totalPoints} +
+

Points Earned

+
+ + +
+ + RANK +
+
+ {stats.rank} +
+

Current Level

+
+
+ +
+ {/* Continue Learning Section */} + +
+

+ Continue Learning +

+ +
+ +
+ {recentCourses.map((course) => ( + +
+
+

+ {course.title} +

+
+ + {course.time} +
+
+ +
+ + {/* Progress Bar */} +
+
+ Progress + {course.progress}% +
+
+
+
+
+ + ))} +
+
+ + {/* Achievements Section */} + +

+ Achievements +

+ +
+ {achievements.map((achievement) => { + const Icon = achievement.icon + return ( + +
+
+ +
+
+

+ {achievement.title} +

+

+ {achievement.unlocked ? 'Unlocked' : 'Locked'} +

+
+
+
+ ) + })} +
+ + +
+
+
+ + {/* Subtle grid background */} +
+
+ ) +} \ No newline at end of file diff --git a/app/(home)/main/profile/page.tsx b/app/(home)/main/profile/page.tsx new file mode 100644 index 0000000..df3b4c0 --- /dev/null +++ b/app/(home)/main/profile/page.tsx @@ -0,0 +1,236 @@ +"use client" + +import { useState } from "react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Progress } from "@/components/ui/progress" +import { User, Award, BookOpen, Trophy, Star } from "lucide-react" +import Link from "next/link" + +export default function ProfilePage() { + const [activeTab, setActiveTab] = useState("profile") + + // Mock user data + const userData = { + name: "Alex Johnson", + level: 5, + xp: 2450, + maxXp: 3000, + rank: "Advanced", + achievements: 8, + coursesCompleted: 12, + totalStudyTime: "124h" + } + + const stats = [ + { label: "Level", value: userData.level, icon: Star }, + { label: "XP", value: `${userData.xp}/${userData.maxXp}`, icon: Trophy }, + { label: "Rank", value: userData.rank, icon: Award } + ] + + const achievements = [ + { title: "First Steps", description: "Complete your first course", unlocked: true }, + { title: "Quick Learner", description: "Complete a course in under 24h", unlocked: true }, + { title: "Dedication", description: "Study 7 days in a row", unlocked: true }, + { title: "Master", description: "Complete 10 courses", unlocked: true }, + { title: "Expert", description: "Reach level 5", unlocked: true }, + { title: "Marathon", description: "Study for 100 hours", unlocked: true }, + { title: "Perfectionist", description: "Score 100% on 5 tests", unlocked: false }, + { title: "Legend", description: "Reach level 10", unlocked: false } + ] + + return ( +
+ {/* Header */} +
+ +
+ + {/* Main Content */} +
+
+ {/* User Profile Card */} + +
+ {/* Avatar */} +
+ +
+ + {/* User Info */} +

+ {userData.name} +

+

+ {userData.rank} • Level {userData.level} +

+ + {/* XP Progress */} +
+
+ XP Progress + + {userData.xp}/{userData.maxXp} + +
+ +
+ + {/* Quick Stats */} +
+
+ +
+ {userData.coursesCompleted} +
+
Courses
+
+
+ +
+ {userData.achievements} +
+
Badges
+
+
+ + +
+
+ + {/* Stats and Achievements */} +
+ {/* Personal Progress Card */} + +

+ Personal progress +

+
+ {stats.map((stat, index) => { + const Icon = stat.icon + return ( +
+ +
+ {stat.value} +
+
+ {stat.label} +
+
+ ) + })} +
+
+ + {/* Achievements Card */} + +

+ Achievements +

+
+ {achievements.map((achievement, index) => ( + +
+
+ +
+
+

+ {achievement.title} +

+

+ {achievement.description} +

+
+
+
+ ))} +
+
+
+
+
+ + {/* Background grid */} +
+
+ ) +} \ No newline at end of file diff --git a/app/(home)/main/progress/page.tsx b/app/(home)/main/progress/page.tsx new file mode 100644 index 0000000..6f78c92 --- /dev/null +++ b/app/(home)/main/progress/page.tsx @@ -0,0 +1,286 @@ +"use client" + +import { useState } from "react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Progress } from "@/components/ui/progress" +import Link from "next/link" +import { CheckCircle2, Circle, Clock } from "lucide-react" + +export default function ProgressPage() { + const [activeTab, setActiveTab] = useState("progress") + + // Mock data + const courses = [ + { id: 1, name: "React", lessons: 12, completed: 8 }, + { id: 2, name: "TypeScript", lessons: 10, completed: 6 }, + { id: 3, name: "Next.js", lessons: 15, completed: 4 }, + { id: 4, name: "Node.js", lessons: 20, completed: 0 } + ] + + const assignments = [ + { id: 1, course: "React", name: "Build a Todo App", grade: 95, status: "completed" }, + { id: 2, course: "TypeScript", name: "Type System", grade: 88, status: "completed" }, + { id: 3, course: "Next.js", name: "SSR Project", grade: null, status: "pending" }, + { id: 4, course: "Node.js", name: "REST API", grade: null, status: "not_started" } + ] + + const weekActivity = [ + { day: "Mon", hours: 2.5 }, + { day: "Tue", hours: 3.2 }, + { day: "Wed", hours: 1.8 }, + { day: "Thu", hours: 4.1 }, + { day: "Fri", hours: 2.9 }, + { day: "Sat", hours: 3.5 }, + { day: "Sun", hours: 1.2 } + ] + + const maxHours = Math.max(...weekActivity.map(d => d.hours)) + + return ( +
+ {/* Header */} +
+ +
+ + {/* Main Content */} +
+
+ {/* Left Column - Course Progress */} +
+ {/* My Courses */} + +

+ My courses +

+
+ {courses.map((course) => { + const progress = (course.completed / course.lessons) * 100 + return ( + +
+

+ {course.name} +

+ + {course.completed}/{course.lessons} + +
+ +
+ ) + })} +
+
+ + {/* My Grades */} + +

+ My grades +

+
+ {assignments.slice(0, 3).map((assignment) => ( + +
+
+
+ {assignment.course} +
+
+ {assignment.name} +
+
+ {assignment.grade ? ( +
+ {assignment.grade}% +
+ ) : ( +
+ Pending +
+ )} +
+
+ ))} +
+
+ + {/* Mathematics (Separate) */} + +

+ Mathematics +

+
+ +
+

+ Algebra +

+ + 8/10 + +
+ +
+
+
+
+ + {/* Center Column - Assignments Table */} + +

+ All Assignments +

+ + {/* Table */} +
+ + + + + + + + + + + + {assignments.map((assignment, index) => ( + + + + + + + + ))} + +
+ # + + Course + + Assignment + + Grade + + Status +
+ {index + 1} + + {assignment.course} + + {assignment.name} + + {assignment.grade ? `${assignment.grade}%` : '-'} + +
+ {assignment.status === 'completed' ? ( + <> + + + Completed + + + ) : assignment.status === 'pending' ? ( + <> + + + Pending + + + ) : ( + <> + + + Not Started + + + )} +
+
+
+ + {/* Weekly Activity Chart */} +
+

+ Weekly Activity +

+
+ {weekActivity.map((day) => ( +
+
+
+
+
+ {day.day} +
+
+ {day.hours}h +
+
+ ))} +
+
+ +
+
+ + {/* Background grid */} +
+
+ ) +} \ No newline at end of file diff --git a/app/(home)/main/reset-password/page.tsx b/app/(home)/main/reset-password/page.tsx new file mode 100644 index 0000000..eb44e18 --- /dev/null +++ b/app/(home)/main/reset-password/page.tsx @@ -0,0 +1,137 @@ +"use client" + +import { useState } from "react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import Link from "next/link" +import { ArrowLeft, Mail } from "lucide-react" + +export default function ResetPasswordPage() { + const [email, setEmail] = useState("") + const [submitted, setSubmitted] = useState(false) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + // Здесь будет логика отправки письма для сброса пароля + console.log("Reset password for:", email) + setSubmitted(true) + } + + return ( +
+ {/* Background grid */} +
+ + {/* Glowing effects */} +
+
+ + + {/* Top accent line */} +
+ +
+ {/* Back button */} + + + Back to login + + + {!submitted ? ( + <> + {/* Header */} +
+
+ +
+

+ Reset my password +

+

+ Enter your email to receive reset instructions +

+
+ + {/* Reset Form */} +
+
+ + setEmail(e.target.value)} + required + className="bg-[#0a0e1a] border-cyan-500/30 text-cyan-400 placeholder:text-cyan-400/30 font-mono focus:border-cyan-400 focus:ring-cyan-400/20" + /> +
+ + +
+ + ) : ( + <> + {/* Success State */} +
+
+ +
+

+ Check your email +

+

+ We've sent password reset instructions to
+ {email} +

+
+

+ Didn't receive the email? Check your spam folder or try again in a few minutes. +

+
+ + + +
+ + )} +
+ + {/* Corner decorations */} +
+
+
+
+ +
+ ) +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index a2dc41e..6cf72ed 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,125 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; } } - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..28bedf0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -13,8 +13,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Dashboard App", + description: "Modern dashboard application", }; export default function RootLayout({ @@ -23,7 +23,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + @@ -31,4 +31,4 @@ export default function RootLayout({ ); -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 295f8fd..c400a0d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,153 @@ -import Image from "next/image"; +"use client" + +import { useState, useEffect } from "react" +import { ArrowRight, Sparkles, Zap, Shield } from "lucide-react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import Link from "next/link" + +export default function WelcomePage() { + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) -export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - +
+ {/* Animated background grid */} +
+ + {/* Glowing orbs */} +
+
+ + {/* Main Content */} +
+
+ {/* Hero Section */} +
+
+
+ + + WELCOME TO THE FUTURE + +
+
+ +

+ Dashboard +

+ +

+ Управляй своим прогрессом в киберпространстве +

+ + + + +
+ + {/* Features Grid */} +
+ +
+ +
+

+ Быстрый доступ +

+

+ Мгновенный доступ ко всем функциям системы +

+
+ + +
+ +
+

+ Безопасность +

+

+ Защищённое хранение данных и прогресса +

+
+ + +
+ +
+

+ Прогресс +

+

+ Отслеживание достижений в реальном времени +

+
+
+ + {/* Stats Section */} +
+ +
+
+
+ 24/7 +
+
+ Онлайн +
+
+
+
+ 100% +
+
+ Безопасность +
+
+
+
+ ∞ +
+
+ Возможности +
+
+
+
+
+ +
- ); -} + ) +} \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000..b7b9791 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..e018a73 --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children} +} \ No newline at end of file diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..37a7d4b --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,62 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/components/ui/input.tsx b/components/ui/input.tsx new file mode 100644 index 0000000..8916905 --- /dev/null +++ b/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/components/ui/label.tsx b/components/ui/label.tsx new file mode 100644 index 0000000..fb5fbc3 --- /dev/null +++ b/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx new file mode 100644 index 0000000..e7a416c --- /dev/null +++ b/components/ui/progress.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Progress } diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/package-lock.json b/package-lock.json index f9dd093..b8d3bbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,17 @@ "name": "s21", "version": "0.1.0", "dependencies": { + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.561.0", "next": "16.0.10", + "next-themes": "^0.4.6", "react": "19.2.1", - "react-dom": "19.2.1" + "react-dom": "19.2.1", + "tailwind-merge": "^3.4.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -20,6 +28,7 @@ "eslint": "^9", "eslint-config-next": "16.0.10", "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", "typescript": "^5" } }, @@ -1226,6 +1235,124 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1559,7 +1686,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1569,7 +1696,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -2571,12 +2698,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2630,7 +2778,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4834,6 +4982,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.561.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz", + "integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5001,6 +5158,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -6018,6 +6185,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -6145,6 +6322,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index d41a23f..7248c49 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,17 @@ "lint": "eslint" }, "dependencies": { + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.561.0", "next": "16.0.10", + "next-themes": "^0.4.6", "react": "19.2.1", - "react-dom": "19.2.1" + "react-dom": "19.2.1", + "tailwind-merge": "^3.4.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -21,6 +29,7 @@ "eslint": "^9", "eslint-config-next": "16.0.10", "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", "typescript": "^5" } }