diff --git a/components.json b/components.json new file mode 100644 index 0000000..b969992 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/styles/index.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/package.json b/package.json index 0faf50e..4b47040 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,21 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.0.9", "axios": "^1.8.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.562.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.5.0", "react-query": "^3.39.3", "react-router": "^7.2.0", - "tailwindcss": "^4.0.9" + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.0.9", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.4.0" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/src/components/dashboard/DashboardHeader.tsx b/src/components/dashboard/DashboardHeader.tsx index 5717cbf..ac13d35 100644 --- a/src/components/dashboard/DashboardHeader.tsx +++ b/src/components/dashboard/DashboardHeader.tsx @@ -2,6 +2,7 @@ import { DASHBOARD_LINKS, DashboardLinkType } from "@/constants/dashboard"; import { IoArrowBack } from "react-icons/io5"; import { MdOutlineRefresh } from "react-icons/md"; import { Link } from "react-router"; +import { Button } from "@/components/ui/button"; interface DashboardHeaderProps { username: string; @@ -41,20 +42,22 @@ function DashboardHeaderToolbar({ onRefresh }: { onRefresh: () => void }) { return (
- +
); } diff --git a/src/components/dashboard/ExerciseTable.tsx b/src/components/dashboard/ExerciseTable.tsx index dd570ea..fcf26a7 100644 --- a/src/components/dashboard/ExerciseTable.tsx +++ b/src/components/dashboard/ExerciseTable.tsx @@ -1,5 +1,13 @@ import { BASE_URL, ExerciseStatus } from "@/constants"; import { Exercise } from "@/types/exercise"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; interface ExerciseTableProps { exercises: Exercise[]; @@ -48,24 +56,24 @@ function ExerciseContextLabel({ exercise }: { exercise: Exercise }) { function ExerciseTable({ exercises, progress }: ExerciseTableProps) { return ( - - - -
+ + + + Exercise - - - - - + + + + {exercises.map((exercise) => { const status = getStatusDisplay(progress.get(exercise.identifier)); const lessonPath = exercise.detour?.lesson.path ?? exercise.parentLesson.path; return ( - - - - + + ); })} - -
+ + Status -
+ + {exercise.identifier} - + + {status.icon} {status.text} -
+ +
); } diff --git a/src/components/dashboard/StatusMessage.tsx b/src/components/dashboard/StatusMessage.tsx index 0291258..ce7c4d2 100644 --- a/src/components/dashboard/StatusMessage.tsx +++ b/src/components/dashboard/StatusMessage.tsx @@ -1,50 +1,55 @@ import { Link } from "react-router"; +import { Button } from "@/components/ui/button"; -type ButtonVariant = "error" | "primary"; +type StatusVariant = "destructive" | "default"; interface StatusMessageProps extends React.PropsWithChildren { buttonText: string; buttonHref: string; - variant?: ButtonVariant; + variant?: StatusVariant; external?: boolean; } -const variantStyles: Record = { - error: "border-red-700 bg-red-700 text-white", - primary: "border-blue-800 bg-blue-800 text-white", -}; - -const textStyles: Record = { - error: "text-red-700", - primary: "", +const textStyles: Record = { + destructive: "text-red-700", + default: "", }; function StatusMessage({ children, buttonText, buttonHref, - variant = "error", + variant = "default", external = false, }: StatusMessageProps) { - const buttonClasses = `hover:cursor-pointer border-1 ${variantStyles[variant]} rounded-sm px-4 py-2 font-semibold`; - return (
{children}
{external ? ( - - {buttonText} - + + {buttonText} + + ) : ( - - {buttonText} - + )}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..c906acb --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +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-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-sm hover:bg-destructive/90", + outline: + "border border-outline hover:bg-outline hover:text-outline-foreground hover:border-outline", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..c0df655 --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/src/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/src/pages/dashboard/(username)/index.tsx b/src/pages/dashboard/(username)/index.tsx index 0a36ed9..aa4fe3c 100644 --- a/src/pages/dashboard/(username)/index.tsx +++ b/src/pages/dashboard/(username)/index.tsx @@ -85,7 +85,7 @@ function DashboardPage() {

User {username} does not exist @@ -99,7 +99,7 @@ function DashboardPage() {

No progress repository found for {username}. @@ -120,7 +120,7 @@ function DashboardPage() {

No exercises available

diff --git a/src/pages/index.tsx b/src/pages/index.tsx index daebb68..d15eae1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,6 @@ import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router'; +import { Button } from '@/components/ui/button'; function HomePage() { const navigate = useNavigate() @@ -29,14 +30,21 @@ function HomePage() {

Git Mastery Progress Dashboard

Enter your Github username to find your progress!

-
+
@
- +
) diff --git a/src/styles/index.css b/src/styles/index.css index 5bbafcc..3bf13af 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,5 +1,10 @@ @import "tailwindcss"; +@plugin "tailwindcss-animate"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; @@ -9,4 +14,126 @@ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + --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); + --outline: oklch(0.446 0 0); + --outline-foreground: oklch(1 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); +} + +@theme inline { + --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); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-outline: var(--outline); + --color-outline-foreground: var(--outline-foreground); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +.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); + --outline: oklch(0.446 0 0); + --outline-foreground: oklch(1 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; + } } diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..fec8c8e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,5 +3,11 @@ "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } - ] + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } } diff --git a/yarn.lock b/yarn.lock index f23fff9..4e35ffa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -437,6 +437,18 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@radix-ui/react-compose-refs@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" + integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== + +"@radix-ui/react-slot@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz#63c0ba05fdf90cc49076b94029c852d7bac1fb83" + integrity sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@rollup/rollup-android-arm-eabi@4.34.8": version "4.34.8" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz#731df27dfdb77189547bcef96ada7bf166bbb2fb" @@ -910,6 +922,18 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +class-variance-authority@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787" + integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg== + dependencies: + clsx "^2.1.1" + +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1585,6 +1609,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lucide-react@^0.562.0: + version "0.562.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.562.0.tgz#217796c2f57624f012b484ea7f08505067c90d51" + integrity sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw== + match-sorter@^6.0.2: version "6.4.0" resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.4.0.tgz#ae9c166cb3c9efd337690b3160c0e28cb8377c13" @@ -1920,6 +1949,16 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +tailwind-merge@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz#5a264e131a096879965f1175d11f8c36e6b64eca" + integrity sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g== + +tailwindcss-animate@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4" + integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== + tailwindcss@4.0.9, tailwindcss@^4.0.9: version "4.0.9" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.0.9.tgz#f6626cee837aabe9e54c29b230b6fb0ed36fe965" @@ -1947,6 +1986,11 @@ turbo-stream@2.4.0: resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.4.0.tgz#1e4fca6725e90fa14ac4adb782f2d3759a5695f0" integrity sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g== +tw-animate-css@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/tw-animate-css/-/tw-animate-css-1.4.0.tgz#b4a06f68244cba39428aa47e65e6e4c0babc21ee" + integrity sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"