diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-12-30 12:52:50 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-30 10:52:50 +0000 |
| commit | a0b4a26ad398137e13c35f3fe0dad99154537d91 (patch) | |
| tree | 6e7f7b8acb7725717fdbb06ad262a122cdd2dfd5 /apps/web/app | |
| parent | 7ab7db8e48360417498643eec2384b0fcb7fbdfb (diff) | |
| download | karakeep-a0b4a26ad398137e13c35f3fe0dad99154537d91.tar.zst | |
feat: 2025 wrapped (#2322)
* feat: 2025 wrapped
* don't add wrapped for new users
Diffstat (limited to 'apps/web/app')
| -rw-r--r-- | apps/web/app/dashboard/layout.tsx | 20 | ||||
| -rw-r--r-- | apps/web/app/dashboard/wrapped/page.tsx | 24 | ||||
| -rw-r--r-- | apps/web/app/settings/stats/page.tsx | 41 |
3 files changed, 70 insertions, 15 deletions
diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx index be65e66a..81b44a3c 100644 --- a/apps/web/app/dashboard/layout.tsx +++ b/apps/web/app/dashboard/layout.tsx @@ -16,6 +16,7 @@ import { Highlighter, Home, Search, + Sparkles, Tag, } from "lucide-react"; @@ -34,9 +35,10 @@ export default async function Dashboard({ redirect("/"); } - const [lists, userSettings] = await Promise.all([ + const [lists, userSettings, showWrapped] = await Promise.all([ tryCatch(api.lists.list()), tryCatch(api.users.settings()), + tryCatch(api.users.hasWrapped()), ]); if (userSettings.error) { @@ -55,6 +57,10 @@ export default async function Dashboard({ throw lists.error; } + if (showWrapped.error) { + throw showWrapped.error; + } + const items = (t: TFunction) => [ { @@ -86,10 +92,20 @@ export default async function Dashboard({ icon: <Archive size={18} />, path: "/dashboard/archive", }, + // Only show wrapped if user has at least 20 bookmarks + showWrapped.data + ? [ + { + name: t("wrapped.button"), + icon: <Sparkles size={18} />, + path: "/dashboard/wrapped", + }, + ] + : [], ].flat(); const mobileSidebar = (t: TFunction) => [ - ...items(t), + ...items(t).filter((item) => item.path !== "/dashboard/wrapped"), { name: t("lists.all_lists"), icon: <ClipboardList size={18} />, diff --git a/apps/web/app/dashboard/wrapped/page.tsx b/apps/web/app/dashboard/wrapped/page.tsx new file mode 100644 index 00000000..f479aca7 --- /dev/null +++ b/apps/web/app/dashboard/wrapped/page.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { WrappedModal } from "@/components/wrapped"; + +export default function WrappedPage() { + const router = useRouter(); + + const handleClose = () => { + router.push("/dashboard/bookmarks"); + }; + + // Always show the modal when this page is loaded + useEffect(() => { + // Prevent page from being scrollable when modal is open + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = ""; + }; + }, []); + + return <WrappedModal open={true} onClose={handleClose} />; +} diff --git a/apps/web/app/settings/stats/page.tsx b/apps/web/app/settings/stats/page.tsx index 944d1c59..6f9db115 100644 --- a/apps/web/app/settings/stats/page.tsx +++ b/apps/web/app/settings/stats/page.tsx @@ -1,10 +1,12 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Skeleton } from "@/components/ui/skeleton"; +import { WrappedModal } from "@/components/wrapped"; import { useTranslation } from "@/lib/i18n/client"; import { api } from "@/lib/trpc"; import { @@ -26,6 +28,7 @@ import { List, Rss, Smartphone, + Sparkles, TrendingUp, Upload, Zap, @@ -162,6 +165,8 @@ export default function StatsPage() { const { t } = useTranslation(); const { data: stats, isLoading } = api.users.stats.useQuery(); const { data: userSettings } = api.users.settings.useQuery(); + const { data: hasWrapped } = api.users.hasWrapped.useQuery(); + const [showWrapped, setShowWrapped] = useState(false); const maxHourlyActivity = useMemo(() => { if (!stats) return 0; @@ -222,20 +227,30 @@ export default function StatsPage() { return ( <div className="space-y-6"> - <div> - <h1 className="text-3xl font-bold"> - {t("settings.stats.usage_statistics")} - </h1> - <p className="text-muted-foreground"> - Insights into your bookmarking habits and collection - {userSettings?.timezone && userSettings.timezone !== "UTC" && ( - <span className="block text-sm"> - Times shown in {userSettings.timezone} timezone - </span> - )} - </p> + <div className="flex items-start justify-between"> + <div> + <h1 className="text-3xl font-bold"> + {t("settings.stats.usage_statistics")} + </h1> + <p className="text-muted-foreground"> + Insights into your bookmarking habits and collection + {userSettings?.timezone && userSettings.timezone !== "UTC" && ( + <span className="block text-sm"> + Times shown in {userSettings.timezone} timezone + </span> + )} + </p> + </div> + {hasWrapped && ( + <Button onClick={() => setShowWrapped(true)} className="gap-2"> + <Sparkles className="h-4 w-4" /> + View Your 2025 Wrapped + </Button> + )} </div> + <WrappedModal open={showWrapped} onClose={() => setShowWrapped(false)} /> + {/* Overview Stats */} <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <StatCard |
