aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/app/dashboard
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-10 17:59:58 +0000
committerMohamedBassem <me@mbassem.com>2024-03-10 17:59:58 +0000
commitd6dd76021226802adf5295b3243d6f2ae4fa5cc2 (patch)
tree7a25423d46db9e0e224b5f58b73cec5768953b44 /packages/web/app/dashboard
parent8ab868d3f94cc6609d278dc952432f1a244c3f84 (diff)
downloadkarakeep-d6dd76021226802adf5295b3243d6f2ae4fa5cc2.tar.zst
refactor: Move all components to the top level directory
Diffstat (limited to 'packages/web/app/dashboard')
-rw-r--r--packages/web/app/dashboard/archive/page.tsx2
-rw-r--r--packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx100
-rw-r--r--packages/web/app/dashboard/bookmarks/components/AddToListModal.tsx168
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarkCardSkeleton.tsx30
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx185
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx109
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarkedTextViewer.tsx20
-rw-r--r--packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx32
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx64
-rw-r--r--packages/web/app/dashboard/bookmarks/components/LinkCard.tsx114
-rw-r--r--packages/web/app/dashboard/bookmarks/components/TagList.tsx39
-rw-r--r--packages/web/app/dashboard/bookmarks/components/TagModal.tsx207
-rw-r--r--packages/web/app/dashboard/bookmarks/components/TextCard.tsx94
-rw-r--r--packages/web/app/dashboard/bookmarks/layout.tsx2
-rw-r--r--packages/web/app/dashboard/bookmarks/page.tsx2
-rw-r--r--packages/web/app/dashboard/components/AllLists.tsx60
-rw-r--r--packages/web/app/dashboard/components/ModileSidebar.tsx24
-rw-r--r--packages/web/app/dashboard/components/ModileSidebarItem.tsx27
-rw-r--r--packages/web/app/dashboard/components/NewListModal.tsx170
-rw-r--r--packages/web/app/dashboard/components/Sidebar.tsx66
-rw-r--r--packages/web/app/dashboard/components/SidebarItem.tsx33
-rw-r--r--packages/web/app/dashboard/components/SidebarProfileOptions.tsx35
-rw-r--r--packages/web/app/dashboard/favourites/page.tsx2
-rw-r--r--packages/web/app/dashboard/layout.tsx4
-rw-r--r--packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx77
-rw-r--r--packages/web/app/dashboard/lists/[listId]/components/ListView.tsx25
-rw-r--r--packages/web/app/dashboard/lists/[listId]/page.tsx4
-rw-r--r--packages/web/app/dashboard/lists/components/AllListsView.tsx66
-rw-r--r--packages/web/app/dashboard/lists/page.tsx2
-rw-r--r--packages/web/app/dashboard/preview/[bookmarkId]/components/BookmarkPreview.tsx101
-rw-r--r--packages/web/app/dashboard/preview/[bookmarkId]/page.tsx2
-rw-r--r--packages/web/app/dashboard/search/page.tsx2
-rw-r--r--packages/web/app/dashboard/settings/components/AddApiKey.tsx167
-rw-r--r--packages/web/app/dashboard/settings/components/ApiKeySettings.tsx49
-rw-r--r--packages/web/app/dashboard/settings/components/DeleteApiKey.tsx74
-rw-r--r--packages/web/app/dashboard/settings/page.tsx2
-rw-r--r--packages/web/app/dashboard/tags/[tagName]/page.tsx2
37 files changed, 13 insertions, 2149 deletions
diff --git a/packages/web/app/dashboard/archive/page.tsx b/packages/web/app/dashboard/archive/page.tsx
index 81eea57c..69559185 100644
--- a/packages/web/app/dashboard/archive/page.tsx
+++ b/packages/web/app/dashboard/archive/page.tsx
@@ -1,4 +1,4 @@
-import Bookmarks from "../bookmarks/components/Bookmarks";
+import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
export default async function ArchivedBookmarkPage() {
return (
diff --git a/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx b/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx
deleted file mode 100644
index d12fc663..00000000
--- a/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-"use client";
-
-import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Pencil, Plus } from "lucide-react";
-import { useForm, SubmitErrorHandler } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import { BookmarkedTextEditor } from "./BookmarkedTextEditor";
-import { useState } from "react";
-
-function AddText() {
- const [isEditorOpen, setEditorOpen] = useState(false);
-
- return (
- <div className="flex">
- <BookmarkedTextEditor open={isEditorOpen} setOpen={setEditorOpen} />
- <Button className="m-auto" onClick={() => setEditorOpen(true)}>
- <Pencil />
- </Button>
- </div>
- );
-}
-
-function AddLink() {
- const formSchema = z.object({
- url: z.string().url({ message: "The link must be a valid URL" }),
- });
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- url: "",
- },
- });
-
- const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate;
- const createBookmarkMutator = api.bookmarks.createBookmark.useMutation({
- onSuccess: () => {
- invalidateBookmarksCache();
- form.reset();
- },
- onError: () => {
- toast({ description: "Something went wrong", variant: "destructive" });
- },
- });
-
- 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
- className="flex-grow"
- onSubmit={form.handleSubmit(
- (value) =>
- createBookmarkMutator.mutate({ url: value.url, type: "link" }),
- onError,
- )}
- >
- <div className="flex w-full items-center space-x-2 py-4">
- <FormField
- control={form.control}
- name="url"
- render={({ field }) => {
- return (
- <FormItem className="flex-1">
- <FormControl>
- <Input type="text" placeholder="Link" {...field} />
- </FormControl>
- </FormItem>
- );
- }}
- />
- <ActionButton type="submit" loading={createBookmarkMutator.isPending}>
- <Plus />
- </ActionButton>
- </div>
- </form>
- </Form>
- );
-}
-
-export default function AddBookmark() {
- return (
- <div className="container flex gap-2">
- <AddLink />
- <AddText />
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/AddToListModal.tsx b/packages/web/app/dashboard/bookmarks/components/AddToListModal.tsx
deleted file mode 100644
index c9fd5da0..00000000
--- a/packages/web/app/dashboard/bookmarks/components/AddToListModal.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormMessage,
-} from "@/components/ui/form";
-
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { useState } from "react";
-
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import LoadingSpinner from "@/components/ui/spinner";
-import { z } from "zod";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-
-export default function AddToListModal({
- bookmarkId,
- open,
- setOpen,
-}: {
- bookmarkId: string;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- const formSchema = z.object({
- listId: z.string({
- required_error: "Please select a list",
- }),
- });
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema),
- });
-
- const { data: lists, isPending: isFetchingListsPending } =
- api.lists.list.useQuery();
-
- const listInvalidationFunction = api.useUtils().lists.get.invalidate;
- const bookmarksInvalidationFunction =
- api.useUtils().bookmarks.getBookmarks.invalidate;
-
- const { mutate: addToList, isPending: isAddingToListPending } =
- api.lists.addToList.useMutation({
- onSuccess: (_resp, req) => {
- toast({
- description: "List has been updated!",
- });
- listInvalidationFunction({ listId: req.listId });
- bookmarksInvalidationFunction();
- },
- onError: (e) => {
- if (e.data?.code == "BAD_REQUEST") {
- toast({
- variant: "destructive",
- description: e.message,
- });
- } else {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- });
- }
- },
- });
-
- const isPending = isFetchingListsPending || isAddingToListPending;
-
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogContent>
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit((value) => {
- addToList({
- bookmarkId: bookmarkId,
- listId: value.listId,
- });
- })}
- >
- <DialogHeader>
- <DialogTitle>Add to List</DialogTitle>
- </DialogHeader>
-
- <div className="py-4">
- {lists ? (
- <FormField
- control={form.control}
- name="listId"
- render={({ field }) => {
- return (
- <FormItem>
- <FormControl>
- <Select onValueChange={field.onChange}>
- <SelectTrigger className="w-full">
- <SelectValue placeholder="Select a list" />
- </SelectTrigger>
- <SelectContent>
- <SelectGroup>
- {lists &&
- lists.lists.map((l) => (
- <SelectItem key={l.id} value={l.id}>
- {l.icon} {l.name}
- </SelectItem>
- ))}
- </SelectGroup>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- ) : (
- <LoadingSpinner />
- )}
- </div>
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={isAddingToListPending}
- disabled={isPending}
- >
- Add
- </ActionButton>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
-
-export function useAddToListModal(bookmarkId: string) {
- const [open, setOpen] = useState(false);
-
- return {
- open,
- setOpen,
- content: (
- <AddToListModal bookmarkId={bookmarkId} open={open} setOpen={setOpen} />
- ),
- };
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkCardSkeleton.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkCardSkeleton.tsx
deleted file mode 100644
index 1f5fa433..00000000
--- a/packages/web/app/dashboard/bookmarks/components/BookmarkCardSkeleton.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import {
- ImageCard,
- ImageCardBody,
- ImageCardContent,
- ImageCardFooter,
- ImageCardTitle,
- ImageCardBanner,
-} from "@/components/ui/imageCard";
-import { Skeleton } from "@/components/ui/skeleton";
-
-export default function BookmarkCardSkeleton() {
- return (
- <ImageCard
- className={
- "border-grey-100 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all"
- }
- >
- <ImageCardBanner src="/blur.avif" />
- <ImageCardContent>
- <ImageCardTitle></ImageCardTitle>
- <ImageCardBody className="space-y-2">
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- </ImageCardBody>
- <ImageCardFooter></ImageCardFooter>
- </ImageCardContent>
- </ImageCard>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx
deleted file mode 100644
index 4f08ebee..00000000
--- a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-"use client";
-
-import { useToast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ZBookmark, ZBookmarkedLink } from "@hoarder/trpc/types/bookmarks";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import {
- Archive,
- Link,
- List,
- MoreHorizontal,
- Pencil,
- RotateCw,
- Star,
- Tags,
- Trash2,
-} from "lucide-react";
-import { useTagModel } from "./TagModal";
-import { useState } from "react";
-import { BookmarkedTextEditor } from "./BookmarkedTextEditor";
-import { useAddToListModal } from "./AddToListModal";
-
-export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
- const { toast } = useToast();
- const linkId = bookmark.id;
-
- const { setOpen: setTagModalIsOpen, content: tagModal } =
- useTagModel(bookmark);
- const { setOpen: setAddToListModalOpen, content: addToListModal } =
- useAddToListModal(bookmark.id);
-
- const [isTextEditorOpen, setTextEditorOpen] = useState(false);
-
- const invalidateAllBookmarksCache =
- api.useUtils().bookmarks.getBookmarks.invalidate;
-
- const invalidateBookmarkCache =
- api.useUtils().bookmarks.getBookmark.invalidate;
-
- const onError = () => {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- description: "There was a problem with your request.",
- });
- };
- const deleteBookmarkMutator = api.bookmarks.deleteBookmark.useMutation({
- onSuccess: () => {
- toast({
- description: "The bookmark has been deleted!",
- });
- },
- onError,
- onSettled: () => {
- invalidateAllBookmarksCache();
- },
- });
-
- const updateBookmarkMutator = api.bookmarks.updateBookmark.useMutation({
- onSuccess: () => {
- toast({
- description: "The bookmark has been updated!",
- });
- },
- onError,
- onSettled: () => {
- invalidateBookmarkCache({ bookmarkId: bookmark.id });
- invalidateAllBookmarksCache();
- },
- });
-
- const crawlBookmarkMutator = api.bookmarks.recrawlBookmark.useMutation({
- onSuccess: () => {
- toast({
- description: "Re-fetch has been enqueued!",
- });
- },
- onError,
- onSettled: () => {
- invalidateBookmarkCache({ bookmarkId: bookmark.id });
- },
- });
-
- return (
- <>
- {tagModal}
- {addToListModal}
- <BookmarkedTextEditor
- bookmark={bookmark}
- open={isTextEditorOpen}
- setOpen={setTextEditorOpen}
- />
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="ghost"
- className="px-1 focus-visible:ring-0 focus-visible:ring-offset-0"
- >
- <MoreHorizontal />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-fit">
- {bookmark.content.type === "text" && (
- <DropdownMenuItem onClick={() => setTextEditorOpen(true)}>
- <Pencil className="mr-2 size-4" />
- <span>Edit</span>
- </DropdownMenuItem>
- )}
- <DropdownMenuItem
- onClick={() =>
- updateBookmarkMutator.mutate({
- bookmarkId: linkId,
- favourited: !bookmark.favourited,
- })
- }
- >
- <Star className="mr-2 size-4" />
- <span>{bookmark.favourited ? "Un-favourite" : "Favourite"}</span>
- </DropdownMenuItem>
- <DropdownMenuItem
- onClick={() =>
- updateBookmarkMutator.mutate({
- bookmarkId: linkId,
- archived: !bookmark.archived,
- })
- }
- >
- <Archive className="mr-2 size-4" />
- <span>{bookmark.archived ? "Un-archive" : "Archive"}</span>
- </DropdownMenuItem>
- {bookmark.content.type === "link" && (
- <DropdownMenuItem
- onClick={() => {
- navigator.clipboard.writeText(
- (bookmark.content as ZBookmarkedLink).url,
- );
- toast({
- description: "Link was added to your clipboard!",
- });
- }}
- >
- <Link className="mr-2 size-4" />
- <span>Copy Link</span>
- </DropdownMenuItem>
- )}
- <DropdownMenuItem onClick={() => setTagModalIsOpen(true)}>
- <Tags className="mr-2 size-4" />
- <span>Edit Tags</span>
- </DropdownMenuItem>
-
- <DropdownMenuItem onClick={() => setAddToListModalOpen(true)}>
- <List className="mr-2 size-4" />
- <span>Add to List</span>
- </DropdownMenuItem>
-
- {bookmark.content.type === "link" && (
- <DropdownMenuItem
- onClick={() =>
- crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id })
- }
- >
- <RotateCw className="mr-2 size-4" />
- <span>Refresh</span>
- </DropdownMenuItem>
- )}
- <DropdownMenuItem
- className="text-destructive"
- onClick={() =>
- deleteBookmarkMutator.mutate({ bookmarkId: bookmark.id })
- }
- >
- <Trash2 className="mr-2 size-4" />
- <span>Delete</span>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx
deleted file mode 100644
index a5b58f1a..00000000
--- a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import { Textarea } from "@/components/ui/textarea";
-import { api } from "@/lib/trpc";
-import { useState } from "react";
-import { toast } from "@/components/ui/use-toast";
-
-export function BookmarkedTextEditor({
- bookmark,
- open,
- setOpen,
-}: {
- bookmark?: ZBookmark;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- const isNewBookmark = bookmark === undefined;
- const [noteText, setNoteText] = useState(
- bookmark && bookmark.content.type == "text" ? bookmark.content.text : "",
- );
-
- const invalidateAllBookmarksCache =
- api.useUtils().bookmarks.getBookmarks.invalidate;
- const invalidateOneBookmarksCache =
- api.useUtils().bookmarks.getBookmark.invalidate;
-
- const { mutate: createBookmarkMutator, isPending: isCreationPending } =
- api.bookmarks.createBookmark.useMutation({
- onSuccess: () => {
- invalidateAllBookmarksCache();
- toast({
- description: "Note created!",
- });
- setOpen(false);
- setNoteText("");
- },
- onError: () => {
- toast({ description: "Something went wrong", variant: "destructive" });
- },
- });
- const { mutate: updateBookmarkMutator, isPending: isUpdatePending } =
- api.bookmarks.updateBookmarkText.useMutation({
- onSuccess: () => {
- invalidateOneBookmarksCache({
- bookmarkId: bookmark!.id,
- });
- toast({
- description: "Note updated!",
- });
- setOpen(false);
- },
- onError: () => {
- toast({ description: "Something went wrong", variant: "destructive" });
- },
- });
- const isPending = isCreationPending || isUpdatePending;
-
- const onSave = () => {
- if (isNewBookmark) {
- createBookmarkMutator({
- type: "text",
- text: noteText,
- });
- } else {
- updateBookmarkMutator({
- bookmarkId: bookmark.id,
- text: noteText,
- });
- }
- };
-
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>{isNewBookmark ? "New Note" : "Edit Note"}</DialogTitle>
- <DialogDescription>
- Write your note with markdown support
- </DialogDescription>
- </DialogHeader>
- <Textarea
- value={noteText}
- onChange={(e) => setNoteText(e.target.value)}
- className="h-52 grow"
- />
- <DialogFooter className="flex-shrink gap-1 sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton type="button" loading={isPending} onClick={onSave}>
- Save
- </ActionButton>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextViewer.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextViewer.tsx
deleted file mode 100644
index 8a620341..00000000
--- a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextViewer.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Dialog, DialogContent } from "@/components/ui/dialog";
-import Markdown from "react-markdown";
-
-export function BookmarkedTextViewer({
- content,
- open,
- setOpen,
-}: {
- content: string;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogContent className="max-h-[75%] overflow-auto">
- <Markdown className="prose">{content}</Markdown>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx b/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx
deleted file mode 100644
index 1ad3670c..00000000
--- a/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { redirect } from "next/navigation";
-import BookmarksGrid from "./BookmarksGrid";
-import { ZGetBookmarksRequest } from "@hoarder/trpc/types/bookmarks";
-import { api } from "@/server/api/client";
-import { getServerAuthSession } from "@/server/auth";
-
-export default async function Bookmarks({
- favourited,
- archived,
- title,
- showDivider,
-}: ZGetBookmarksRequest & { title: string; showDivider?: boolean }) {
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/");
- }
-
- const query = {
- favourited,
- archived,
- };
-
- const bookmarks = await api.bookmarks.getBookmarks(query);
-
- return (
- <div className="container flex flex-col gap-3">
- <div className="text-2xl">{title}</div>
- {showDivider && <hr />}
- <BookmarksGrid query={query} bookmarks={bookmarks.bookmarks} />
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
deleted file mode 100644
index 4d5b6b0a..00000000
--- a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-"use client";
-
-import LinkCard from "./LinkCard";
-import { ZBookmark, ZGetBookmarksRequest } from "@hoarder/trpc/types/bookmarks";
-import { api } from "@/lib/trpc";
-import TextCard from "./TextCard";
-import { Slot } from "@radix-ui/react-slot";
-import Masonry from "react-masonry-css";
-import resolveConfig from "tailwindcss/resolveConfig";
-import tailwindConfig from "@/tailwind.config";
-import { useMemo } from "react";
-
-function getBreakpointConfig() {
- const fullConfig = resolveConfig(tailwindConfig);
-
- const breakpointColumnsObj: { [key: number]: number; default: number } = {
- default: 3,
- };
- breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = 2;
- breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = 1;
- breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = 1;
- return breakpointColumnsObj;
-}
-
-function renderBookmark(bookmark: ZBookmark) {
- let comp;
- switch (bookmark.content.type) {
- case "link":
- comp = <LinkCard bookmark={bookmark} />;
- break;
- case "text":
- comp = <TextCard bookmark={bookmark} />;
- break;
- }
- return (
- <Slot
- key={bookmark.id}
- className="border-grey-100 mb-4 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all"
- >
- {comp}
- </Slot>
- );
-}
-
-export default function BookmarksGrid({
- query,
- bookmarks: initialBookmarks,
-}: {
- query: ZGetBookmarksRequest;
- bookmarks: ZBookmark[];
-}) {
- const { data } = api.bookmarks.getBookmarks.useQuery(query, {
- initialData: { bookmarks: initialBookmarks },
- });
- const breakpointConfig = useMemo(() => getBreakpointConfig(), []);
- if (data.bookmarks.length == 0) {
- return <p>No bookmarks</p>;
- }
- return (
- <Masonry className="flex gap-4" breakpointCols={breakpointConfig}>
- {data.bookmarks.map((b) => renderBookmark(b))}
- </Masonry>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
deleted file mode 100644
index 50f30e47..00000000
--- a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-"use client";
-
-import {
- ImageCard,
- ImageCardBanner,
- ImageCardBody,
- ImageCardContent,
- ImageCardFooter,
- ImageCardTitle,
-} from "@/components/ui/imageCard";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import Link from "next/link";
-import BookmarkOptions from "./BookmarkOptions";
-import { api } from "@/lib/trpc";
-import { Maximize2, Star } from "lucide-react";
-import TagList from "./TagList";
-import {
- isBookmarkStillCrawling,
- isBookmarkStillLoading,
- isBookmarkStillTagging,
-} from "@/lib/bookmarkUtils";
-
-export default function LinkCard({
- bookmark: initialData,
- className,
-}: {
- bookmark: ZBookmark;
- className?: string;
-}) {
- const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
- {
- bookmarkId: initialData.id,
- },
- {
- initialData,
- refetchInterval: (query) => {
- const data = query.state.data;
- if (!data) {
- return false;
- }
- // If the link is not crawled or not tagged
- if (isBookmarkStillLoading(data)) {
- return 1000;
- }
- return false;
- },
- },
- );
- const link = bookmark.content;
- if (link.type != "link") {
- throw new Error("Unexpected bookmark type");
- }
- const parsedUrl = new URL(link.url);
-
- // A dummy white pixel for when there's no image.
- // TODO: Better handling for cards with no images
- const image =
- link.imageUrl ??
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII=";
-
- return (
- <ImageCard className={className}>
- <Link href={link.url}>
- <ImageCardBanner
- src={isBookmarkStillCrawling(bookmark) ? "/blur.avif" : image}
- />
- </Link>
- <ImageCardContent>
- <ImageCardTitle>
- <Link className="line-clamp-2" href={link.url} target="_blank">
- {link?.title ?? parsedUrl.host}
- </Link>
- </ImageCardTitle>
- {/* There's a hack here. Every tag has the full hight of the container itself. That why, when we enable flex-wrap,
- the overflowed don't show up. */}
- <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden">
- <TagList
- bookmark={bookmark}
- loading={isBookmarkStillTagging(bookmark)}
- />
- </ImageCardBody>
- <ImageCardFooter>
- <div className="mt-1 flex justify-between text-gray-500">
- <div className="my-auto">
- <Link
- className="line-clamp-1 hover:text-black"
- href={link.url}
- target="_blank"
- >
- {parsedUrl.host}
- </Link>
- </div>
- <div className="flex">
- {bookmark.favourited && (
- <Star
- className="m-1 size-8 rounded p-1"
- color="#ebb434"
- fill="#ebb434"
- />
- )}
- <Link
- className="my-auto block px-2"
- href={`/dashboard/preview/${bookmark.id}`}
- >
- <Maximize2 size="20" />
- </Link>
- <BookmarkOptions bookmark={bookmark} />
- </div>
- </div>
- </ImageCardFooter>
- </ImageCardContent>
- </ImageCard>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/TagList.tsx b/packages/web/app/dashboard/bookmarks/components/TagList.tsx
deleted file mode 100644
index 6c9d2d22..00000000
--- a/packages/web/app/dashboard/bookmarks/components/TagList.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { badgeVariants } from "@/components/ui/badge";
-import Link from "next/link";
-import { Skeleton } from "@/components/ui/skeleton";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import { cn } from "@/lib/utils";
-
-export default function TagList({
- bookmark,
- loading,
-}: {
- bookmark: ZBookmark;
- loading?: boolean;
-}) {
- if (loading) {
- return (
- <div className="flex w-full flex-col justify-end space-y-2 p-2">
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- </div>
- );
- }
- return (
- <>
- {bookmark.tags.map((t) => (
- <div key={t.id} className="flex h-full flex-col justify-end">
- <Link
- className={cn(
- badgeVariants({ variant: "outline" }),
- "hover:bg-foreground hover:text-secondary text-nowrap",
- )}
- href={`/dashboard/tags/${t.name}`}
- >
- {t.name}
- </Link>
- </div>
- ))}
- </>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx
deleted file mode 100644
index 8c09d00e..00000000
--- a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import { ZAttachedByEnum } from "@hoarder/trpc/types/tags";
-import { cn } from "@/lib/utils";
-import { Sparkles, X } from "lucide-react";
-import { useState, KeyboardEvent, useEffect } from "react";
-
-type EditableTag = { attachedBy: ZAttachedByEnum; id?: string; name: string };
-
-function TagAddInput({ addTag }: { addTag: (tag: string) => void }) {
- const onKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
- addTag(e.currentTarget.value);
- e.currentTarget.value = "";
- }
- };
- return (
- <Input
- onKeyUp={onKeyUp}
- className="h-8 w-full border-none focus-visible:ring-0 focus-visible:ring-offset-0"
- />
- );
-}
-
-function TagPill({
- tag,
- deleteCB,
-}: {
- tag: { attachedBy: ZAttachedByEnum; id?: string; name: string };
- deleteCB: () => void;
-}) {
- const isAttachedByAI = tag.attachedBy == "ai";
- return (
- <div
- className={cn(
- "flex min-h-8 space-x-1 rounded px-2",
- isAttachedByAI
- ? "bg-gradient-to-tr from-purple-500 to-purple-400 text-white"
- : "bg-gray-200",
- )}
- >
- {isAttachedByAI && <Sparkles className="m-auto size-4" />}
- <p className="m-auto">{tag.name}</p>
- <button className="m-auto size-4" onClick={deleteCB}>
- <X className="size-4" />
- </button>
- </div>
- );
-}
-
-function TagEditor({
- tags,
- setTags,
-}: {
- tags: Map<string, EditableTag>;
- setTags: (
- cb: (m: Map<string, EditableTag>) => Map<string, EditableTag>,
- ) => void;
-}) {
- return (
- <div className="mt-4 flex flex-wrap gap-2 rounded border p-2">
- {[...tags.values()].map((t) => (
- <TagPill
- key={t.name}
- tag={t}
- deleteCB={() =>
- setTags((m) => {
- const newMap = new Map(m);
- newMap.delete(t.name);
- return newMap;
- })
- }
- />
- ))}
- <div className="flex-1">
- <TagAddInput
- addTag={(val) => {
- setTags((m) => {
- if (m.has(val)) {
- // Tag already exists
- // Do nothing
- return m;
- }
- const newMap = new Map(m);
- newMap.set(val, { attachedBy: "human", name: val });
- return newMap;
- });
- }}
- />
- </div>
- </div>
- );
-}
-
-export default function TagModal({
- bookmark,
- open,
- setOpen,
-}: {
- bookmark: ZBookmark;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- const [tags, setTags] = useState<Map<string, EditableTag>>(new Map());
- useEffect(() => {
- const m = new Map<string, EditableTag>();
- for (const t of bookmark.tags) {
- m.set(t.name, { attachedBy: t.attachedBy, id: t.id, name: t.name });
- }
- setTags(m);
- }, [bookmark.tags]);
-
- const bookmarkInvalidationFunction =
- api.useUtils().bookmarks.getBookmark.invalidate;
-
- const { mutate, isPending } = api.bookmarks.updateTags.useMutation({
- onSuccess: () => {
- toast({
- description: "Tags has been updated!",
- });
- bookmarkInvalidationFunction({ bookmarkId: bookmark.id });
- },
- onError: () => {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- description: "There was a problem with your request.",
- });
- },
- });
-
- const onSaveButton = () => {
- const exitingTags = new Set(bookmark.tags.map((t) => t.name));
-
- const attach = [];
- const detach = [];
- for (const t of tags.values()) {
- if (!exitingTags.has(t.name)) {
- attach.push({ tag: t.name });
- }
- }
- for (const t of bookmark.tags) {
- if (!tags.has(t.name)) {
- detach.push({ tagId: t.id });
- }
- }
- mutate({
- bookmarkId: bookmark.id,
- attach,
- detach,
- });
- };
-
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Edit Tags</DialogTitle>
- </DialogHeader>
- <TagEditor tags={tags} setTags={setTags} />
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="button"
- loading={isPending}
- onClick={onSaveButton}
- >
- Save
- </ActionButton>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
-
-export function useTagModel(bookmark: ZBookmark) {
- const [open, setOpen] = useState(false);
-
- return {
- open,
- setOpen,
- content: (
- <TagModal
- key={bookmark.id}
- bookmark={bookmark}
- open={open}
- setOpen={setOpen}
- />
- ),
- };
-}
diff --git a/packages/web/app/dashboard/bookmarks/components/TextCard.tsx b/packages/web/app/dashboard/bookmarks/components/TextCard.tsx
deleted file mode 100644
index 2565e69d..00000000
--- a/packages/web/app/dashboard/bookmarks/components/TextCard.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-"use client";
-
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import BookmarkOptions from "./BookmarkOptions";
-import { api } from "@/lib/trpc";
-import { Maximize2, Star } from "lucide-react";
-import { cn } from "@/lib/utils";
-import TagList from "./TagList";
-import Markdown from "react-markdown";
-import { useState } from "react";
-import { BookmarkedTextViewer } from "./BookmarkedTextViewer";
-import Link from "next/link";
-import { isBookmarkStillTagging } from "@/lib/bookmarkUtils";
-
-export default function TextCard({
- bookmark: initialData,
- className,
-}: {
- bookmark: ZBookmark;
- className?: string;
-}) {
- const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
- {
- bookmarkId: initialData.id,
- },
- {
- initialData,
- refetchInterval: (query) => {
- const data = query.state.data;
- if (!data) {
- return false;
- }
- if (isBookmarkStillTagging(data)) {
- return 1000;
- }
- return false;
- },
- },
- );
- const [previewModalOpen, setPreviewModalOpen] = useState(false);
- const bookmarkedText = bookmark.content;
- if (bookmarkedText.type != "text") {
- throw new Error("Unexpected bookmark type");
- }
-
- return (
- <>
- <BookmarkedTextViewer
- content={bookmarkedText.text}
- open={previewModalOpen}
- setOpen={setPreviewModalOpen}
- />
- <div
- className={cn(
- className,
- cn(
- "flex h-min max-h-96 flex-col gap-y-1 overflow-hidden rounded-lg p-2 shadow-md",
- ),
- )}
- >
- <Markdown className="prose grow overflow-hidden">
- {bookmarkedText.text}
- </Markdown>
- <div className="mt-4 flex flex-none flex-wrap gap-1 overflow-hidden">
- <TagList
- bookmark={bookmark}
- loading={isBookmarkStillTagging(bookmark)}
- />
- </div>
- <div className="flex w-full justify-between">
- <div />
- <div className="flex gap-0 text-gray-500">
- <div>
- {bookmark.favourited && (
- <Star
- className="my-1 size-8 rounded p-1"
- color="#ebb434"
- fill="#ebb434"
- />
- )}
- </div>
- <Link
- className="my-auto block px-2"
- href={`/dashboard/preview/${bookmark.id}`}
- >
- <Maximize2 size="20" />
- </Link>
- <BookmarkOptions bookmark={bookmark} />
- </div>
- </div>
- </div>
- </>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/layout.tsx b/packages/web/app/dashboard/bookmarks/layout.tsx
index b03a9a19..6a588823 100644
--- a/packages/web/app/dashboard/bookmarks/layout.tsx
+++ b/packages/web/app/dashboard/bookmarks/layout.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import AddBookmark from "./components/AddBookmark";
+import AddBookmark from "@/components/dashboard/bookmarks/AddBookmark";
import type { Metadata } from "next";
export const metadata: Metadata = {
diff --git a/packages/web/app/dashboard/bookmarks/page.tsx b/packages/web/app/dashboard/bookmarks/page.tsx
index 517dc184..c9391d85 100644
--- a/packages/web/app/dashboard/bookmarks/page.tsx
+++ b/packages/web/app/dashboard/bookmarks/page.tsx
@@ -1,4 +1,4 @@
-import Bookmarks from "./components/Bookmarks";
+import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
export default async function BookmarksPage() {
return <Bookmarks title="Bookmarks" archived={false} />;
diff --git a/packages/web/app/dashboard/components/AllLists.tsx b/packages/web/app/dashboard/components/AllLists.tsx
deleted file mode 100644
index a77252d0..00000000
--- a/packages/web/app/dashboard/components/AllLists.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-"use client";
-
-import { api } from "@/lib/trpc";
-import SidebarItem from "./SidebarItem";
-import NewListModal, { useNewListModal } from "./NewListModal";
-import { Plus } from "lucide-react";
-import Link from "next/link";
-import { ZBookmarkList } from "@hoarder/trpc/types/lists";
-
-export default function AllLists({
- initialData,
-}: {
- initialData: { lists: ZBookmarkList[] };
-}) {
- let { data: lists } = api.lists.list.useQuery(undefined, {
- initialData,
- });
- // TODO: This seems to be a bug in react query
- lists ||= initialData;
- const { setOpen } = useNewListModal();
-
- return (
- <ul className="max-h-full gap-y-2 overflow-auto text-sm font-medium">
- <NewListModal />
- <li className="flex justify-between pb-2 font-bold">
- <p>Lists</p>
- <Link href="#" onClick={() => setOpen(true)}>
- <Plus />
- </Link>
- </li>
- <SidebarItem
- logo={<span className="text-lg">📋</span>}
- name="All Lists"
- path={`/dashboard/lists`}
- className="py-0.5"
- />
- <SidebarItem
- logo={<span className="text-lg">⭐️</span>}
- name="Favourties"
- path={`/dashboard/favourites`}
- className="py-0.5"
- />
- <SidebarItem
- logo={<span className="text-lg">🗄️</span>}
- name="Archive"
- path={`/dashboard/archive`}
- className="py-0.5"
- />
- {lists.lists.map((l) => (
- <SidebarItem
- key={l.id}
- logo={<span className="text-lg"> {l.icon}</span>}
- name={l.name}
- path={`/dashboard/lists/${l.id}`}
- className="py-0.5"
- />
- ))}
- </ul>
- );
-}
diff --git a/packages/web/app/dashboard/components/ModileSidebar.tsx b/packages/web/app/dashboard/components/ModileSidebar.tsx
deleted file mode 100644
index 4bd6a347..00000000
--- a/packages/web/app/dashboard/components/ModileSidebar.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import MobileSidebarItem from "./ModileSidebarItem";
-import {
- Tag,
- PackageOpen,
- Settings,
- Search,
- ClipboardList,
-} from "lucide-react";
-import SidebarProfileOptions from "./SidebarProfileOptions";
-
-export default async function MobileSidebar() {
- return (
- <aside className="w-full">
- <ul className="flex justify-between space-x-2 border-b-black bg-gray-100 px-5 py-2 pt-5">
- <MobileSidebarItem logo={<PackageOpen />} path="/dashboard/bookmarks" />
- <MobileSidebarItem logo={<Search />} path="/dashboard/search" />
- <MobileSidebarItem logo={<ClipboardList />} path="/dashboard/lists" />
- <MobileSidebarItem logo={<Tag />} path="/dashboard/tags" />
- <MobileSidebarItem logo={<Settings />} path="/dashboard/settings" />
- <SidebarProfileOptions />
- </ul>
- </aside>
- );
-}
diff --git a/packages/web/app/dashboard/components/ModileSidebarItem.tsx b/packages/web/app/dashboard/components/ModileSidebarItem.tsx
deleted file mode 100644
index 9389d2e4..00000000
--- a/packages/web/app/dashboard/components/ModileSidebarItem.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-
-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-gray-50",
- path == currentPath ? "bg-gray-50" : "",
- )}
- >
- <Link href={path} className="mx-auto px-3 py-2">
- {logo}
- </Link>
- </li>
- );
-}
diff --git a/packages/web/app/dashboard/components/NewListModal.tsx b/packages/web/app/dashboard/components/NewListModal.tsx
deleted file mode 100644
index f51616ed..00000000
--- a/packages/web/app/dashboard/components/NewListModal.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-"use client";
-
-import data from "@emoji-mart/data";
-import Picker from "@emoji-mart/react";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormMessage,
-} from "@/components/ui/form";
-
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-
-import { z } from "zod";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Input } from "@/components/ui/input";
-
-import { create } from "zustand";
-
-export const useNewListModal = create<{
- open: boolean;
- setOpen: (v: boolean) => void;
-}>((set) => ({
- open: false,
- setOpen: (open: boolean) => set(() => ({ open })),
-}));
-
-export default function NewListModal() {
- const { open, setOpen } = useNewListModal();
-
- const formSchema = z.object({
- name: z.string(),
- icon: z.string(),
- });
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- name: "",
- icon: "💡",
- },
- });
-
- const listsInvalidationFunction = api.useUtils().lists.list.invalidate;
-
- const { mutate: createList, isPending } = api.lists.create.useMutation({
- onSuccess: () => {
- toast({
- description: "List has been created!",
- });
- listsInvalidationFunction();
- setOpen(false);
- },
- onError: (e) => {
- if (e.data?.code == "BAD_REQUEST") {
- toast({
- variant: "destructive",
- description: e.message,
- });
- } else {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- });
- }
- },
- });
-
- return (
- <Dialog
- open={open}
- onOpenChange={(s) => {
- form.reset();
- setOpen(s);
- }}
- >
- <DialogContent>
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit((value) => {
- createList(value);
- })}
- >
- <DialogHeader>
- <DialogTitle>New List</DialogTitle>
- </DialogHeader>
- <div className="flex w-full gap-2 py-4">
- <FormField
- control={form.control}
- name="icon"
- render={({ field }) => {
- return (
- <FormItem>
- <FormControl>
- <Popover>
- <PopoverTrigger className="border-input h-full rounded border px-2 text-2xl">
- {field.value}
- </PopoverTrigger>
- <PopoverContent>
- <Picker
- data={data}
- onEmojiSelect={(e: { native: string }) =>
- field.onChange(e.native)
- }
- />
- </PopoverContent>
- </Popover>
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
-
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => {
- return (
- <FormItem className="grow">
- <FormControl>
- <Input
- type="text"
- className="w-full"
- placeholder="List Name"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- </div>
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton type="submit" loading={isPending}>
- Create
- </ActionButton>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/app/dashboard/components/Sidebar.tsx b/packages/web/app/dashboard/components/Sidebar.tsx
deleted file mode 100644
index a5c1d7a5..00000000
--- a/packages/web/app/dashboard/components/Sidebar.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { Tag, Home, PackageOpen, Settings, Search, Shield } from "lucide-react";
-import { redirect } from "next/navigation";
-import SidebarItem from "./SidebarItem";
-import { getServerAuthSession } from "@/server/auth";
-import Link from "next/link";
-import SidebarProfileOptions from "./SidebarProfileOptions";
-import { Separator } from "@/components/ui/separator";
-import AllLists from "./AllLists";
-import serverConfig from "@hoarder/shared/config";
-import { api } from "@/server/api/client";
-
-export default async function Sidebar() {
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/");
- }
-
- const lists = await api.lists.list();
-
- return (
- <aside className="flex h-screen w-60 flex-col gap-5 border-r p-4">
- <Link href={"/dashboard/bookmarks"}>
- <div className="flex items-center rounded-lg px-1 text-slate-900">
- <PackageOpen />
- <span className="ml-2 text-base font-semibold">Hoarder</span>
- </div>
- </Link>
- <hr />
- <div>
- <ul className="space-y-2 text-sm font-medium">
- <SidebarItem
- logo={<Home />}
- name="Home"
- path="/dashboard/bookmarks"
- />
- {serverConfig.meilisearch && (
- <SidebarItem
- logo={<Search />}
- name="Search"
- path="/dashboard/search"
- />
- )}
- <SidebarItem logo={<Tag />} name="Tags" path="/dashboard/tags" />
- <SidebarItem
- logo={<Settings />}
- name="Settings"
- path="/dashboard/settings"
- />
- {session.user.role == "admin" && (
- <SidebarItem
- logo={<Shield />}
- name="Admin"
- path="/dashboard/admin"
- />
- )}
- </ul>
- </div>
- <Separator />
- <AllLists initialData={lists} />
- <div className="mt-auto flex justify-between justify-self-end">
- <div className="my-auto"> {session.user.name} </div>
- <SidebarProfileOptions />
- </div>
- </aside>
- );
-}
diff --git a/packages/web/app/dashboard/components/SidebarItem.tsx b/packages/web/app/dashboard/components/SidebarItem.tsx
deleted file mode 100644
index 856bdffd..00000000
--- a/packages/web/app/dashboard/components/SidebarItem.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-
-export default function SidebarItem({
- name,
- logo,
- path,
- className,
-}: {
- name: string;
- logo: React.ReactNode;
- path: string;
- className?: string;
-}) {
- const currentPath = usePathname();
- return (
- <li
- className={cn(
- "rounded-lg px-3 py-2 hover:bg-slate-100",
- path == currentPath ? "bg-gray-50" : "",
- className,
- )}
- >
- <Link href={path} className="flex w-full gap-x-2">
- {logo}
- <span className="my-auto"> {name} </span>
- </Link>
- </li>
- );
-}
diff --git a/packages/web/app/dashboard/components/SidebarProfileOptions.tsx b/packages/web/app/dashboard/components/SidebarProfileOptions.tsx
deleted file mode 100644
index f931b63e..00000000
--- a/packages/web/app/dashboard/components/SidebarProfileOptions.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { LogOut, MoreHorizontal } from "lucide-react";
-import { signOut } from "next-auth/react";
-
-export default function SidebarProfileOptions() {
- return (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost">
- <MoreHorizontal />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-fit">
- <DropdownMenuItem
- onClick={() =>
- signOut({
- callbackUrl: "/",
- })
- }
- >
- <LogOut className="mr-2 size-4" />
- <span>Sign Out</span>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/packages/web/app/dashboard/favourites/page.tsx b/packages/web/app/dashboard/favourites/page.tsx
index 2dc555d2..de17461d 100644
--- a/packages/web/app/dashboard/favourites/page.tsx
+++ b/packages/web/app/dashboard/favourites/page.tsx
@@ -1,4 +1,4 @@
-import Bookmarks from "../bookmarks/components/Bookmarks";
+import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
export default async function FavouritesBookmarkPage() {
return (
diff --git a/packages/web/app/dashboard/layout.tsx b/packages/web/app/dashboard/layout.tsx
index 59e293d2..31d592fb 100644
--- a/packages/web/app/dashboard/layout.tsx
+++ b/packages/web/app/dashboard/layout.tsx
@@ -1,6 +1,6 @@
import { Separator } from "@/components/ui/separator";
-import MobileSidebar from "./components/ModileSidebar";
-import Sidebar from "./components/Sidebar";
+import MobileSidebar from "@/components/dashboard/sidebar/ModileSidebar";
+import Sidebar from "@/components/dashboard/sidebar/Sidebar";
export default async function Dashboard({
children,
diff --git a/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx b/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx
deleted file mode 100644
index 5303b217..00000000
--- a/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { Trash } from "lucide-react";
-import { useRouter } from "next/navigation";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ActionButton } from "@/components/ui/action-button";
-import { useState } from "react";
-import { ZBookmarkList } from "@hoarder/trpc/types/lists";
-
-export default function DeleteListButton({ list }: { list: ZBookmarkList }) {
- const [isDialogOpen, setDialogOpen] = useState(false);
-
- const router = useRouter();
-
- const listsInvalidationFunction = api.useUtils().lists.list.invalidate;
- const { mutate: deleteList, isPending } = api.lists.delete.useMutation({
- onSuccess: () => {
- listsInvalidationFunction();
- toast({
- description: `List "${list.icon} ${list.name}" is deleted!`,
- });
- router.push("/");
- },
- onError: () => {
- toast({
- variant: "destructive",
- description: `Something went wrong`,
- });
- },
- });
- return (
- <Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
- <DialogTrigger asChild>
- <Button className="mt-auto flex gap-2" variant="destructive">
- <Trash className="size-5" />
- <span className="hidden md:block">Delete List</span>
- </Button>
- </DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>
- Delete {list.icon} {list.name}?
- </DialogTitle>
- </DialogHeader>
- <span>
- Are you sure you want to delete {list.icon} {list.name}?
- </span>
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="button"
- variant="destructive"
- loading={isPending}
- onClick={() => deleteList({ listId: list.id })}
- >
- Delete
- </ActionButton>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx b/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx
deleted file mode 100644
index 979b522f..00000000
--- a/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-"use client";
-
-import BookmarksGrid from "@/app/dashboard/bookmarks/components/BookmarksGrid";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import { ZBookmarkListWithBookmarks } from "@hoarder/trpc/types/lists";
-import { api } from "@/lib/trpc";
-
-export default function ListView({
- bookmarks,
- list: initialData,
-}: {
- list: ZBookmarkListWithBookmarks;
- bookmarks: ZBookmark[];
-}) {
- const { data } = api.lists.get.useQuery(
- { listId: initialData.id },
- {
- initialData,
- },
- );
-
- return (
- <BookmarksGrid query={{ ids: data.bookmarks }} bookmarks={bookmarks} />
- );
-}
diff --git a/packages/web/app/dashboard/lists/[listId]/page.tsx b/packages/web/app/dashboard/lists/[listId]/page.tsx
index 397a0f1e..006fd3ad 100644
--- a/packages/web/app/dashboard/lists/[listId]/page.tsx
+++ b/packages/web/app/dashboard/lists/[listId]/page.tsx
@@ -2,8 +2,8 @@ import { api } from "@/server/api/client";
import { getServerAuthSession } from "@/server/auth";
import { TRPCError } from "@trpc/server";
import { notFound, redirect } from "next/navigation";
-import ListView from "./components/ListView";
-import DeleteListButton from "./components/DeleteListButton";
+import ListView from "@/components/dashboard/lists/ListView";
+import DeleteListButton from "@/components/dashboard/lists/DeleteListButton";
export default async function ListPage({
params,
diff --git a/packages/web/app/dashboard/lists/components/AllListsView.tsx b/packages/web/app/dashboard/lists/components/AllListsView.tsx
deleted file mode 100644
index 0e2f898b..00000000
--- a/packages/web/app/dashboard/lists/components/AllListsView.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import { api } from "@/lib/trpc";
-import { ZBookmarkList } from "@hoarder/trpc/types/lists";
-import { keepPreviousData } from "@tanstack/react-query";
-import { Plus } from "lucide-react";
-import Link from "next/link";
-import { useNewListModal } from "../../components/NewListModal";
-
-function ListItem({
- name,
- icon,
- path,
-}: {
- name: string;
- icon: string;
- path: string;
-}) {
- return (
- <Link href={path}>
- <div className="bg-background rounded-md border border-gray-200 px-4 py-2 text-lg">
- <p className="text-nowrap">
- {icon} {name}
- </p>
- </div>
- </Link>
- );
-}
-
-export default function AllListsView({
- initialData,
-}: {
- initialData: ZBookmarkList[];
-}) {
- const { setOpen: setIsNewListModalOpen } = useNewListModal();
- let { data: lists } = api.lists.list.useQuery(undefined, {
- initialData: { lists: initialData },
- placeholderData: keepPreviousData,
- });
-
- // TODO: This seems to be a bug in react query
- lists ||= { lists: initialData };
-
- return (
- <div className="flex flex-col flex-wrap gap-2 md:flex-row">
- <Button
- className="my-auto flex h-full"
- onClick={() => setIsNewListModalOpen(true)}
- >
- <Plus />
- <span className="my-auto">New List</span>
- </Button>
- <ListItem name="Favourites" icon="⭐️" path={`/dashboard/favourites`} />
- <ListItem name="Archive" icon="🗄️" path={`/dashboard/archive`} />
- {lists.lists.map((l) => (
- <ListItem
- key={l.id}
- name={l.name}
- icon={l.icon}
- path={`/dashboard/lists/${l.id}`}
- />
- ))}
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/lists/page.tsx b/packages/web/app/dashboard/lists/page.tsx
index 62e328b0..88eeda47 100644
--- a/packages/web/app/dashboard/lists/page.tsx
+++ b/packages/web/app/dashboard/lists/page.tsx
@@ -1,5 +1,5 @@
import { api } from "@/server/api/client";
-import AllListsView from "./components/AllListsView";
+import AllListsView from "@/components/dashboard/lists/AllListsView";
export default async function ListsPage() {
const lists = await api.lists.list();
diff --git a/packages/web/app/dashboard/preview/[bookmarkId]/components/BookmarkPreview.tsx b/packages/web/app/dashboard/preview/[bookmarkId]/components/BookmarkPreview.tsx
deleted file mode 100644
index 2a8ae1b1..00000000
--- a/packages/web/app/dashboard/preview/[bookmarkId]/components/BookmarkPreview.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-"use client";
-
-import { BackButton } from "@/components/ui/back-button";
-import { Skeleton } from "@/components/ui/skeleton";
-import { isBookmarkStillCrawling } from "@/lib/bookmarkUtils";
-import { api } from "@/lib/trpc";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import { ArrowLeftCircle, CalendarDays, ExternalLink } from "lucide-react";
-import Link from "next/link";
-import Markdown from "react-markdown";
-
-export default function BookmarkPreview({
- initialData,
-}: {
- initialData: ZBookmark;
-}) {
- const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
- {
- bookmarkId: initialData.id,
- },
- {
- initialData,
- refetchInterval: (query) => {
- const data = query.state.data;
- if (!data) {
- return false;
- }
- // If the link is not crawled or not tagged
- if (isBookmarkStillCrawling(data)) {
- return 1000;
- }
- return false;
- },
- },
- );
-
- const linkHeader = bookmark.content.type == "link" && (
- <div className="flex flex-col space-y-2">
- <p className="text-center text-3xl">
- {bookmark.content.title || bookmark.content.url}
- </p>
- <Link href={bookmark.content.url} className="mx-auto flex gap-2">
- <span className="my-auto">View Original</span>
- <ExternalLink />
- </Link>
- </div>
- );
-
- let content;
- switch (bookmark.content.type) {
- case "link": {
- if (!bookmark.content.htmlContent) {
- content = (
- <div className="text-red-500">Failed to fetch link content ...</div>
- );
- } else {
- content = (
- <div
- dangerouslySetInnerHTML={{
- __html: bookmark.content.htmlContent || "",
- }}
- className="prose"
- />
- );
- }
- break;
- }
- case "text": {
- content = <Markdown className="prose">{bookmark.content.text}</Markdown>;
- break;
- }
- }
-
- return (
- <div className="bg-background m-4 min-h-screen space-y-4 rounded-md border p-4">
- <div className="flex justify-between">
- <BackButton className="ghost" variant="ghost">
- <ArrowLeftCircle />
- </BackButton>
- <div className="my-auto">
- <span className="my-auto flex gap-2">
- <CalendarDays /> {bookmark.createdAt.toLocaleString()}
- </span>
- </div>
- </div>
- <hr />
- {linkHeader}
- <div className="mx-auto flex h-full border-x p-2 px-4 lg:w-2/3">
- {isBookmarkStillCrawling(bookmark) ? (
- <div className="flex w-full flex-col gap-2">
- <Skeleton className="h-4" />
- <Skeleton className="h-4" />
- <Skeleton className="h-4" />
- </div>
- ) : (
- content
- )}
- </div>
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx b/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx
index 47aeb891..707d2b69 100644
--- a/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx
+++ b/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx
@@ -1,5 +1,5 @@
import { api } from "@/server/api/client";
-import BookmarkPreview from "./components/BookmarkPreview";
+import BookmarkPreview from "@/components/dashboard/bookmarks/BookmarkPreview";
export default async function BookmarkPreviewPage({
params,
diff --git a/packages/web/app/dashboard/search/page.tsx b/packages/web/app/dashboard/search/page.tsx
index 1c26608e..514c546d 100644
--- a/packages/web/app/dashboard/search/page.tsx
+++ b/packages/web/app/dashboard/search/page.tsx
@@ -2,7 +2,7 @@
import { api } from "@/lib/trpc";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
-import BookmarksGrid from "../bookmarks/components/BookmarksGrid";
+import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid";
import { Input } from "@/components/ui/input";
import Loading from "../bookmarks/loading";
import { keepPreviousData } from "@tanstack/react-query";
diff --git a/packages/web/app/dashboard/settings/components/AddApiKey.tsx b/packages/web/app/dashboard/settings/components/AddApiKey.tsx
deleted file mode 100644
index a4fd9c25..00000000
--- a/packages/web/app/dashboard/settings/components/AddApiKey.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { z } from "zod";
-import { useRouter } from "next/navigation";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm, SubmitErrorHandler } from "react-hook-form";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { useState } from "react";
-import { Check, Copy } from "lucide-react";
-import { ActionButton } from "@/components/ui/action-button";
-
-function ApiKeySuccess({ apiKey }: { apiKey: string }) {
- const [isCopied, setCopied] = useState(false);
-
- const onCopy = () => {
- navigator.clipboard.writeText(apiKey);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- };
-
- 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 />
- <Button onClick={onCopy}>
- {!isCopied ? (
- <Copy className="size-4" />
- ) : (
- <Check className="size-4" />
- )}
- </Button>
- </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/packages/web/app/dashboard/settings/components/ApiKeySettings.tsx b/packages/web/app/dashboard/settings/components/ApiKeySettings.tsx
deleted file mode 100644
index 1598f25f..00000000
--- a/packages/web/app/dashboard/settings/components/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 DeleteApiKey from "./DeleteApiKey";
-import AddApiKey from "./AddApiKey";
-
-export default async function ApiKeys() {
- const keys = await api.apiKeys.list();
- return (
- <div className="pt-4">
- <span className="text-xl">API Keys</span>
- <hr className="my-2" />
- <div className="flex flex-col space-y-3">
- <div className="flex flex-1 justify-end">
- <AddApiKey />
- </div>
- <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/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx b/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx
deleted file mode 100644
index 566136af..00000000
--- a/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import { Trash } from "lucide-react";
-
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { useRouter } from "next/navigation";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ActionButton } from "@/components/ui/action-button";
-import { useState } from "react";
-
-export default function DeleteApiKey({
- name,
- id,
-}: {
- name: string;
- id: string;
-}) {
- const [isDialogOpen, setDialogOpen] = useState(false);
- const router = useRouter();
- const mutator = api.apiKeys.revoke.useMutation({
- onSuccess: () => {
- toast({
- description: "Key was successfully deleted",
- });
- setDialogOpen(false);
- router.refresh();
- },
- });
-
- return (
- <Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
- <DialogTrigger asChild>
- <Button variant="destructive">
- <Trash className="size-5" />
- </Button>
- </DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Delete API Key</DialogTitle>
- <DialogDescription>
- Are you sure you want to delete the API key &quot;{name}&quot;? Any
- service using this API key will lose access.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="button"
- variant="destructive"
- loading={mutator.isPending}
- onClick={() => mutator.mutate({ id })}
- >
- Delete
- </ActionButton>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/app/dashboard/settings/page.tsx b/packages/web/app/dashboard/settings/page.tsx
index 95637d8c..38091e6c 100644
--- a/packages/web/app/dashboard/settings/page.tsx
+++ b/packages/web/app/dashboard/settings/page.tsx
@@ -1,4 +1,4 @@
-import ApiKeySettings from "./components/ApiKeySettings";
+import ApiKeySettings from "@/components/dashboard/settings/ApiKeySettings";
export default async function Settings() {
return (
<div className="m-4 flex flex-col space-y-2 rounded-md border bg-white p-4">
diff --git a/packages/web/app/dashboard/tags/[tagName]/page.tsx b/packages/web/app/dashboard/tags/[tagName]/page.tsx
index fa3a1f8e..c978b86a 100644
--- a/packages/web/app/dashboard/tags/[tagName]/page.tsx
+++ b/packages/web/app/dashboard/tags/[tagName]/page.tsx
@@ -1,7 +1,7 @@
import { getServerAuthSession } from "@/server/auth";
import { db } from "@hoarder/db";
import { notFound, redirect } from "next/navigation";
-import BookmarksGrid from "../../bookmarks/components/BookmarksGrid";
+import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid";
import { api } from "@/server/api/client";
import { bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema";
import { and, eq } from "drizzle-orm";