aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-12-30 12:52:50 +0200
committerGitHub <noreply@github.com>2025-12-30 10:52:50 +0000
commita0b4a26ad398137e13c35f3fe0dad99154537d91 (patch)
tree6e7f7b8acb7725717fdbb06ad262a122cdd2dfd5 /apps/web/app
parent7ab7db8e48360417498643eec2384b0fcb7fbdfb (diff)
downloadkarakeep-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.tsx20
-rw-r--r--apps/web/app/dashboard/wrapped/page.tsx24
-rw-r--r--apps/web/app/settings/stats/page.tsx41
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