aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components/dashboard')
-rw-r--r--apps/web/components/dashboard/header/ProfileOptions.tsx2
-rw-r--r--apps/web/components/dashboard/settings/AISettings.tsx326
-rw-r--r--apps/web/components/dashboard/settings/AddApiKey.tsx157
-rw-r--r--apps/web/components/dashboard/settings/ApiKeySettings.tsx49
-rw-r--r--apps/web/components/dashboard/settings/ChangePassword.tsx133
-rw-r--r--apps/web/components/dashboard/settings/DeleteApiKey.tsx55
-rw-r--r--apps/web/components/dashboard/settings/ImportExport.tsx263
-rw-r--r--apps/web/components/dashboard/settings/UserDetails.tsx33
-rw-r--r--apps/web/components/dashboard/sidebar/AllLists.tsx2
-rw-r--r--apps/web/components/dashboard/sidebar/ModileSidebar.tsx3
-rw-r--r--apps/web/components/dashboard/sidebar/ModileSidebarItem.tsx27
-rw-r--r--apps/web/components/dashboard/sidebar/Sidebar.tsx2
-rw-r--r--apps/web/components/dashboard/sidebar/SidebarItem.tsx55
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&apos;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&apos;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 &quot;{name}&quot;? 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>
- );
-}