From 5cdd337e44d52ae5dc1e3b3dca33eb3ed29b52f4 Mon Sep 17 00:00:00 2001 From: BHARGAVI CHAUDHARY Date: Sat, 16 May 2026 13:39:18 +0530 Subject: [PATCH 1/3] feat: added framer dependency latest version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 74ac821..7fa66d5 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "canvas-confetti": "^1.9.4", "clsx": "^2.1.1", "drizzle-orm": "^0.36.4", - "framer-motion": "^11.0.0", + "framer-motion": "^12.0.0", "groq-sdk": "^0.9.1", "inngest": "^3.27.0", "lucide-react": "^0.400.0", From ca415b292b33cee1c2d9d0fb82ed849f58ba4bd8 Mon Sep 17 00:00:00 2001 From: BHARGAVI CHAUDHARY Date: Sat, 16 May 2026 14:10:20 +0530 Subject: [PATCH 2/3] feat: version modified --- package.json | 2 +- src/app/(app)/dashboard/rec-cards.tsx | 67 ++++---- src/app/(app)/dashboard/sync-button.tsx | 16 +- src/app/(app)/issues/issues-list.tsx | 30 ++-- src/app/(app)/layout.tsx | 73 ++++----- src/components/toast.tsx | 194 ++++++++++++++++++++++++ 6 files changed, 303 insertions(+), 79 deletions(-) create mode 100644 src/components/toast.tsx diff --git a/package.json b/package.json index 7fa66d5..6c32b45 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@octokit/auth-app": "^7.1.1", "@octokit/rest": "^21.0.2", "@octokit/webhooks": "^13.4.1", - "@supabase/ssr": "^0.5.2", + "@supabase/ssr": "^0.5.0", "@supabase/supabase-js": "^2.46.0", "@types/three": "^0.184.1", "@upstash/redis": "^1.34.3", diff --git a/src/app/(app)/dashboard/rec-cards.tsx b/src/app/(app)/dashboard/rec-cards.tsx index 7de8b62..c257e1c 100644 --- a/src/app/(app)/dashboard/rec-cards.tsx +++ b/src/app/(app)/dashboard/rec-cards.tsx @@ -9,6 +9,7 @@ import { type RecCard, } from '@/app/actions/recommendations'; import { sendHelpRequest } from '@/app/actions/help'; +import { useToast } from '@/components/toast'; const PR_URL_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/; @@ -24,35 +25,41 @@ export default function RecCards({ recs: initial }: { recs: RecCard[] }) { const [pending, startTransition] = useTransition(); const [busyId, setBusyId] = useState(null); const [error, setError] = useState(null); + const { addToast } = useToast(); function handleClaim(rec: RecCard) { setBusyId(rec.id); setError(null); - startTransition(async () => { - const res = await claimRecommendation(rec.id); - if (res.ok) { - setRecs((prev) => prev.map((r) => (r.id === rec.id ? { ...r, status: 'claimed' } : r))); - } else { - setError(`${rec.title}: ${res.error.message}`); - } - setBusyId(null); + startTransition(() => { + void (async () => { + const res = await claimRecommendation(rec.id); + if (res.ok) { + setRecs((prev) => prev.map((r) => (r.id === rec.id ? { ...r, status: 'claimed' } : r))); + addToast(`+${rec.xpReward} XP โ€” ISSUE CLAIMED`, 'success'); + } else { + setError(`${rec.title}: ${res.error.message}`); + } + setBusyId(null); + })(); }); } function handleSkip(rec: RecCard) { setBusyId(rec.id); setError(null); - startTransition(async () => { - const res = await skipRecommendation(rec.id); - if (res.ok) { - setRecs((prev) => { - const without = prev.filter((r) => r.id !== rec.id); - return res.data.replacement ? [...without, res.data.replacement] : without; - }); - } else { - setError(`${rec.title}: ${res.error.message}`); - } - setBusyId(null); + startTransition(() => { + void (async () => { + const res = await skipRecommendation(rec.id); + if (res.ok) { + setRecs((prev) => { + const without = prev.filter((r) => r.id !== rec.id); + return res.data.replacement ? [...without, res.data.replacement] : without; + }); + } else { + setError(`${rec.title}: ${res.error.message}`); + } + setBusyId(null); + })(); }); } @@ -149,10 +156,12 @@ function ClaimedActions({ rec, onError }: { rec: RecCard; onError: (msg: string function onLink() { if (!isValidPrUrl) return; onError(null); - startTransition(async () => { - const res = await linkPrToRec(rec.id, input.trim()); - if (res.ok) setLinked(true); - else onError(`${rec.title}: ${res.error.message}`); + startTransition(() => { + void (async () => { + const res = await linkPrToRec(rec.id, input.trim()); + if (res.ok) setLinked(true); + else onError(`${rec.title}: ${res.error.message}`); + })(); }); } @@ -162,10 +171,12 @@ function ClaimedActions({ rec, onError }: { rec: RecCard; onError: (msg: string return; } onError(null); - startTransition(async () => { - const res = await sendHelpRequest({ recId: rec.id, prUrl: input.trim() }); - if (res.ok) setHelpSent(true); - else onError(`${rec.title}: ${res.error.message}`); + startTransition(() => { + void (async () => { + const res = await sendHelpRequest({ recId: rec.id, prUrl: input.trim() }); + if (res.ok) setHelpSent(true); + else onError(`${rec.title}: ${res.error.message}`); + })(); }); } @@ -219,4 +230,4 @@ function ClaimedActions({ rec, onError }: { rec: RecCard; onError: (msg: string )} ); -} +} \ No newline at end of file diff --git a/src/app/(app)/dashboard/sync-button.tsx b/src/app/(app)/dashboard/sync-button.tsx index 5c37827..4ddb658 100644 --- a/src/app/(app)/dashboard/sync-button.tsx +++ b/src/app/(app)/dashboard/sync-button.tsx @@ -4,6 +4,7 @@ import { useState, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { RefreshCw } from 'lucide-react'; import { syncGitHubStats } from '@/app/actions/github-sync'; +import { useToast } from '@/components/toast'; type Props = { lastSyncedAt: string | null; @@ -15,6 +16,7 @@ export function SyncButton({ lastSyncedAt }: Props) { const [cooldown, setCooldown] = useState(false); const [localSyncedAt, setLocalSyncedAt] = useState(lastSyncedAt); const router = useRouter(); + const { addToast } = useToast(); const handleSync = useCallback(async () => { if (syncing || cooldown) return; @@ -28,11 +30,21 @@ export function SyncButton({ lastSyncedAt }: Props) { setLocalSyncedAt(new Date().toISOString()); setCooldown(true); setTimeout(() => setCooldown(false), 60_000); + + // Show sync-complete toast + const { merges, streak } = result.data; + addToast( + merges > 0 + ? `GITHUB SYNCED โ€” ${merges} MERGE${merges !== 1 ? 'S' : ''} ยท ${streak}D STREAK` + : 'GITHUB SYNCED', + 'success', + ); + router.refresh(); } else { setError(result.error.message); } - }, [syncing, cooldown, router]); + }, [syncing, cooldown, router, addToast]); return (
@@ -65,4 +77,4 @@ function formatSyncedAt(iso: string | null): string { const hrs = Math.floor(mins / 60); if (hrs < 24) return `LAST SYNCED ${hrs}H AGO`; return `LAST SYNCED ${Math.floor(hrs / 24)}D AGO`; -} +} \ No newline at end of file diff --git a/src/app/(app)/issues/issues-list.tsx b/src/app/(app)/issues/issues-list.tsx index e3ce96c..cff6fb3 100644 --- a/src/app/(app)/issues/issues-list.tsx +++ b/src/app/(app)/issues/issues-list.tsx @@ -11,6 +11,7 @@ import { type IssuesPageResult, type RepoOption, } from '@/app/actions/issues'; +import { useToast } from '@/components/toast'; const DIFFICULTY_LABEL: Record = { E: 'L1', M: 'L2', H: 'L3' }; const DIFFICULTY_COLOR: Record = { @@ -36,7 +37,7 @@ function IssueCard({ actionPending, }: { issue: IssueWithStatus; - onClaim: (id: number) => void; + onClaim: (id: number, xpReward: number | null) => void; onUnclaim: (recId: number) => void; actionPending: boolean; }) { @@ -48,12 +49,8 @@ function IssueCard({ const handleCopy = async () => { await navigator.clipboard.writeText(issue.url); - setCopied(true); - - setTimeout(() => { - setCopied(false); - }, 1500); + setTimeout(() => setCopied(false), 1500); }; return ( @@ -160,7 +157,7 @@ function IssueCard({ ) : ( <>
+ ); -} +} \ No newline at end of file diff --git a/src/components/toast.tsx b/src/components/toast.tsx new file mode 100644 index 0000000..7cfb109 --- /dev/null +++ b/src/components/toast.tsx @@ -0,0 +1,194 @@ +'use client'; + +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; + +export type ToastVariant = 'success' | 'level-up'; + +interface ToastItem { + id: string; + message: string; + variant: ToastVariant; + exiting: boolean; +} + +interface ToastContextValue { + addToast: (message: string, variant?: ToastVariant) => void; +} + +const ToastContext = createContext(null); + +export function useToast(): ToastContextValue { + const ctx = useContext(ToastContext); + if (!ctx) throw new Error('useToast must be used within '); + return ctx; +} + + +const DURATION: Record = { + success: 4000, + 'level-up': 6000, +}; + +const EXIT_MS = 320; + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = useState([]); + const timers = useRef>>(new Map()); + + useEffect(() => { + const t = timers.current; + return () => t.forEach((id) => clearTimeout(id)); + }, []); + + const removeToast = useCallback((id: string) => { + setToasts((prev) => + prev.map((t) => (t.id === id ? { ...t, exiting: true } : t)), + ); + const timer = setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + timers.current.delete(id); + }, EXIT_MS); + timers.current.set(`exit-${id}`, timer); + }, []); + + const addToast = useCallback( + (message: string, variant: ToastVariant = 'success') => { + const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + const duration = DURATION[variant]; + + setToasts((prev) => [...prev, { id, message, variant, exiting: false }]); + + + const timer = setTimeout(() => removeToast(id), duration); + timers.current.set(id, timer); + }, + [removeToast], + ); + + return ( + + {children} + + +
+ {toasts.map((toast) => ( + removeToast(toast.id)} + /> + ))} +
+
+ ); +} + +function Toast({ + toast, + onDismiss, +}: { + toast: ToastItem; + onDismiss: () => void; +}) { + + const [entered, setEntered] = useState(false); + + useEffect(() => { + + const raf1 = requestAnimationFrame(() => { + const raf2 = requestAnimationFrame(() => setEntered(true)); + return () => cancelAnimationFrame(raf2); + }); + return () => cancelAnimationFrame(raf1); + }, []); + + const isLevelUp = toast.variant === 'level-up'; + const visible = entered && !toast.exiting; + + const enterEasing = 'cubic-bezier(0.34, 1.26, 0.64, 1)'; + const exitEasing = 'cubic-bezier(0.4, 0, 1, 1)'; + + return ( +
+
+ {isLevelUp ? ( + + ) : ( + + )} + + + {toast.message} + +
+ + +
+ ); +} From 40d16cf0e6cf492cc89374b7b4c1df17c19316a1 Mon Sep 17 00:00:00 2001 From: BHARGAVI CHAUDHARY Date: Sat, 16 May 2026 14:16:17 +0530 Subject: [PATCH 3/3] feat(toast): add XP and level-up toast notification system --- src/app/(app)/dashboard/level-up-banner.tsx | 30 +++++++++----- src/app/(app)/dashboard/rec-cards.tsx | 2 +- src/app/(app)/issues/issues-list.tsx | 7 ++-- src/components/toast.tsx | 45 ++++++++++++++++----- 4 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/app/(app)/dashboard/level-up-banner.tsx b/src/app/(app)/dashboard/level-up-banner.tsx index dc1c47f..51391a9 100644 --- a/src/app/(app)/dashboard/level-up-banner.tsx +++ b/src/app/(app)/dashboard/level-up-banner.tsx @@ -6,34 +6,46 @@ import { acknowledgeLevelUp, type LevelUpRow, } from '@/app/actions/level-ups'; +import { useToast } from '@/components/toast'; + -/** - * Reads any unacknowledged level-up rows on mount and shows a dismissable - * banner. Acknowledges optimistically so a refresh-spam can't re-trigger - * the same celebration. - */ export default function LevelUpBanner() { const [rows, setRows] = useState([]); const [, startTransition] = useTransition(); + const { addToast } = useToast(); useEffect(() => { let live = true; (async () => { const res = await getUnacknowledgedLevelUps(); - if (live && res.ok) setRows(res.data); + if (!live || !res.ok) return; + + const levelUps = res.data; + setRows(levelUps); + + + levelUps.forEach((row, i) => { + setTimeout(() => { + addToast(`โฌ† LEVEL UP โ€” YOU ARE NOW L${row.toLevel}`, 'level-up'); + }, i * 400); + }); })(); return () => { live = false; }; + }, []); + if (rows.length === 0) return null; const top = rows[0]!; function dismiss(id: number) { setRows((prev) => prev.filter((r) => r.id !== id)); - startTransition(async () => { - await acknowledgeLevelUp(id); + startTransition(() => { + void (async () => { + await acknowledgeLevelUp(id); + })(); }); } @@ -58,4 +70,4 @@ export default function LevelUpBanner() { ); -} +} \ No newline at end of file diff --git a/src/app/(app)/dashboard/rec-cards.tsx b/src/app/(app)/dashboard/rec-cards.tsx index c257e1c..4345211 100644 --- a/src/app/(app)/dashboard/rec-cards.tsx +++ b/src/app/(app)/dashboard/rec-cards.tsx @@ -230,4 +230,4 @@ function ClaimedActions({ rec, onError }: { rec: RecCard; onError: (msg: string )} ); -} \ No newline at end of file +} diff --git a/src/app/(app)/issues/issues-list.tsx b/src/app/(app)/issues/issues-list.tsx index cff6fb3..f24a68b 100644 --- a/src/app/(app)/issues/issues-list.tsx +++ b/src/app/(app)/issues/issues-list.tsx @@ -288,6 +288,7 @@ export function IssuesList({ return (
+ {/* Filters */}
@@ -366,12 +367,12 @@ export function IssuesList({
)} - + {/* Count */}
{isPending ? 'LOADING...' : `${initialData.total} ISSUES`}
- + {/* List */}
{initialData.issues.length === 0 ? (
@@ -390,7 +391,7 @@ export function IssuesList({ )}
- + {/* Pagination */} {totalPages > 1 && (
); -} +} \ No newline at end of file