From 139891656ebd2bc2a19d0ddbbb97b99e70c9bbd2 Mon Sep 17 00:00:00 2001 From: ap-ananya Date: Mon, 18 May 2026 21:54:25 +0530 Subject: [PATCH 1/3] feat: add streak milestone celebration banner --- package-lock.json | 13 ------- src/app/dashboard/page.tsx | 2 +- src/app/page.tsx | 1 + src/components/StreakMilestoneBanner.tsx | 28 +++++++++++++++ src/components/StreakTracker.tsx | 44 ++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 src/components/StreakMilestoneBanner.tsx diff --git a/package-lock.json b/package-lock.json index 467c781..df0ab10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -746,7 +746,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1185,7 +1184,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1638,7 +1636,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2508,7 +2505,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2677,7 +2673,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4118,7 +4113,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -4190,7 +4184,6 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", @@ -5033,7 +5026,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5204,7 +5196,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -5295,7 +5286,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5308,7 +5298,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6362,7 +6351,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6532,7 +6520,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 9e4bdb6..d806fa7 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -33,7 +33,7 @@ export default async function DashboardPage() { - +
diff --git a/src/app/page.tsx b/src/app/page.tsx index cb51474..ae20dff 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,6 +3,7 @@ import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; import { redirect } from "next/navigation"; + export default async function HomePage() { const session = await getServerSession(authOptions); diff --git a/src/components/StreakMilestoneBanner.tsx b/src/components/StreakMilestoneBanner.tsx new file mode 100644 index 0000000..16f5f35 --- /dev/null +++ b/src/components/StreakMilestoneBanner.tsx @@ -0,0 +1,28 @@ +"use client"; +type Props = { + streak: number; + onDismiss?: () => void; +}; + +export default function StreakMilestoneBanner({ + streak, + onDismiss, +}: Props) { + return ( +
+
+ 🎉 You reached a {streak}-day streak! Keep it up! +
+ + +
+ ); +} \ No newline at end of file diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index 141e322..4c0d1ca 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from "react"; import { useAccount } from "@/components/AccountContext"; import { useCountUp } from "@/hooks/useCountUp"; +import StreakMilestoneBanner from "@/components/StreakMilestoneBanner"; interface StreakData { current: number; @@ -26,6 +27,8 @@ export default function StreakTracker() { const [data, setData] = useState(null); const [contributionData, setContributionData] = useState(null); const [loading, setLoading] = useState(true); + const milestones = [7, 30, 50, 100, 200, 365]; + const [dismissedMilestones, setDismissedMilestones] = useState([]); const [lastUpdated, setLastUpdated] = useState(null); const [minutesAgo, setMinutesAgo] = useState(0); const [copied, setCopied] = useState(false); @@ -90,6 +93,16 @@ export default function StreakTracker() { fetchFreeze(); }, [fetchStreak]); + useEffect(() => { + const stored = localStorage.getItem( + "devtrack:dismissed-milestones" + ); + + if (stored) { + setDismissedMilestones(JSON.parse(stored)); + } + }, []); + useEffect(() => { if (!lastUpdated) return; const interval = setInterval(() => { @@ -235,7 +248,37 @@ export default function StreakTracker() { }).catch(() => {}); }; + const currentMilestone = + milestones.find( + (m) => m === data?.current + ); + const shouldShowBanner = + currentMilestone && + !dismissedMilestones.includes(currentMilestone); + const handleDismissBanner = () => { + if (!currentMilestone) return; + + const updated = [ + ...dismissedMilestones, + currentMilestone, + ]; + + setDismissedMilestones(updated); + + localStorage.setItem( + "devtrack:dismissed-milestones", + JSON.stringify(updated) + ); + }; + return ( + <> + {shouldShowBanner && currentMilestone && ( + + )}

@@ -385,6 +428,7 @@ export default function StreakTracker() { /> ) : null}

+ ); } From f59fb50e1886187528e02ebec3a8391058845bca Mon Sep 17 00:00:00 2001 From: ap-ananya Date: Tue, 19 May 2026 13:02:30 +0530 Subject: [PATCH 2/3] fix: address review feedback --- src/components/StreakMilestoneBanner.tsx | 8 ++++++-- src/components/StreakTracker.tsx | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/StreakMilestoneBanner.tsx b/src/components/StreakMilestoneBanner.tsx index 16f5f35..f6567cf 100644 --- a/src/components/StreakMilestoneBanner.tsx +++ b/src/components/StreakMilestoneBanner.tsx @@ -13,14 +13,18 @@ export default function StreakMilestoneBanner({ className="rounded-xl p-4 mb-4 flex items-center justify-between" style={{ background: "var(--accent)", - color: "white", + color: "var(--accent-foreground)", }} >
🎉 You reached a {streak}-day streak! Keep it up!
-
diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index 4c0d1ca..b80d732 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -99,7 +99,11 @@ export default function StreakTracker() { ); if (stored) { - setDismissedMilestones(JSON.parse(stored)); + try { + setDismissedMilestones(JSON.parse(stored)); + }catch { + // ignore invalid localStorage data + } } }, []); From 0802c3038d13e29dc28d620395bfd9ac6e76fda6 Mon Sep 17 00:00:00 2001 From: ap-ananya Date: Tue, 19 May 2026 21:28:34 +0530 Subject: [PATCH 3/3] apply final review fixes --- src/app/page.tsx | 1 - src/components/StreakMilestoneBanner.tsx | 3 ++- src/components/StreakTracker.tsx | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index ae20dff..cb51474 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,6 @@ import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; import { redirect } from "next/navigation"; - export default async function HomePage() { const session = await getServerSession(authOptions); diff --git a/src/components/StreakMilestoneBanner.tsx b/src/components/StreakMilestoneBanner.tsx index f6567cf..7cde29b 100644 --- a/src/components/StreakMilestoneBanner.tsx +++ b/src/components/StreakMilestoneBanner.tsx @@ -24,9 +24,10 @@ export default function StreakMilestoneBanner({ type="button" onClick={() => onDismiss?.()} aria-label="Dismiss milestone banner" + className="ml-4 rounded-md p-1 opacity-70 hover:opacity-100 transition-opacity" > ✕
); -} \ No newline at end of file +} diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index 8513bad..b27e3e5 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -6,6 +6,8 @@ import { useCountUp } from "@/hooks/useCountUp"; import StreakMilestoneBanner from "@/components/StreakMilestoneBanner"; import { useHeatmapTheme } from "@/hooks/useHeatmapTheme"; +const STREAK_MILESTONES = [7, 30, 50, 100, 200, 365]; + interface StreakData { current: number; longest: number; @@ -28,7 +30,6 @@ export default function StreakTracker() { const [data, setData] = useState(null); const [contributionData, setContributionData] = useState(null); const [loading, setLoading] = useState(true); - const milestones = [7, 30, 50, 100, 200, 365]; const [dismissedMilestones, setDismissedMilestones] = useState([]); const [lastUpdated, setLastUpdated] = useState(null); const [minutesAgo, setMinutesAgo] = useState(0); @@ -255,7 +256,7 @@ export default function StreakTracker() { }; const currentMilestone = - milestones.find( + STREAK_MILESTONES.find( (m) => m === data?.current ); const shouldShowBanner =