From 29c50f4ebfc5f9e1dae17b399cc14b3c12901230 Mon Sep 17 00:00:00 2001 From: Ishan Raj Singh Date: Mon, 19 Jan 2026 21:16:45 +0530 Subject: [PATCH 1/6] Create read only gmail o auth --- .../src/app/dashboard/SummaryWidget.tsx | 187 ---------- .../src/app/dashboard/SummaryWidgets.tsx | 157 ++++++++ .../src/app/dashboard/UpcomingRenewals.tsx | 168 +++++---- .../client/src/app/dashboard/page.tsx | 335 ++++++++++++------ .../ishanrajsingh/client/src/app/page.tsx | 326 +++++++++++++++-- .../client/src/app/subscriptions/page.tsx | 98 +++-- .../ishanrajsingh/server/package.json | 5 +- contributors/ishanrajsingh/server/src/app.js | 2 + .../server/src/config/googleOAuth.js | 14 + .../src/controllers/gmailAuthController.js | 52 +++ .../server/src/models/GmailToken.js | 10 + .../server/src/routes/gmailAuth.routes.js | 12 + 12 files changed, 933 insertions(+), 433 deletions(-) delete mode 100644 contributors/ishanrajsingh/client/src/app/dashboard/SummaryWidget.tsx create mode 100644 contributors/ishanrajsingh/client/src/app/dashboard/SummaryWidgets.tsx create mode 100644 contributors/ishanrajsingh/server/src/config/googleOAuth.js create mode 100644 contributors/ishanrajsingh/server/src/controllers/gmailAuthController.js create mode 100644 contributors/ishanrajsingh/server/src/models/GmailToken.js create mode 100644 contributors/ishanrajsingh/server/src/routes/gmailAuth.routes.js diff --git a/contributors/ishanrajsingh/client/src/app/dashboard/SummaryWidget.tsx b/contributors/ishanrajsingh/client/src/app/dashboard/SummaryWidget.tsx deleted file mode 100644 index 7c5178c4..00000000 --- a/contributors/ishanrajsingh/client/src/app/dashboard/SummaryWidget.tsx +++ /dev/null @@ -1,187 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { Card } from '@/components/ui/card'; -import { TrendingUp, Calendar, CreditCard, Zap } from 'lucide-react'; -import { useAuth } from '@clerk/nextjs'; - -interface Subscription { - amount: number; - billingCycle: 'monthly' | 'yearly' | string; - status: string; - isTrial?: boolean; -} - -type Metrics = { - monthlySpend: number; - yearlySpend: number; - activeCount: number; - trialCount: number; -}; - -const API_URL = - process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; - -async function fetchMetrics(token?: string | null): Promise { - const headers: Record = {}; - if (token) headers.Authorization = `Bearer ${token}`; - - const res = await fetch(`${API_URL}/api/subscriptions`, { headers }); - const json = await res.json(); - - const list: Subscription[] = Array.isArray(json.data) - ? json.data - : []; - const meta = json.meta || {}; - - let active = 0; - let trials = 0; - - list.forEach(s => { - if (s.status === 'active') active++; - if (s.isTrial) trials++; - }); - - return { - monthlySpend: - typeof meta.monthlySpend === 'number' ? meta.monthlySpend : 0, - yearlySpend: - typeof meta.yearlySpend === 'number' ? meta.yearlySpend : 0, - activeCount: active, - trialCount: trials, - }; -} - -/* ----------------------------- - Count-up animation (UNCHANGED) ------------------------------- */ -function useCountUp(target: number, duration = 900) { - const [value, setValue] = useState(0); - - useEffect(() => { - let startTime: number | null = null; - - const animate = (t: number) => { - if (!startTime) startTime = t; - const progress = Math.min((t - startTime) / duration, 1); - setValue(Math.floor(target * progress)); - if (progress < 1) requestAnimationFrame(animate); - else setValue(target); - }; - - requestAnimationFrame(animate); - return () => setValue(target); - }, [target, duration]); - - return value; -} - -export default function SummaryWidgets() { - const { getToken } = useAuth(); - const [metrics, setMetrics] = useState(null); - - useEffect(() => { - (async () => { - const token = await getToken?.(); - const data = await fetchMetrics(token); - setMetrics(data); - })(); - }, [getToken]); - - const monthly = useCountUp(metrics?.monthlySpend ?? 0); - const yearly = useCountUp(metrics?.yearlySpend ?? 0); - const active = useCountUp(metrics?.activeCount ?? 0); - const trials = useCountUp(metrics?.trialCount ?? 0); - - const cards = [ - { - label: 'Monthly Spend', - value: `₹${monthly.toLocaleString()}`, - icon: TrendingUp, - primary: true, - gradient: 'from-blue-500/20 via-blue-400/10 to-transparent', - glow: 'shadow-blue-500/30', - }, - { - label: 'Yearly Spend', - value: `₹${yearly.toLocaleString()}`, - icon: Calendar, - gradient: 'from-purple-500/20 via-purple-400/10 to-transparent', - glow: 'shadow-purple-500/20', - }, - { - label: 'Active Subscriptions', - value: active, - icon: CreditCard, - gradient: 'from-emerald-500/20 via-emerald-400/10 to-transparent', - glow: 'shadow-emerald-500/20', - }, - { - label: 'Trials', - value: trials, - icon: Zap, - gradient: 'from-amber-500/20 via-amber-400/10 to-transparent', - glow: 'shadow-amber-500/20', - }, - ]; - - return ( -
- {cards.map(card => { - const Icon = card.icon; - - return ( - - {/* Gradient wash */} -
- - {/* Content */} -
- {/* Top */} -
- - {card.label} - - -
- -
-
- - {/* Value */} - {metrics ? ( - - {card.value} - - ) : ( -
- )} -
- - {/* Hover glow */} -
-
-
- - ); - })} -
- ); -} diff --git a/contributors/ishanrajsingh/client/src/app/dashboard/SummaryWidgets.tsx b/contributors/ishanrajsingh/client/src/app/dashboard/SummaryWidgets.tsx new file mode 100644 index 00000000..1a00132e --- /dev/null +++ b/contributors/ishanrajsingh/client/src/app/dashboard/SummaryWidgets.tsx @@ -0,0 +1,157 @@ +import { Card } from '@/components/ui/card'; +import { useAuth } from '@clerk/nextjs'; +import { Calendar, CreditCard, TrendingUp, Zap } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +interface Subscription { + amount: number; + billingCycle: 'monthly' | 'yearly' | string; + status: string; + isTrial?: boolean; +} + +type Metrics = { + monthlySpend: number; + yearlySpend: number; + activeCount: number; + trialCount: number; +}; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; + +const fetchMetrics = async ( + token: string | null | undefined +): Promise => { + const headers: Record = {}; + if (token) headers['Authorization'] = `Bearer ${token}`; + + const res = await fetch(`${API_BASE_URL}/api/subscriptions`, { headers }); + const data = await res.json(); + + const subs = Array.isArray(data.data) ? data.data : []; + const meta = data.meta || {}; + + const monthlySpend = + typeof meta.monthlySpend === 'number' ? meta.monthlySpend : 0; + const yearlySpend = + typeof meta.yearlySpend === 'number' ? meta.yearlySpend : 0; + + let activeCount = 0; + let trialCount = 0; + + subs.forEach((sub: Subscription) => { + if (sub.status === 'active') activeCount++; + if (sub.isTrial) trialCount++; + }); + + return { monthlySpend, yearlySpend, activeCount, trialCount }; +}; + +// CountUp hook +function useCountUp(target: number, duration = 1000) { + const [value, setValue] = useState(0); + + useEffect(() => { + let startTime: number | null = null; + + function animate(ts: number) { + if (!startTime) startTime = ts; + const progress = Math.min((ts - startTime) / duration, 1); + + setValue(Math.floor(target * progress)); + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + setValue(target); + } + } + + requestAnimationFrame(animate); + }, [target, duration]); + + return value; +} + +const SummaryWidgets = () => { + const [metrics, setMetrics] = useState(null); + const { getToken } = useAuth(); + + useEffect(() => { + (async () => { + const token = await getToken?.(); + fetchMetrics(token).then(setMetrics); + })(); + }, [getToken]); + + const animatedMonthly = useCountUp(metrics?.monthlySpend ?? 0); + const animatedYearly = useCountUp(metrics?.yearlySpend ?? 0); + const animatedActive = useCountUp(metrics?.activeCount ?? 0); + const animatedTrial = useCountUp(metrics?.trialCount ?? 0); + + const widgetData = [ + { + label: 'Monthly Spend', + value: metrics ? `₹${animatedMonthly.toLocaleString()}` : null, + icon: , + highlight: true, + bg: 'bg-gradient-to-br from-[#101c2c] to-[#0a0f1a]', + ring: 'ring-2 ring-blue-500/40 shadow-blue-500/20', + }, + { + label: 'Yearly Spend', + value: metrics ? `₹${animatedYearly.toLocaleString()}` : null, + icon: , + highlight: false, + bg: 'bg-gradient-to-br from-[#1a102c] to-[#120a1a]', + ring: 'ring-1 ring-purple-900/30', + }, + { + label: 'Active Subscriptions', + value: metrics ? animatedActive : null, + icon: , + highlight: false, + bg: 'bg-gradient-to-br from-[#102c1a] to-[#0a1a12]', + ring: 'ring-1 ring-green-900/30', + }, + { + label: 'Trials Count', + value: metrics ? animatedTrial : null, + icon: , + highlight: false, + bg: 'bg-gradient-to-br from-[#2c2410] to-[#1a150a]', + ring: 'ring-1 ring-yellow-900/30', + }, + ]; + + return ( +
+ {widgetData.map((w) => ( + +
{w.icon}
+ + + {w.label} + + + {w.value !== null ? ( + + {w.value} + + ) : ( + + )} +
+ ))} +
+ ); +}; + +export default SummaryWidgets; diff --git a/contributors/ishanrajsingh/client/src/app/dashboard/UpcomingRenewals.tsx b/contributors/ishanrajsingh/client/src/app/dashboard/UpcomingRenewals.tsx index f5b29841..31cc0d95 100644 --- a/contributors/ishanrajsingh/client/src/app/dashboard/UpcomingRenewals.tsx +++ b/contributors/ishanrajsingh/client/src/app/dashboard/UpcomingRenewals.tsx @@ -1,96 +1,124 @@ -'use client'; +import { Card } from '@/components/ui/card'; +import { getServiceColors, getServiceIcon } from '@/lib/service-icons'; +import { getDaysUntilRenewal } from '@/lib/utils'; +import { format } from 'date-fns'; +import { AlertTriangle, Calendar } from 'lucide-react'; +import React from 'react'; -import { Subscription } from '@/lib/api'; -import { AlertTriangle, Clock } from 'lucide-react'; - -function daysUntil(dateISO: string) { - const today = new Date(); - const target = new Date(dateISO); - - today.setHours(0, 0, 0, 0); - target.setHours(0, 0, 0, 0); - - return Math.ceil((target.getTime() - today.getTime()) / 86400000); +interface Subscription { + name: string; + renewalDate: string; + amount: number; + billingCycle: string; + isTrial?: boolean; + status: string; } -function formatDate(dateISO: string) { - return new Date(dateISO).toLocaleDateString(undefined, { - day: 'numeric', - month: 'short', - year: 'numeric', - }); +interface UpcomingRenewalsProps { + subscriptions: Subscription[]; } -type Props = { - subscriptions: Subscription[]; +const getUrgency = (days: number) => { + if (days <= 7) return 'urgent'; + if (days <= 30) return 'soon'; + return 'normal'; }; -export default function UpcomingRenewals({ subscriptions }: Props) { +const UpcomingRenewals: React.FC = ({ + subscriptions, +}) => { const upcoming = subscriptions - .filter(s => s.status === 'active') - .map(s => ({ ...s, daysLeft: daysUntil(s.renewalDate) })) - .filter(s => s.daysLeft >= 0 && s.daysLeft <= 30) - .sort((a, b) => a.daysLeft - b.daysLeft); + .filter((sub) => { + const days = getDaysUntilRenewal(sub.renewalDate); + return sub.status === 'active' && days >= 0 && days <= 30; + }) + .sort( + (a, b) => + new Date(a.renewalDate).getTime() - new Date(b.renewalDate).getTime() + ); if (upcoming.length === 0) { return ( -
-

- Upcoming Renewals -

-

- No renewals in the next 30 days 🎉 -

-
+ + + No upcoming renewals in the next 30 days. + + ); } return ( -
-
- -

- Upcoming Renewals -

+
+
+ +

Upcoming Renewals

- -
- {upcoming.map(sub => { - const urgent = sub.daysLeft <= 7; - +
+ {upcoming.map((sub) => { + const days = getDaysUntilRenewal(sub.renewalDate); + const urgency = getUrgency(days); + const icon = getServiceIcon(sub.name); + const colors = getServiceColors(sub.name); return ( -
-
-
- {sub.name} - {urgent && ( - - - Urgent +
+ {icon && ( + + + {icon} - )} -
-
- Renews on {formatDate(sub.renewalDate)} + + )} +
+ + {sub.name} + + + {format(new Date(sub.renewalDate), 'MMM d, yyyy')} •{' '} + {sub.billingCycle} +
- -
- {sub.daysLeft === 0 - ? 'Renews today' - : `Renews in ${sub.daysLeft} days`} +
+ + ₹{sub.amount} + + + {urgency === 'urgent' && ( + + )} + Renews in {days} day{days !== 1 ? 's' : ''} +
-
+ ); })}
-
+
); -} +}; + +export default UpcomingRenewals; diff --git a/contributors/ishanrajsingh/client/src/app/dashboard/page.tsx b/contributors/ishanrajsingh/client/src/app/dashboard/page.tsx index b5b4f078..7172ad0d 100644 --- a/contributors/ishanrajsingh/client/src/app/dashboard/page.tsx +++ b/contributors/ishanrajsingh/client/src/app/dashboard/page.tsx @@ -1,12 +1,16 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { Subscription } from '@/lib/api'; +import { getServiceColors, getServiceIcon } from '@/lib/service-icons'; +import { + cn, + formatCurrency, + formatDate, + getCategoryColor, + getDaysUntilRenewal, + isUrgentRenewal, +} from '@/lib/utils'; import { useAuth } from '@clerk/nextjs'; -import Link from 'next/link'; -import DashboardLayout from '../components/DashboardLayout'; -import SummaryWidgets from './SummaryWidget'; -import UpcomingRenewals from './UpcomingRenewals'; - import { AlertTriangle, ArrowRight, @@ -14,17 +18,11 @@ import { Plus, TrendingUp, } from 'lucide-react'; - -import { Subscription } from '@/lib/api'; -import { - cn, - formatCurrency, - getDaysUntilRenewal, - isUrgentRenewal, - getCategoryColor, - formatDate, -} from '@/lib/utils'; -import { getServiceIcon, getServiceColors } from '@/lib/service-icons'; +import Link from 'next/link'; +import { useEffect, useState } from 'react'; +import DashboardLayout from '../components/DashboardLayout'; +import SummaryWidgets from './SummaryWidgets'; +import UpcomingRenewals from './UpcomingRenewals'; // Mock data for demonstration const mockSubscriptions: Subscription[] = [ @@ -36,7 +34,7 @@ const mockSubscriptions: Subscription[] = [ currency: 'USD', billingCycle: 'monthly', category: 'entertainment', - renewalDate: new Date(Date.now() + 2 * 86400000).toISOString(), + renewalDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(), isTrial: false, source: 'manual', status: 'active', @@ -77,7 +75,7 @@ const mockSubscriptions: Subscription[] = [ _id: '4', userId: 'user1', name: 'ChatGPT Plus', - amount: 20, + amount: 20.0, currency: 'USD', billingCycle: 'monthly', category: 'productivity', @@ -93,7 +91,7 @@ const mockSubscriptions: Subscription[] = [ _id: '5', userId: 'user1', name: 'GitHub Copilot', - amount: 10, + amount: 10.0, currency: 'USD', billingCycle: 'monthly', category: 'productivity', @@ -108,7 +106,7 @@ const mockSubscriptions: Subscription[] = [ _id: '6', userId: 'user1', name: 'Notion', - amount: 8, + amount: 8.0, currency: 'USD', billingCycle: 'monthly', category: 'productivity', @@ -121,67 +119,107 @@ const mockSubscriptions: Subscription[] = [ }, ]; -export default function DashboardPage() { +export default function Dashboard() { const { getToken } = useAuth(); - const [subs, setSubs] = useState([]); - const [loading, setLoading] = useState(true); + const [subscriptions, setSubscriptions] = useState([]); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { - const load = async () => { + const fetchData = async () => { try { - await new Promise(r => setTimeout(r, 600)); - setSubs(mockSubscriptions); + const token = await getToken?.(); + + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/subscriptions`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + const data = await res.json(); + setSubscriptions(Array.isArray(data.data) ? data.data : []); + } catch (err) { + console.error('Failed to fetch subscriptions', err); } finally { - setLoading(false); + setIsLoading(false); } }; - load(); + + fetchData(); }, [getToken]); - const activeSubs = subs.filter(s => s.status === 'active'); - const urgent = subs.filter( - s => s.status === 'active' && isUrgentRenewal(s.renewalDate), + // Calculate stats + const activeSubscriptions = subscriptions.filter( + (s) => s.status === 'active' + ); + const monthlyTotal = activeSubscriptions.reduce((sum, sub) => { + if (sub.billingCycle === 'monthly') return sum + sub.amount; + if (sub.billingCycle === 'yearly') return sum + sub.amount / 12; + if (sub.billingCycle === 'weekly') return sum + sub.amount * 4.33; + return sum + sub.amount; + }, 0); + const urgentRenewals = subscriptions.filter( + (s) => s.status === 'active' && isUrgentRenewal(s.renewalDate) + ); + const trialsEnding = subscriptions.filter( + (s) => s.isTrial && s.trialEndsAt && getDaysUntilRenewal(s.trialEndsAt) <= 7 ); - const upcoming = [...activeSubs] + // Get recent subscriptions sorted by renewal date + const recentSubscriptions = [...subscriptions] + .filter((s) => s.status === 'active') .sort( (a, b) => - new Date(a.renewalDate).getTime() - - new Date(b.renewalDate).getTime(), + new Date(a.renewalDate).getTime() - new Date(b.renewalDate).getTime() ) .slice(0, 5); - if (loading) { + if (isLoading) { return ( - -
- + +
+
+ +

Loading dashboard...

+
); } return ( - - {/* Welcome */} -
-
+ + {/* Welcome Banner */} +
+
-

Welcome back 👋

+

+ Welcome back! 👋 +

- {urgent.length > 0 ? ( + You have{' '} + {urgentRenewals.length > 0 ? ( - {urgent.length} renewal{urgent.length > 1 && 's'} soon + {urgentRenewals.length} upcoming renewal + {urgentRenewals.length > 1 ? 's' : ''} ) : ( - 'No urgent renewals' - )} + 'no urgent renewals' + )}{' '} + in the next 3 days.

- Add Subscription @@ -189,73 +227,83 @@ export default function DashboardPage() {
- {/* Summary Widgets */} + {/* Dashboard Summary Widgets */} - {/* 🔥 ADDED: Issue 15 Upcoming Renewals Section */} - + {/* Upcoming Renewals Section */} + - {/* Existing Main Grid (UNCHANGED) */} -
-
-
+ {/* Main Content Grid */} +
+ {/* Recent Subscriptions */} +
+

Upcoming Renewals

View all
- {upcoming.length === 0 ? ( -

- No active subscriptions -

+ {recentSubscriptions.length === 0 ? ( +
+

No active subscriptions

+ + Add your first subscription + +
) : (
- {upcoming.map(sub => { - const days = getDaysUntilRenewal(sub.renewalDate); - const urgentFlag = isUrgentRenewal(sub.renewalDate); + {recentSubscriptions.map((sub) => { + const daysUntil = getDaysUntilRenewal(sub.renewalDate); + const isUrgent = isUrgentRenewal(sub.renewalDate); const categoryColors = getCategoryColor(sub.category); - const icon = getServiceIcon(sub.name); + const initials = sub.name + .split(' ') + .map((w) => w[0]) + .slice(0, 2) + .join('') + .toUpperCase(); + + // Get service-specific icon and colors + const serviceIcon = getServiceIcon(sub.name); const serviceColors = getServiceColors(sub.name); + const iconBg = serviceColors?.bg || categoryColors.bg; + const iconText = serviceColors?.text || categoryColors.text; return (
- {icon || - sub.name - .split(' ') - .map(w => w[0]) - .slice(0, 2) - .join('') - .toUpperCase()} + {serviceIcon || initials}
- -
+
- + {sub.name} {sub.isTrial && ( - + Trial )} @@ -264,23 +312,28 @@ export default function DashboardPage() { {sub.category}
- +
+
+ {formatCurrency(sub.amount)} +
+
+ /{sub.billingCycle === 'yearly' ? 'year' : 'mo'} +
+
-
- {urgentFlag && ( - - )} - - {days === 0 +
+ {isUrgent && } + + {daysUntil === 0 ? 'Today' - : days === 1 - ? 'Tomorrow' - : `${days}d`} + : daysUntil === 1 + ? 'Tomorrow' + : `${daysUntil}d`}
@@ -294,25 +347,91 @@ export default function DashboardPage() { )}
+ {/* Quick Actions & Insights */}
-
-

+ {/* Quick Actions */} +
+

Quick Actions

- - -
-
- View Analytics +
+ +
+
-
- Spending insights +
+
Add Subscription
+
+ Track a new service +
-
- + + +
+ +
+
+
View Analytics
+
+ See spending trends +
+
+ +
+
+ + {/* Spending Insight */} +
+
+ +

Spending Insight

+
+

+ Your productivity tools account for{' '} + + {formatCurrency( + subscriptions + .filter( + (s) => + s.category === 'productivity' && s.status === 'active' + ) + .reduce((sum, s) => sum + s.amount, 0) + )} + {' '} + of your monthly spend. +

+
+ {Array.from( + new Set( + subscriptions + .filter((s) => s.status === 'active') + .map((s) => s.category) + ) + ).map((category) => { + const colors = getCategoryColor(category); + const count = subscriptions.filter( + (s) => s.category === category && s.status === 'active' + ).length; + return ( + + {category} ({count}) + + ); + })} +
diff --git a/contributors/ishanrajsingh/client/src/app/page.tsx b/contributors/ishanrajsingh/client/src/app/page.tsx index ee91f08e..29bda5ae 100644 --- a/contributors/ishanrajsingh/client/src/app/page.tsx +++ b/contributors/ishanrajsingh/client/src/app/page.tsx @@ -1,41 +1,303 @@ -import { SignedIn, SignedOut, SignInButton, SignUpButton } from '@clerk/nextjs'; +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useUser } from '@clerk/nextjs'; import Link from 'next/link'; +const INTRO_STORAGE_KEY = 'subsentry_intro_seen'; + export default function Home() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { isLoaded, isSignedIn } = useUser(); + const [showIntro, setShowIntro] = useState(false); + const [isExiting, setIsExiting] = useState(false); + const [videoError, setVideoError] = useState(false); + + useEffect(() => { + if (typeof window === 'undefined') return; + if (!isLoaded) return; + + if (isSignedIn) { + router.replace('/dashboard'); + return; + } + + const forceIntro = searchParams?.get('intro') === '1'; + const seen = window.localStorage.getItem(INTRO_STORAGE_KEY); + if (forceIntro || !seen) { + setShowIntro(true); + } + }, [isLoaded, isSignedIn, router, searchParams]); + + const finishIntro = useCallback(() => { + if (typeof window !== 'undefined') { + window.localStorage.setItem(INTRO_STORAGE_KEY, '1'); + } + setIsExiting(true); + setTimeout(() => { + setShowIntro(false); + }, 300); + }, []); + + const handleSkip = useCallback(() => { + finishIntro(); + }, [finishIntro]); + + useEffect(() => { + if (!showIntro || typeof window === 'undefined') return; + + const controller = new AbortController(); + setIsExiting(false); + setVideoError(false); + + fetch('/fintech-intro.mp4', { method: 'HEAD', signal: controller.signal }) + .then((res) => { + if (!res.ok) { + throw new Error('intro video missing'); + } + }) + .catch(() => { + setVideoError(true); + finishIntro(); + }); + + return () => controller.abort(); + }, [showIntro, finishIntro]); + + if (!isLoaded) { + return null; + } + return ( -
-
-

SubSentry

- -

- Secure subscription management with industry-grade authentication. -

- -
- - - - - - - - - - - - - Go to Dashboard + <> + {showIntro && ( +
+
+ )} + + {!showIntro && !isSignedIn && ( +
+ {/* Gradient Background Effects */} +
+
+
+
+
+ + {/* Grid Pattern Overlay */} +
+ + {/* Navigation */} + + + {/* Hero Section */} +
+ {/* Badge */} +
+
+ Trusted by 10,000+ users worldwide +
+ + {/* Main Heading */} +

+ Never lose track of +
+ + subscriptions again + +

+ + {/* Subheading */} +

+ SubSentry helps you manage all your subscriptions in one place. Get real-time alerts, + track spending, and save money effortlessly. +

+ + {/* CTA Buttons */} + {/* CTA Buttons */} +
+ + + + + + +
+ + {/* Micro-copy */} +
+
+ + + + No credit card required +
+
+ + + + Free forever plan +
+
+ + + + Cancel anytime +
+
+ + {/* Feature Cards */} +
+ + + + } + title="Smart Alerts" + description="Get notified before every renewal. Never be surprised by unexpected charges again." + gradient="from-blue-500 to-cyan-500" + /> + + + + } + title="Analytics Dashboard" + description="Visualize your spending patterns and find opportunities to save money." + gradient="from-purple-500 to-pink-500" + /> + + + + } + title="Bank-grade Security" + description="256-bit encryption and SOC 2 compliance keep your data safe and private." + gradient="from-green-500 to-emerald-500" + /> +
+
+ + {/* Footer */} +
+
+ Privacy Policy + Terms of Service + Contact +
+

+ © 2026 SubSentry. All rights reserved. +

+
-
+ )} + + ); +} + +function FeatureCard({ + icon, + title, + description, + gradient, +}: { + icon: React.ReactNode; + title: string; + description: string; + gradient: string; +}) { + return ( +
+
+ {icon} +
+

{title}

+

{description}

+ {/* Hover glow effect */} +
); } diff --git a/contributors/ishanrajsingh/client/src/app/subscriptions/page.tsx b/contributors/ishanrajsingh/client/src/app/subscriptions/page.tsx index 6a858582..75371d76 100644 --- a/contributors/ishanrajsingh/client/src/app/subscriptions/page.tsx +++ b/contributors/ishanrajsingh/client/src/app/subscriptions/page.tsx @@ -1,30 +1,35 @@ 'use client'; -import { useState, useEffect, useMemo } from 'react'; +import { ShimmerButton } from '@/components/ui/aceternity'; +import { Button } from '@/components/ui/button'; +import { + Subscription, + deleteSubscription, + getSubscriptions, + updateSubscription, +} from '@/lib/api'; +import { cn } from '@/lib/utils'; import { useAuth } from '@clerk/nextjs'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Loader2, RefreshCw } from 'lucide-react'; import Link from 'next/link'; -import { Plus, Loader2, RefreshCw } from 'lucide-react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { useEffect, useMemo, useState } from 'react'; import DashboardLayout from '../components/DashboardLayout'; import { - SubscriptionCard, - FilterBar, - SortDropdown, - ViewToggle, EmptyState, - QuickStats, - FilterStatus, + FilterBar, FilterBillingCycle, FilterCategory, + FilterStatus, + QuickStats, + RemoveSubscriptionDialog, + SortDropdown, SortField, SortOrder, + SubscriptionCard, UpdateSubscriptionModal, - RemoveSubscriptionDialog, + ViewToggle, } from '../components/subscriptions'; -import { Subscription, getSubscriptions, updateSubscription, deleteSubscription } from '@/lib/api'; -import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; -import { ShimmerButton } from '@/components/ui/aceternity'; export default function SubscriptionsPage() { const { getToken } = useAuth(); @@ -37,7 +42,8 @@ export default function SubscriptionsPage() { // Filter state const [statusFilter, setStatusFilter] = useState('all'); - const [billingCycleFilter, setBillingCycleFilter] = useState('all'); + const [billingCycleFilter, setBillingCycleFilter] = + useState('all'); const [categoryFilter, setCategoryFilter] = useState('all'); // Sort state @@ -45,8 +51,10 @@ export default function SubscriptionsPage() { const [sortOrder, setSortOrder] = useState('asc'); // Edit/Delete modal state - const [editingSubscription, setEditingSubscription] = useState(null); - const [deletingSubscription, setDeletingSubscription] = useState(null); + const [editingSubscription, setEditingSubscription] = + useState(null); + const [deletingSubscription, setDeletingSubscription] = + useState(null); // Fetch subscriptions from real API useEffect(() => { @@ -62,7 +70,9 @@ export default function SubscriptionsPage() { const data = await getSubscriptions(token); setSubscriptions(data.data || []); } catch (err) { - setError('Failed to load subscriptions. Make sure the server is running.'); + setError( + 'Failed to load subscriptions. Make sure the server is running.' + ); console.error(err); } finally { setIsLoading(false); @@ -85,13 +95,13 @@ export default function SubscriptionsPage() { // Apply filters if (statusFilter !== 'all') { - result = result.filter(s => s.status === statusFilter); + result = result.filter((s) => s.status === statusFilter); } if (billingCycleFilter !== 'all') { - result = result.filter(s => s.billingCycle === billingCycleFilter); + result = result.filter((s) => s.billingCycle === billingCycleFilter); } if (categoryFilter !== 'all') { - result = result.filter(s => s.category === categoryFilter); + result = result.filter((s) => s.category === categoryFilter); } // Apply sorting @@ -99,7 +109,9 @@ export default function SubscriptionsPage() { let comparison = 0; switch (sortField) { case 'renewalDate': - comparison = new Date(a.renewalDate).getTime() - new Date(b.renewalDate).getTime(); + comparison = + new Date(a.renewalDate).getTime() - + new Date(b.renewalDate).getTime(); break; case 'amount': comparison = a.amount - b.amount; @@ -108,14 +120,22 @@ export default function SubscriptionsPage() { comparison = a.name.localeCompare(b.name); break; case 'createdAt': - comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + comparison = + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); break; } return sortOrder === 'asc' ? comparison : -comparison; }); return result; - }, [subscriptions, statusFilter, billingCycleFilter, categoryFilter, sortField, sortOrder]); + }, [ + subscriptions, + statusFilter, + billingCycleFilter, + categoryFilter, + sortField, + sortOrder, + ]); const clearFilters = () => { setStatusFilter('all'); @@ -142,7 +162,10 @@ export default function SubscriptionsPage() { } }; - const handleEditSubscription = async (id: string, data: Partial) => { + const handleEditSubscription = async ( + id: string, + data: Partial + ) => { const token = await getToken(); if (!token) { throw new Error('Authentication required'); @@ -165,7 +188,10 @@ export default function SubscriptionsPage() { }; return ( - + {/* Quick Stats */} {!isLoading && subscriptions.length > 0 && (
@@ -191,7 +217,9 @@ export default function SubscriptionsPage() { sortField={sortField} sortOrder={sortOrder} onSortChange={setSortField} - onOrderToggle={() => setSortOrder(o => o === 'asc' ? 'desc' : 'asc')} + onOrderToggle={() => + setSortOrder((o) => (o === 'asc' ? 'desc' : 'asc')) + } /> @@ -206,10 +234,9 @@ export default function SubscriptionsPage() { - - - - Add New + + + + Add New
@@ -240,7 +267,9 @@ export default function SubscriptionsPage() { ) : (
-

No subscriptions match your filters

+

+ No subscriptions match your filters +