From 2e4fe8be2257478b3678374813066b36d94fefd9 Mon Sep 17 00:00:00 2001 From: corgifist Date: Thu, 24 Jul 2025 02:43:34 +0300 Subject: [PATCH] ui: implemented project creation functionality and persistent project storage --- package-lock.json | 97 ++++++++++++++- package.json | 6 +- src/Dockerfile | 4 - src/app/favicon.ico | Bin 0 -> 5391 bytes src/app/favicon.svg | 106 ----------------- src/app/layout.tsx | 11 +- src/app/page.tsx | 125 ++++++++++++++++---- src/app/persistence-provider.tsx | 106 +++++++++++++++++ src/app/settings/page.tsx | 49 +++++++- src/components/dashboard/Dashboard.tsx | 14 ++- src/components/ui/alert-dialog.tsx | 157 +++++++++++++++++++++++++ src/components/ui/checkbox.tsx | 32 +++++ src/components/ui/dialog.tsx | 143 ++++++++++++++++++++++ src/components/ui/sonner.tsx | 25 ++++ src/components/ui/textarea.tsx | 18 +++ src/favicon.svg | 106 ----------------- src/lib/db.ts | 72 +++++++++++- src/lib/uuid.ts | 7 ++ src/types/Project.ts | 10 +- 19 files changed, 831 insertions(+), 257 deletions(-) create mode 100644 src/app/favicon.ico delete mode 100644 src/app/favicon.svg create mode 100644 src/app/persistence-provider.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/sonner.tsx create mode 100644 src/components/ui/textarea.tsx delete mode 100644 src/favicon.svg create mode 100644 src/lib/uuid.ts diff --git a/package-lock.json b/package-lock.json index e916d75..38a04ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { - "name": "clipfusion-community", + "name": "clipfusion", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "clipfusion-editor", + "name": "clipfusion", "version": "1.0.0", "dependencies": { "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-separator": "^1.1.7", @@ -17,6 +19,7 @@ "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-tooltip": "^1.2.7", + "@react-hook/hotkey": "^3.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dexie": "^4.0.11", @@ -27,6 +30,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.60.0", + "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "zod": "^4.0.5" }, @@ -1041,6 +1045,34 @@ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz", + "integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.14", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "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-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -1087,6 +1119,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1658,6 +1720,27 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@react-hook/event": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@react-hook/event/-/event-1.2.6.tgz", + "integrity": "sha512-JUL5IluaOdn5w5Afpe/puPa1rj8X6udMlQ9dt4hvMuKmTrBS1Ya6sb4sVgvfe2eU4yDuOfAhik8xhbcCekbg9Q==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@react-hook/hotkey": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@react-hook/hotkey/-/hotkey-3.1.0.tgz", + "integrity": "sha512-ekIIW8S12P/fP9krrsOWZCIqzQPzjz2WVkD3v1epP6SAy/XxDIWgJrItd2ySh0RKLkYcfW8z+eii3W/h1TKjOg==", + "license": "MIT", + "dependencies": { + "@react-hook/event": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -6356,6 +6439,16 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sonner": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz", + "integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index d64aa46..922a380 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "clipfusion-community", + "name": "clipfusion", "version": "1.0.0", "private": true, "scripts": { @@ -10,7 +10,9 @@ }, "dependencies": { "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-separator": "^1.1.7", @@ -18,6 +20,7 @@ "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-tooltip": "^1.2.7", + "@react-hook/hotkey": "^3.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dexie": "^4.0.11", @@ -28,6 +31,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.60.0", + "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "zod": "^4.0.5" }, diff --git a/src/Dockerfile b/src/Dockerfile index a640085..af50048 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -1,7 +1,3 @@ -ARG ENABLE_ANALYTICS=false -ARG ANALYTICS_SCRIPT -ARG ANALYTICS_WEBSITE_ID - FROM node:lts-alpine3.22 AS base FROM base AS deps diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d37e877aec4062a3cadf0d76c0a10fa1847e8b02 GIT binary patch literal 5391 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 vQE)tJHy`{Hf&C3* literal 0 HcmV?d00001 diff --git a/src/app/favicon.svg b/src/app/favicon.svg deleted file mode 100644 index f23a57c..0000000 --- a/src/app/favicon.svg +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 44b2931..3e747d0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,8 @@ import Dashboard from "@/components/dashboard"; import "./globals.css"; import ThemeProvider from "./theme-provider"; import Analytics from "./analytics"; +import PersistenceProvider from "./persistence-provider"; +import { Toaster } from "@/components/ui/sonner"; const geist = Geist({ variable: "--font-geist", @@ -23,7 +25,7 @@ export const metadata: Metadata = { }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: ReactNode; @@ -37,10 +39,13 @@ export default function RootLayout({ -
- {children} +
+ + {children} +
+ diff --git a/src/app/page.tsx b/src/app/page.tsx index 66b4e90..af8c306 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,23 +1,41 @@ "use client"; import { SidebarTrigger } from "@/components/ui/sidebar"; -import { ReactNode } from "react"; +import { FormEvent, ReactNode, useState } from "react"; import { useLiveQuery } from "dexie-react-hooks"; import { db } from "@/lib/db"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; -import { ListIcon, PlusIcon } from "lucide-react"; +import { ListCheckIcon, PlusIcon } from "lucide-react"; import { Toggle } from "@/components/ui/toggle"; import Search from "@/components/search"; import { useIsMobile } from "@/hooks/use-mobile"; import { AspectRatio } from "@/components/ui/aspect-ratio"; import { Card } from "@/components/ui/card"; +import Project from "@/types/Project"; +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod" +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Textarea } from "@/components/ui/textarea"; +import { generateUUID } from "@/lib/uuid"; -const Project = (): ReactNode => { +const NewProjectFormSchema = z.object({ + title: z.string().nonempty("Title cannot be empty"), + description: z.string().or(z.literal("")) +}); + +const ProjectContainer = ({ + project +}: { + project: Project +}): ReactNode => { return ( - + -

Project Title

-

Project description goes here.

+

{project.title}

+ {project.description &&

{project.description}

}
) @@ -25,39 +43,104 @@ const Project = (): ReactNode => { export default function Home(): ReactNode { const isMobile = useIsMobile(); + const [search, setSearch] = useState(''); - const projectsCount = useLiveQuery(() => { - return db.projects.count(); + const projects = useLiveQuery(() => { + return db.projects.filter((project) => project.title.includes(search)).toArray(); }); + const newProjectForm = useForm>({ + resolver: zodResolver(NewProjectFormSchema), + defaultValues: { + title: "New ClipFusion Project", + description: "" + } + }); + + const newProjectSubmit = async (data: z.infer) => { + const date = Date.now(); + await db.projects.add({ + uuid: generateUUID(), + creationDate: date, + editDate: date, + title: data.title, + description: data.description, + origin: "" + }); + }; + return ( -
+

Project Library

- + {projects && }
- + + + + + + + + Create New Project + + + Fill in the information about your project. You can change it at any time later. + + +
+ + ( + + Title + + + + + + )}/> + ( + + Description + +