diff options
| -rw-r--r-- | apps/web/app/dashboard/layout.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/layout.tsx | 4 | ||||
| -rw-r--r-- | apps/web/components/DemoModeBanner.tsx | 7 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx | 7 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx | 1 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/TagsEditor.tsx | 3 | ||||
| -rw-r--r-- | apps/web/components/dashboard/sidebar/NewListModal.tsx | 17 | ||||
| -rw-r--r-- | apps/web/components/signin/CredentialsForm.tsx | 6 | ||||
| -rw-r--r-- | apps/web/components/ui/action-button.tsx | 9 | ||||
| -rw-r--r-- | apps/web/lib/clientConfig.tsx | 11 | ||||
| -rw-r--r-- | apps/web/lib/providers.tsx | 21 | ||||
| -rw-r--r-- | packages/shared/config.ts | 5 | ||||
| -rw-r--r-- | packages/trpc/routers/lists.ts | 15 |
13 files changed, 89 insertions, 19 deletions
diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx index 27e06955..b79fd0f9 100644 --- a/apps/web/app/dashboard/layout.tsx +++ b/apps/web/app/dashboard/layout.tsx @@ -1,6 +1,7 @@ import MobileSidebar from "@/components/dashboard/sidebar/ModileSidebar"; import Sidebar from "@/components/dashboard/sidebar/Sidebar"; import UploadDropzone from "@/components/dashboard/UploadDropzone"; +import DemoModeBanner from "@/components/DemoModeBanner"; import { Separator } from "@/components/ui/separator"; export default async function Dashboard({ @@ -14,6 +15,7 @@ export default async function Dashboard({ <Sidebar /> </div> <main className="flex-1 bg-gray-100 sm:overflow-y-auto"> + <DemoModeBanner /> <div className="block w-full sm:hidden"> <MobileSidebar /> <Separator /> diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 6ec9c3e4..395d7297 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -10,6 +10,8 @@ import Providers from "@/lib/providers"; import { getServerAuthSession } from "@/server/auth"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { clientConfig } from "@hoarder/shared/config"; + const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { @@ -42,7 +44,7 @@ export default async function RootLayout({ return ( <html lang="en"> <body className={inter.className}> - <Providers session={session}> + <Providers session={session} clientConfig={clientConfig}> {children} <ReactQueryDevtools initialIsOpen={false} /> </Providers> diff --git a/apps/web/components/DemoModeBanner.tsx b/apps/web/components/DemoModeBanner.tsx new file mode 100644 index 00000000..6250be87 --- /dev/null +++ b/apps/web/components/DemoModeBanner.tsx @@ -0,0 +1,7 @@ +export default function DemoModeBanner() { + return ( + <div className="h-min w-full rounded bg-yellow-100 px-4 py-2 text-center"> + Demo mode is on. All modifications are disabled. + </div> + ); +} diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx index 3656a435..692d7d78 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx @@ -9,6 +9,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useToast } from "@/components/ui/use-toast"; +import { useClientConfig } from "@/lib/clientConfig"; import { api } from "@/lib/trpc"; import { Archive, @@ -32,6 +33,8 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); const linkId = bookmark.id; + const demoMode = useClientConfig().demoMode; + const { setOpen: setTagModalIsOpen, content: tagModal } = useTagModel(bookmark); const { setOpen: setAddToListModalOpen, content: addToListModal } = @@ -115,6 +118,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { </DropdownMenuItem> )} <DropdownMenuItem + disabled={demoMode} onClick={() => updateBookmarkMutator.mutate({ bookmarkId: linkId, @@ -126,6 +130,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { <span>{bookmark.favourited ? "Un-favourite" : "Favourite"}</span> </DropdownMenuItem> <DropdownMenuItem + disabled={demoMode} onClick={() => updateBookmarkMutator.mutate({ bookmarkId: linkId, @@ -163,6 +168,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { {bookmark.content.type === "link" && ( <DropdownMenuItem + disabled={demoMode} onClick={() => crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }) } @@ -172,6 +178,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { </DropdownMenuItem> )} <DropdownMenuItem + disabled={demoMode} className="text-destructive" onClick={() => deleteBookmarkMutator.mutate({ bookmarkId: bookmark.id }) diff --git a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx index b40e6e42..4b0dc4fd 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx @@ -92,6 +92,7 @@ export default function BookmarksGrid({ </Masonry> {hasNextPage && ( <ActionButton + ignoreDemoMode={true} loading={isFetchingNextPage} onClick={() => fetchNextPage()} className="mx-auto w-min" diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx index 38f01bdd..4cccfc02 100644 --- a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx @@ -1,5 +1,6 @@ import type { ActionMeta } from "react-select"; import { toast } from "@/components/ui/use-toast"; +import { useClientConfig } from "@/lib/clientConfig"; import { api } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import { Sparkles } from "lucide-react"; @@ -15,6 +16,7 @@ interface EditableTag { } export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) { + const demoMode = useClientConfig().demoMode; const bookmarkInvalidationFunction = api.useUtils().bookmarks.getBookmark.invalidate; @@ -79,6 +81,7 @@ export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) { return ( <CreateableSelect + isDisabled={demoMode} onChange={onChange} options={ existingTags?.tags.map((t) => ({ diff --git a/apps/web/components/dashboard/sidebar/NewListModal.tsx b/apps/web/components/dashboard/sidebar/NewListModal.tsx index e244411d..31c35d6c 100644 --- a/apps/web/components/dashboard/sidebar/NewListModal.tsx +++ b/apps/web/components/dashboard/sidebar/NewListModal.tsx @@ -67,10 +67,19 @@ export default function NewListModal() { }, onError: (e) => { if (e.data?.code == "BAD_REQUEST") { - toast({ - variant: "destructive", - description: e.message, - }); + if (e.data.zodError) { + toast({ + variant: "destructive", + description: Object.values(e.data.zodError.fieldErrors) + .flat() + .join("\n"), + }); + } else { + toast({ + variant: "destructive", + description: e.message, + }); + } } else { toast({ variant: "destructive", diff --git a/apps/web/components/signin/CredentialsForm.tsx b/apps/web/components/signin/CredentialsForm.tsx index 59dfeb21..8e1423eb 100644 --- a/apps/web/components/signin/CredentialsForm.tsx +++ b/apps/web/components/signin/CredentialsForm.tsx @@ -86,7 +86,11 @@ function SignIn() { ); }} /> - <ActionButton type="submit" loading={form.formState.isSubmitting}> + <ActionButton + ignoreDemoMode + type="submit" + loading={form.formState.isSubmitting} + > Sign In </ActionButton> </div> diff --git a/apps/web/components/ui/action-button.tsx b/apps/web/components/ui/action-button.tsx index 11b02a5f..5b862e07 100644 --- a/apps/web/components/ui/action-button.tsx +++ b/apps/web/components/ui/action-button.tsx @@ -1,3 +1,5 @@ +import { useClientConfig } from "@/lib/clientConfig"; + import type { ButtonProps } from "./button"; import { Button } from "./button"; import LoadingSpinner from "./spinner"; @@ -7,13 +9,18 @@ export function ActionButton({ loading, spinner, disabled, + ignoreDemoMode = false, ...props }: ButtonProps & { loading: boolean; spinner?: React.ReactNode; + ignoreDemoMode?: boolean; }) { + const clientConfig = useClientConfig(); spinner ||= <LoadingSpinner />; - if (disabled !== undefined) { + if (!ignoreDemoMode && clientConfig.demoMode) { + disabled = true; + } else if (disabled !== undefined) { disabled ||= loading; } else if (loading) { disabled = true; diff --git a/apps/web/lib/clientConfig.tsx b/apps/web/lib/clientConfig.tsx new file mode 100644 index 00000000..fac76d3b --- /dev/null +++ b/apps/web/lib/clientConfig.tsx @@ -0,0 +1,11 @@ +import { createContext, useContext } from "react"; + +import type { ClientConfig } from "@hoarder/shared/config"; + +export const ClientConfigCtx = createContext<ClientConfig>({ + demoMode: false, +}); + +export function useClientConfig() { + return useContext(ClientConfigCtx); +} diff --git a/apps/web/lib/providers.tsx b/apps/web/lib/providers.tsx index db51c361..ce667f8d 100644 --- a/apps/web/lib/providers.tsx +++ b/apps/web/lib/providers.tsx @@ -7,6 +7,9 @@ import { httpBatchLink, loggerLink } from "@trpc/client"; import { SessionProvider } from "next-auth/react"; import superjson from "superjson"; +import type { ClientConfig } from "@hoarder/shared/config"; + +import { ClientConfigCtx } from "./clientConfig"; import { api } from "./trpc"; function makeQueryClient() { @@ -40,9 +43,11 @@ function getQueryClient() { export default function Providers({ children, session, + clientConfig, }: { children: React.ReactNode; session: Session | null; + clientConfig: ClientConfig; }) { const queryClient = getQueryClient(); @@ -64,12 +69,14 @@ export default function Providers({ ); return ( - <SessionProvider session={session}> - <api.Provider client={trpcClient} queryClient={queryClient}> - <QueryClientProvider client={queryClient}> - {children} - </QueryClientProvider> - </api.Provider> - </SessionProvider> + <ClientConfigCtx.Provider value={clientConfig}> + <SessionProvider session={session}> + <api.Provider client={trpcClient} queryClient={queryClient}> + <QueryClientProvider client={queryClient}> + {children} + </QueryClientProvider> + </api.Provider> + </SessionProvider> + </ClientConfigCtx.Provider> ); } diff --git a/packages/shared/config.ts b/packages/shared/config.ts index 6dc5d0d1..3126fa68 100644 --- a/packages/shared/config.ts +++ b/packages/shared/config.ts @@ -40,4 +40,9 @@ const serverConfig = { dataDir: process.env.DATA_DIR ?? "", }; +export const clientConfig = { + demoMode: serverConfig.demoMode, +}; +export type ClientConfig = typeof clientConfig; + export default serverConfig; diff --git a/packages/trpc/routers/lists.ts b/packages/trpc/routers/lists.ts index fa97929d..cbce3970 100644 --- a/packages/trpc/routers/lists.ts +++ b/packages/trpc/routers/lists.ts @@ -1,9 +1,11 @@ -import { Context, authedProcedure, router } from "../index"; -import { SqliteError } from "@hoarder/db"; +import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; import { z } from "zod"; -import { TRPCError, experimental_trpcMiddleware } from "@trpc/server"; + +import { SqliteError } from "@hoarder/db"; import { bookmarkLists, bookmarksInLists } from "@hoarder/db/schema"; -import { and, eq } from "drizzle-orm"; + +import { authedProcedure, Context, router } from "../index"; import { zBookmarkListSchema } from "../types/lists"; const ensureListOwnership = experimental_trpcMiddleware<{ @@ -42,7 +44,10 @@ export const listsAppRouter = router({ create: authedProcedure .input( z.object({ - name: z.string().min(1).max(20), + name: z + .string() + .min(1, "List name can't be empty") + .max(20, "List name is at most 20 chars"), icon: z.string(), }), ) |
