diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..d91f28f --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,19 @@ +name: CI + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Dependencies + run: npm install -g pnpm && pnpm install + - name: Copy .env.example files + shell: bash + run: find . -type f -name ".env.example" -exec sh -c 'cp "$1" "${1%.*}"' _ {} \; + - name: Typecheck + run: pnpm typecheck + - name: Lint + run: pnpm lint diff --git a/README.md b/README.md index c5e5227..b46dd48 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,19 @@ official documentation linked below. Additional support is available via the - [Drizzle](https://orm.drizzle.team) - [Tailwind CSS](https://tailwindcss.com) +### UI Scaffolding + +The base UI for this project was created using [v0](https://v0.dev/), a tool +that enables fast UI generation through _vibe coding_. An +[example](https://v0.dev/chat/google-drive-clone-ui-6jEAM0wxOgc?b=b_fFQhsfElqQi&f=0) +of this approach can be seen in Theo’s walkthrough on YouTube. + +To apply the same base UI in a project, run the following command: + +```bash +npx shadcn@latest add "https://v0.dev/chat/b/b_fFQhsfElqQi" +``` + ### Learn More about the T3 Stack To explore more about the [T3 Stack](https://create.t3.gg/), refer to the diff --git a/components.json b/components.json new file mode 100644 index 0000000..f5d25ec --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 121c4f4..952eb1e 100644 --- a/next.config.js +++ b/next.config.js @@ -1,10 +1,13 @@ /** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful - * for Docker builds. + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. + * This is especially useful for Docker builds. */ import "./src/env.js"; /** @type {import("next").NextConfig} */ -const config = {}; +const config = { + eslint: { ignoreDuringBuilds: true }, + typescript: { ignoreBuildErrors: true }, +}; export default config; diff --git a/package.json b/package.json index 30880fd..8809492 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,16 @@ }, "dependencies": { "@libsql/client": "^0.14.0", + "@radix-ui/react-slot": "^1.2.3", "@t3-oss/env-nextjs": "^0.12.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "drizzle-orm": "^0.41.0", + "lucide-react": "^0.511.0", "next": "^15.2.3", "react": "^19.0.0", "react-dom": "^19.0.0", + "tailwind-merge": "^3.3.0", "zod": "^3.24.2" }, "devDependencies": { @@ -42,6 +47,7 @@ "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", "tailwindcss": "^4.0.15", + "tw-animate-css": "^1.3.0", "typescript": "^5.8.2", "typescript-eslint": "^8.27.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1eee3e..ae36264 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,24 @@ importers: '@libsql/client': specifier: ^0.14.0 version: 0.14.0 + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.1.2)(react@19.1.0) '@t3-oss/env-nextjs': specifier: ^0.12.0 version: 0.12.0(typescript@5.8.3)(zod@3.24.4) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 drizzle-orm: specifier: ^0.41.0 version: 0.41.0(@libsql/client@0.14.0)(gel@2.1.0) + lucide-react: + specifier: ^0.511.0 + version: 0.511.0(react@19.1.0) next: specifier: ^15.2.3 version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -26,6 +38,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + tailwind-merge: + specifier: ^3.3.0 + version: 3.3.0 zod: specifier: ^3.24.2 version: 3.24.4 @@ -69,6 +84,9 @@ importers: tailwindcss: specifier: ^4.0.15 version: 4.1.5 + tw-animate-css: + specifier: ^1.3.0 + version: 1.3.0 typescript: specifier: ^5.8.2 version: 5.8.3 @@ -674,6 +692,24 @@ packages: '@petamoriken/float16@3.9.2': resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==} + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1090,9 +1126,16 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1863,7 +1906,6 @@ packages: libsql@0.4.7: resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} - cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] lightningcss-darwin-arm64@1.29.2: @@ -1941,6 +1983,11 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lucide-react@0.511.0: + resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2436,6 +2483,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwind-merge@3.3.0: + resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} + tailwindcss@4.1.5: resolution: {integrity: sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==} @@ -2467,6 +2517,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tw-animate-css@1.3.0: + resolution: {integrity: sha512-jrJ0XenzS9KVuDThJDvnhalbl4IYiMQ/XvpA0a2FL8KmlK+6CSMviO7ROY/I7z1NnUs5NnDhlM6fXmF40xPxzw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3013,6 +3066,19 @@ snapshots: '@petamoriken/float16@3.9.2': {} + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.11.0': {} @@ -3424,8 +3490,14 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + client-only@0.0.1: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4407,6 +4479,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + lucide-react@0.511.0(react@19.1.0): + dependencies: + react: 19.1.0 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -4914,6 +4990,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tailwind-merge@3.3.0: {} + tailwindcss@4.1.5: {} tapable@2.2.1: {} @@ -4942,6 +5020,8 @@ snapshots: tslib@2.8.1: {} + tw-animate-css@1.3.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/src/app/page.tsx b/src/app/page.tsx index a04f619..2808d30 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,8 +1,123 @@ +"use client"; + +import { ChevronRight, FileIcon, Folder, Upload } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; + +import { Button } from "~/components/ui/button"; +import { mockFiles } from "~/lib/mock-data"; + +export default function GoogleDriveClone() { + const [currentFolder, setCurrentFolder] = useState(null); + + const getCurrentFiles = () => { + return mockFiles.filter((file) => file.parent === currentFolder); + }; + + const handleFolderClick = (folderId: string) => { + setCurrentFolder(folderId); + }; + + const getBreadcrumbs = () => { + const breadcrumbs = []; + let currentId = currentFolder; + + while (currentId !== null) { + const folder = mockFiles.find((file) => file.id === currentId); + if (folder) { + breadcrumbs.unshift(folder); + currentId = folder.parent; + } else { + break; + } + } + + return breadcrumbs; + }; + + const handleUpload = () => { + alert("Upload functionality would be implemented here"); + }; -export default function HomePage() { return ( -
-

Hello World!

-
+
+
+
+
+ + {getBreadcrumbs().map((folder) => ( +
+ + +
+ ))} +
+ +
+
+
+
+
Name
+
Type
+
Size
+
+
+
    + {getCurrentFiles().map((file) => ( +
  • +
    +
    + {file.type === "folder" ? ( + + ) : ( + + + {file.name} + + )} +
    +
    + {file.type === "folder" ? "Folder" : "File"} +
    +
    + {file.type === "folder" ? "--" : "2 MB"} +
    +
    +
  • + ))} +
+
+
+
); } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..1cab9b9 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +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 shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs 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 shadow-xs 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", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts new file mode 100644 index 0000000..3ebc462 --- /dev/null +++ b/src/lib/mock-data.ts @@ -0,0 +1,22 @@ +export interface File { + id: string + name: string + type: "file" | "folder" + url?: string + parent: string | null + size?: string +} + +export const mockFiles: File[] = [ + { id: "1", name: "Documents", type: "folder", parent: null }, + { id: "2", name: "Images", type: "folder", parent: null }, + { id: "3", name: "Work", type: "folder", parent: null }, + { id: "4", name: "Resume.pdf", type: "file", url: "/files/resume.pdf", parent: "1", size: "1.2 MB" }, + { id: "5", name: "Project Proposal.docx", type: "file", url: "/files/proposal.docx", parent: "1", size: "2.5 MB" }, + { id: "6", name: "Vacation.jpg", type: "file", url: "/files/vacation.jpg", parent: "2", size: "3.7 MB" }, + { id: "7", name: "Profile Picture.png", type: "file", url: "/files/profile.png", parent: "2", size: "1.8 MB" }, + { id: "8", name: "Presentations", type: "folder", parent: "3" }, + { id: "9", name: "Q4 Report.pptx", type: "file", url: "/files/q4-report.pptx", parent: "8", size: "5.2 MB" }, + { id: "10", name: "Budget.xlsx", type: "file", url: "/files/budget.xlsx", parent: "3", size: "1.5 MB" }, +] + 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/styles/globals.css b/src/styles/globals.css index 8fe04fa..48b3fb5 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,6 +1,125 @@ @import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); @theme { --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --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-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); +} + +: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; + } +}