diff --git a/.env b/.env index bd5e614..6d783eb 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ PUBLIC_CLERK_KEY="" PUBLIC_JAZZ_KEY="" +PUBLIC_JAZZ_WORKER_ACCOUNT="" +JAZZ_WORKER_SECRET="" diff --git a/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx new file mode 100644 index 0000000..dfe0438 --- /dev/null +++ b/app/components/ui/dialog.tsx @@ -0,0 +1,83 @@ +import type * as React from 'react' +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { XIcon } from 'lucide-react' + +import { cn } from '@/utils/tailwind' + +function Dialog({ ...props }: React.ComponentProps) { + return +} + +function DialogTrigger({ ...props }: React.ComponentProps) { + return +} + +function DialogPortal({ ...props }: React.ComponentProps) { + return +} + +function DialogClose({ ...props }: React.ComponentProps) { + return +} + +function DialogOverlay({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return
+} + +function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { + return
+} + +function DialogTitle({ className, ...props }: React.ComponentProps) { + return +} + +function DialogDescription({ className, ...props }: React.ComponentProps) { + return +} + +export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } diff --git a/app/components/ui/input.tsx b/app/components/ui/input.tsx index 7d6b632..f47f5f5 100644 --- a/app/components/ui/input.tsx +++ b/app/components/ui/input.tsx @@ -2,21 +2,13 @@ import type * as React from 'react' import { cn } from '@/utils/tailwind' -type InputProps = React.ComponentProps<'input'> & { lang?: 'ru' | 'en' | undefined} +type InputProps = React.ComponentProps<'input'> & { lang?: 'ru' | 'en' | undefined } function InputWithLang({ className, lang, ...props }: InputProps) { return ( -
-
- {lang} -
- +
+
{lang}
+
) } diff --git a/app/jazz/requests/game/create.ts b/app/jazz/requests/game/create.ts new file mode 100644 index 0000000..3862c1c --- /dev/null +++ b/app/jazz/requests/game/create.ts @@ -0,0 +1,27 @@ +import { experimental_defineRequest, z } from 'jazz-tools' +import { GameSchema } from '@/schema' + +const workerId = import.meta.env.PUBLIC_JAZZ_WORKER_ACCOUNT + +export const createGameRequest = experimental_defineRequest({ + url: '/api/game/create', + workerId, + + request: { + schema: { + title: z.string(), + }, + + resolve: {}, + }, + + response: { + schema: { + game: GameSchema, + }, + + resolve: { + game: true, + }, + }, +}) diff --git a/app/routes.ts b/app/routes.ts index 198f89a..2ca4aa5 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -2,6 +2,13 @@ import { type RouteConfig, index, route, layout } from '@react-router/dev/routes export default [ index('routes/home.tsx'), - - layout('routes/app-layout.tsx', [route('dashboard', 'routes/dashboard.tsx'), route('games', 'routes/games.tsx'), route('games/:gameId', 'routes/game.tsx'), route('games/create', 'routes/game-create.tsx'), route('forms', 'routes/forms.tsx'), route('forms/:formId', 'routes/form.tsx')]), + route('api/game/create', 'routes/api/game/create.ts'), + layout('routes/app-layout.tsx', [ + route('dashboard', 'routes/dashboard.tsx'), + route('games', 'routes/games.tsx'), + route('games/:gameId', 'routes/game.tsx'), + route('games/create', 'routes/game-create.tsx'), + route('forms', 'routes/forms.tsx'), + route('forms/:formId', 'routes/form.tsx'), + ]), ] satisfies RouteConfig diff --git a/app/routes/api/game/create.ts b/app/routes/api/game/create.ts new file mode 100644 index 0000000..b9ed955 --- /dev/null +++ b/app/routes/api/game/create.ts @@ -0,0 +1,37 @@ +import type { Route } from './+types/create' +import { Group } from 'jazz-tools' +import { jazz } from '@/server/jazz' +import { GameSchema, AppAccount } from '@/schema' +import { createGameRequest } from '@/jazz/requests/game/create' + +export const action = async ({ request }: Route.ActionArgs) => { + return await createGameRequest.handle(request, jazz.worker, async (req, account) => { + const { title } = req + + const ownerAccount = await AppAccount.load(account.$jazz.id, { + resolve: { + profile: true, + }, + }) + + if (!ownerAccount) { + throw new Error('Owner account not found') + } + + const group = Group.create(jazz.worker) + group.addMember(account, 'writer') + group.makePublic('reader') + + const game = GameSchema.create( + { + owner: account, + title, + }, + group, + ) + + return { + game, + } + }) +} diff --git a/app/routes/app-layout.tsx b/app/routes/app-layout.tsx index 789de42..a7193ed 100644 --- a/app/routes/app-layout.tsx +++ b/app/routes/app-layout.tsx @@ -1,18 +1,23 @@ +import { useMemo } from 'react' import { Outlet } from 'react-router' import AppBar from '@/scopes/app/app-bar' import ThemeProvider from '@/scopes/app/theme-provider' import { useIsAuthenticated } from 'jazz-tools/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function AppLayout() { const isAuthenticated = useIsAuthenticated() + const queryClient = useMemo(() => new QueryClient(), []) return ( -
- + +
+ - {isAuthenticated && } -
+ {isAuthenticated && } +
+
) } diff --git a/app/routes/game-create.tsx b/app/routes/game-create.tsx index c501797..bc2bc27 100644 --- a/app/routes/game-create.tsx +++ b/app/routes/game-create.tsx @@ -3,13 +3,13 @@ import CreateGame from '@/scopes/games/create-game' import { Page } from '@/components/ui/page' export function meta(_: Route.MetaArgs) { - return [{ title: 'New Game' }, { name: 'description', content: 'description' }] + return [{ title: 'New Game' }, { name: 'description', content: 'description' }] } export default function GamePage() { - return ( - - - - ) -} \ No newline at end of file + return ( + + + + ) +} diff --git a/app/routes/games.tsx b/app/routes/games.tsx index 3f51c94..35a5f2e 100644 --- a/app/routes/games.tsx +++ b/app/routes/games.tsx @@ -1,5 +1,5 @@ -import { Outlet } from 'react-router' +import { Games } from '@/scopes/games/Games' export default function GamesPage() { - return + return } diff --git a/app/schema.ts b/app/schema.ts index 9da9a0d..d6fa437 100644 --- a/app/schema.ts +++ b/app/schema.ts @@ -1,22 +1,17 @@ import { co, z } from 'jazz-tools' -export const Game = co.map({ +export const ProfileSchema = co.profile() + +export const GameSchema = co.map({ title: z.string(), + owner: co.account(), }) export const AppRoot = co.map({ - games: co.list(Game), + games: co.list(GameSchema), }) -export const AppAccount = co - .account({ - profile: co.profile(), - root: AppRoot, - }) - .withMigration(async (account) => { - if (account.root === undefined) { - account.root = AppRoot.create({ - games: co.list(Game).create([], { owner: account }), - }) - } - }) +export const AppAccount = co.account({ + profile: ProfileSchema, + root: AppRoot, +}) diff --git a/app/scopes/games/Games/CreateGameDialog/index.tsx b/app/scopes/games/Games/CreateGameDialog/index.tsx new file mode 100644 index 0000000..cf632b5 --- /dev/null +++ b/app/scopes/games/Games/CreateGameDialog/index.tsx @@ -0,0 +1,78 @@ +import { useId, useState } from 'react' +import type { GameSchema } from '@/schema' +import { useMutation } from '@tanstack/react-query' +import { createGameRequest } from '@/jazz/requests/game/create' +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import type { co } from 'jazz-tools' + +type CreateGameDialogProps = { + onCreated: (game: co.loaded) => void +} + +export const CreateGameDialog = (props: CreateGameDialogProps) => { + const { onCreated } = props + const titleInputId = useId() + const [title, setTitle] = useState('New game') + + const createGameMutation = useMutation({ + mutationFn: async () => { + const input = { + title: title.trim() || 'New game', + } + + const { game } = await createGameRequest.send(input) + return game + }, + + onSuccess: async (game) => { + onCreated(game) + }, + }) + + return ( + + + + + + + + Create a new game + + Create a new game to get started. + + +
+
+ + + setTitle(e.currentTarget.value)} /> +
+
+ + + + + + + + +
+
+ ) +} diff --git a/app/scopes/games/Games/DeleteGameDialog/index.tsx b/app/scopes/games/Games/DeleteGameDialog/index.tsx new file mode 100644 index 0000000..77bad21 --- /dev/null +++ b/app/scopes/games/Games/DeleteGameDialog/index.tsx @@ -0,0 +1,45 @@ +import type { GameSchema } from '@/schema' +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import type { co } from 'jazz-tools' + +type DeleteGameDialogProps = { + game: co.loaded + onDelete: VoidFunction +} + +export const DeleteGameDialog = (props: DeleteGameDialogProps) => { + const { game, onDelete } = props + + return ( + + + + + + + + Delete game? + + + Are you sure you want to delete {game.title} game? + + + + + + + + + + + + + ) +} diff --git a/app/scopes/games/Games/index.tsx b/app/scopes/games/Games/index.tsx index e2a98ca..1082aab 100644 --- a/app/scopes/games/Games/index.tsx +++ b/app/scopes/games/Games/index.tsx @@ -1,43 +1,50 @@ import { useAccount } from 'jazz-tools/react' -import { AppAccount, Game } from '@/schema' -import { Group } from 'jazz-tools' +import { AppAccount } from '@/schema' +import { Page } from '@/components/ui/page' +import { NavLink, useNavigate } from 'react-router' +import { CreateGameDialog } from './CreateGameDialog' +import { DeleteGameDialog } from '@/scopes/games/Games/DeleteGameDialog' export const Games = () => { const { me } = useAccount(AppAccount, { resolve: { root: { games: { $each: {} } } }, }) + const navigate = useNavigate() + + if (!me) { + return
Loading...
+ } + return ( -
-

Games

+ +

Games

    {me?.root.games.map((game, i) => ( -
  • +
  • {game.title} - + + +
    + { + me.root.games.$jazz.splice(i, 1) + }} + /> +
  • ))}
- -
+ /> + ) } diff --git a/app/scopes/games/create-game/index.tsx b/app/scopes/games/create-game/index.tsx index 440e966..e89caf5 100644 --- a/app/scopes/games/create-game/index.tsx +++ b/app/scopes/games/create-game/index.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useRef, useState } from 'react' +import type React from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { InputWithLang } from '@/components/ui/input' import { Button } from '@/components/ui/button' @@ -6,106 +7,78 @@ type LangText = { ru: string; en: string } type Question = { id: number; title: LangText } export default function CreateGame() { - const [gameName, setGameName] = useState({ ru: '', en: '' }) - const [questions, setQuestions] = useState([]) + const [gameName, setGameName] = useState({ ru: '', en: '' }) + const [questions, setQuestions] = useState([]) + const idRef = useRef(1) + const nextId = useCallback(() => idRef.current++, []) + const createEmptyQuestion = useCallback((): Question => ({ id: nextId(), title: { ru: '', en: '' } }), [nextId]) - const idRef = useRef(1) - const nextId = () => idRef.current++ + useEffect(() => { + if (questions.length === 0) setQuestions([createEmptyQuestion()]) + }, [questions, createEmptyQuestion]) - const createEmptyQuestion = (): Question => ({ id: nextId(), title: { ru: '', en: '' } }) + const handleQuestionTitleChange = (idx: number, lang: keyof LangText, value: string) => { + setQuestions((prev) => { + const next = structuredClone(prev) as Question[] + next[idx].title[lang] = value - useEffect(() => { - if (questions.length === 0) setQuestions([createEmptyQuestion()]) - }, []) + const isLast = idx === next.length - 1 + const nowHasText = !!(next[idx].title.ru.trim() || next[idx].title.en.trim()) + if (isLast && nowHasText) { + next.push(createEmptyQuestion()) + } + return next + }) + } - const handleQuestionTitleChange = ( - idx: number, - lang: keyof LangText, - value: string, - ) => { - setQuestions(prev => { - const next = structuredClone(prev) as Question[] - next[idx].title[lang] = value + const removeQuestion = (qIdx: number) => { + setQuestions((prev) => { + const filtered = prev.filter((_, i) => i !== qIdx) + return filtered.length ? filtered : [createEmptyQuestion()] + }) + } - const isLast = idx === next.length - 1 - const nowHasText = !!(next[idx].title.ru.trim() || next[idx].title.en.trim()) - if (isLast && nowHasText) { - next.push(createEmptyQuestion()) - } - return next - }) - } + return ( +
+
+
+ ) => setGameName((g) => ({ ...g, ru: e.target.value }))} /> + ) => setGameName((g) => ({ ...g, en: e.target.value }))} /> +
- const removeQuestion = (qIdx: number) => { - setQuestions(prev => { - const filtered = prev.filter((_, i) => i !== qIdx) - return filtered.length ? filtered : [createEmptyQuestion()] - }) - } +
+ +
+
- return ( -
-
-
- ) => - setGameName(g => ({ ...g, ru: e.target.value })) - } - /> - ) => - setGameName(g => ({ ...g, en: e.target.value })) - } - /> -
+ {questions.map((question, qIdx) => ( +
+
+
+ ) => handleQuestionTitleChange(qIdx, 'ru', e.target.value)} + /> + ) => handleQuestionTitleChange(qIdx, 'en', e.target.value)} + /> +
+
-
- -
-
- - {questions.map((question, qIdx) => ( -
-
-
- ) => - handleQuestionTitleChange(qIdx, 'ru', e.target.value) - } - /> - ) => - handleQuestionTitleChange(qIdx, 'en', e.target.value) - } - /> -
-
- -
- -
-
- ))} -
- ) -} \ No newline at end of file +
+ +
+
+ ))} +
+ ) +} diff --git a/app/server/jazz.ts b/app/server/jazz.ts new file mode 100644 index 0000000..0fd584f --- /dev/null +++ b/app/server/jazz.ts @@ -0,0 +1,15 @@ +import { startWorker } from 'jazz-tools/worker' +import { co } from 'jazz-tools' +import { AppRoot } from '@/schema' + +export const WorkerAccount = co.account({ + profile: co.profile(), + root: AppRoot, +}) + +export const jazz = await startWorker({ + syncServer: `wss://cloud.jazz.tools/?key=${process.env.PUBLIC_JAZZ_KEY}`, + AccountSchema: WorkerAccount, + accountID: process.env.PUBLIC_JAZZ_WORKER_ACCOUNT, + accountSecret: process.env.JAZZ_WORKER_SECRET, +}) diff --git a/app/vite-env.d.ts b/app/vite-env.d.ts index a693d48..508370e 100644 --- a/app/vite-env.d.ts +++ b/app/vite-env.d.ts @@ -7,6 +7,7 @@ interface ViteTypeOptions { interface ImportMetaEnv { readonly PUBLIC_CLERK_KEY: string readonly PUBLIC_JAZZ_KEY: string + readonly PUBLIC_JAZZ_WORKER_ACCOUNT: string } interface ImportMeta { diff --git a/biome.json b/biome.json index 0bfb25e..cc93645 100644 --- a/biome.json +++ b/biome.json @@ -24,6 +24,9 @@ "style": { "useImportType": "error", "useTemplate": "off" + }, + "suspicious": { + "noUnknownAtRules": "off" } } }, diff --git a/package-lock.json b/package-lock.json index 2abc51b..6558aff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "name": "vibesync", "dependencies": { "@clerk/clerk-react": "^5.33.0", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-progress": "^1.1.7", @@ -14,12 +15,13 @@ "@radix-ui/react-slot": "^1.2.3", "@react-router/node": "^7.5.3", "@react-router/serve": "^7.5.3", + "@tanstack/react-query": "^5.85.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "isbot": "^5.1.27", - "jazz-tools": "^0.15.9", + "jazz-tools": "^0.18.0", "lucide-react": "^0.525.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -1194,9 +1196,9 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" @@ -1393,6 +1395,114 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "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-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -2691,6 +2801,32 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tanstack/query-core": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz", + "integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz", + "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.85.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tiptap/core": { "version": "2.26.1", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.26.1.tgz", @@ -2752,15 +2888,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/image-blob-reduce": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@types/image-blob-reduce/-/image-blob-reduce-4.1.4.tgz", - "integrity": "sha512-IMG+KVL7iy/g05zaJQFqc+Y0l+SlBVVW3GVKLM+ZkBtENoJSZ5nqIgtSZcs04QJvF+ticYJn9n3ZG427wg7Yhg==", - "license": "MIT", - "dependencies": { - "@types/pica": "*" - } - }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -2802,12 +2929,6 @@ "integrity": "sha512-dxKo80TqYx3YtBipHwA/SdFmMMyLCnP+5mkEqN0eMjcTBzHkiiX0ES118DsjDBjvD+zeSsSU9jULTZ+frog+Gw==", "license": "MIT" }, - "node_modules/@types/pica": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@types/pica/-/pica-9.0.5.tgz", - "integrity": "sha512-OSd4905yxFNtRanHuyyQAfC9AkxiYcbhlzP606Gl6rFcYRgq4vdLCZuYKokLQBihgrkNzyPkoeykvJDWcPjaCw==", - "license": "MIT" - }, "node_modules/@types/prosemirror-model": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/@types/prosemirror-model/-/prosemirror-model-1.16.2.tgz", @@ -3211,9 +3332,9 @@ } }, "node_modules/cojson": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/cojson/-/cojson-0.15.9.tgz", - "integrity": "sha512-dPZUjYNRC2IeTvcgDverj6hg10zhfIIStc8X9aRQc/L94nXIa8KgR/g9Uc/3+12JQeB5xBEHruyjo0MRtfrRBw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/cojson/-/cojson-0.18.0.tgz", + "integrity": "sha512-5E+hvczYbGv0okwl7nv2hHxRlYbQlwFKnaG4/ntfcwrg9cPrAxa9B0zf1As+Uky2KhHbafOlHorM35lIs/BAkg==", "license": "MIT", "dependencies": { "@noble/ciphers": "^1.3.0", @@ -3221,27 +3342,33 @@ "@noble/hashes": "^1.8.0", "@opentelemetry/api": "^1.9.0", "@scure/base": "1.2.1", - "jazz-crypto-rs": "0.0.7", + "cojson-core-wasm": "0.18.0", "neverthrow": "^7.0.1", "unicode-segmenter": "^0.12.0" } }, + "node_modules/cojson-core-wasm": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/cojson-core-wasm/-/cojson-core-wasm-0.18.0.tgz", + "integrity": "sha512-eRRXcspYDBPz6BpU91Z0VhCoElacZ/IHkmObrmPitGtubMiShewcwawJP2mBpvs0pv0VZNMEATKGRy6B+5Wfog==" + }, "node_modules/cojson-storage-indexeddb": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/cojson-storage-indexeddb/-/cojson-storage-indexeddb-0.15.9.tgz", - "integrity": "sha512-BO9LoV6fgyhl3u3XpqLXS4PF0QZKG22OPxeWmZCogjYa+AjXkinLJLt4J1l64pb2OmPoFPxBuqwSGUbmke8kVA==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/cojson-storage-indexeddb/-/cojson-storage-indexeddb-0.18.0.tgz", + "integrity": "sha512-nacr1493X3JOrc0MP64dlyTZw7JgRP5xBIDZQR26cOpwYXNkq1cWauQ32Re/aGtFlDv05+NoU9DdciQaAK6ESw==", "license": "MIT", "dependencies": { - "cojson": "0.15.9" + "cojson": "0.18.0" } }, "node_modules/cojson-transport-ws": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/cojson-transport-ws/-/cojson-transport-ws-0.15.9.tgz", - "integrity": "sha512-oB+TyYuYSz2WfYwT2vITtTS75ApHDwbVplXuH5OeFn1osx6ew4xqBF4K8sgBHuelNrovJsthTpd0pemvPn5ZFw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/cojson-transport-ws/-/cojson-transport-ws-0.18.0.tgz", + "integrity": "sha512-SdFvNePT4NWKkIvavp29j2u4JuRjHb2Foq9gmsvyFyzmsFKGgnvZOkEO2V/bU+/4loL8v/gTsOyybAKaY9ZY8Q==", "license": "MIT", "dependencies": { - "cojson": "0.15.9" + "@opentelemetry/api": "^1.9.0", + "cojson": "0.18.0" } }, "node_modules/color-convert": { @@ -3980,12 +4107,6 @@ "dev": true, "license": "MIT" }, - "node_modules/glur": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/glur/-/glur-1.1.2.tgz", - "integrity": "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==", - "license": "MIT" - }, "node_modules/goober": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", @@ -4148,15 +4269,6 @@ "node": ">=0.10.0" } }, - "node_modules/image-blob-reduce": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/image-blob-reduce/-/image-blob-reduce-4.1.0.tgz", - "integrity": "sha512-iljleP8Fr7tS1ezrAazWi30abNPYXtBGXb9R9oTZDWObqiKq18AQJGTUb0wkBOtdCZ36/IirkuuAIIHTjBJIjA==", - "license": "MIT", - "dependencies": { - "pica": "^9.0.0" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4230,52 +4342,44 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jazz-crypto-rs": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/jazz-crypto-rs/-/jazz-crypto-rs-0.0.7.tgz", - "integrity": "sha512-Pzs8Zu1zgKVURkBVvqqF1B2r78FaSZdwPbmeCWx0Nb3nibzPuB/kSKEO7LJsKnb3P5HO0v+lTIJ53mGC5H1urQ==" - }, "node_modules/jazz-tools": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/jazz-tools/-/jazz-tools-0.15.9.tgz", - "integrity": "sha512-B3My8Qv0tEv/CuYvX4C6KGl1Wz6+0rpIlF4u3uzGULYEV2ARwQqZUE9HdK7wBGPKEnXkYEhk4I2jdRh7yrLU9A==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/jazz-tools/-/jazz-tools-0.18.0.tgz", + "integrity": "sha512-5Q32lJFrG9ifVXzdnvghHktvrWiSdU2LDOg0cmOZykOqzDXpyfSZU5Iasy/csj+XuLhVjtKzXZpSnHpobM7YwA==", "license": "MIT", "dependencies": { "@manuscripts/prosemirror-recreate-steps": "^0.1.4", "@scure/base": "1.2.1", "@scure/bip39": "^1.3.0", "@tiptap/core": "^2.12.0", - "@types/image-blob-reduce": "^4.1.1", "clsx": "^2.0.0", - "cojson": "0.15.9", - "cojson-storage-indexeddb": "0.15.9", - "cojson-transport-ws": "0.15.9", + "cojson": "0.18.0", + "cojson-storage-indexeddb": "0.18.0", + "cojson-transport-ws": "0.18.0", "fast-myers-diff": "^3.2.0", "goober": "^2.1.16", - "image-blob-reduce": "^4.1.0", - "pica": "^9.0.1", "prosemirror-example-setup": "^1.2.2", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.21.1", "prosemirror-schema-basic": "^1.2.2", "prosemirror-state": "^1.4.3", "prosemirror-transform": "^1.9.0", - "zod": "3.25.28" + "zod": "3.25.76" }, "peerDependencies": { "@bam.tech/react-native-image-resizer": "*", - "@op-engineering/op-sqlite": "^11.4.8", + "@op-engineering/op-sqlite": "*", "@react-native-community/netinfo": "*", "expo-file-system": "*", "expo-secure-store": "*", - "expo-sqlite": "15.2.9", + "expo-sqlite": "*", "react": "*", "react-dom": "*", "react-native": "*", "react-native-fast-encoder": "^0.2.0", - "react-native-mmkv": "^3.2.0", - "react-native-nitro-modules": "0.25.2", - "react-native-quick-crypto": "1.0.0-beta.16", + "react-native-mmkv": "^3.3.0", + "react-native-nitro-modules": "^0.26.4", + "react-native-quick-crypto": "^1.0.0-beta.18", "svelte": "^5.0.0" }, "peerDependenciesMeta": { @@ -4877,16 +4981,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/multimath": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/multimath/-/multimath-2.0.0.tgz", - "integrity": "sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==", - "license": "MIT", - "dependencies": { - "glur": "^1.1.2", - "object-assign": "^4.1.1" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5022,15 +5116,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5133,18 +5218,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pica": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/pica/-/pica-9.0.1.tgz", - "integrity": "sha512-v0U4vY6Z3ztz9b4jBIhCD3WYoecGXCQeCsYep+sXRefViL+mVVoTL+wqzdPeE+GpBFsRUtQZb6dltvAt2UkMtQ==", - "license": "MIT", - "dependencies": { - "glur": "^1.1.2", - "multimath": "^2.0.0", - "object-assign": "^4.1.1", - "webworkify": "^1.5.0" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6640,12 +6713,6 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, - "node_modules/webworkify": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.5.0.tgz", - "integrity": "sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==", - "license": "MIT" - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -6778,9 +6845,9 @@ "license": "ISC" }, "node_modules/zod": { - "version": "3.25.28", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.28.tgz", - "integrity": "sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index d1a99a2..8fd3ebd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@clerk/clerk-react": "^5.33.0", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-progress": "^1.1.7", @@ -20,12 +21,13 @@ "@radix-ui/react-slot": "^1.2.3", "@react-router/node": "^7.5.3", "@react-router/serve": "^7.5.3", + "@tanstack/react-query": "^5.85.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "isbot": "^5.1.27", - "jazz-tools": "^0.15.9", + "jazz-tools": "^0.18.0", "lucide-react": "^0.525.0", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/react-router.config.ts b/react-router.config.ts index 3afb75a..e45e273 100644 --- a/react-router.config.ts +++ b/react-router.config.ts @@ -1,5 +1,5 @@ import type { Config } from "@react-router/dev/config"; export default { - ssr: false, + ssr: true, } satisfies Config;