+ {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
+
+
+
+
+ {/* 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 */}
+
-
+ )}
+ >
+ );
+}
+
+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..a87b962a 100644
--- a/contributors/ishanrajsingh/client/src/app/subscriptions/page.tsx
+++ b/contributors/ishanrajsingh/client/src/app/subscriptions/page.tsx
@@ -1,30 +1,37 @@
'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';
+import { useRef } from 'react';
+import EmailImportModal from '../components/subscriptions/EmailImportModal';
export default function SubscriptionsPage() {
const { getToken } = useAuth();
@@ -37,7 +44,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 +53,19 @@ 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);
+
+ const [isImporting, setIsImporting] = useState(false);
+ const [importSuccess, setImportSuccess] = useState(null);
+ const [importError, setImportError] = useState(null);
+ const [showImportModal, setShowImportModal] = useState(false);
+ const [importState, setImportState] = useState<
+ 'explain' | 'loading' | 'success' | 'error' | 'stopped'
+ >('explain');
+ const abortControllerRef = useRef(null);
// Fetch subscriptions from real API
useEffect(() => {
@@ -62,7 +81,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 +106,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 +120,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 +131,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 +173,98 @@ export default function SubscriptionsPage() {
}
};
- const handleEditSubscription = async (id: string, data: Partial) => {
+ const openEmailImportModal = () => {
+ setShowImportModal(true);
+ setImportState('explain');
+ setImportError(null);
+ setImportSuccess(null);
+ };
+
+ const handleEmailImport = async () => {
+ setImportState('loading');
+ setImportError(null);
+ setImportSuccess(null);
+
+ const start = Date.now();
+
+ const controller = new AbortController();
+ abortControllerRef.current = controller;
+
+ try {
+ const token = await getToken();
+ if (!token) throw new Error('Authentication required');
+
+ const endpoint = `${process.env.NEXT_PUBLIC_API_URL}/api/subscriptions/email/import`;
+
+ const res = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({}),
+ signal: controller.signal,
+ });
+
+ const rawText = await res.text();
+
+ let data: any;
+ try {
+ data = JSON.parse(rawText);
+ } catch {
+ throw new Error('Invalid server response');
+ }
+
+ if (!res.ok) {
+ throw new Error(data?.message || 'Email import failed');
+ }
+
+ const elapsed = Date.now() - start;
+ if (elapsed < 3000) {
+ await new Promise((r) => setTimeout(r, 3000 - elapsed));
+ }
+
+ const inserted = Number(data.inserted || 0);
+ const skipped = Number(data.skipped || 0);
+
+ setImportSuccess(
+ `Imported ${inserted} subscription${inserted !== 1 ? 's' : ''}` +
+ (skipped ? `, skipped ${skipped}` : '')
+ );
+
+ setImportState('success');
+
+ await handleRefresh();
+ } catch (err: any) {
+ const elapsed = Date.now() - start;
+ if (elapsed < 3000) {
+ await new Promise((r) => setTimeout(r, 3000 - elapsed));
+ }
+
+ if (err.name === 'AbortError') {
+ setImportError('Import stopped by user');
+ setImportState('stopped');
+ return;
+ }
+
+ setImportError(err.message || 'Failed to import subscriptions');
+ setImportState('error');
+ } finally {
+ abortControllerRef.current = null;
+ }
+};
+const handleStopImport = () => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+};
+
+
+
+ const handleEditSubscription = async (
+ id: string,
+ data: Partial
+ ) => {
const token = await getToken();
if (!token) {
throw new Error('Authentication required');
@@ -165,7 +287,10 @@ export default function SubscriptionsPage() {
};
return (
-
+
{/* Quick Stats */}
{!isLoading && subscriptions.length > 0 && (
@@ -191,7 +316,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,12 +333,35 @@ export default function SubscriptionsPage() {
-
-
-
- Add New
-
-
+
+ {/* Import from Email */}
+
+
+ {/* Add New */}
+
+
+ + Add New
+
+
+
+
@@ -240,7 +390,9 @@ export default function SubscriptionsPage() {
) : (
-
No subscriptions match your filters
+
+ No subscriptions match your filters
+