From b89ad25a35d27b080ba5c63a9a1d3d7dd820396f Mon Sep 17 00:00:00 2001 From: tacosjs <6282845+tacosjs@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:59:45 -0500 Subject: [PATCH 1/8] add documents page base --- convex/_generated/api.d.ts | 2 + convex/documents.ts | 57 ++++++++++++ convex/schema.ts | 17 ++++ messages/en.json | 1 + messages/fr.json | 1 + .../Layouts/Navigation/MainNavigation.tsx | 15 +++- src/hooks/useEncryption.ts | 1 + src/routeTree.gen.ts | 82 ++++++++++++++++- src/routes/documents/create.tsx | 20 +++++ src/routes/documents/details.$id.tsx | 19 ++++ src/routes/documents/list.tsx | 20 +++++ src/types/routes.ts | 3 + .../DocumentsView/DocumentCreationView.tsx | 87 +++++++++++++++++++ .../DocumentsView/DocumentDetailsView.tsx | 82 +++++++++++++++++ src/views/DocumentsView/DocumentsListView.tsx | 69 +++++++++++++++ src/views/DocumentsView/index.ts | 3 + 16 files changed, 475 insertions(+), 4 deletions(-) create mode 100644 convex/documents.ts create mode 100644 src/routes/documents/create.tsx create mode 100644 src/routes/documents/details.$id.tsx create mode 100644 src/routes/documents/list.tsx create mode 100644 src/views/DocumentsView/DocumentCreationView.tsx create mode 100644 src/views/DocumentsView/DocumentDetailsView.tsx create mode 100644 src/views/DocumentsView/DocumentsListView.tsx create mode 100644 src/views/DocumentsView/index.ts diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 24f85d2..879fa09 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -8,6 +8,7 @@ * @module */ +import type * as documents from "../documents.js"; import type * as userKeys from "../userKeys.js"; import type { @@ -17,6 +18,7 @@ import type { } from "convex/server"; declare const fullApi: ApiFromModules<{ + documents: typeof documents; userKeys: typeof userKeys; }>; diff --git a/convex/documents.ts b/convex/documents.ts new file mode 100644 index 0000000..051f803 --- /dev/null +++ b/convex/documents.ts @@ -0,0 +1,57 @@ +import { v } from 'convex/values' + +import type { Id } from './_generated/dataModel' +import { mutation, query } from './_generated/server' + +/** + * Get the documents for the user + */ +export const getDocuments = query({ + handler: async (ctx) => { + const identity = await ctx.auth.getUserIdentity() + if (!identity) return null + + const documents = await ctx.db + .query('documents') + .withIndex('by_user', (q) => q.eq('userId', identity.subject)) + .collect() + + return documents + }, +}) + +/** + * Get a single document by id + */ +export const getDocumentById = query({ + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity() + if (!identity) return null + + const document = await ctx.db.get('documents', args.id as Id<'documents'>) + if (!document || document.userId !== identity.subject) return null + + return document + }, +}) + +/** + * Create a new document for the user + */ +export const createDocument = mutation({ + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity() + if (!identity) throw new Error('Unauthorized') + + return await ctx.db.insert('documents', { + id: crypto.randomUUID().toString(), + userId: identity.subject, + createdAt: Date.now(), + encryptedData: args.encryptedData, + updatedAt: Date.now(), + }) + }, + args: { + encryptedData: v.string(), + }, +}) diff --git a/convex/schema.ts b/convex/schema.ts index e8f0d54..fbc486e 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -2,6 +2,23 @@ import { defineSchema, defineTable } from 'convex/server' import { v } from 'convex/values' export default defineSchema({ + /** + * Documents + * This table stores the documents for the user. + * It is used to store the encrypted data for the documents. + */ + documents: defineTable({ + id: v.string(), + userId: v.string(), + createdAt: v.number(), + encryptedData: v.string(), + updatedAt: v.number(), + }) + .index('by_user', ['userId']) + .index('id', ['id']) + .index('createdAt', ['createdAt']) + .index('updatedAt', ['updatedAt']), + /** * User keys * This table stores the user's encryption keys. diff --git a/messages/en.json b/messages/en.json index c98008b..0a2495f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3,6 +3,7 @@ "navigation": { "dashboard": "Dashboard", "encrypt": "Encryption Demo", + "documents": "Documents", "logout": "Logout" }, "sign_in": { diff --git a/messages/fr.json b/messages/fr.json index 567672b..1c3443a 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3,6 +3,7 @@ "navigation": { "dashboard": "Tableau de bord", "encrypt": "Démo de chiffrement", + "documents": "Documents", "logout": "Se déconnecter" }, "sign_in": { diff --git a/src/components/Layouts/Navigation/MainNavigation.tsx b/src/components/Layouts/Navigation/MainNavigation.tsx index 6050c0a..68254e3 100644 --- a/src/components/Layouts/Navigation/MainNavigation.tsx +++ b/src/components/Layouts/Navigation/MainNavigation.tsx @@ -1,5 +1,9 @@ import { useNavigate } from '@tanstack/react-router' -import { LayoutDashboardIcon, MessageSquareLockIcon } from 'lucide-react' +import { + FileIcon, + LayoutDashboardIcon, + MessageSquareLockIcon, +} from 'lucide-react' import { SidebarGroup, @@ -30,6 +34,15 @@ export const MainNavigation = () => { tooltip={m['navigation.encrypt']()} onClick={() => navigate({ to: RoutesPath.DEMO_ENCRYPT.toString() })} /> + + } + label={m['navigation.documents']()} + tooltip={m['navigation.documents']()} + onClick={() => + navigate({ to: RoutesPath.DOCUMENTS_LIST.toString() }) + } + /> diff --git a/src/hooks/useEncryption.ts b/src/hooks/useEncryption.ts index b374e24..9933d05 100644 --- a/src/hooks/useEncryption.ts +++ b/src/hooks/useEncryption.ts @@ -29,5 +29,6 @@ export const useEncryption = () => { return { decryptData, encryptData, + userKeys, } } diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 10dd35f..c254e17 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -10,15 +10,28 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' +import { Route as DocumentsListRouteImport } from './routes/documents/list' +import { Route as DocumentsCreateRouteImport } from './routes/documents/create' import { Route as DemoEncryptRouteImport } from './routes/demo/encrypt' import { Route as AuthSignupRouteImport } from './routes/auth/signup' import { Route as AuthSigninRouteImport } from './routes/auth/signin' +import { Route as DocumentsDetailsIdRouteImport } from './routes/documents/details.$id' const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) +const DocumentsListRoute = DocumentsListRouteImport.update({ + id: '/documents/list', + path: '/documents/list', + getParentRoute: () => rootRouteImport, +} as any) +const DocumentsCreateRoute = DocumentsCreateRouteImport.update({ + id: '/documents/create', + path: '/documents/create', + getParentRoute: () => rootRouteImport, +} as any) const DemoEncryptRoute = DemoEncryptRouteImport.update({ id: '/demo/encrypt', path: '/demo/encrypt', @@ -34,18 +47,29 @@ const AuthSigninRoute = AuthSigninRouteImport.update({ path: '/auth/signin', getParentRoute: () => rootRouteImport, } as any) +const DocumentsDetailsIdRoute = DocumentsDetailsIdRouteImport.update({ + id: '/documents/details/$id', + path: '/documents/details/$id', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/auth/signin': typeof AuthSigninRoute '/auth/signup': typeof AuthSignupRoute '/demo/encrypt': typeof DemoEncryptRoute + '/documents/create': typeof DocumentsCreateRoute + '/documents/list': typeof DocumentsListRoute + '/documents/details/$id': typeof DocumentsDetailsIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/auth/signin': typeof AuthSigninRoute '/auth/signup': typeof AuthSignupRoute '/demo/encrypt': typeof DemoEncryptRoute + '/documents/create': typeof DocumentsCreateRoute + '/documents/list': typeof DocumentsListRoute + '/documents/details/$id': typeof DocumentsDetailsIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -53,13 +77,38 @@ export interface FileRoutesById { '/auth/signin': typeof AuthSigninRoute '/auth/signup': typeof AuthSignupRoute '/demo/encrypt': typeof DemoEncryptRoute + '/documents/create': typeof DocumentsCreateRoute + '/documents/list': typeof DocumentsListRoute + '/documents/details/$id': typeof DocumentsDetailsIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/auth/signin' | '/auth/signup' | '/demo/encrypt' + fullPaths: + | '/' + | '/auth/signin' + | '/auth/signup' + | '/demo/encrypt' + | '/documents/create' + | '/documents/list' + | '/documents/details/$id' fileRoutesByTo: FileRoutesByTo - to: '/' | '/auth/signin' | '/auth/signup' | '/demo/encrypt' - id: '__root__' | '/' | '/auth/signin' | '/auth/signup' | '/demo/encrypt' + to: + | '/' + | '/auth/signin' + | '/auth/signup' + | '/demo/encrypt' + | '/documents/create' + | '/documents/list' + | '/documents/details/$id' + id: + | '__root__' + | '/' + | '/auth/signin' + | '/auth/signup' + | '/demo/encrypt' + | '/documents/create' + | '/documents/list' + | '/documents/details/$id' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -67,6 +116,9 @@ export interface RootRouteChildren { AuthSigninRoute: typeof AuthSigninRoute AuthSignupRoute: typeof AuthSignupRoute DemoEncryptRoute: typeof DemoEncryptRoute + DocumentsCreateRoute: typeof DocumentsCreateRoute + DocumentsListRoute: typeof DocumentsListRoute + DocumentsDetailsIdRoute: typeof DocumentsDetailsIdRoute } declare module '@tanstack/react-router' { @@ -78,6 +130,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/documents/list': { + id: '/documents/list' + path: '/documents/list' + fullPath: '/documents/list' + preLoaderRoute: typeof DocumentsListRouteImport + parentRoute: typeof rootRouteImport + } + '/documents/create': { + id: '/documents/create' + path: '/documents/create' + fullPath: '/documents/create' + preLoaderRoute: typeof DocumentsCreateRouteImport + parentRoute: typeof rootRouteImport + } '/demo/encrypt': { id: '/demo/encrypt' path: '/demo/encrypt' @@ -99,6 +165,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthSigninRouteImport parentRoute: typeof rootRouteImport } + '/documents/details/$id': { + id: '/documents/details/$id' + path: '/documents/details/$id' + fullPath: '/documents/details/$id' + preLoaderRoute: typeof DocumentsDetailsIdRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -107,6 +180,9 @@ const rootRouteChildren: RootRouteChildren = { AuthSigninRoute: AuthSigninRoute, AuthSignupRoute: AuthSignupRoute, DemoEncryptRoute: DemoEncryptRoute, + DocumentsCreateRoute: DocumentsCreateRoute, + DocumentsListRoute: DocumentsListRoute, + DocumentsDetailsIdRoute: DocumentsDetailsIdRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/documents/create.tsx b/src/routes/documents/create.tsx new file mode 100644 index 0000000..dfbad80 --- /dev/null +++ b/src/routes/documents/create.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { Layout } from '@/components/Layouts' +import { ProtectedRoutes } from '@/components/ProtectedRoutes' +import { RoutesPath } from '@/types/routes' +import { DocumentCreationView } from '@/views/DocumentsView' + +export const Route = createFileRoute(RoutesPath.DOCUMENTS_CREATE)({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( + + + + + + ) +} diff --git a/src/routes/documents/details.$id.tsx b/src/routes/documents/details.$id.tsx new file mode 100644 index 0000000..6707c18 --- /dev/null +++ b/src/routes/documents/details.$id.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { Layout } from '@/components/Layouts' +import { ProtectedRoutes } from '@/components/ProtectedRoutes' +import { DocumentDetailsView } from '@/views/DocumentsView' + +export const Route = createFileRoute('/documents/details/$id')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( + + + + + + ) +} diff --git a/src/routes/documents/list.tsx b/src/routes/documents/list.tsx new file mode 100644 index 0000000..99a3b64 --- /dev/null +++ b/src/routes/documents/list.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { Layout } from '@/components/Layouts' +import { ProtectedRoutes } from '@/components/ProtectedRoutes' +import { RoutesPath } from '@/types/routes' +import { DocumentsListView } from '@/views/DocumentsView' + +export const Route = createFileRoute(RoutesPath.DOCUMENTS_LIST)({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( + + + + + + ) +} diff --git a/src/types/routes.ts b/src/types/routes.ts index 0552425..081da27 100644 --- a/src/types/routes.ts +++ b/src/types/routes.ts @@ -3,4 +3,7 @@ export enum RoutesPath { SIGN_IN = '/auth/signin', SIGN_UP = '/auth/signup', DEMO_ENCRYPT = '/demo/encrypt', + DOCUMENTS_CREATE = '/documents/create', + DOCUMENTS_LIST = '/documents/list', + DOCUMENTS_DETAILS = '/documents/details', } diff --git a/src/views/DocumentsView/DocumentCreationView.tsx b/src/views/DocumentsView/DocumentCreationView.tsx new file mode 100644 index 0000000..998d092 --- /dev/null +++ b/src/views/DocumentsView/DocumentCreationView.tsx @@ -0,0 +1,87 @@ +import { useState } from 'react' + +import { api } from '@convex/_generated/api' +import { useNavigate } from '@tanstack/react-router' +import { useMutation } from 'convex/react' + +import { Button } from '@/components/ui/button' +import { + Field, + FieldDescription, + FieldError, + FieldGroup, + FieldLabel, +} from '@/components/ui/field' +import { Textarea } from '@/components/ui/textarea' +import { useEncryption } from '@/hooks/useEncryption' +import { RoutesPath } from '@/types/routes' + +export const DocumentCreationView = () => { + const navigate = useNavigate() + const { encryptData } = useEncryption() + const createDocument = useMutation(api.documents.createDocument) + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(false) + + const handleCancel = () => { + navigate({ to: RoutesPath.DOCUMENTS_LIST.toString() }) + } + + const handleCreateDocument = async ( + e: React.SubmitEvent, + ) => { + e.preventDefault() + + const formData = new FormData(e.target as HTMLFormElement) + const message = formData.get('message') as string + if (!message) throw new Error('Message is required') + + try { + setIsLoading(true) + setError(null) + const encryptedMessage = await encryptData(message) + await createDocument({ encryptedData: encryptedMessage }) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create document') + } finally { + setIsLoading(false) + navigate({ to: RoutesPath.DOCUMENTS_LIST.toString() }) + } + } + + return ( +
+

Create Document

+
+ + + Message + + This message will be encrypted and stored in the database. + +