diff options
| author | Mohamed Bassem <me@mbassem.com> | 2024-10-27 12:03:14 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2024-10-27 12:03:14 +0000 |
| commit | eb7da996a7c2d617d276f296cac07a6fd5648664 (patch) | |
| tree | 4711de55b6f5fed3ac0cf3539099a9c0f115647e /apps/web/components/dashboard | |
| parent | 801ba36af5900c84af5a88dea37aa7d2f793fed9 (diff) | |
| download | karakeep-eb7da996a7c2d617d276f296cac07a6fd5648664.tar.zst | |
ui: Redesign the settings page and move it to its own layout
Diffstat (limited to 'apps/web/components/dashboard')
13 files changed, 4 insertions, 1103 deletions
diff --git a/apps/web/components/dashboard/header/ProfileOptions.tsx b/apps/web/components/dashboard/header/ProfileOptions.tsx index ea8c7d12..ee6cac01 100644 --- a/apps/web/components/dashboard/header/ProfileOptions.tsx +++ b/apps/web/components/dashboard/header/ProfileOptions.tsx @@ -62,7 +62,7 @@ export default function SidebarProfileOptions() { </div> <Separator className="my-2" /> <DropdownMenuItem asChild> - <Link href="/dashboard/settings"> + <Link href="/settings"> <Settings className="mr-2 size-4" /> User Settings </Link> diff --git a/apps/web/components/dashboard/settings/AISettings.tsx b/apps/web/components/dashboard/settings/AISettings.tsx deleted file mode 100644 index 0a8db147..00000000 --- a/apps/web/components/dashboard/settings/AISettings.tsx +++ /dev/null @@ -1,326 +0,0 @@ -"use client"; - -import { ActionButton } from "@/components/ui/action-button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/components/ui/form"; -import { FullPageSpinner } from "@/components/ui/full-page-spinner"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { toast } from "@/components/ui/use-toast"; -import { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Plus, Save, Trash2 } from "lucide-react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { buildImagePrompt, buildTextPrompt } from "@hoarder/shared/prompts"; -import { - zNewPromptSchema, - ZPrompt, - zUpdatePromptSchema, -} from "@hoarder/shared/types/prompts"; - -export function PromptEditor() { - const apiUtils = api.useUtils(); - - const form = useForm<z.infer<typeof zNewPromptSchema>>({ - resolver: zodResolver(zNewPromptSchema), - defaultValues: { - text: "", - appliesTo: "all", - }, - }); - - const { mutateAsync: createPrompt, isPending: isCreating } = - api.prompts.create.useMutation({ - onSuccess: () => { - toast({ - description: "Prompt has been created!", - }); - apiUtils.prompts.list.invalidate(); - }, - }); - - return ( - <Form {...form}> - <form - className="flex gap-2" - onSubmit={form.handleSubmit(async (value) => { - await createPrompt(value); - form.resetField("text"); - })} - > - <FormField - control={form.control} - name="text" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormControl> - <Input - placeholder="Add a custom prompt" - type="text" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - - <FormField - control={form.control} - name="appliesTo" - render={({ field }) => { - return ( - <FormItem className="flex-0"> - <FormControl> - <Select - onValueChange={field.onChange} - defaultValue={field.value} - > - <SelectTrigger> - <SelectValue placeholder="Applies To" /> - </SelectTrigger> - <SelectContent> - <SelectGroup> - <SelectItem value="all">All</SelectItem> - <SelectItem value="text">Text</SelectItem> - <SelectItem value="images">Images</SelectItem> - </SelectGroup> - </SelectContent> - </Select> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <ActionButton - type="submit" - loading={isCreating} - variant="default" - className="items-center" - > - <Plus className="mr-2 size-4" /> - Add - </ActionButton> - </form> - </Form> - ); -} - -export function PromptRow({ prompt }: { prompt: ZPrompt }) { - const apiUtils = api.useUtils(); - const { mutateAsync: updatePrompt, isPending: isUpdating } = - api.prompts.update.useMutation({ - onSuccess: () => { - toast({ - description: "Prompt has been updated!", - }); - apiUtils.prompts.list.invalidate(); - }, - }); - const { mutate: deletePrompt, isPending: isDeleting } = - api.prompts.delete.useMutation({ - onSuccess: () => { - toast({ - description: "Prompt has been deleted!", - }); - apiUtils.prompts.list.invalidate(); - }, - }); - - const form = useForm<z.infer<typeof zUpdatePromptSchema>>({ - resolver: zodResolver(zUpdatePromptSchema), - defaultValues: { - promptId: prompt.id, - text: prompt.text, - appliesTo: prompt.appliesTo, - }, - }); - - return ( - <Form {...form}> - <form - className="flex gap-2" - onSubmit={form.handleSubmit(async (value) => { - await updatePrompt(value); - })} - > - <FormField - control={form.control} - name="promptId" - render={({ field }) => { - return ( - <FormItem className="hidden"> - <FormControl> - <Input - placeholder="Add a custom prompt" - type="hidden" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <FormField - control={form.control} - name="text" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormControl> - <Input - placeholder="Add a custom prompt" - type="text" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - - <FormField - control={form.control} - name="appliesTo" - render={({ field }) => { - return ( - <FormItem className="flex-0"> - <FormControl> - <Select - onValueChange={field.onChange} - defaultValue={field.value} - > - <SelectTrigger> - <SelectValue placeholder="Applies To" /> - </SelectTrigger> - <SelectContent> - <SelectGroup> - <SelectItem value="all">All</SelectItem> - <SelectItem value="text">Text</SelectItem> - <SelectItem value="images">Images</SelectItem> - </SelectGroup> - </SelectContent> - </Select> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <ActionButton - loading={isUpdating} - variant="secondary" - type="submit" - className="items-center" - > - <Save className="mr-2 size-4" /> - Save - </ActionButton> - <ActionButton - loading={isDeleting} - variant="destructive" - onClick={() => deletePrompt({ promptId: prompt.id })} - className="items-center" - type="button" - > - <Trash2 className="mr-2 size-4" /> - Delete - </ActionButton> - </form> - </Form> - ); -} - -export function TaggingRules() { - const { data: prompts, isLoading } = api.prompts.list.useQuery(); - - return ( - <div className="mt-2 flex flex-col gap-2"> - <div className="w-full text-xl font-medium sm:w-1/3">Tagging Rules</div> - <p className="mb-1 text-xs italic text-muted-foreground"> - Prompts that you add here will be included as rules to the model during - tag generation. You can view the final prompts in the prompt preview - section. - </p> - {isLoading && <FullPageSpinner />} - {prompts && prompts.length == 0 && ( - <p className="rounded-md bg-muted p-2 text-sm text-muted-foreground"> - You don't have any custom prompts yet. - </p> - )} - {prompts && - prompts.map((prompt) => <PromptRow key={prompt.id} prompt={prompt} />)} - <PromptEditor /> - </div> - ); -} - -export function PromptDemo() { - const { data: prompts } = api.prompts.list.useQuery(); - const clientConfig = useClientConfig(); - return ( - <div className="flex flex-col gap-2"> - <div className="mb-4 w-full text-xl font-medium sm:w-1/3"> - Prompt Preview - </div> - <p>Text Prompt</p> - <code className="whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground"> - {buildTextPrompt( - clientConfig.inference.inferredTagLang, - (prompts ?? []) - .filter((p) => p.appliesTo == "text" || p.appliesTo == "all") - .map((p) => p.text), - "\n<CONTENT_HERE>\n", - /* context length */ 1024 /* The value here doesn't matter */, - ).trim()} - </code> - <p>Image Prompt</p> - <code className="whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground"> - {buildImagePrompt( - clientConfig.inference.inferredTagLang, - (prompts ?? []) - .filter((p) => p.appliesTo == "images" || p.appliesTo == "all") - .map((p) => p.text), - ).trim()} - </code> - </div> - ); -} - -export default function AISettings() { - return ( - <> - <div className="rounded-md border bg-background p-4"> - <div className="mb-2 flex flex-col gap-3"> - <div className="w-full text-2xl font-medium sm:w-1/3"> - AI Settings - </div> - <TaggingRules /> - </div> - </div> - <div className="mt-4 rounded-md border bg-background p-4"> - <PromptDemo /> - </div> - </> - ); -} diff --git a/apps/web/components/dashboard/settings/AddApiKey.tsx b/apps/web/components/dashboard/settings/AddApiKey.tsx deleted file mode 100644 index 34fd2df7..00000000 --- a/apps/web/components/dashboard/settings/AddApiKey.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client"; - -import type { SubmitErrorHandler } from "react-hook-form"; -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import { ActionButton } from "@/components/ui/action-button"; -import { Button } from "@/components/ui/button"; -import CopyBtn from "@/components/ui/copy-button"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -function ApiKeySuccess({ apiKey }: { apiKey: string }) { - return ( - <div> - <div className="py-4"> - Note: please copy the key and store it somewhere safe. Once you close - the dialog, you won't be able to access it again. - </div> - <div className="flex space-x-2 pt-2"> - <Input value={apiKey} readOnly /> - <CopyBtn - getStringToCopy={() => { - return apiKey; - }} - /> - </div> - </div> - ); -} - -function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) { - const formSchema = z.object({ - name: z.string(), - }); - const router = useRouter(); - const mutator = api.apiKeys.create.useMutation({ - onSuccess: (resp) => { - onSuccess(resp.key); - router.refresh(); - }, - onError: () => { - toast({ description: "Something went wrong", variant: "destructive" }); - }, - }); - - const form = useForm<z.infer<typeof formSchema>>({ - resolver: zodResolver(formSchema), - }); - - async function onSubmit(value: z.infer<typeof formSchema>) { - mutator.mutate({ name: value.name }); - } - - const onError: SubmitErrorHandler<z.infer<typeof formSchema>> = (errors) => { - toast({ - description: Object.values(errors) - .map((v) => v.message) - .join("\n"), - variant: "destructive", - }); - }; - - return ( - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit, onError)} - className="flex w-full space-x-3 space-y-8 pt-4" - > - <FormField - control={form.control} - name="name" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormLabel>Name</FormLabel> - <FormControl> - <Input type="text" placeholder="Name" {...field} /> - </FormControl> - <FormDescription> - Give your API key a unique name - </FormDescription> - <FormMessage /> - </FormItem> - ); - }} - /> - <ActionButton - className="h-full" - type="submit" - loading={mutator.isPending} - > - Create - </ActionButton> - </form> - </Form> - ); -} - -export default function AddApiKey() { - const [key, setKey] = useState<string | undefined>(undefined); - const [dialogOpen, setDialogOpen] = useState<boolean>(false); - return ( - <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> - <DialogTrigger asChild> - <Button>New API Key</Button> - </DialogTrigger> - <DialogContent> - <DialogHeader> - <DialogTitle> - {key ? "Key was successfully created" : "Create API key"} - </DialogTitle> - <DialogDescription> - {key ? ( - <ApiKeySuccess apiKey={key} /> - ) : ( - <AddApiKeyForm onSuccess={setKey} /> - )} - </DialogDescription> - </DialogHeader> - <DialogFooter className="sm:justify-end"> - <DialogClose asChild> - <Button - type="button" - variant="outline" - onClick={() => setKey(undefined)} - > - Close - </Button> - </DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/apps/web/components/dashboard/settings/ApiKeySettings.tsx b/apps/web/components/dashboard/settings/ApiKeySettings.tsx deleted file mode 100644 index 4d43be7a..00000000 --- a/apps/web/components/dashboard/settings/ApiKeySettings.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { api } from "@/server/api/client"; - -import AddApiKey from "./AddApiKey"; -import DeleteApiKey from "./DeleteApiKey"; - -export default async function ApiKeys() { - const keys = await api.apiKeys.list(); - return ( - <div> - <div className="flex items-center justify-between"> - <div className="mb-2 text-lg font-medium">API Keys</div> - <AddApiKey /> - </div> - <div className="mt-2"> - <Table> - <TableHeader> - <TableRow> - <TableHead>Name</TableHead> - <TableHead>Key</TableHead> - <TableHead>Created At</TableHead> - <TableHead>Action</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {keys.keys.map((k) => ( - <TableRow key={k.id}> - <TableCell>{k.name}</TableCell> - <TableCell>**_{k.keyId}_**</TableCell> - <TableCell>{k.createdAt.toLocaleString()}</TableCell> - <TableCell> - <DeleteApiKey name={k.name} id={k.id} /> - </TableCell> - </TableRow> - ))} - <TableRow></TableRow> - </TableBody> - </Table> - </div> - </div> - ); -} diff --git a/apps/web/components/dashboard/settings/ChangePassword.tsx b/apps/web/components/dashboard/settings/ChangePassword.tsx deleted file mode 100644 index aa27f223..00000000 --- a/apps/web/components/dashboard/settings/ChangePassword.tsx +++ /dev/null @@ -1,133 +0,0 @@ -"use client"; - -import type { z } from "zod"; -import { ActionButton } from "@/components/ui/action-button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; - -import { zChangePasswordSchema } from "@hoarder/shared/types/users"; - -export function ChangePassword() { - const form = useForm<z.infer<typeof zChangePasswordSchema>>({ - resolver: zodResolver(zChangePasswordSchema), - defaultValues: { - currentPassword: "", - newPassword: "", - newPasswordConfirm: "", - }, - }); - - const mutator = api.users.changePassword.useMutation({ - onSuccess: () => { - toast({ description: "Password changed successfully" }); - form.reset(); - }, - onError: (e) => { - if (e.data?.code == "UNAUTHORIZED") { - toast({ - description: "Your current password is incorrect", - variant: "destructive", - }); - } else { - toast({ description: "Something went wrong", variant: "destructive" }); - } - }, - }); - - async function onSubmit(value: z.infer<typeof zChangePasswordSchema>) { - mutator.mutate({ - currentPassword: value.currentPassword, - newPassword: value.newPassword, - }); - } - - return ( - <div className="flex flex-col sm:flex-row"> - <div className="mb-4 w-full text-lg font-medium sm:w-1/3"> - Change Password - </div> - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit)} - className="flex w-full flex-col gap-2" - > - <FormField - control={form.control} - name="currentPassword" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormLabel>Current Password</FormLabel> - <FormControl> - <Input - type="password" - placeholder="Current Password" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <FormField - control={form.control} - name="newPassword" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormLabel>New Password</FormLabel> - <FormControl> - <Input - type="password" - placeholder="New Password" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <FormField - control={form.control} - name="newPasswordConfirm" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormLabel>Confirm New Password</FormLabel> - <FormControl> - <Input - type="Password" - placeholder="Confirm New Password" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <ActionButton - className="mt-4 h-10 w-max px-8" - type="submit" - loading={mutator.isPending} - > - Save - </ActionButton> - </form> - </Form> - </div> - ); -} diff --git a/apps/web/components/dashboard/settings/DeleteApiKey.tsx b/apps/web/components/dashboard/settings/DeleteApiKey.tsx deleted file mode 100644 index e2334c44..00000000 --- a/apps/web/components/dashboard/settings/DeleteApiKey.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { ActionButton } from "@/components/ui/action-button"; -import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; -import { Button } from "@/components/ui/button"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { Trash } from "lucide-react"; - -export default function DeleteApiKey({ - name, - id, -}: { - name: string; - id: string; -}) { - const router = useRouter(); - const mutator = api.apiKeys.revoke.useMutation({ - onSuccess: () => { - toast({ - description: "Key was successfully deleted", - }); - router.refresh(); - }, - }); - - return ( - <ActionConfirmingDialog - title={"Delete API Key"} - description={ - <p> - Are you sure you want to delete the API key "{name}"? Any - service using this API key will lose access. - </p> - } - actionButton={(setDialogOpen) => ( - <ActionButton - type="button" - variant="destructive" - loading={mutator.isPending} - onClick={() => - mutator.mutate({ id }, { onSuccess: () => setDialogOpen(false) }) - } - > - Delete - </ActionButton> - )} - > - <Button variant="outline"> - <Trash size={18} color="red" /> - </Button> - </ActionConfirmingDialog> - ); -} diff --git a/apps/web/components/dashboard/settings/ImportExport.tsx b/apps/web/components/dashboard/settings/ImportExport.tsx deleted file mode 100644 index 1145a42d..00000000 --- a/apps/web/components/dashboard/settings/ImportExport.tsx +++ /dev/null @@ -1,263 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { buttonVariants } from "@/components/ui/button"; -import FilePickerButton from "@/components/ui/file-picker-button"; -import { Progress } from "@/components/ui/progress"; -import { toast } from "@/components/ui/use-toast"; -import { - ParsedBookmark, - parseHoarderBookmarkFile, - parseNetscapeBookmarkFile, - parsePocketBookmarkFile, -} from "@/lib/importBookmarkParser"; -import { cn } from "@/lib/utils"; -import { useMutation } from "@tanstack/react-query"; -import { TRPCClientError } from "@trpc/client"; -import { Download, Upload } from "lucide-react"; - -import { - useCreateBookmarkWithPostHook, - useUpdateBookmark, - useUpdateBookmarkTags, -} from "@hoarder/shared-react/hooks/bookmarks"; -import { - useAddBookmarkToList, - useCreateBookmarkList, -} from "@hoarder/shared-react/hooks/lists"; -import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; - -export function ExportButton() { - return ( - <Link - href="/api/bookmarks/export" - className={cn( - buttonVariants({ variant: "default" }), - "flex items-center gap-2", - )} - > - <Download /> - <p>Export Links and Notes</p> - </Link> - ); -} - -export function ImportExportRow() { - const router = useRouter(); - - const [importProgress, setImportProgress] = useState<{ - done: number; - total: number; - } | null>(null); - - const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook(); - const { mutateAsync: updateBookmark } = useUpdateBookmark(); - const { mutateAsync: createList } = useCreateBookmarkList(); - const { mutateAsync: addToList } = useAddBookmarkToList(); - const { mutateAsync: updateTags } = useUpdateBookmarkTags(); - - const { mutateAsync: parseAndCreateBookmark } = useMutation({ - mutationFn: async (toImport: { - bookmark: ParsedBookmark; - listId: string; - }) => { - const bookmark = toImport.bookmark; - if (bookmark.content === undefined) { - throw new Error("Content is undefined"); - } - const created = await createBookmark( - bookmark.content.type === BookmarkTypes.LINK - ? { - type: BookmarkTypes.LINK, - url: bookmark.content.url, - } - : { - type: BookmarkTypes.TEXT, - text: bookmark.content.text, - }, - ); - - await Promise.all([ - // Update title and createdAt if they're set - bookmark.title.length > 0 || bookmark.addDate - ? updateBookmark({ - bookmarkId: created.id, - title: bookmark.title, - createdAt: bookmark.addDate - ? new Date(bookmark.addDate * 1000) - : undefined, - note: bookmark.notes, - }).catch(() => { - /* empty */ - }) - : undefined, - - // Add to import list - addToList({ - bookmarkId: created.id, - listId: toImport.listId, - }).catch((e) => { - if ( - e instanceof TRPCClientError && - e.message.includes("already in the list") - ) { - /* empty */ - } else { - throw e; - } - }), - - // Update tags - bookmark.tags.length > 0 - ? updateTags({ - bookmarkId: created.id, - attach: bookmark.tags.map((t) => ({ tagName: t })), - detach: [], - }) - : undefined, - ]); - return created; - }, - }); - - const { mutateAsync: runUploadBookmarkFile } = useMutation({ - mutationFn: async ({ - file, - source, - }: { - file: File; - source: "html" | "pocket" | "hoarder"; - }) => { - if (source === "html") { - return await parseNetscapeBookmarkFile(file); - } else if (source === "pocket") { - return await parsePocketBookmarkFile(file); - } else if (source === "hoarder") { - return await parseHoarderBookmarkFile(file); - } else { - throw new Error("Unknown source"); - } - }, - onSuccess: async (resp) => { - const importList = await createList({ - name: `Imported Bookmarks`, - icon: "⬆️", - }); - setImportProgress({ done: 0, total: resp.length }); - - const successes = []; - const failed = []; - const alreadyExisted = []; - // Do the imports one by one - for (const parsedBookmark of resp) { - try { - const result = await parseAndCreateBookmark({ - bookmark: parsedBookmark, - listId: importList.id, - }); - if (result.alreadyExists) { - alreadyExisted.push(parsedBookmark); - } else { - successes.push(parsedBookmark); - } - } catch (e) { - failed.push(parsedBookmark); - } - setImportProgress((prev) => ({ - done: (prev?.done ?? 0) + 1, - total: resp.length, - })); - } - - if (successes.length > 0 || alreadyExisted.length > 0) { - toast({ - description: `Imported ${successes.length} bookmarks and skipped ${alreadyExisted.length} bookmarks that already existed`, - variant: "default", - }); - } - - if (failed.length > 0) { - toast({ - description: `Failed to import ${failed.length} bookmarks`, - variant: "destructive", - }); - } - - router.push(`/dashboard/lists/${importList.id}`); - }, - onError: (error) => { - toast({ - description: error.message, - variant: "destructive", - }); - }, - }); - - return ( - <div className="flex flex-col gap-3"> - <div className="flex flex-row flex-wrap gap-2"> - <FilePickerButton - loading={false} - accept=".html" - multiple={false} - className="flex items-center gap-2" - onFileSelect={(file) => - runUploadBookmarkFile({ file, source: "html" }) - } - > - <Upload /> - <p>Import Bookmarks from HTML file</p> - </FilePickerButton> - - <FilePickerButton - loading={false} - accept=".html" - multiple={false} - className="flex items-center gap-2" - onFileSelect={(file) => - runUploadBookmarkFile({ file, source: "pocket" }) - } - > - <Upload /> - <p>Import Bookmarks from Pocket export</p> - </FilePickerButton> - <FilePickerButton - loading={false} - accept=".json" - multiple={false} - className="flex items-center gap-2" - onFileSelect={(file) => - runUploadBookmarkFile({ file, source: "hoarder" }) - } - > - <Upload /> - <p>Import Bookmarks from Hoarder export</p> - </FilePickerButton> - <ExportButton /> - </div> - {importProgress && ( - <div className="flex flex-col gap-2"> - <p className="shrink-0 text-sm"> - Processed {importProgress.done} of {importProgress.total} bookmarks - </p> - <div className="w-full"> - <Progress - value={(importProgress.done * 100) / importProgress.total} - /> - </div> - </div> - )} - </div> - ); -} - -export default function ImportExport() { - return ( - <div className="flex w-full flex-col gap-2"> - <p className="mb-4 text-lg font-medium">Import / Export Bookmarks</p> - <ImportExportRow /> - </div> - ); -} diff --git a/apps/web/components/dashboard/settings/UserDetails.tsx b/apps/web/components/dashboard/settings/UserDetails.tsx deleted file mode 100644 index 471a6e09..00000000 --- a/apps/web/components/dashboard/settings/UserDetails.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Input } from "@/components/ui/input"; -import { api } from "@/server/api/client"; - -export default async function UserDetails() { - const whoami = await api.users.whoami(); - - const details = [ - { - label: "Name", - value: whoami.name ?? undefined, - }, - { - label: "Email", - value: whoami.email ?? undefined, - }, - ]; - - return ( - <div className="mb-8 flex w-full flex-col sm:flex-row"> - <div className="mb-4 w-full text-lg font-medium sm:w-1/3"> - Basic Details - </div> - <div className="w-full"> - {details.map(({ label, value }) => ( - <div className="mb-2" key={label}> - <div className="mb-2 text-sm font-medium">{label}</div> - <Input value={value} disabled /> - </div> - ))} - </div> - </div> - ); -} diff --git a/apps/web/components/dashboard/sidebar/AllLists.tsx b/apps/web/components/dashboard/sidebar/AllLists.tsx index b6cadea9..c48ddb0f 100644 --- a/apps/web/components/dashboard/sidebar/AllLists.tsx +++ b/apps/web/components/dashboard/sidebar/AllLists.tsx @@ -3,6 +3,7 @@ import { useCallback } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import SidebarItem from "@/components/shared/sidebar/SidebarItem"; import { Button } from "@/components/ui/button"; import { CollapsibleTriggerTriangle } from "@/components/ui/collapsible"; import { MoreHorizontal, Plus } from "lucide-react"; @@ -13,7 +14,6 @@ import { ZBookmarkListTreeNode } from "@hoarder/shared/utils/listUtils"; import { CollapsibleBookmarkLists } from "../lists/CollapsibleBookmarkLists"; import { EditListModal } from "../lists/EditListModal"; import { ListOptions } from "../lists/ListOptions"; -import SidebarItem from "./SidebarItem"; export default function AllLists({ initialData, diff --git a/apps/web/components/dashboard/sidebar/ModileSidebar.tsx b/apps/web/components/dashboard/sidebar/ModileSidebar.tsx index 7ccf6b8d..bfa91afa 100644 --- a/apps/web/components/dashboard/sidebar/ModileSidebar.tsx +++ b/apps/web/components/dashboard/sidebar/ModileSidebar.tsx @@ -1,8 +1,7 @@ +import MobileSidebarItem from "@/components/shared/sidebar/ModileSidebarItem"; import HoarderLogoIcon from "@/public/icons/logo-icon.svg"; import { ClipboardList, Search, Tag } from "lucide-react"; -import MobileSidebarItem from "./ModileSidebarItem"; - export default async function MobileSidebar() { return ( <aside className="w-full"> diff --git a/apps/web/components/dashboard/sidebar/ModileSidebarItem.tsx b/apps/web/components/dashboard/sidebar/ModileSidebarItem.tsx deleted file mode 100644 index 4d3436ea..00000000 --- a/apps/web/components/dashboard/sidebar/ModileSidebarItem.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { cn } from "@/lib/utils"; - -export default function MobileSidebarItem({ - logo, - path, -}: { - logo: React.ReactNode; - path: string; -}) { - const currentPath = usePathname(); - return ( - <li - className={cn( - "flex w-full rounded-lg hover:bg-background", - path == currentPath ? "bg-background" : "", - )} - > - <Link href={path} className="m-auto px-3 py-2"> - {logo} - </Link> - </li> - ); -} diff --git a/apps/web/components/dashboard/sidebar/Sidebar.tsx b/apps/web/components/dashboard/sidebar/Sidebar.tsx index 14d019ff..8021ad36 100644 --- a/apps/web/components/dashboard/sidebar/Sidebar.tsx +++ b/apps/web/components/dashboard/sidebar/Sidebar.tsx @@ -1,4 +1,5 @@ import { redirect } from "next/navigation"; +import SidebarItem from "@/components/shared/sidebar/SidebarItem"; import { Separator } from "@/components/ui/separator"; import { api } from "@/server/api/client"; import { getServerAuthSession } from "@/server/auth"; @@ -7,7 +8,6 @@ import { Archive, Home, Search, Tag } from "lucide-react"; import serverConfig from "@hoarder/shared/config"; import AllLists from "./AllLists"; -import SidebarItem from "./SidebarItem"; export default async function Sidebar() { const session = await getServerAuthSession(); diff --git a/apps/web/components/dashboard/sidebar/SidebarItem.tsx b/apps/web/components/dashboard/sidebar/SidebarItem.tsx deleted file mode 100644 index 83ce776e..00000000 --- a/apps/web/components/dashboard/sidebar/SidebarItem.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; - -import React from "react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { cn } from "@/lib/utils"; - -export default function SidebarItem({ - name, - logo, - path, - className, - linkClassName, - style, - collapseButton, - right = null, -}: { - name: string; - logo: React.ReactNode; - path: string; - style?: React.CSSProperties; - className?: string; - linkClassName?: string; - right?: React.ReactNode; - collapseButton?: React.ReactNode; -}) { - const currentPath = usePathname(); - return ( - <li - className={cn( - "relative rounded-lg hover:bg-accent", - path == currentPath ? "bg-accent/50" : "", - className, - )} - style={style} - > - {collapseButton} - <Link - href={path} - className={cn( - "flex w-full items-center rounded-[inherit] px-3 py-2", - linkClassName, - )} - > - <div className="flex w-full justify-between"> - <div className="flex items-center gap-x-2"> - {logo} - <span>{name}</span> - </div> - {right} - </div> - </Link> - </li> - ); -} |
