aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-06-07 17:03:02 +0000
committerMohamed Bassem <me@mbassem.com>2025-06-07 18:19:06 +0000
commit3a0f5fa0bbc907e81a0ddf2f8728a36761499928 (patch)
tree603dc4a282f963bd1b9cc9786b8b35ce13499458
parentbc65a73872cf0707d2433c289d1f04423325ed95 (diff)
downloadkarakeep-3a0f5fa0bbc907e81a0ddf2f8728a36761499928.tar.zst
feat(ui): Improve the look of the public bookmarks page
-rw-r--r--apps/web/app/public/layout.tsx9
-rw-r--r--apps/web/app/public/lists/[listId]/page.tsx18
-rw-r--r--apps/web/components/public/lists/PublicListHeader.tsx64
-rw-r--r--packages/api/routes/rss.ts17
4 files changed, 77 insertions, 31 deletions
diff --git a/apps/web/app/public/layout.tsx b/apps/web/app/public/layout.tsx
index 4203c44c..b4628e11 100644
--- a/apps/web/app/public/layout.tsx
+++ b/apps/web/app/public/layout.tsx
@@ -1,16 +1,11 @@
-import KarakeepLogo from "@/components/KarakeepIcon";
-
export default function PublicLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
- <div className="h-screen flex-col overflow-y-auto bg-muted">
- <header className="sticky left-0 right-0 top-0 z-50 flex h-16 items-center justify-between overflow-x-auto overflow-y-hidden bg-background p-4 shadow">
- <KarakeepLogo height={38} />
- </header>
- <main className="container mx-3 mt-3 flex-1">{children}</main>
+ <div className="h-screen items-center justify-center overflow-y-auto bg-muted">
+ <main className="container mt-3">{children}</main>
</div>
);
}
diff --git a/apps/web/app/public/lists/[listId]/page.tsx b/apps/web/app/public/lists/[listId]/page.tsx
index cdfc46d0..4a4ce414 100644
--- a/apps/web/app/public/lists/[listId]/page.tsx
+++ b/apps/web/app/public/lists/[listId]/page.tsx
@@ -3,7 +3,6 @@ import { notFound } from "next/navigation";
import NoBookmarksBanner from "@/components/dashboard/bookmarks/NoBookmarksBanner";
import PublicBookmarkGrid from "@/components/public/lists/PublicBookmarkGrid";
import PublicListHeader from "@/components/public/lists/PublicListHeader";
-import { Separator } from "@/components/ui/separator";
import { api } from "@/server/api/client";
import { TRPCError } from "@trpc/server";
@@ -50,22 +49,15 @@ export default async function PublicListPage({
listId: params.listId,
});
return (
- <div className="flex flex-col gap-3">
- <div className="flex items-center gap-2">
- <span className="text-2xl">
- {list.icon} {list.name}
- {list.description && (
- <span className="mx-2 text-lg text-gray-400">
- {`(${list.description})`}
- </span>
- )}
- </span>
- </div>
- <Separator />
+ <div className="space-y-3">
<PublicListHeader
list={{
id: params.listId,
+ name: list.name,
+ description: list.description,
+ icon: list.icon,
numItems: list.numItems,
+ ownerName: list.ownerName,
}}
/>
{list.numItems > 0 ? (
diff --git a/apps/web/components/public/lists/PublicListHeader.tsx b/apps/web/components/public/lists/PublicListHeader.tsx
index 1f016351..acdb845a 100644
--- a/apps/web/components/public/lists/PublicListHeader.tsx
+++ b/apps/web/components/public/lists/PublicListHeader.tsx
@@ -1,17 +1,71 @@
+import Link from "next/link";
+import KarakeepLogo from "@/components/KarakeepIcon";
+import { buttonVariants } from "@/components/ui/button";
+import { BookmarkIcon, RssIcon } from "lucide-react";
+
export default function PublicListHeader({
list,
}: {
list: {
id: string;
+ name: string;
+ description: string | null | undefined;
+ icon: string;
+ ownerName: string;
numItems: number;
};
}) {
+ const rssLink = `/api/v1/rss/lists/${list.id}`;
return (
- <div className="flex w-full justify-between">
- <span />
- <p className="text-xs font-light uppercase text-gray-500">
- {list.numItems} bookmarks
- </p>
+ <div className="rounded-lg border bg-gradient-to-br from-purple-50/50 via-purple-100/30 to-purple-200/40 p-6 transition-all duration-300 dark:from-purple-950/20 dark:via-purple-900/15 dark:to-purple-800/20">
+ <div className="space-y-4">
+ <KarakeepLogo height={38} />
+ <div className="flex flex-col items-start justify-between gap-6 md:flex-row md:items-center">
+ {/* Header */}
+ <div className="flex min-w-0 flex-1 items-start gap-3">
+ <span className="text-3xl transition-transform duration-200 hover:scale-110">
+ {list.icon}
+ </span>
+ <div className="min-w-0 flex-1">
+ <h1 className="text-3xl font-bold leading-tight text-foreground">
+ {list.name}
+ </h1>
+ {list.description && list.description.length > 0 && (
+ <p className="mt-2 text-lg leading-relaxed text-muted-foreground">
+ {list.description}
+ </p>
+ )}
+ </div>
+ </div>
+ {/* Created by */}
+ <div className="flex gap-3 md:justify-end">
+ <div className="flex aspect-square size-10 flex-col items-center justify-center rounded-full bg-primary font-medium text-primary-foreground transition-all duration-200 hover:scale-105 hover:shadow-md">
+ {list.ownerName[0]?.toUpperCase()}
+ </div>
+ <div>
+ <p className="text-sm text-muted-foreground">Created by</p>
+ <p className="font-medium text-foreground">{list.ownerName}</p>
+ </div>
+ </div>
+ </div>
+ {/* Options */}
+ <div className="flex items-center justify-start gap-1 md:justify-end">
+ <div className="flex items-center gap-1 text-xs font-light uppercase text-gray-500 transition-colors duration-200 hover:text-gray-700 dark:hover:text-gray-300">
+ <BookmarkIcon
+ size={12}
+ className="transition-transform duration-200 hover:scale-110"
+ />
+ <span>{list.numItems} bookmarks</span>
+ </div>
+ <Link
+ href={rssLink}
+ target="_blank"
+ className={buttonVariants({ variant: "none", size: "icon" })}
+ >
+ <RssIcon className="size-3 text-gray-500" />
+ </Link>
+ </div>
+ </div>
</div>
);
}
diff --git a/packages/api/routes/rss.ts b/packages/api/routes/rss.ts
index 88b943ad..94bfc775 100644
--- a/packages/api/routes/rss.ts
+++ b/packages/api/routes/rss.ts
@@ -14,7 +14,7 @@ const app = new Hono().get(
zValidator(
"query",
z.object({
- token: z.string().min(1),
+ token: z.string().min(1).optional(),
limit: z.coerce
.number()
.min(1)
@@ -28,11 +28,16 @@ const app = new Hono().get(
const searchParams = c.req.valid("query");
const token = searchParams.token;
- const res = await List.getPublicListContents(c.var.ctx, listId, token, {
- limit: searchParams.limit ?? 20,
- order: "desc",
- cursor: null,
- });
+ const res = await List.getPublicListContents(
+ c.var.ctx,
+ listId,
+ token ?? null,
+ {
+ limit: searchParams.limit ?? 20,
+ order: "desc",
+ cursor: null,
+ },
+ );
const list = res.list;
const rssFeed = toRSS(