aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app/public
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-06-01 20:46:41 +0100
committerGitHub <noreply@github.com>2025-06-01 20:46:41 +0100
commitea1d0023bfee55358ebb1a96f3d06e783a219c0d (patch)
tree5bddd451728cb7dd377574a9ea1ea591bca069c4 /apps/web/app/public
parent3afe1e21df6dcc0483e74e0db02d9d82af32ecea (diff)
downloadkarakeep-ea1d0023bfee55358ebb1a96f3d06e783a219c0d.tar.zst
feat: Add support for public lists (#1511)
* WIP: public lists * Drop viewing modes * Add the public endpoint for assets * regen the openapi spec * proper handling for different asset types * Add num bookmarks and a no bookmark banner * Correctly set page title * Add a not-found page * merge the RSS and public list endpoints * Add e2e tests for the public endpoints * Redesign the share list modal * Make NEXTAUTH_SECRET not required * propery render text bookmarks * rebase migration * fix public token tests * Add more tests
Diffstat (limited to 'apps/web/app/public')
-rw-r--r--apps/web/app/public/layout.tsx16
-rw-r--r--apps/web/app/public/lists/[listId]/not-found.tsx18
-rw-r--r--apps/web/app/public/lists/[listId]/page.tsx84
3 files changed, 118 insertions, 0 deletions
diff --git a/apps/web/app/public/layout.tsx b/apps/web/app/public/layout.tsx
new file mode 100644
index 00000000..4203c44c
--- /dev/null
+++ b/apps/web/app/public/layout.tsx
@@ -0,0 +1,16 @@
+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>
+ );
+}
diff --git a/apps/web/app/public/lists/[listId]/not-found.tsx b/apps/web/app/public/lists/[listId]/not-found.tsx
new file mode 100644
index 00000000..a6fd71dc
--- /dev/null
+++ b/apps/web/app/public/lists/[listId]/not-found.tsx
@@ -0,0 +1,18 @@
+import { X } from "lucide-react";
+
+export default function PublicListPageNotFound() {
+ return (
+ <div className="mx-auto flex max-w-md flex-1 flex-col items-center justify-center px-4 py-16 text-center">
+ <div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-slate-100 dark:bg-slate-700">
+ <X className="h-12 w-12 text-gray-300" strokeWidth={1.5} />
+ </div>
+ <h1 className="mb-3 text-2xl font-semibold text-gray-800">
+ List not found
+ </h1>
+ <p className="text-center text-gray-500">
+ The list you&apos;re looking for doesn&apos;t exist or may have been
+ removed.
+ </p>
+ </div>
+ );
+}
diff --git a/apps/web/app/public/lists/[listId]/page.tsx b/apps/web/app/public/lists/[listId]/page.tsx
new file mode 100644
index 00000000..c0495b9f
--- /dev/null
+++ b/apps/web/app/public/lists/[listId]/page.tsx
@@ -0,0 +1,84 @@
+import type { Metadata } from "next";
+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";
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { listId: string };
+}): Promise<Metadata> {
+ // TODO: Don't load the entire list, just create an endpoint to get the list name
+ try {
+ const resp = await api.publicBookmarks.getPublicBookmarksInList({
+ listId: params.listId,
+ });
+ return {
+ title: `${resp.list.name} - Karakeep`,
+ };
+ } catch (e) {
+ if (e instanceof TRPCError && e.code === "NOT_FOUND") {
+ notFound();
+ }
+ }
+ return {
+ title: "Karakeep",
+ };
+}
+
+export default async function PublicListPage({
+ params,
+}: {
+ params: { listId: string };
+}) {
+ try {
+ const { list, bookmarks, nextCursor } =
+ await api.publicBookmarks.getPublicBookmarksInList({
+ 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 />
+ <PublicListHeader
+ list={{
+ id: params.listId,
+ numItems: list.numItems,
+ }}
+ />
+ {list.numItems > 0 ? (
+ <PublicBookmarkGrid
+ list={{
+ id: params.listId,
+ name: list.name,
+ description: list.description,
+ icon: list.icon,
+ numItems: list.numItems,
+ }}
+ bookmarks={bookmarks}
+ nextCursor={nextCursor}
+ />
+ ) : (
+ <NoBookmarksBanner />
+ )}
+ </div>
+ );
+ } catch (e) {
+ if (e instanceof TRPCError && e.code === "NOT_FOUND") {
+ notFound();
+ }
+ }
+}