diff --git a/.env.example b/.env.example index 9c1a7f8..fb607be 100644 --- a/.env.example +++ b/.env.example @@ -9,9 +9,9 @@ # When adding additional environment variables, the schema in "/src/env.js" # should be updated accordingly. -# Drizzle -DATABASE_URL="file:./db.sqlite" - -# Example: -# SERVERVAR="foo" -# NEXT_PUBLIC_CLIENTVAR="bar" +# SingleStore +SINGLESTORE_USER="username" +SINGLESTORE_PASSWORD="password" +SINGLESTORE_HOST="localhost" +SINGLESTORE_PORT="1000" +SINGLESTORE_DATABASE="database" diff --git a/README.md b/README.md index 38fa1bd..22b7c40 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,39 @@ Visit the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) for feedback and contributions. +## πŸš€ Getting Started + +### πŸ› οΈ Environment Variables + +To configure the environment variables, copy the example file and update the +values as needed: + +```bash +cp .env.example .env +``` + +Open the `.env` file and fill in the required fields based on the project’s +needs (e.g., database credentials, API keys, etc.). + +### πŸ—„οΈ Database Setup + +This project uses [SingleStore](https://www.singlestore.com/) as the primary +database, with [Drizzle ORM](https://orm.drizzle.team) for type-safe, efficient +data access and schema management. + +```bash +pnpm run db:push # Push the schema to the database +pnpm run db:studio # Launch Drizzle Studio +``` + +### πŸ•ΉοΈ Development Server + +To start the local development server: + +```bash +pnpm run dev +``` + ## 🚧 Development Logbook Tracking progress on key features and tasks for the project. @@ -56,3 +89,12 @@ Tracking progress on key features and tasks for the project. - [ ] πŸ”— Sync folder open state with the URL - [ ] πŸ” Implement user authentication - [ ] πŸ“ Enable file upload functionality + +### πŸ“ Note from 5-28-2025 + +Just finished up the database connection, next steps: + +- [ ] Update schema to show files and folders +- [ ] Manually insert examples +- [ ] Render them in the UI +- [ ] Push and make sure it all works diff --git a/drizzle.config.ts b/drizzle.config.ts index be8eee2..7b67552 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -4,9 +4,14 @@ import { env } from "~/env"; export default { schema: "./src/server/db/schema.ts", - dialect: "sqlite", + dialect: "singlestore", dbCredentials: { - url: env.DATABASE_URL, + host: env.SINGLESTORE_HOST, + port: env.SINGLESTORE_PORT, + user: env.SINGLESTORE_USER, + password: env.SINGLESTORE_PASSWORD, + database: env.SINGLESTORE_DATABASE, + ssl: {}, }, - tablesFilter: ["drive-tutorial_*"], + tablesFilter: ["drive_tutorial_*"], } satisfies Config; diff --git a/package.json b/package.json index 8809492..918b68f 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "clsx": "^2.1.1", "drizzle-orm": "^0.41.0", "lucide-react": "^0.511.0", + "mysql2": "^3.14.1", "next": "^15.2.3", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae36264..a5801e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,10 +25,13 @@ importers: version: 2.1.1 drizzle-orm: specifier: ^0.41.0 - version: 0.41.0(@libsql/client@0.14.0)(gel@2.1.0) + version: 0.41.0(@libsql/client@0.14.0)(gel@2.1.0)(mysql2@3.14.1) lucide-react: specifier: ^0.511.0 version: 0.511.0(react@19.1.0) + mysql2: + specifier: ^3.14.1 + version: 3.14.1 next: specifier: ^15.2.3 version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1067,6 +1070,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axe-core@4.10.3: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} @@ -1227,6 +1234,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1641,6 +1652,9 @@ packages: engines: {node: '>= 18.0.0'} hasBin: true + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1809,6 +1823,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1979,10 +1996,21 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.2: + resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide-react@0.511.0: resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} peerDependencies: @@ -2029,6 +2057,14 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mysql2@3.14.1: + resolution: {integrity: sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2355,6 +2391,9 @@ packages: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -2420,6 +2459,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -3423,6 +3466,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + aws-ssl-profiles@1.1.2: {} + axe-core@4.10.3: {} axobject-query@4.1.0: {} @@ -3585,6 +3630,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + denque@2.1.0: {} + depd@2.0.0: {} detect-libc@2.0.2: {} @@ -3605,10 +3652,11 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.41.0(@libsql/client@0.14.0)(gel@2.1.0): + drizzle-orm@0.41.0(@libsql/client@0.14.0)(gel@2.1.0)(mysql2@3.14.1): optionalDependencies: '@libsql/client': 0.14.0 gel: 2.1.0 + mysql2: 3.14.1 dunder-proto@1.0.1: dependencies: @@ -4138,6 +4186,10 @@ snapshots: transitivePeerDependencies: - supports-color + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4315,6 +4367,8 @@ snapshots: is-promise@4.0.0: {} + is-property@1.0.2: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -4475,10 +4529,16 @@ snapshots: lodash.merge@4.6.2: {} + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 + lru-cache@7.18.3: {} + + lru.min@1.1.2: {} + lucide-react@0.511.0(react@19.1.0): dependencies: react: 19.1.0 @@ -4514,6 +4574,22 @@ snapshots: ms@2.1.3: {} + mysql2@3.14.1: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.3.2 + lru.min: 1.1.2 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + nanoid@3.3.11: {} napi-postinstall@0.2.3: {} @@ -4808,6 +4884,8 @@ snapshots: transitivePeerDependencies: - supports-color + seq-queue@0.0.5: {} + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -4919,6 +4997,8 @@ snapshots: source-map@0.6.1: {} + sqlstring@2.3.3: {} + stable-hash@0.0.5: {} statuses@2.0.1: {} diff --git a/src/app/file-row.tsx b/src/app/file-row.tsx new file mode 100644 index 0000000..01788bb --- /dev/null +++ b/src/app/file-row.tsx @@ -0,0 +1,57 @@ +import { FileIcon, Folder as FolderIcon } from "lucide-react"; + +import type { File, Folder } from "~/lib/mock-data"; + +export function FileRow(props: { file: File }) { + const { file } = props; + + return ( +
  • +
    +
    + + + {file.name} + +
    +
    {"file"}
    +
    {file.size}
    +
    +
  • + ); +} + +export function FolderRow(props: { + folder: Folder; + onFolderClick: () => void; +}) { + const { folder, onFolderClick } = props; + + return ( +
  • +
    +
    + +
    +
    +
    +
    +
  • + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 2808d30..47de2c9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,39 +1,43 @@ "use client"; -import { ChevronRight, FileIcon, Folder, Upload } from "lucide-react"; -import Link from "next/link"; -import { useState } from "react"; +import { ChevronRight, Upload } from "lucide-react"; +import { useMemo, useState } from "react"; import { Button } from "~/components/ui/button"; -import { mockFiles } from "~/lib/mock-data"; +import { mockFiles, mockFolders } from "~/lib/mock-data"; +import { FileRow, FolderRow } from "./file-row"; export default function GoogleDriveClone() { - const [currentFolder, setCurrentFolder] = useState(null); + const [currentFolder, setCurrentFolder] = useState("root"); const getCurrentFiles = () => { return mockFiles.filter((file) => file.parent === currentFolder); }; + const getCurrentFolders = () => { + return mockFolders.filter((folder) => folder.parent === currentFolder); + }; + const handleFolderClick = (folderId: string) => { setCurrentFolder(folderId); }; - const getBreadcrumbs = () => { + const breadcrumbs = useMemo(() => { const breadcrumbs = []; let currentId = currentFolder; - while (currentId !== null) { - const folder = mockFiles.find((file) => file.id === currentId); + while (currentId !== "root") { + const folder = mockFolders.find((file) => file.id === currentId); if (folder) { breadcrumbs.unshift(folder); - currentId = folder.parent; + currentId = folder.parent ?? "root"; } else { break; } } return breadcrumbs; - }; + }, [currentFolder]); const handleUpload = () => { alert("Upload functionality would be implemented here"); @@ -45,13 +49,13 @@ export default function GoogleDriveClone() {
    - {getBreadcrumbs().map((folder) => ( + {breadcrumbs.map((folder) => (
      + {getCurrentFolders().map((folder) => ( + handleFolderClick(folder.id)} + /> + ))} {getCurrentFiles().map((file) => ( -
    • -
      -
      - {file.type === "folder" ? ( - - ) : ( - - - {file.name} - - )} -
      -
      - {file.type === "folder" ? "Folder" : "File"} -
      -
      - {file.type === "folder" ? "--" : "2 MB"} -
      -
      -
    • + ))}
    diff --git a/src/env.js b/src/env.js index 6ca7f3e..9f7c80d 100644 --- a/src/env.js +++ b/src/env.js @@ -7,10 +7,14 @@ export const env = createEnv({ * isn't built with invalid env vars. */ server: { - DATABASE_URL: z.string().url(), NODE_ENV: z .enum(["development", "test", "production"]) .default("development"), + SINGLESTORE_USER: z.string(), + SINGLESTORE_PASSWORD: z.string(), + SINGLESTORE_HOST: z.string(), + SINGLESTORE_PORT: z.coerce.number().default(3306), + SINGLESTORE_DATABASE: z.string(), }, /** @@ -27,8 +31,12 @@ export const env = createEnv({ * middlewares) or client-side so we need to destruct manually. */ runtimeEnv: { - DATABASE_URL: process.env.DATABASE_URL, NODE_ENV: process.env.NODE_ENV, + SINGLESTORE_USER: process.env.SINGLESTORE_USER, + SINGLESTORE_PASSWORD: process.env.SINGLESTORE_PASSWORD, + SINGLESTORE_HOST: process.env.SINGLESTORE_HOST, + SINGLESTORE_PORT: process.env.SINGLESTORE_PORT, + SINGLESTORE_DATABASE: process.env.SINGLESTORE_DATABASE, // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, }, /** diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts index 3ebc462..ba2e9a2 100644 --- a/src/lib/mock-data.ts +++ b/src/lib/mock-data.ts @@ -1,22 +1,34 @@ -export interface File { - id: string - name: string - type: "file" | "folder" - url?: string - parent: string | null - size?: string -} +export type File = { + id: string; + name: string; + type: "file"; + url: string; + parent: string; + size: string; +}; +export type Folder = { + id: string; + name: string; + type: "folder"; + parent: string | null; +}; + +// prettier-ignore +export const mockFolders: Folder[] = [ + { id: "root", name: "root", type: "folder", parent: null }, // the root folder + { id: "1", name: "Documents", type: "folder", parent: "root" }, + { id: "2", name: "Images", type: "folder", parent: "root" }, + { id: "3", name: "Work", type: "folder", parent: "root" }, + { id: "8", name: "Presentations", type: "folder", parent: "3" }, +]; + +// prettier-ignore 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: "4", name: "Resume.pdf", type: "file", url: "/files/resume.pdf", parent: "root", 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/server/db/index.ts b/src/server/db/index.ts index ef1df14..8270e4e 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -1,19 +1,29 @@ -import { createClient, type Client } from "@libsql/client"; -import { drizzle } from "drizzle-orm/libsql"; +import { drizzle } from "drizzle-orm/singlestore"; +import { createPool, type Pool } from "mysql2/promise"; import { env } from "~/env"; import * as schema from "./schema"; /** - * Cache the database connection in development. This avoids creating a new connection on every HMR - * update. + * Cache the database connection in development. This avoids creating a new + * connection on every HMR update. */ const globalForDb = globalThis as unknown as { - client: Client | undefined; + conn: Pool | undefined; }; -export const client = - globalForDb.client ?? createClient({ url: env.DATABASE_URL }); -if (env.NODE_ENV !== "production") globalForDb.client = client; +const conn = + globalForDb.conn ?? + createPool({ + host: env.SINGLESTORE_HOST, + port: env.SINGLESTORE_PORT, + user: env.SINGLESTORE_USER, + password: env.SINGLESTORE_PASSWORD, + database: env.SINGLESTORE_DATABASE, + ssl: {}, + maxIdle: 0, + }); -export const db = drizzle(client, { schema }); +if (env.NODE_ENV !== "production") globalForDb.conn = conn; + +export const db = drizzle(conn, { schema }); diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index ac751c4..d5c0a68 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -1,29 +1,12 @@ -// Example model schema from the Drizzle docs -// https://orm.drizzle.team/docs/sql-schema-declaration +import { + int, + bigint, + text, + singlestoreTable, +} from "drizzle-orm/singlestore-core"; -import { sql } from "drizzle-orm"; -import { index, sqliteTableCreator } from "drizzle-orm/sqlite-core"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = sqliteTableCreator( - (name) => `drive-tutorial_${name}`, -); - -export const posts = createTable( - "post", - (d) => ({ - id: d.integer({ mode: "number" }).primaryKey({ autoIncrement: true }), - name: d.text({ length: 256 }), - createdAt: d - .integer({ mode: "timestamp" }) - .default(sql`(unixepoch())`) - .notNull(), - updatedAt: d.integer({ mode: "timestamp" }).$onUpdate(() => new Date()), - }), - (t) => [index("name_idx").on(t.name)], -); +export const users = singlestoreTable("users_table", { + id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), + name: text("name"), + age: int("age"), +});