Skip to content

Latest commit

 

History

History
3439 lines (3167 loc) · 143 KB

File metadata and controls

3439 lines (3167 loc) · 143 KB

PHASE 4: Frontend Web App (Next.js)

4.1 Supabase Client Setup

client/src/lib/supabase.js

// ====================================
// Supabase Client for Frontend
// ====================================
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

/**
 * Get the current session
 */
export const getSession = async () => {
    const { data: { session } } = await supabase.auth.getSession();
    return session;
};

/**
 * Get auth token for API calls
 */
export const getAuthToken = async () => {
    const session = await getSession();
    return session?.access_token || null;
};

client/src/lib/api.js

// ====================================
// API Client — Axios wrapper
// ====================================
import axios from 'axios';
import { getAuthToken } from './supabase';

const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api';

const api = axios.create({
    baseURL: API_BASE,
    timeout: 15000,
    headers: {
        'Content-Type': 'application/json'
    }
});

// Attach auth token to every request
api.interceptors.request.use(async (config) => {
    const token = await getAuthToken();
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});

// Handle response errors globally
api.interceptors.response.use(
    (response) => response.data,
    (error) => {
        const message = error.response?.data?.message || 'Something went wrong';
        console.error('API Error:', message);

        if (error.response?.status === 401) {
            // Redirect to login if unauthorized
            if (typeof window !== 'undefined') {
                window.location.href = '/login';
            }
        }

        return Promise.reject({ message, status: error.response?.status });
    }
);

// ============ API METHODS ============

// Auth
export const authAPI = {
    register: (data) => api.post('/auth/register', data),
    login: (data) => api.post('/auth/login', data),
    getMe: () => api.get('/auth/me'),
    logout: () => api.post('/auth/logout'),
};

// Tasks
export const taskAPI = {
    getAvailable: (params) => api.get('/tasks', { params }),
    getMyTasks: (params) => api.get('/tasks/my', { params }),
    getTodaysTask: () => api.get('/tasks/today'),
    getById: (id) => api.get(`/tasks/${id}`),
    accept: (id) => api.post(`/tasks/${id}/accept`),
    submit: (id, formData) => api.post(`/tasks/${id}/submit`, formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
    }),
    getCategories: () => api.get('/tasks/categories/list'),
};

// Issues
export const issueAPI = {
    getAll: (params) => api.get('/issues', { params }),
    getById: (id) => api.get(`/issues/${id}`),
    create: (formData) => api.post('/issues', formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
    }),
    addUpdate: (id, formData) => api.post(`/issues/${id}/update`, formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
    }),
    upvote: (id) => api.post(`/issues/${id}/upvote`),
    getAIAnalysis: (id) => api.get(`/issues/${id}/ai-analysis`),
};

// Circles
export const circleAPI = {
    getAll: (params) => api.get('/circles', { params }),
    getById: (id) => api.get(`/circles/${id}`),
    create: (data) => api.post('/circles', data),
    join: (id) => api.post(`/circles/${id}/join`),
    adoptIssue: (id, data) => api.post(`/circles/${id}/adopt-issue`, data),
};

// Wards
export const wardAPI = {
    getAll: () => api.get('/wards'),
    getDashboard: (id) => api.get(`/wards/${id}/dashboard`),
    getLeaderboard: () => api.get('/wards/leaderboard/global'),
    getWardLeaderboard: (id) => api.get(`/wards/${id}/leaderboard`),
};

// Portfolio
export const portfolioAPI = {
    get: (userId) => api.get(`/portfolio/${userId}`),
    generateSummary: (userId) => api.get(`/portfolio/${userId}/generate-summary`),
    getCertificate: (userId) => api.get(`/portfolio/${userId}/certificate`),
};

// Users
export const userAPI = {
    getProfile: () => api.get('/users/profile'),
    updateProfile: (data) => api.put('/users/profile', data),
    updateAvatar: (formData) => api.put('/users/avatar', formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
    }),
    completeOnboarding: (data) => api.post('/users/onboarding', data),
    getStats: () => api.get('/users/stats'),
    getNotifications: () => api.get('/users/notifications'),
    markNotificationRead: (id) => api.put(`/users/notifications/${id}/read`),
};

export default api;

client/src/lib/utils.js

// ====================================
// Utility Functions
// ====================================
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";

/**
 * Merge Tailwind classes
 */
export function cn(...inputs) {
    return twMerge(clsx(inputs));
}

/**
 * Format XP number with comma separators
 */
export function formatXP(xp) {
    return new Intl.NumberFormat('en-IN').format(xp || 0);
}

/**
 * Get level color based on level name
 */
export function getLevelColor(level) {
    const colors = {
        'Newcomer': 'text-gray-500',
        'Curious Citizen': 'text-green-500',
        'Active Citizen': 'text-blue-500',
        'Ward Warrior': 'text-purple-500',
        'Civic Champion': 'text-orange-500',
        'CivicStreak Fellow': 'text-yellow-500',
    };
    return colors[level] || 'text-gray-500';
}

/**
 * Get level badge emoji
 */
export function getLevelEmoji(level) {
    const emojis = {
        'Newcomer': '🌱',
        'Curious Citizen': '🔥',
        'Active Citizen': '💪',
        'Ward Warrior': '⚔️',
        'Civic Champion': '🏆',
        'CivicStreak Fellow': '🎖️',
    };
    return emojis[level] || '🌱';
}

/**
 * Get category icon
 */
export function getCategoryIcon(category) {
    const icons = {
        'DOCUMENT': '📸',
        'LEARN': '📝',
        'VOICE': '🌊',
        'CONNECT': '🤝',
        'TRACK': '📊',
        'MENTOR': '🎓',
    };
    return icons[category] || '📋';
}

/**
 * Get status badge styling
 */
export function getStatusStyle(status) {
    const styles = {
        'reported': 'bg-yellow-100 text-yellow-800',
        'acknowledged': 'bg-blue-100 text-blue-800',
        'in_progress': 'bg-purple-100 text-purple-800',
        'resolved': 'bg-green-100 text-green-800',
        'stale': 'bg-red-100 text-red-800',
    };
    return styles[status] || 'bg-gray-100 text-gray-800';
}

/**
 * Time ago formatter
 */
export function timeAgo(date) {
    const seconds = Math.floor((new Date() - new Date(date)) / 1000);

    const intervals = [
        { label: 'year', seconds: 31536000 },
        { label: 'month', seconds: 2592000 },
        { label: 'week', seconds: 604800 },
        { label: 'day', seconds: 86400 },
        { label: 'hour', seconds: 3600 },
        { label: 'minute', seconds: 60 },
    ];

    for (const interval of intervals) {
        const count = Math.floor(seconds / interval.seconds);
        if (count >= 1) {
            return `${count} ${interval.label}${count > 1 ? 's' : ''} ago`;
        }
    }
    return 'Just now';
}

4.2 Auth Context Provider

client/src/context/AuthContext.js

'use client';
// ====================================
// Auth Context — Global auth state
// ====================================
import { createContext, useContext, useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
import { authAPI, userAPI } from '@/lib/api';

const AuthContext = createContext({});

export const useAuth = () => useContext(AuthContext);

export function AuthProvider({ children }) {
    const [user, setUser] = useState(null);
    const [profile, setProfile] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        // Check initial session
        checkUser();

        // Listen for auth changes
        const { data: { subscription } } = supabase.auth.onAuthStateChange(
            async (event, session) => {
                if (event === 'SIGNED_IN' && session) {
                    setUser(session.user);
                    await fetchProfile();
                } else if (event === 'SIGNED_OUT') {
                    setUser(null);
                    setProfile(null);
                }
            }
        );

        return () => subscription?.unsubscribe();
    }, []);

    const checkUser = async () => {
        try {
            const { data: { session } } = await supabase.auth.getSession();
            if (session) {
                setUser(session.user);
                await fetchProfile();
            }
        } catch (error) {
            console.error('Check user error:', error);
        } finally {
            setLoading(false);
        }
    };

    const fetchProfile = async () => {
        try {
            const response = await authAPI.getMe();
            if (response.success) {
                setProfile(response.data);
            }
        } catch (error) {
            console.error('Fetch profile error:', error);
        }
    };

    const signUp = async (email, password, metadata) => {
        const { data, error } = await supabase.auth.signUp({
            email,
            password,
            options: { data: metadata }
        });
        if (error) throw error;
        return data;
    };

    const signIn = async (email, password) => {
        const { data, error } = await supabase.auth.signInWithPassword({
            email,
            password
        });
        if (error) throw error;
        await fetchProfile();
        return data;
    };

    const signOut = async () => {
        await supabase.auth.signOut();
        setUser(null);
        setProfile(null);
    };

    const refreshProfile = async () => {
        await fetchProfile();
    };

    return (
        <AuthContext.Provider value={{
            user,
            profile,
            loading,
            signUp,
            signIn,
            signOut,
            refreshProfile,
            isAuthenticated: !!user
        }}>
            {children}
        </AuthContext.Provider>
    );
}

4.3 Layout & Root Page

client/src/app/layout.js

// ====================================
// Root Layout
// ====================================
import { Inter } from 'next/font/google';
import './globals.css';
import { AuthProvider } from '@/context/AuthContext';
import { Toaster } from 'react-hot-toast';

const inter = Inter({ subsets: ['latin'] });

export const metadata = {
    title: 'CivicStreak — Your City, Your Commitment',
    description: 'Transforming episodic volunteering into sustained civic habit through gamified micro-engagement',
    keywords: 'civic engagement, youth, democracy, Mumbai, India, volunteering',
};

export default function RootLayout({ children }) {
    return (
        <html lang="en">
            <body className={inter.className}>
                <AuthProvider>
                    <Toaster
                        position="top-right"
                        toastOptions={{
                            duration: 4000,
                            style: {
                                background: '#1a1a2e',
                                color: '#fff',
                                borderRadius: '12px',
                            },
                        }}
                    />
                    {children}
                </AuthProvider>
            </body>
        </html>
    );
}

client/src/app/globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
    --primary: #6366f1;       /* Indigo */
    --primary-dark: #4f46e5;
    --secondary: #f59e0b;     /* Amber — for streaks */
    --success: #10b981;       /* Emerald */
    --danger: #ef4444;
    --bg-dark: #0f172a;       /* Slate 900 */
    --bg-card: #1e293b;       /* Slate 800 */
    --text-primary: #f8fafc;  /* Slate 50 */
    --text-secondary: #94a3b8; /* Slate 400 */
}

@layer base {
    body {
        @apply bg-slate-950 text-white min-h-screen;
    }
}

@layer components {
    .card {
        @apply bg-slate-800/50 backdrop-blur-sm border border-slate-700/50 
               rounded-2xl p-6 shadow-lg;
    }

    .btn-primary {
        @apply bg-indigo-600 hover:bg-indigo-700 text-white font-semibold 
               py-3 px-6 rounded-xl transition-all duration-200 
               active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed;
    }

    .btn-secondary {
        @apply bg-slate-700 hover:bg-slate-600 text-white font-semibold 
               py-3 px-6 rounded-xl transition-all duration-200 active:scale-95;
    }

    .btn-success {
        @apply bg-emerald-600 hover:bg-emerald-700 text-white font-semibold 
               py-3 px-6 rounded-xl transition-all duration-200 active:scale-95;
    }

    .input-field {
        @apply w-full bg-slate-700/50 border border-slate-600 rounded-xl 
               py-3 px-4 text-white placeholder-slate-400 
               focus:outline-none focus:ring-2 focus:ring-indigo-500 
               focus:border-transparent transition-all;
    }

    .badge {
        @apply inline-flex items-center px-3 py-1 rounded-full text-xs 
               font-semibold;
    }

    .streak-fire {
        @apply text-amber-400 animate-pulse;
    }

    .xp-glow {
        @apply text-indigo-400 font-bold;
    }

    .glass-card {
        @apply bg-white/5 backdrop-blur-lg border border-white/10 
               rounded-2xl p-6 shadow-2xl;
    }
}

/* Custom scrollbar */
::-webkit-scrollbar {
    width: 6px;
}
::-webkit-scrollbar-track {
    background: transparent;
}
::-webkit-scrollbar-thumb {
    background: #4b5563;
    border-radius: 3px;
}

/* Animations */
@keyframes slideUp {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: translateY(0); }
}

.animate-slide-up {
    animation: slideUp 0.5s ease-out;
}

@keyframes countUp {
    from { opacity: 0; transform: scale(0.5); }
    to { opacity: 1; transform: scale(1); }
}

.animate-count-up {
    animation: countUp 0.3s ease-out;
}

client/src/app/page.js (Landing Page)

'use client';
// ====================================
// Landing Page
// ====================================
import Link from 'next/link';
import { motion } from 'framer-motion';
import { ArrowRight, MapPin, Award, Users, BarChart3 } from 'lucide-react';

export default function LandingPage() {
    return (
        <div className="min-h-screen bg-gradient-to-b from-slate-950 via-indigo-950/20 to-slate-950">
            {/* Navigation */}
            <nav className="flex items-center justify-between px-6 py-4 max-w-7xl mx-auto">
                <div className="flex items-center gap-2">
                    <span className="text-2xl">🏙️</span>
                    <span className="text-xl font-bold text-white">
                        CivicStreak
                    </span>
                </div>
                <div className="flex gap-4">
                    <Link href="/login" className="btn-secondary text-sm py-2 px-4">
                        Login
                    </Link>
                    <Link href="/register" className="btn-primary text-sm py-2 px-4">
                        Get Started
                    </Link>
                </div>
            </nav>

            {/* Hero Section */}
            <section className="max-w-7xl mx-auto px-6 pt-20 pb-32 text-center">
                <motion.div
                    initial={{ opacity: 0, y: 30 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ duration: 0.8 }}
                >
                    <div className="inline-flex items-center gap-2 bg-indigo-500/10 border border-indigo-500/20 rounded-full px-4 py-2 mb-8">
                        <span className="text-sm text-indigo-400">
                            🇮🇳 Bridging the civic engagement gap for Indian youth
                        </span>
                    </div>

                    <h1 className="text-5xl md:text-7xl font-bold mb-6 leading-tight">
                        <span className="text-white">Your City,</span>
                        <br />
                        <span className="bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">
                            Your Commitment
                        </span>
                    </h1>

                    <p className="text-xl text-slate-400 max-w-2xl mx-auto mb-4">
                        CivicStreak turns democracy from a once-in-5-years vote 
                        into a <strong className="text-white">daily 5-minute habit</strong>.
                    </p>

                    <p className="text-lg text-slate-500 max-w-xl mx-auto mb-10">
                        Break down massive civic goals into quick, visible, social micro-actions.
                        Make democracy feel like a daily habit, not a boring duty.
                    </p>

                    <div className="flex flex-col sm:flex-row gap-4 justify-center">
                        <Link href="/register" className="btn-primary text-lg py-4 px-8 flex items-center gap-2 justify-center">
                            Start Your Civic Journey <ArrowRight size={20} />
                        </Link>
                        <Link href="/ward" className="btn-secondary text-lg py-4 px-8">
                            Explore Ward Dashboards
                        </Link>
                    </div>
                </motion.div>

                {/* Stats Bar */}
                <motion.div
                    initial={{ opacity: 0, y: 50 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ duration: 0.8, delay: 0.3 }}
                    className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-6 max-w-4xl mx-auto"
                >
                    {[
                        { number: '500+', label: 'Active CivicStreaks', icon: '👥' },
                        { number: '25+', label: 'Community Circles', icon: '🤝' },
                        { number: '200+', label: 'Issues Documented', icon: '📸' },
                        { number: '30+', label: 'RTIs Filed', icon: '📜' },
                    ].map((stat, i) => (
                        <div key={i} className="card text-center">
                            <div className="text-3xl mb-2">{stat.icon}</div>
                            <div className="text-2xl font-bold text-white">{stat.number}</div>
                            <div className="text-sm text-slate-400">{stat.label}</div>
                        </div>
                    ))}
                </motion.div>
            </section>

            {/* Four Pillars Section */}
            <section className="max-w-7xl mx-auto px-6 py-20">
                <h2 className="text-3xl font-bold text-center mb-4">
                    The Four Pillars of CivicStreak
                </h2>
                <p className="text-slate-400 text-center mb-16 max-w-xl mx-auto">
                    A complete ecosystem designed to make civic engagement 
                    feel like scrolling social media — fast, rewarding, and social.
                </p>

                <div className="grid md:grid-cols-2 gap-8">
                    {[
                        {
                            icon: <MapPin className="text-indigo-400" size={32} />,
                            title: '🎯 Micro-Tasks ("Civic Bites")',
                            description: 'Break down massive civic goals into 5-15 minute daily tasks. Photograph a budget board, take a ward quiz, sign a petition.',
                            features: ['Location-based', 'Skill-matched', 'Time-flexible']
                        },
                        {
                            icon: <Award className="text-amber-400" size={32} />,
                            title: '🔥 Streaks & Portfolio',
                            description: 'Duolingo-style streaks keep you coming back. Build a shareable "Civic Portfolio" — your democratic resume for college & career.',
                            features: ['XP System', 'Level progression', 'Shareable certificates']
                        },
                        {
                            icon: <Users className="text-green-400" size={32} />,
                            title: '🤝 Community Circles',
                            description: 'Teams of 5-8 youth from the same ward. Adopt one civic issue, meet weekly, present quarterly to ward councilors.',
                            features: ['Team accountability', 'Rotating roles', 'Mentor support']
                        },
                        {
                            icon: <BarChart3 className="text-purple-400" size={32} />,
                            title: '📊 Ward Impact Dashboard',
                            description: 'Real-time ward-level transparency. Track issues, measure responsiveness, compare wards — making invisible work visible.',
                            features: ['Issue tracking', 'Ward scoring', 'Public accountability']
                        }
                    ].map((pillar, i) => (
                        <motion.div
                            key={i}
                            initial={{ opacity: 0, y: 30 }}
                            whileInView={{ opacity: 1, y: 0 }}
                            transition={{ duration: 0.5, delay: i * 0.1 }}
                            viewport={{ once: true }}
                            className="card hover:border-indigo-500/30 transition-colors"
                        >
                            <div className="mb-4">{pillar.icon}</div>
                            <h3 className="text-xl font-bold mb-2">{pillar.title}</h3>
                            <p className="text-slate-400 mb-4">{pillar.description}</p>
                            <div className="flex flex-wrap gap-2">
                                {pillar.features.map((f, j) => (
                                    <span key={j} className="badge bg-slate-700 text-slate-300">
                                        {f}
                                    </span>
                                ))}
                            </div>
                        </motion.div>
                    ))}
                </div>
            </section>

            {/* CTA Section */}
            <section className="max-w-4xl mx-auto px-6 py-20 text-center">
                <div className="glass-card">
                    <h2 className="text-3xl font-bold mb-4">
                        Every CivicStreak starts with one micro-task.
                    </h2>
                    <p className="text-slate-400 mb-8">
                        5 minutes today. A better city tomorrow. 
                        Join the civic revolution.
                    </p>
                    <Link href="/register" className="btn-primary text-lg py-4 px-10 inline-flex items-center gap-2">
                        Become a CivicStreak <ArrowRight size={20} />
                    </Link>
                </div>
            </section>

            {/* Footer */}
            <footer className="border-t border-slate-800 py-8 px-6">
                <div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
                    <div className="flex items-center gap-2">
                        <span className="text-xl">🏙️</span>
                        <span className="font-bold">CivicStreak</span>
                        <span className="text-slate-500 text-sm ml-2">
                            नगरमित्र — Your City, Your Commitment
                        </span>
                    </div>
                    <div className="text-slate-500 text-sm">
                        Built with ❤️ for Indian democracy
                    </div>
                </div>
            </footer>
        </div>
    );
}

4.4 Authentication Pages

client/src/app/(auth)/register/page.js

'use client';
// ====================================
// Registration Page
// ====================================
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useAuth } from '@/context/AuthContext';
import { userAPI, wardAPI } from '@/lib/api';
import toast from 'react-hot-toast';
import { motion } from 'framer-motion';
import { useEffect } from 'react';

export default function RegisterPage() {
    const router = useRouter();
    const { signUp } = useAuth();

    const [step, setStep] = useState(1); // 1: Account, 2: Profile, 3: Interests
    const [loading, setLoading] = useState(false);
    const [wards, setWards] = useState([]);

    const [formData, setFormData] = useState({
        email: '',
        password: '',
        full_name: '',
        phone: '',
        age: '',
        college: '',
        ward_id: '',
        interests: [],
        preferred_language: 'English'
    });

    useEffect(() => {
        fetchWards();
    }, []);

    const fetchWards = async () => {
        try {
            const response = await wardAPI.getAll();
            if (response.success) {
                setWards(response.data);
            }
        } catch (error) {
            console.error('Fetch wards error:', error);
        }
    };

    const handleChange = (e) => {
        setFormData({ ...formData, [e.target.name]: e.target.value });
    };

    const toggleInterest = (interest) => {
        setFormData(prev => ({
            ...prev,
            interests: prev.interests.includes(interest)
                ? prev.interests.filter(i => i !== interest)
                : [...prev.interests, interest]
        }));
    };

    const handleSubmit = async () => {
        setLoading(true);
        try {
            // Step 1: Create account
            await signUp(formData.email, formData.password, {
                full_name: formData.full_name,
                phone: formData.phone
            });

            // Step 2: Complete onboarding
            await userAPI.completeOnboarding({
                ward_id: formData.ward_id,
                interests: formData.interests,
                college: formData.college,
                age: parseInt(formData.age),
                preferred_language: formData.preferred_language
            });

            toast.success('Welcome to CivicStreak! 🎉');
            router.push('/dashboard');
        } catch (error) {
            toast.error(error.message || 'Registration failed');
        } finally {
            setLoading(false);
        }
    };

    const interests = [
        'Environment', 'Infrastructure', 'Education',
        'Health', 'Safety', 'Governance',
        'Sanitation', 'Transportation', 'Community'
    ];

    return (
        <div className="min-h-screen flex items-center justify-center px-4 py-10">
            <motion.div
                initial={{ opacity: 0, y: 20 }}
                animate={{ opacity: 1, y: 0 }}
                className="w-full max-w-md"
            >
                {/* Logo */}
                <div className="text-center mb-8">
                    <span className="text-4xl">🏙️</span>
                    <h1 className="text-2xl font-bold mt-2">Join CivicStreak</h1>
                    <p className="text-slate-400 mt-1">Your civic journey starts here</p>
                </div>

                {/* Progress indicator */}
                <div className="flex items-center justify-center gap-2 mb-8">
                    {[1, 2, 3].map(s => (
                        <div key={s} className={`h-2 rounded-full transition-all ${
                            s <= step ? 'w-10 bg-indigo-500' : 'w-6 bg-slate-700'
                        }`} />
                    ))}
                </div>

                <div className="card">
                    {/* Step 1: Account Details */}
                    {step === 1 && (
                        <div className="space-y-4">
                            <h2 className="text-lg font-semibold mb-4">Create Account</h2>
                            <div>
                                <label className="block text-sm text-slate-400 mb-1">Full Name</label>
                                <input
                                    type="text"
                                    name="full_name"
                                    value={formData.full_name}
                                    onChange={handleChange}
                                    className="input-field"
                                    placeholder="Rohan Sharma"
                                />
                            </div>
                            <div>
                                <label className="block text-sm text-slate-400 mb-1">Email</label>
                                <input
                                    type="email"
                                    name="email"
                                    value={formData.email}
                                    onChange={handleChange}
                                    className="input-field"
                                    placeholder="rohan@example.com"
                                />
                            </div>
                            <div>
                                <label className="block text-sm text-slate-400 mb-1">Password</label>
                                <input
                                    type="password"
                                    name="password"
                                    value={formData.password}
                                    onChange={handleChange}
                                    className="input-field"
                                    placeholder="Min 6 characters"
                                />
                            </div>
                            <div>
                                <label className="block text-sm text-slate-400 mb-1">Phone (WhatsApp)</label>
                                <input
                                    type="tel"
                                    name="phone"
                                    value={formData.phone}
                                    onChange={handleChange}
                                    className="input-field"
                                    placeholder="+919876543210"
                                />
                            </div>
                            <button
                                onClick={() => setStep(2)}
                                className="btn-primary w-full"
                                disabled={!formData.email || !formData.password || !formData.full_name}
                            >
                                Next →
                            </button>
                        </div>
                    )}

                    {/* Step 2: Profile Details */}
                    {step === 2 && (
                        <div className="space-y-4">
                            <h2 className="text-lg font-semibold mb-4">Your Profile</h2>
                            <div>
                                <label className="block text-sm text-slate-400 mb-1">Age</label>
                                <input
                                    type="number"
                                    name="age"
                                    value={formData.age}
                                    onChange={handleChange}
                                    className="input-field"
                                    placeholder="18"
                                    min="14"
                                    max="35"
                                />
                            </div>
                            <div>
                                <label className="block text-sm text-slate-400 mb-1">College / Institution</label>
                                <input
                                    type="text"
                                    name="college"
                                    value={formData.college}
                                    onChange={handleChange}
                                    className="input-field"
                                    placeholder="Mumbai University"
                                />
                            </div>
                            <div>
                                <label className="block text-sm text-slate-400 mb-1">Your Ward</label>
                                <select
                                    name="ward_id"
                                    value={formData.ward_id}
                                    onChange={handleChange}
                                    className="input-field"
                                >
                                    <option value="">Select your ward</option>
                                    {wards.map(ward => (
                                        <option key={ward.id} value={ward.id}>
                                            Ward {ward.ward_number}{ward.ward_name}
                                        </option>
                                    ))}
                                </select>
                            </div>
                            <div>
                                <label className="block text-sm text-slate-400 mb-1">Preferred Language</label>
                                <select
                                    name="preferred_language"
                                    value={formData.preferred_language}
                                    onChange={handleChange}
                                    className="input-field"
                                >
                                    <option value="English">English</option>
                                    <option value="Hindi">Hindi</option>
                                    <option value="Marathi">Marathi</option>
                                </select>
                            </div>
                            <div className="flex gap-3">
                                <button onClick={() => setStep(1)} className="btn-secondary flex-1">
                                    ← Back
                                </button>
                                <button
                                    onClick={() => setStep(3)}
                                    className="btn-primary flex-1"
                                    disabled={!formData.ward_id}
                                >
                                    Next →
                                </button>
                            </div>
                        </div>
                    )}

                    {/* Step 3: Interests */}
                    {step === 3 && (
                        <div className="space-y-4">
                            <h2 className="text-lg font-semibold mb-2">What do you care about?</h2>
                            <p className="text-sm text-slate-400 mb-4">
                                Select topics you're interested in. We'll personalize your tasks.
                            </p>
                            <div className="flex flex-wrap gap-2">
                                {interests.map(interest => (
                                    <button
                                        key={interest}
                                        onClick={() => toggleInterest(interest)}
                                        className={`badge text-sm py-2 px-4 cursor-pointer transition-all ${
                                            formData.interests.includes(interest)
                                                ? 'bg-indigo-600 text-white'
                                                : 'bg-slate-700 text-slate-300 hover:bg-slate-600'
                                        }`}
                                    >
                                        {interest}
                                    </button>
                                ))}
                            </div>
                            <div className="flex gap-3 mt-6">
                                <button onClick={() => setStep(2)} className="btn-secondary flex-1">
                                    ← Back
                                </button>
                                <button
                                    onClick={handleSubmit}
                                    className="btn-success flex-1"
                                    disabled={loading || formData.interests.length === 0}
                                >
                                    {loading ? '🔄 Creating...' : '🎉 Join CivicStreak'}
                                </button>
                            </div>
                        </div>
                    )}
                </div>

                {/* Login link */}
                <p className="text-center text-slate-400 mt-6">
                    Already a CivicStreak?{' '}
                    <Link href="/login" className="text-indigo-400 hover:text-indigo-300">
                        Login here
                    </Link>
                </p>
            </motion.div>
        </div>
    );
}

client/src/app/(auth)/login/page.js

'use client';
// ====================================
// Login Page
// ====================================
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useAuth } from '@/context/AuthContext';
import toast from 'react-hot-toast';
import { motion } from 'framer-motion';

export default function LoginPage() {
    const router = useRouter();
    const { signIn } = useAuth();
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [loading, setLoading] = useState(false);

    const handleLogin = async (e) => {
        e.preventDefault();
        setLoading(true);
        try {
            await signIn(email, password);
            toast.success('Welcome back! 🏙️');
            router.push('/dashboard');
        } catch (error) {
            toast.error(error.message || 'Invalid credentials');
        } finally {
            setLoading(false);
        }
    };

    return (
        <div className="min-h-screen flex items-center justify-center px-4">
            <motion.div
                initial={{ opacity: 0, y: 20 }}
                animate={{ opacity: 1, y: 0 }}
                className="w-full max-w-md"
            >
                <div className="text-center mb-8">
                    <span className="text-4xl">🏙️</span>
                    <h1 className="text-2xl font-bold mt-2">Welcome Back</h1>
                    <p className="text-slate-400 mt-1">Continue your civic journey</p>
                </div>

                <div className="card">
                    <form onSubmit={handleLogin} className="space-y-4">
                        <div>
                            <label className="block text-sm text-slate-400 mb-1">Email</label>
                            <input
                                type="email"
                                value={email}
                                onChange={(e) => setEmail(e.target.value)}
                                className="input-field"
                                placeholder="rohan@example.com"
                                required
                            />
                        </div>
                        <div>
                            <label className="block text-sm text-slate-400 mb-1">Password</label>
                            <input
                                type="password"
                                value={password}
                                onChange={(e) => setPassword(e.target.value)}
                                className="input-field"
                                placeholder="Your password"
                                required
                            />
                        </div>
                        <button
                            type="submit"
                            className="btn-primary w-full"
                            disabled={loading}
                        >
                            {loading ? '🔄 Logging in...' : '🔑 Login'}
                        </button>
                    </form>
                </div>

                <p className="text-center text-slate-400 mt-6">
                    New to CivicStreak?{' '}
                    <Link href="/register" className="text-indigo-400 hover:text-indigo-300">
                        Register here
                    </Link>
                </p>
            </motion.div>
        </div>
    );
}

4.5 Dashboard (Main App Shell)

client/src/components/layout/Sidebar.js

'use client';
// ====================================
// Sidebar Navigation
// ====================================
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useAuth } from '@/context/AuthContext';
import { formatXP, getLevelEmoji } from '@/lib/utils';
import {
    Home, Target, Briefcase, Users, Map,
    Trophy, Bell, Settings, LogOut, Flame
} from 'lucide-react';

const navItems = [
    { href: '/dashboard', label: 'Dashboard', icon: Home },
    { href: '/tasks', label: 'Micro-Tasks', icon: Target },
    { href: '/portfolio', label: 'My Portfolio', icon: Briefcase },
    { href: '/circles', label: 'Circles', icon: Users },
    { href: '/ward', label: 'Ward Dashboard', icon: Map },
    { href: '/leaderboard', label: 'Leaderboard', icon: Trophy },
];

export default function Sidebar() {
    const pathname = usePathname();
    const { profile, signOut } = useAuth();

    return (
        <aside className="fixed left-0 top-0 h-screen w-64 bg-slate-900 border-r border-slate-800 flex flex-col z-50">
            {/* Logo */}
            <div className="p-6 border-b border-slate-800">
                <Link href="/dashboard" className="flex items-center gap-2">
                    <span className="text-2xl">🏙️</span>
                    <span className="text-lg font-bold">CivicStreak</span>
                </Link>
            </div>

            {/* User summary card */}
            {profile && (
                <div className="p-4 mx-3 mt-4 rounded-xl bg-slate-800/50 border border-slate-700/50">
                    <div className="flex items-center gap-3 mb-3">
                        <div className="w-10 h-10 rounded-full bg-indigo-600 flex items-center justify-center text-sm font-bold">
                            {profile.full_name?.charAt(0)?.toUpperCase()}
                        </div>
                        <div className="flex-1 min-w-0">
                            <p className="text-sm font-semibold truncate">
                                {profile.full_name}
                            </p>
                            <p className="text-xs text-slate-400">
                                {getLevelEmoji(profile.level)} {profile.level}
                            </p>
                        </div>
                    </div>
                    <div className="grid grid-cols-2 gap-2 text-center">
                        <div className="bg-slate-700/50 rounded-lg py
                        <div className="bg-slate-700/50 rounded-lg py-1.5">
                            <div className="flex items-center justify-center gap-1">
                                <Flame size={12} className="text-amber-400" />
                                <span className="text-sm font-bold">{profile.current_streak}</span>
                            </div>
                            <p className="text-[10px] text-slate-400">Streak</p>
                        </div>
                        <div className="bg-slate-700/50 rounded-lg py-1.5">
                            <div className="text-sm font-bold text-indigo-400">
                                {formatXP(profile.xp_points)}
                            </div>
                            <p className="text-[10px] text-slate-400">XP</p>
                        </div>
                    </div>
                </div>
            )}

            {/* Navigation Links */}
            <nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
                {navItems.map(item => {
                    const Icon = item.icon;
                    const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
                    return (
                        <Link
                            key={item.href}
                            href={item.href}
                            className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all ${
                                isActive
                                    ? 'bg-indigo-600/20 text-indigo-400 border border-indigo-500/30'
                                    : 'text-slate-400 hover:bg-slate-800 hover:text-white'
                            }`}
                        >
                            <Icon size={18} />
                            {item.label}
                        </Link>
                    );
                })}
            </nav>

            {/* Bottom Actions */}
            <div className="p-3 border-t border-slate-800 space-y-1">
                <Link
                    href="/notifications"
                    className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm text-slate-400 hover:bg-slate-800 hover:text-white transition-all"
                >
                    <Bell size={18} />
                    Notifications
                </Link>
                <Link
                    href="/settings"
                    className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm text-slate-400 hover:bg-slate-800 hover:text-white transition-all"
                >
                    <Settings size={18} />
                    Settings
                </Link>
                <button
                    onClick={signOut}
                    className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm text-red-400 hover:bg-red-500/10 transition-all w-full"
                >
                    <LogOut size={18} />
                    Logout
                </button>
            </div>
        </aside>
    );
}

client/src/components/layout/MobileNav.js

'use client';
// ====================================
// Mobile Bottom Navigation
// ====================================
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Home, Target, Briefcase, Users, Map } from 'lucide-react';

const navItems = [
    { href: '/dashboard', label: 'Home', icon: Home },
    { href: '/tasks', label: 'Tasks', icon: Target },
    { href: '/portfolio', label: 'Portfolio', icon: Briefcase },
    { href: '/circles', label: 'Circles', icon: Users },
    { href: '/ward', label: 'Ward', icon: Map },
];

export default function MobileNav() {
    const pathname = usePathname();

    return (
        <nav className="md:hidden fixed bottom-0 left-0 right-0 bg-slate-900/95 backdrop-blur-lg border-t border-slate-800 z-50 safe-area-bottom">
            <div className="flex justify-around items-center py-2">
                {navItems.map(item => {
                    const Icon = item.icon;
                    const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
                    return (
                        <Link
                            key={item.href}
                            href={item.href}
                            className={`flex flex-col items-center gap-1 px-3 py-2 rounded-xl transition-all ${
                                isActive
                                    ? 'text-indigo-400'
                                    : 'text-slate-500'
                            }`}
                        >
                            <Icon size={20} />
                            <span className="text-[10px] font-medium">{item.label}</span>
                        </Link>
                    );
                })}
            </div>
        </nav>
    );
}

client/src/app/dashboard/layout.js (App Shell Layout)

'use client';
// ====================================
// Dashboard Layout (App Shell)
// Wraps all authenticated pages
// ====================================
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/context/AuthContext';
import Sidebar from '@/components/layout/Sidebar';
import MobileNav from '@/components/layout/MobileNav';

export default function DashboardLayout({ children }) {
    const { user, loading, profile } = useAuth();
    const router = useRouter();

    useEffect(() => {
        if (!loading && !user) {
            router.push('/login');
        }
        if (!loading && user && profile && !profile.onboarding_completed) {
            router.push('/register');
        }
    }, [user, loading, profile, router]);

    if (loading) {
        return (
            <div className="min-h-screen flex items-center justify-center">
                <div className="text-center">
                    <span className="text-5xl animate-bounce inline-block">🏙️</span>
                    <p className="text-slate-400 mt-4">Loading CivicStreak...</p>
                </div>
            </div>
        );
    }

    if (!user) return null;

    return (
        <div className="min-h-screen flex">
            {/* Desktop Sidebar */}
            <div className="hidden md:block">
                <Sidebar />
            </div>

            {/* Main Content */}
            <main className="flex-1 md:ml-64 pb-20 md:pb-0">
                <div className="max-w-6xl mx-auto px-4 md:px-8 py-6">
                    {children}
                </div>
            </main>

            {/* Mobile Nav */}
            <MobileNav />
        </div>
    );
}

4.6 Dashboard Home Page

client/src/app/dashboard/page.js

'use client';
// ====================================
// Dashboard Home
// The main hub after login
// ====================================
import { useState, useEffect } from 'react';
import { useAuth } from '@/context/AuthContext';
import { taskAPI, userAPI } from '@/lib/api';
import { formatXP, getLevelEmoji, getCategoryIcon, timeAgo } from '@/lib/utils';
import { motion } from 'framer-motion';
import Link from 'next/link';
import toast from 'react-hot-toast';
import {
    Flame, Star, Target, Trophy, ArrowRight,
    Clock, CheckCircle, TrendingUp, ChevronRight
} from 'lucide-react';

export default function DashboardPage() {
    const { profile, refreshProfile } = useAuth();
    const [todaysTask, setTodaysTask] = useState(null);
    const [recentTasks, setRecentTasks] = useState([]);
    const [stats, setStats] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        fetchDashboardData();
    }, []);

    const fetchDashboardData = async () => {
        try {
            const [taskRes, myTasksRes, statsRes] = await Promise.all([
                taskAPI.getTodaysTask(),
                taskAPI.getMyTasks({ status: 'submitted' }),
                userAPI.getStats()
            ]);

            if (taskRes.success) setTodaysTask(taskRes.data);
            if (myTasksRes.success) setRecentTasks(myTasksRes.data?.slice(0, 5) || []);
            if (statsRes.success) setStats(statsRes.data);
        } catch (error) {
            console.error('Dashboard fetch error:', error);
        } finally {
            setLoading(false);
        }
    };

    const acceptTodaysTask = async () => {
        if (!todaysTask) return;
        try {
            const response = await taskAPI.accept(todaysTask.id);
            if (response.success) {
                toast.success('Task accepted! 💪');
            }
        } catch (error) {
            toast.error(error.message || 'Failed to accept task');
        }
    };

    if (loading) {
        return (
            <div className="animate-pulse space-y-6">
                <div className="h-8 bg-slate-800 rounded w-1/3"></div>
                <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
                    {[...Array(4)].map((_, i) => (
                        <div key={i} className="h-28 bg-slate-800 rounded-2xl"></div>
                    ))}
                </div>
                <div className="h-60 bg-slate-800 rounded-2xl"></div>
            </div>
        );
    }

    return (
        <div className="space-y-6">
            {/* Welcome Header */}
            <motion.div
                initial={{ opacity: 0, y: -10 }}
                animate={{ opacity: 1, y: 0 }}
                className="flex flex-col md:flex-row md:items-center justify-between gap-4"
            >
                <div>
                    <h1 className="text-2xl font-bold">
                        Namaste, {profile?.full_name?.split(' ')[0]}! 🙏
                    </h1>
                    <p className="text-slate-400 mt-1">
                        {profile?.wards?.ward_name
                            ? `Ward ${profile.wards.ward_number}${profile.wards.ward_name}`
                            : 'Welcome to CivicStreak'}
                    </p>
                </div>
                <div className="flex items-center gap-3">
                    <div className="flex items-center gap-1 bg-amber-500/10 border border-amber-500/20 rounded-full px-4 py-2">
                        <Flame size={18} className="text-amber-400" />
                        <span className="text-amber-400 font-bold">{profile?.current_streak || 0}</span>
                        <span className="text-amber-400/70 text-sm">day streak</span>
                    </div>
                    <div className="flex items-center gap-1 bg-indigo-500/10 border border-indigo-500/20 rounded-full px-4 py-2">
                        <Star size={18} className="text-indigo-400" />
                        <span className="text-indigo-400 font-bold">{formatXP(profile?.xp_points)}</span>
                        <span className="text-indigo-400/70 text-sm">XP</span>
                    </div>
                </div>
            </motion.div>

            {/* Stats Cards */}
            <motion.div
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: 0.1 }}
                className="grid grid-cols-2 md:grid-cols-4 gap-4"
            >
                <StatCard
                    icon={<Target className="text-indigo-400" size={24} />}
                    label="Tasks Done"
                    value={profile?.tasks_completed || 0}
                    color="indigo"
                />
                <StatCard
                    icon={<Flame className="text-amber-400" size={24} />}
                    label="Current Streak"
                    value={`${profile?.current_streak || 0} days`}
                    color="amber"
                />
                <StatCard
                    icon={<Trophy className="text-purple-400" size={24} />}
                    label="Level"
                    value={profile?.level || 'Newcomer'}
                    color="purple"
                    small
                />
                <StatCard
                    icon={<TrendingUp className="text-emerald-400" size={24} />}
                    label="Issues Resolved"
                    value={profile?.issues_resolved || 0}
                    color="emerald"
                />
            </motion.div>

            {/* Today's Civic Bite */}
            <motion.div
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: 0.2 }}
            >
                <h2 className="text-lg font-semibold mb-3 flex items-center gap-2">
                    🍽️ Today's Civic Bite
                </h2>
                {todaysTask ? (
                    <div className="card border-indigo-500/30 bg-gradient-to-r from-indigo-950/30 to-slate-800/50">
                        <div className="flex items-start justify-between gap-4">
                            <div className="flex-1">
                                <div className="flex items-center gap-2 mb-2">
                                    <span className="text-2xl">
                                        {getCategoryIcon(todaysTask.category)}
                                    </span>
                                    <span className="badge bg-indigo-500/20 text-indigo-400">
                                        {todaysTask.category}
                                    </span>
                                    <span className="badge bg-slate-700 text-slate-300">
                                        <Clock size={10} className="mr-1" />
                                        {todaysTask.estimated_minutes} min
                                    </span>
                                </div>
                                <h3 className="text-lg font-semibold mb-1">
                                    {todaysTask.title}
                                </h3>
                                <p className="text-slate-400 text-sm mb-4">
                                    {todaysTask.description}
                                </p>
                                <div className="flex items-center gap-3">
                                    <Link
                                        href={`/tasks/${todaysTask.id}`}
                                        className="btn-primary text-sm py-2 px-4 inline-flex items-center gap-1"
                                    >
                                        Start Task <ArrowRight size={14} />
                                    </Link>
                                    <span className="text-sm text-indigo-400 font-semibold">
                                        +{todaysTask.xp_reward} XP
                                    </span>
                                </div>
                            </div>
                        </div>
                    </div>
                ) : (
                    <div className="card text-center py-10">
                        <span className="text-4xl mb-4 block">🌟</span>
                        <h3 className="font-semibold mb-1">All caught up!</h3>
                        <p className="text-slate-400 text-sm">
                            You've completed all available tasks. Check back tomorrow!
                        </p>
                    </div>
                )}
            </motion.div>

            {/* Two column layout */}
            <div className="grid md:grid-cols-2 gap-6">
                {/* Recent Activity */}
                <motion.div
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ delay: 0.3 }}
                >
                    <div className="flex items-center justify-between mb-3">
                        <h2 className="text-lg font-semibold">Recent Activity</h2>
                        <Link href="/tasks?tab=completed" className="text-indigo-400 text-sm hover:underline flex items-center gap-1">
                            View all <ChevronRight size={14} />
                        </Link>
                    </div>
                    <div className="card space-y-3">
                        {recentTasks.length > 0 ? (
                            recentTasks.map((task, i) => (
                                <div
                                    key={task.id}
                                    className="flex items-center gap-3 p-3 rounded-xl bg-slate-700/30 hover:bg-slate-700/50 transition-colors"
                                >
                                    <CheckCircle size={18} className="text-emerald-400 flex-shrink-0" />
                                    <div className="flex-1 min-w-0">
                                        <p className="text-sm font-medium truncate">
                                            {task.micro_tasks?.title || 'Task'}
                                        </p>
                                        <p className="text-xs text-slate-400">
                                            {timeAgo(task.submitted_at)}
                                        </p>
                                    </div>
                                    <span className="text-xs font-semibold text-indigo-400">
                                        +{task.xp_earned} XP
                                    </span>
                                </div>
                            ))
                        ) : (
                            <div className="text-center py-8 text-slate-500">
                                <Target size={32} className="mx-auto mb-2 opacity-50" />
                                <p className="text-sm">No completed tasks yet</p>
                                <Link href="/tasks" className="text-indigo-400 text-sm hover:underline">
                                    Browse tasks →
                                </Link>
                            </div>
                        )}
                    </div>
                </motion.div>

                {/* Quick Actions */}
                <motion.div
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ delay: 0.4 }}
                >
                    <h2 className="text-lg font-semibold mb-3">Quick Actions</h2>
                    <div className="space-y-3">
                        {[
                            {
                                href: '/tasks',
                                icon: '🎯',
                                title: 'Browse Tasks',
                                desc: 'Find micro-tasks in your ward',
                                color: 'from-indigo-500/10 to-transparent'
                            },
                            {
                                href: '/issues/new',
                                icon: '📢',
                                title: 'Report an Issue',
                                desc: 'Document a civic problem',
                                color: 'from-red-500/10 to-transparent'
                            },
                            {
                                href: '/circles',
                                icon: '🤝',
                                title: 'Join a Circle',
                                desc: 'Find your civic squad',
                                color: 'from-green-500/10 to-transparent'
                            },
                            {
                                href: `/portfolio/${profile?.id}`,
                                icon: '📋',
                                title: 'View Portfolio',
                                desc: 'Your democratic resume',
                                color: 'from-purple-500/10 to-transparent'
                            },
                        ].map((action, i) => (
                            <Link
                                key={i}
                                href={action.href}
                                className={`card flex items-center gap-4 py-4 hover:border-indigo-500/30 transition-all bg-gradient-to-r ${action.color}`}
                            >
                                <span className="text-2xl">{action.icon}</span>
                                <div className="flex-1">
                                    <h3 className="font-semibold text-sm">{action.title}</h3>
                                    <p className="text-xs text-slate-400">{action.desc}</p>
                                </div>
                                <ChevronRight size={18} className="text-slate-500" />
                            </Link>
                        ))}
                    </div>
                </motion.div>
            </div>

            {/* Level Progress */}
            <motion.div
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: 0.5 }}
            >
                <LevelProgress profile={profile} />
            </motion.div>
        </div>
    );
}

// ============ Sub-Components ============

function StatCard({ icon, label, value, color, small }) {
    return (
        <div className="card flex flex-col items-center text-center py-4">
            {icon}
            <p className={`${small ? 'text-sm' : 'text-xl'} font-bold mt-2`}>
                {value}
            </p>
            <p className="text-xs text-slate-400 mt-1">{label}</p>
        </div>
    );
}

function LevelProgress({ profile }) {
    const levels = [
        { name: 'Newcomer', minStreak: 0 },
        { name: 'Curious Citizen', minStreak: 7 },
        { name: 'Active Citizen', minStreak: 30 },
        { name: 'Ward Warrior', minStreak: 90 },
        { name: 'Civic Champion', minStreak: 180 },
        { name: 'CivicStreak Fellow', minStreak: 365 },
    ];

    const currentLevelIndex = levels.findIndex(l => l.name === profile?.level) || 0;
    const nextLevel = levels[currentLevelIndex + 1];
    const currentStreak = profile?.current_streak || 0;

    const progress = nextLevel
        ? ((currentStreak - levels[currentLevelIndex].minStreak) /
           (nextLevel.minStreak - levels[currentLevelIndex].minStreak)) * 100
        : 100;

    return (
        <div className="card">
            <h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
                🏅 Level Progress
            </h2>
            <div className="flex items-center justify-between mb-2">
                <span className="text-sm font-medium">
                    {getLevelEmoji(profile?.level)} {profile?.level}
                </span>
                {nextLevel && (
                    <span className="text-sm text-slate-400">
                        Next: {nextLevel.name} ({nextLevel.minStreak - currentStreak} days to go)
                    </span>
                )}
            </div>
            <div className="w-full h-3 bg-slate-700 rounded-full overflow-hidden">
                <motion.div
                    className="h-full bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full"
                    initial={{ width: 0 }}
                    animate={{ width: `${Math.min(progress, 100)}%` }}
                    transition={{ duration: 1, ease: "easeOut" }}
                />
            </div>
            <div className="flex justify-between mt-4">
                {levels.map((level, i) => (
                    <div
                        key={level.name}
                        className={`text-center ${
                            i <= currentLevelIndex ? 'opacity-100' : 'opacity-30'
                        }`}
                    >
                        <div className={`w-3 h-3 rounded-full mx-auto mb-1 ${
                            i <= currentLevelIndex ? 'bg-indigo-500' : 'bg-slate-600'
                        }`} />
                        <span className="text-[9px] text-slate-400 hidden md:block">
                            {level.name}
                        </span>
                    </div>
                ))}
            </div>
        </div>
    );
}

4.7 Tasks Page

client/src/app/tasks/page.js

'use client';
// ====================================
// Tasks Page — Browse & manage micro-tasks
// ====================================
import { useState, useEffect } from 'react';
import { taskAPI } from '@/lib/api';
import { getCategoryIcon, timeAgo } from '@/lib/utils';
import Link from 'next/link';
import toast from 'react-hot-toast';
import { motion } from 'framer-motion';
import { Search, Filter, Clock, Star, ChevronRight, CheckCircle } from 'lucide-react';

export default function TasksPage() {
    const [tab, setTab] = useState('available'); // available, my, completed
    const [tasks, setTasks] = useState([]);
    const [categories, setCategories] = useState([]);
    const [loading, setLoading] = useState(true);
    const [filters, setFilters] = useState({
        category: '',
        difficulty: '',
        time: ''
    });

    useEffect(() => {
        fetchTasks();
        fetchCategories();
    }, [tab, filters]);

    const fetchTasks = async () => {
        setLoading(true);
        try {
            let response;
            if (tab === 'available') {
                response = await taskAPI.getAvailable(filters);
            } else if (tab === 'my') {
                response = await taskAPI.getMyTasks({ status: 'in_progress' });
            } else {
                response = await taskAPI.getMyTasks({ status: 'submitted' });
            }
            if (response.success) {
                setTasks(response.data || []);
            }
        } catch (error) {
            console.error('Fetch tasks error:', error);
        } finally {
            setLoading(false);
        }
    };

    const fetchCategories = async () => {
        try {
            const response = await taskAPI.getCategories();
            if (response.success) {
                setCategories(response.data || []);
            }
        } catch (error) {
            console.error('Fetch categories error:', error);
        }
    };

    const handleAcceptTask = async (taskId) => {
        try {
            const response = await taskAPI.accept(taskId);
            if (response.success) {
                toast.success(response.message);
                fetchTasks();
            }
        } catch (error) {
            toast.error(error.message || 'Failed to accept task');
        }
    };

    const tabs = [
        { key: 'available', label: '🎯 Available' },
        { key: 'my', label: '⏳ In Progress' },
        { key: 'completed', label: '✅ Completed' },
    ];

    return (
        <div className="space-y-6">
            {/* Header */}
            <div>
                <h1 className="text-2xl font-bold">Micro-Tasks</h1>
                <p className="text-slate-400 mt-1">
                    Quick civic actions — 5 to 15 minutes each
                </p>
            </div>

            {/* Category Filter Pills */}
            <div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
                <button
                    onClick={() => setFilters({ ...filters, category: '' })}
                    className={`badge py-2 px-4 whitespace-nowrap cursor-pointer transition-all ${
                        !filters.category
                            ? 'bg-indigo-600 text-white'
                            : 'bg-slate-700 text-slate-300 hover:bg-slate-600'
                    }`}
                >
                    All Tasks
                </button>
                {categories.map(cat => (
                    <button
                        key={cat.key}
                        onClick={() => setFilters({ ...filters, category: cat.key })}
                        className={`badge py-2 px-4 whitespace-nowrap cursor-pointer transition-all ${
                            filters.category === cat.key
                                ? 'bg-indigo-600 text-white'
                                : 'bg-slate-700 text-slate-300 hover:bg-slate-600'
                        }`}
                    >
                        {cat.label} ({cat.count})
                    </button>
                ))}
            </div>

            {/* Tabs */}
            <div className="flex gap-1 bg-slate-800 rounded-xl p-1">
                {tabs.map(t => (
                    <button
                        key={t.key}
                        onClick={() => setTab(t.key)}
                        className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-all ${
                            tab === t.key
                                ? 'bg-indigo-600 text-white'
                                : 'text-slate-400 hover:text-white'
                        }`}
                    >
                        {t.label}
                    </button>
                ))}
            </div>

            {/* Difficulty & Time Filters */}
            <div className="flex gap-3">
                <select
                    value={filters.difficulty}
                    onChange={(e) => setFilters({ ...filters, difficulty: e.target.value })}
                    className="input-field text-sm py-2 w-auto"
                >
                    <option value="">All Levels</option>
                    <option value="Beginner">🌱 Beginner</option>
                    <option value="Intermediate">🌿 Intermediate</option>
                    <option value="Advanced">🌳 Advanced</option>
                </select>
                <select
                    value={filters.time}
                    onChange={(e) => setFilters({ ...filters, time: e.target.value })}
                    className="input-field text-sm py-2 w-auto"
                >
                    <option value="">Any Time</option>
                    <option value="5">⚡ 5 min or less</option>
                    <option value="10">🕐 10 min or less</option>
                    <option value="15">🕑 15 min or less</option>
                    <option value="30">🕒 30 min or less</option>
                </select>
            </div>

            {/* Task List */}
            {loading ? (
                <div className="space-y-4">
                    {[...Array(4)].map((_, i) => (
                        <div key={i} className="card animate-pulse">
                            <div className="h-5 bg-slate-700 rounded w-3/4 mb-3"></div>
                            <div className="h-4 bg-slate-700 rounded w-1/2 mb-2"></div>
                            <div className="h-4 bg-slate-700 rounded w-1/4"></div>
                        </div>
                    ))}
                </div>
            ) : tasks.length > 0 ? (
                <div className="space-y-3">
                    {tasks.map((task, i) => (
                        <TaskCard
                            key={task.id}
                            task={tab === 'available' ? task : task}
                            index={i}
                            tab={tab}
                            onAccept={handleAcceptTask}
                        />
                    ))}
                </div>
            ) : (
                <div className="card text-center py-12">
                    <span className="text-4xl mb-4 block">
                        {tab === 'available' ? '🔍' : tab === 'my' ? '📭' : '🌟'}
                    </span>
                    <h3 className="font-semibold mb-1">
                        {tab === 'available' && 'No tasks available'}
                        {tab === 'my' && 'No tasks in progress'}
                        {tab === 'completed' && 'No completed tasks yet'}
                    </h3>
                    <p className="text-sm text-slate-400">
                        {tab === 'available' && 'Check back soon for new civic bites!'}
                        {tab === 'my' && 'Accept a task to get started!'}
                        {tab === 'completed' && 'Complete your first task to see it here!'}
                    </p>
                </div>
            )}
        </div>
    );
}

// ============ Task Card Component ============

function TaskCard({ task, index, tab, onAccept }) {
    // For "my" and "completed" tabs, task data is nested
    const taskData = task.micro_tasks || task;
    const userTask = task.micro_tasks ? task : null;

    return (
        <motion.div
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ delay: index * 0.05 }}
        >
            <Link
                href={`/tasks/${taskData.id}`}
                className="card flex items-start gap-4 hover:border-indigo-500/30 transition-all cursor-pointer"
            >
                {/* Category Icon */}
                <div className="text-3xl flex-shrink-0 mt-1">
                    {getCategoryIcon(taskData.category)}
                </div>

                {/* Task Details */}
                <div className="flex-1 min-w-0">
                    <div className="flex items-center gap-2 mb-1 flex-wrap">
                        <span className={`badge text-[10px] ${
                            taskData.difficulty === 'Beginner' ? 'bg-green-500/20 text-green-400' :
                            taskData.difficulty === 'Intermediate' ? 'bg-yellow-500/20 text-yellow-400' :
                            'bg-red-500/20 text-red-400'
                        }`}>
                            {taskData.difficulty}
                        </span>
                        <span className="badge bg-slate-700 text-slate-300 text-[10px]">
                            {taskData.category}
                        </span>
                        {userTask?.status === 'submitted' && (
                            <span className="badge bg-emerald-500/20 text-emerald-400 text-[10px]">
                                <CheckCircle size={10} className="mr-1" /> Completed
                            </span>
                        )}
                    </div>

                    <h3 className="font-semibold text-sm mb-1 line-clamp-1">
                        {taskData.title}
                    </h3>
                    <p className="text-xs text-slate-400 line-clamp-2 mb-2">
                        {taskData.description}
                    </p>

                    <div className="flex items-center gap-4 text-xs text-slate-500">
                        <span className="flex items-center gap-1">
                            <Clock size={12} /> {taskData.estimated_minutes} min
                        </span>
                        <span className="flex items-center gap-1 text-indigo-400 font-semibold">
                            <Star size={12} /> +{taskData.xp_reward} XP
                        </span>
                        {userTask?.submitted_at && (
                            <span>{timeAgo(userTask.submitted_at)}</span>
                        )}
                    </div>
                </div>

                {/* Action */}
                <div className="flex-shrink-0">
                    {tab === 'available' ? (
                        <button
                            onClick={(e) => {
                                e.preventDefault();
                                onAccept(taskData.id);
                            }}
                            className="btn-primary text-xs py-2 px-3"
                        >
                            Accept
                        </button>
                    ) : (
                        <ChevronRight size={18} className="text-slate-500" />
                    )}
                </div>
            </Link>
        </motion.div>
    );
}

client/src/app/tasks/[id]/page.js

'use client';
// ====================================
// Single Task Detail + Submission Page
// ====================================
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { taskAPI } from '@/lib/api';
import { getCategoryIcon, getLevelEmoji } from '@/lib/utils';
import toast from 'react-hot-toast';
import { motion } from 'framer-motion';
import Confetti from 'react-confetti';
import {
    Clock, Star, ArrowLeft, Upload, Camera,
    CheckCircle, AlertTriangle, Send
} from 'lucide-react';

export default function TaskDetailPage() {
    const { id } = useParams();
    const router = useRouter();
    const [task, setTask] = useState(null);
    const [loading, setLoading] = useState(true);
    const [submitting, setSubmitting] = useState(false);
    const [showConfetti, setShowConfetti] = useState(false);
    const [submission, setSubmission] = useState({
        proof_text: '',
        proof_file: null,
        quiz_score: null
    });
    const [previewUrl, setPreviewUrl] = useState(null);

    useEffect(() => {
        fetchTask();
    }, [id]);

    const fetchTask = async () => {
        try {
            const response = await taskAPI.getById(id);
            if (response.success) {
                setTask(response.data);
            }
        } catch (error) {
            toast.error('Task not found');
            router.push('/tasks');
        } finally {
            setLoading(false);
        }
    };

    const handleAccept = async () => {
        try {
            const response = await taskAPI.accept(id);
            if (response.success) {
                toast.success(response.message);
                fetchTask(); // Refresh to show updated status
            }
        } catch (error) {
            toast.error(error.message || 'Failed to accept task');
        }
    };

    const handleFileChange = (e) => {
        const file = e.target.files[0];
        if (file) {
            setSubmission({ ...submission, proof_file: file });
            setPreviewUrl(URL.createObjectURL(file));
        }
    };

    const handleSubmit = async () => {
        setSubmitting(true);
        try {
            const formData = new FormData();
            if (submission.proof_file) {
                formData.append('proof', submission.proof_file);
            }
            if (submission.proof_text) {
                formData.append('proof_text', submission.proof_text);
            }
            if (submission.quiz_score !== null) {
                formData.append('quiz_score', submission.quiz_score);
            }

            const response = await taskAPI.submit(id, formData);
            if (response.success) {
                setShowConfetti(true);
                toast.success(response.message, {
                    duration: 5000,
                    icon: '🎉'
                });

                // Show achievement notifications
                if (response.data.new_achievements?.length > 0) {
                    response.data.new_achievements.forEach(achievement => {
                        setTimeout(() => {
                            toast.success(
                                `🏅 New Achievement: ${achievement.icon} ${achievement.name}!`,
                                { duration: 6000 }
                            );
                        }, 2000);
                    });
                }

                // Redirect after celebration
                setTimeout(() => {
                    setShowConfetti(false);
                    router.push('/dashboard');
                }, 4000);
            }
        } catch (error) {
            toast.error(error.message || 'Submission failed');
        } finally {
            setSubmitting(false);
        }
    };

    if (loading) {
        return (
            <div className="animate-pulse space-y-4">
                <div className="h-8 bg-slate-800 rounded w-2/3"></div>
                <div className="h-40 bg-slate-800 rounded-2xl"></div>
                <div className="h-60 bg-slate-800 rounded-2xl"></div>
            </div>
        );
    }

    if (!task) return null;

    const userStatus = task.user_status;
    const isAccepted = userStatus && ['assigned', 'in_progress'].includes(userStatus.status);
    const isCompleted = userStatus && ['submitted', 'verified'].includes(userStatus.status);

    return (
        <div className="max-w-2xl mx-auto space-y-6">
            {showConfetti && <Confetti recycle={false} numberOfPieces={300} />}

            {/* Back Button */}
            <button
                onClick={() => router.back()}
                className="flex items-center gap-2 text-slate-400 hover:text-white transition-colors"
            >
                <ArrowLeft size={18} /> Back to Tasks
            </button>

            {/* Task Header */}
            <motion.div
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                className="card"
            >
                <div className="flex items-center gap-3 mb-4">
                    <span className="text-4xl">{getCategoryIcon(task.category)}</span>
                    <div>
                        <div className="flex items-center gap-2 mb-1">
                            <span className={`badge text-xs ${
                                task.difficulty === 'Beginner' ? 'bg-green-500/20 text-green-400' :
                                task.difficulty === 'Intermediate' ? 'bg-yellow-500/20 text-yellow-400' :
                                'bg-red-500/20 text-red-400'
                            }`}>
                                {task.difficulty}
                            </span>
                            <span className="badge bg-slate-700 text-slate-300 text-xs">
                                {task.category}
                            </span>
                        </div>
                        <h1 className="text-xl font-bold">{task.title}</h1>
                    </div>
                </div>

                <p className="text-slate-300 mb-4">{task.description}</p>

                <div className="flex items-center gap-6 text-sm">
                    <span className="flex items-center gap-1 text-slate-400">
                        <Clock size={16} /> {task.estimated_minutes} minutes
                    </span>
                    <span className="flex items-center gap-1 text-indigo-400 font-semibold">
                        <Star size={16} /> +{task.xp_reward} XP
                    </span>
                    <span className="text-slate-400">
                        Proof: {task.required_proof === 'photo' ? '📸 Photo' :
                                task.required_proof === 'text' ? '✍️ Text' :
                                task.required_proof === 'quiz' ? '❓ Quiz' : '✅ None'}
                    </span>
                </div>

                {/* Location hint */}
                {task.location_hint && (
                    <div className="mt-4 p-3 rounded-xl bg-slate-700/50 flex items-center gap-2 text-sm">
                        <span>📍</span>
                        <span className="text-slate-300">{task.location_hint}</span>
                    </div>
                )}
            </motion.div>

            {/* Instructions */}
            {task.instructions && (
                <motion.div
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ delay: 0.1 }}
                    className="card"
                >
                    <h2 className="text-lg font-semibold mb-3">📋 Instructions</h2>
                    <div className="text-slate-300 text-sm leading-relaxed whitespace-pre-line">
                        {task.instructions}
                    </div>
                </motion.div>
            )}

            {/* Action Area */}
            <motion.div
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: 0.2 }}
            >
                {/* Not yet accepted */}
                {!userStatus && (
                    <button onClick={handleAccept} className="btn-primary w-full text-lg py-4">
                        Accept This Task 💪
                    </button>
                )}

                {/* Already completed */}
                {isCompleted && (
                    <div className="card text-center py-8 border-emerald-500/30">
                        <CheckCircle size={48} className="mx-auto text-emerald-400 mb-4" />
                        <h3 className="text-lg font-semibold text-emerald-400">Task Completed!</h3>
                        <p className="text-slate-400 mt-1">
                            You earned +{userStatus.xp_earned} XP for this task
                        </p>
                    </div>
                )}

                {/* In progress — Show submission form */}
                {isAccepted && (
                    <div className="card">
                        <h2 className="text-lg font-semibold mb-4">📤 Submit Your Work</h2>

                        {/* Photo Upload */}
                        {task.required_proof === 'photo' && (
                            <div className="mb-4">
                                <label className="block text-sm text-slate-400 mb-2">
                                    Upload Photo Proof
                                </label>
                                <div
                                    className="border-2 border-dashed border-slate-600 rounded-xl p-6 text-center cursor-pointer hover:border-indigo-500 transition-colors"
                                    onClick={() => document.getElementById('file-input').click()}
                                >
                                    {previewUrl ? (
                                        <img
                                            src={previewUrl}
                                            alt="Preview"
                                            className="max-h-60 mx-auto rounded-lg"
                                        />
                                    ) : (
                                        <>
                                            <Camera size={32} className="mx-auto text-slate-500 mb-2" />
                                            <p className="text-sm text-slate-400">
                                                Click to upload or drag & drop
                                            </p>
                                            <p className="text-xs text-slate-500 mt-1">
                                                JPG, PNG up to 5MB
                                            </p>
                                        </>
                                    )}
                                    <input
                                        id="file-input"
                                        type="file"
                                        accept="image/*"
                                        className="hidden"
                                        onChange={handleFileChange}
                                    />
                                </div>
                            </div>
                        )}

                        {/* Text Submission */}
                        {(task.required_proof === 'text' || task.required_proof === 'photo') && (
                            <div className="mb-4">
                                <label className="block text-sm text-slate-400 mb-2">
                                    {task.required_proof === 'photo'
                                        ? 'Add a note (optional)'
                                        : 'Your Response'}
                                </label>
                                <textarea
                                    value={submission.proof_text}
                                    onChange={(e) => setSubmission({
                                        ...submission,
                                        proof_text: e.target.value
                                    })}
                                    className="input-field min-h-[120px] resize-none"
                                    placeholder="Describe what you did, what you observed..."
                                    rows={4}
                                />
                            </div>
                        )}

                        {/* Submit Button */}
                        <button
                            onClick={handleSubmit}
                            disabled={submitting || (
                                task.required_proof === 'photo' && !submission.proof_file
                            ) || (
                                task.required_proof === 'text' && !submission.proof_text
                            )}
                            className="btn-success w-full flex items-center justify-center gap-2"
                        >
                            {submitting ? (
                                <>🔄 Submitting...</>
                            ) : (
                                <>
                                    <Send size={18} />
                                    Submit & Earn {task.xp_reward} XP
                                </>
                            )}
                        </button>
                    </div>
                )}
            </motion.div>
        </div>
    );
}

4.8 Ward Impact Dashboard

client/src/app/ward/page.js

'use client';
// ====================================
// Ward Impact Dashboard
// ====================================
import { useState, useEffect } from 'react';
import { useAuth } from '@/context/AuthContext';
import { wardAPI, issueAPI } from '@/lib/api';
import { getStatusStyle, timeAgo, formatXP } from '@/lib/utils';
import { motion } from 'framer-motion';
import Link from 'next/link';
import {
    Users, Target, CheckCircle, AlertTriangle,
    TrendingUp, MapPin, Award, ChevronDown
} from 'lucide-react';

export default function WardDashboardPage() {
    const { profile } = useAuth();
    const [wards, setWards] = useState([]);
    const [selectedWardId, setSelectedWardId] = useState(null);
    const [dashboard, setDashboard] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        fetchWards();
    }, []);

    useEffect(() => {
        if (selectedWardId) {
            fetchDashboard(selectedWardId);
        }
    }, [selectedWardId]);

    const fetchWards = async () => {
        try {
            const response = await wardAPI.getAll();
            if (response.success) {
                setWards(response.data);
                // Default to user's ward
                const defaultWard = profile?.ward_id || response.data[0]?.id;
                setSelectedWardId(defaultWard);
            }
        } catch (error) {
            console.error('Fetch wards error:', error);
        }
    };

    const fetchDashboard = async (wardId) => {
        setLoading(true);
        try {
            const response = await wardAPI.getDashboard(wardId);
            if (response.success) {
                setDashboard(response.data);
            }
        } catch (error) {
            console.error('Fetch dashboard error:', error);
        } finally {
            setLoading(false);
        }
    };

    return (
        <div className="space-y-6">
            {/* Header */}
            <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
                <div>
                    <h1 className="text-2xl font-bold">Ward Impact Dashboard</h1>
                    <p className="text-slate-400 mt-1">
                        Making invisible civic work visible 📊
                    </p>
                </div>

                {/* Ward Selector */}
                <select
                    value={selectedWardId || ''}
                    onChange={(e) => setSelectedWardId(e.target.value)}
                    className="input-field w-auto text-sm py-2"
                >
                    {wards.map(ward => (
                        <option key={ward.id} value={ward.id}>
                            Ward {ward.ward_number}{ward.ward_name}
                            {ward.id === profile?.ward_id ? ' (Your Ward)' : ''}
                        </option>
                    ))}
                </select>
            </div>

            {loading ? (
                <div className="animate-pulse space-y-4">
                    <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
                        {[...Array(4)].map((_, i) => (
                            <div key={i} className="h-28 bg-slate-800 rounded-2xl"></div>
                        ))}
                    </div>
                    <div className="h-60 bg-slate-800 rounded-2xl"></div>
                </div>
            ) : dashboard ? (
                <>
                    {/* Ward Header Card */}
                    <motion.div
                        initial={{ opacity: 0, y: 10 }}
                        animate={{ opacity: 1, y: 0 }}
                        className="card bg-gradient-to-r from-indigo-950/30 to-purple-950/30 border-indigo-500/20"
                    >
                        <div className="flex items-center justify-between">
                            <div>
                                <div className="flex items-center gap-2 mb-1">
                                    <MapPin size={18} className="text-indigo-400" />
                                    <h2 className="text-xl font-bold">
                                        Ward {dashboard.ward.ward_number}{dashboard.ward.ward_name}
                                    </h2>
                                </div>
                                <p className="text-slate-400 text-sm">
                                    {dashboard.ward.area_description || dashboard.ward.city}
                                </p>
                            </div>
                            <div className="text-center">
                                <div className="text-3xl font-bold text-indigo-400">
                                    {dashboard.responsiveness_score?.toFixed(1) || '—'}
                                </div>
                                <div className="text-xs text-slate-400">
                                    Responsiveness<br/>Score /10
                                </div>
                            </div>
                        </div>
                    </motion.div>

                    {/* Stats Grid */}
                    <motion.div
                        initial={{ opacity: 0, y: 10 }}
                        animate={{ opacity: 1, y: 0 }}
                        transition={{ delay: 0.1 }}
                        className="grid grid-cols-2 md:grid-cols-4 gap-4"
                    >
                        <WardStatCard
                            icon={<Users className="text-blue-400" size={24} />}
                            value={dashboard.stats.active_CivicStreaks}
                            label="Active CivicStreaks"
                        />
                        <WardStatCard
                            icon={<Target className="text-purple-400" size={24} />}
                            value={dashboard.stats.active_circles}
                            label="Active Circles"
                        />
                        <WardStatCard
                            icon={<AlertTriangle className="text-amber-400" size={24} />}
                            value={dashboard.stats.issues.total}
                            label="Issues Tracked"
                        />
                        <WardStatCard
                            icon={<CheckCircle className="text-emerald-400" size={24} />}
                            value={dashboard.stats.issues.resolved}
                            label="Issues Resolved"
                        />
                    </motion.div>

                    {/* Two Column Layout */}
                    <div className="grid md:grid-cols-2 gap-6">
                        {/* Top Issues */}
                        <motion.div
                            initial={{ opacity: 0, y: 10 }}
                            animate={{ opacity: 1, y: 0 }}
                            transition={{ delay: 0.2 }}
                        >
                            <h2 className="text-lg font-semibold mb-3">
                                📌 Top Issues This Month
                            </h2>
                            <div className="card space-y-3">
                                {dashboard.top_issues.length > 0 ? (
                                    dashboard.top_issues.map((issue, i) => (
                                        <Link
                                            key={issue.id}
                                            href={`/issues/${issue.id}`}
                                            className="flex items-center gap-3 p-3 rounded-xl bg-slate-700/30 hover:bg-slate-700/50 transition-colors"
                                        >
                                            <span className="text-lg font-bold text-slate-500 w-6">
                                                {i + 1}.
                                            </span>
                                            <div className="flex-1 min-w-0">
                                                <p className="text-sm font-medium truncate">
                                                    {issue.title}
                                                </p>
                                                <div className="flex items-center gap-2 mt-1">
                                                    <span className={`badge text-[10px] ${getStatusStyle(issue.status)}`}>
                                                        {issue.status.replace('_', ' ')}
                                                    </span>
                                                    <span className="text-[10px] text-slate-500">
                                                        Day {issue.days_ago}
                                                    </span>
                                                </div>
                                            </div>
                                            <span className="text-xs text-slate-500">
                                                👍 {issue.upvotes}
                                            </span>
                                        </Link>
                                    ))
                                ) : (
                                    <p className="text-center text-slate-500 py-6 text-sm">
                                        No issues reported yet for this ward.
                                    </p>
                                )}

                                <Link
                                    href={`/issues?ward=${selectedWardId}`}
                                    className="block text-center text-indigo-400 text-sm hover:underline pt-2"
                                >
                                    View all issues →
                                </Link>
                            </div>
                        </motion.div>

                        {/* Ward Leaderboard */}
                        <motion.div
                            initial={{ opacity: 0, y: 10 }}
                            animate={{ opacity: 1, y: 0 }}
                            transition={{ delay: 0.3 }}
                        >
                            <h2 className="text-lg font-semibold mb-3">
                                🏆 Ward Leaderboard
                            </h2>
                            <div className="card space-y-3">
                                {dashboard.top_users.length > 0 ? (
                                    dashboard.top_users.map((user, i) => (
                                        <div
                                            key={i}
                                            className="flex items-center gap-3 p-3 rounded-xl bg-slate-700/30"
                                        >
                                            <span className="text-lg font-bold w-6 text-center">
                                                {i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`}
                                            </span>
                                            <div className="w-8 h-8 rounded-full bg-indigo-600 flex items-center justify-center text-xs font-bold flex-shrink-0">
                                                {user.full_name?.charAt(0)?.toUpperCase()}
                                            </div>
                                            <div className="flex-1 min-w-0">
                                                <p className="text-sm font-medium truncate">
                                                    {user.full_name}
                                                </p>
                                                <p className="text-[10px] text-slate-400">
                                                    {user.level} • 🔥 {user.current_streak} day streak
                                                </p>
                                            </div>
                                            <span className="text-sm font-bold text-indigo-400">
                                                {formatXP(user.xp_points)} XP
                                            </span>
                                        </div>
                                    ))
                                ) : (
                                    <p className="text-center text-slate-500 py-6 text-sm">
                                        No active users yet in this ward.
                                    </p>
                                )}

                                <Link
                                    href="/leaderboard"
                                    className="block text-center text-indigo-400 text-sm hover:underline pt-2"
                                >
                                    View full leaderboard →
                                </Link>
                            </div>
                        </motion.div>
                    </div>

                    {/* Issue Status Breakdown */}
                    <motion.div
                        initial={{ opacity: 0, y: 10 }}
                        animate={{ opacity: 1, y: 0 }}
                        transition={{ delay: 0.4 }}
                        className="card"
                    >
                        <h2 className="text-lg font-semibold mb-4">
                            📊 Issue Status Breakdown
                        </h2>
                        <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
                            {[
                                { label: 'Reported', value: dashboard.stats.issues.reported, color: 'bg-yellow-500' },
                                { label: 'In Progress', value: dashboard.stats.issues.in_progress, color: 'bg-purple-500' },
                                { label: 'Resolved', value: dashboard.stats.issues.resolved, color: 'bg-emerald-500' },
                                { label: 'Stale', value: dashboard.stats.issues.stale, color: 'bg-red-500' },
                                { label: 'Total', value: dashboard.stats.issues.total, color: 'bg-indigo-500' },
                            ].map((item, i) => (
                                <div key={i} className="text-center p-3 rounded-xl bg-slate-700/30">
                                    <div className={`w-3 h-3 rounded-full ${item.color} mx-auto mb-2`} />
                                    <div className="text-xl font-bold">{item.value}</div>
                                    <div className="text-xs text-slate-400">{item.label}</div>
                                </div>
                            ))}
                        </div>

                        {/* Simple Bar Visual */}
                        {dashboard.stats.issues.total > 0 && (
                            <div className="mt-4 h-4 bg-slate-700 rounded-full overflow-hidden flex">
                                <div
                                    className="bg-emerald-500 h-full transition-all"
                                    style={{ width: `${(dashboard.stats.issues.resolved / dashboard.stats.issues.total) * 100}%` }}
                                    title="Resolved"
                                />
                                <div
                                    className="bg-purple-500 h-full transition-all"
                                    style={{ width: `${(dashboard.stats.issues.in_progress / dashboard.stats.
                                issues.total) * 100}%` }}
                                    title="In Progress"
                                />
                                <div
                                    className="bg-yellow-500 h-full transition-all"
                                    style={{ width: `${(dashboard.stats.issues.reported / dashboard.stats.issues.total) * 100}%` }}
                                    title="Reported"
                                />
                                <div
                                    className="bg-red-500 h-full transition-all"
                                    style={{ width: `${(dashboard.stats.issues.stale / dashboard.stats.issues.total) * 100}%` }}
                                    title="Stale"
                                />
                            </div>
                        )}
                    </motion.div>
                </>
            ) : (
                <div className="card text-center py-12">
                    <MapPin size={48} className="mx-auto text-slate-600 mb-4" />
                    <h3 className="font-semibold mb-1">Select a ward to view its dashboard</h3>
                    <p className="text-sm text-slate-400">
                        Track issues, see active volunteers, and measure impact.
                    </p>
                </div>
            )}
        </div>
    );
}

// ============ Sub-Components ============

function WardStatCard({ icon, value, label }) {
    return (
        <div className="card flex flex-col items-center text-center py-4">
            {icon}
            <p className="text-2xl font-bold mt-2">{value}</p>
            <p className="text-xs text-slate-400 mt-1">{label}</p>
        </div>
    );
}

4.9 Portfolio Page

client/src/app/portfolio/[userId]/page.js

'use client';
// ====================================
// Civic Portfolio — Public Profile Page
// "Your Democratic Resume"
// ====================================
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import { portfolioAPI } from '@/lib/api';
import { formatXP, getLevelEmoji, getCategoryIcon, getStatusStyle, timeAgo } from '@/lib/utils';
import { motion } from 'framer-motion';
import toast from 'react-hot-toast';
import {
    Flame, Star, Target, Award, MapPin,
    Calendar, ExternalLink, Download, Share2,
    CheckCircle, BookOpen, FileText, Users
} from 'lucide-react';

export default function PortfolioPage() {
    const { userId } = useParams();
    const [portfolio, setPortfolio] = useState(null);
    const [aiSummary, setAiSummary] = useState(null);
    const [loading, setLoading] = useState(true);
    const [activeTab, setActiveTab] = useState('overview');

    useEffect(() => {
        fetchPortfolio();
    }, [userId]);

    const fetchPortfolio = async () => {
        try {
            const response = await portfolioAPI.get(userId);
            if (response.success) {
                setPortfolio(response.data);
            }
        } catch (error) {
            console.error('Fetch portfolio error:', error);
            toast.error('Portfolio not found');
        } finally {
            setLoading(false);
        }
    };

    const generateSummary = async () => {
        try {
            toast.loading('Generating AI summary...');
            const response = await portfolioAPI.generateSummary(userId);
            if (response.success) {
                setAiSummary(response.data.summary);
                toast.dismiss();
                toast.success('Summary generated!');
            }
        } catch (error) {
            toast.dismiss();
            toast.error('Failed to generate summary');
        }
    };

    const handleShare = async () => {
        const url = `${window.location.origin}/portfolio/${userId}`;
        if (navigator.share) {
            await navigator.share({
                title: `${portfolio.profile.full_name}'s Civic Portfolio — CivicStreak`,
                text: `Check out my civic engagement portfolio on CivicStreak!`,
                url
            });
        } else {
            await navigator.clipboard.writeText(url);
            toast.success('Portfolio link copied! 📋');
        }
    };

    const handleDownloadCertificate = async () => {
        try {
            const response = await portfolioAPI.getCertificate(userId);
            if (response.success) {
                // Generate certificate using browser (simplified)
                generateCertificatePDF(response.data);
                toast.success('Certificate downloaded! 📜');
            }
        } catch (error) {
            toast.error('Failed to generate certificate');
        }
    };

    if (loading) {
        return (
            <div className="max-w-4xl mx-auto animate-pulse space-y-6">
                <div className="h-48 bg-slate-800 rounded-2xl"></div>
                <div className="grid grid-cols-4 gap-4">
                    {[...Array(4)].map((_, i) => (
                        <div key={i} className="h-24 bg-slate-800 rounded-2xl"></div>
                    ))}
                </div>
                <div className="h-80 bg-slate-800 rounded-2xl"></div>
            </div>
        );
    }

    if (!portfolio) {
        return (
            <div className="text-center py-20">
                <span className="text-5xl">🔍</span>
                <h2 className="text-xl font-bold mt-4">Portfolio Not Found</h2>
            </div>
        );
    }

    const { profile, completed_tasks, reported_issues, achievements, circles, skills, category_breakdown, streak_history } = portfolio;

    const tabs = [
        { key: 'overview', label: '📊 Overview' },
        { key: 'tasks', label: '✅ Tasks' },
        { key: 'issues', label: '📢 Issues' },
        { key: 'achievements', label: '🏅 Achievements' },
    ];

    return (
        <div className="max-w-4xl mx-auto space-y-6">
            {/* Profile Header Card */}
            <motion.div
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                className="card bg-gradient-to-br from-indigo-950/40 to-purple-950/30 border-indigo-500/20"
            >
                <div className="flex flex-col md:flex-row items-start md:items-center gap-6">
                    {/* Avatar */}
                    <div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-3xl font-bold flex-shrink-0">
                        {profile.avatar_url ? (
                            <img src={profile.avatar_url} alt={profile.full_name} className="w-full h-full rounded-2xl object-cover" />
                        ) : (
                            profile.full_name?.charAt(0)?.toUpperCase()
                        )}
                    </div>

                    {/* Info */}
                    <div className="flex-1">
                        <h1 className="text-2xl font-bold">{profile.full_name}</h1>
                        <div className="flex flex-wrap items-center gap-3 mt-2 text-sm">
                            {profile.wards && (
                                <span className="flex items-center gap-1 text-slate-400">
                                    <MapPin size={14} />
                                    Ward {profile.wards.ward_number}{profile.wards.ward_name}
                                </span>
                            )}
                            {profile.college && (
                                <span className="flex items-center gap-1 text-slate-400">
                                    <BookOpen size={14} />
                                    {profile.college}
                                </span>
                            )}
                            <span className="flex items-center gap-1 text-slate-400">
                                <Calendar size={14} />
                                Active for {profile.days_since_joining} days
                            </span>
                        </div>
                        <div className="flex items-center gap-3 mt-3">
                            <span className="badge bg-indigo-500/20 text-indigo-400">
                                {getLevelEmoji(profile.level)} {profile.level}
                            </span>
                            <span className="badge bg-amber-500/20 text-amber-400">
                                🔥 {profile.current_streak} day streak
                            </span>
                            <span className="badge bg-purple-500/20 text-purple-400">{formatXP(profile.xp_points)} XP
                            </span>
                        </div>
                    </div>

                    {/* Actions */}
                    <div className="flex gap-2 flex-shrink-0">
                        <button onClick={handleShare} className="btn-secondary text-sm py-2 px-3 flex items-center gap-1">
                            <Share2 size={14} /> Share
                        </button>
                        <button onClick={handleDownloadCertificate} className="btn-primary text-sm py-2 px-3 flex items-center gap-1">
                            <Download size={14} /> Certificate
                        </button>
                    </div>
                </div>
            </motion.div>

            {/* Impact Stats Grid */}
            <motion.div
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: 0.1 }}
                className="grid grid-cols-3 md:grid-cols-6 gap-3"
            >
                {[
                    { icon: '✅', value: profile.tasks_completed, label: 'Tasks Done' },
                    { icon: '📢', value: profile.issues_reported, label: 'Issues Reported' },
                    { icon: '✨', value: profile.issues_resolved, label: 'Resolved' },
                    { icon: '📜', value: profile.rtis_filed, label: 'RTIs Filed' },
                    { icon: '🏛️', value: profile.meetings_attended, label: 'Meetings' },
                    { icon: '🎓', value: profile.people_mentored, label: 'Mentored' },
                ].map((stat, i) => (
                    <div key={i} className="card text-center py-3">
                        <span className="text-lg">{stat.icon}</span>
                        <p className="text-xl font-bold mt-1">{stat.value}</p>
                        <p className="text-[10px] text-slate-400">{stat.label}</p>
                    </div>
                ))}
            </motion.div>

            {/* AI Summary */}
            <motion.div
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: 0.15 }}
                className="card"
            >
                <div className="flex items-center justify-between mb-3">
                    <h2 className="text-lg font-semibold">🤖 AI Portfolio Summary</h2>
                    <button
                        onClick={generateSummary}
                        className="btn-secondary text-xs py-1.5 px-3"
                    >
                        {aiSummary ? 'Regenerate' : 'Generate'}
                    </button>
                </div>
                {aiSummary ? (
                    <p className="text-slate-300 leading-relaxed italic">
                        "{aiSummary}"
                    </p>
                ) : (
                    <p className="text-slate-500 text-sm">
                        Click "Generate" to create an AI-powered summary of your civic engagement for college applications, resumes, and LinkedIn.
                    </p>
                )}
            </motion.div>

            {/* Skills Earned */}
            {skills.length > 0 && (
                <motion.div
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ delay: 0.2 }}
                    className="card"
                >
                    <h2 className="text-lg font-semibold mb-3">🛠️ Skills Earned</h2>
                    <div className="flex flex-wrap gap-2">
                        {skills.map((skill, i) => (
                            <span
                                key={i}
                                className="badge bg-emerald-500/20 text-emerald-400 py-2 px-4"
                            >
                                {skill.verified ? '✅' : '⬜'} {skill.name}
                            </span>
                        ))}
                    </div>
                </motion.div>
            )}

            {/* Tabs */}
            <div className="flex gap-1 bg-slate-800 rounded-xl p-1 sticky top-4 z-10">
                {tabs.map(t => (
                    <button
                        key={t.key}
                        onClick={() => setActiveTab(t.key)}
                        className={`flex-1 py-2 px-3 rounded-lg text-sm font-medium transition-all ${
                            activeTab === t.key
                                ? 'bg-indigo-600 text-white'
                                : 'text-slate-400 hover:text-white'
                        }`}
                    >
                        {t.label}
                    </button>
                ))}
            </div>

            {/* Tab Content */}
            <motion.div
                key={activeTab}
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
            >
                {/* Overview Tab */}
                {activeTab === 'overview' && (
                    <div className="space-y-6">
                        {/* Category Breakdown */}
                        <div className="card">
                            <h3 className="font-semibold mb-4">📊 Task Category Breakdown</h3>
                            <div className="space-y-3">
                                {Object.entries(category_breakdown).map(([cat, count]) => {
                                    const total = Object.values(category_breakdown).reduce((a, b) => a + b, 0);
                                    const percent = total > 0 ? (count / total) * 100 : 0;
                                    return (
                                        <div key={cat}>
                                            <div className="flex items-center justify-between text-sm mb-1">
                                                <span>{getCategoryIcon(cat)} {cat}</span>
                                                <span className="text-slate-400">{count} tasks ({percent.toFixed(0)}%)</span>
                                            </div>
                                            <div className="w-full h-2 bg-slate-700 rounded-full overflow-hidden">
                                                <motion.div
                                                    className="h-full bg-indigo-500 rounded-full"
                                                    initial={{ width: 0 }}
                                                    animate={{ width: `${percent}%` }}
                                                    transition={{ duration: 0.8 }}
                                                />
                                            </div>
                                        </div>
                                    );
                                })}
                            </div>
                        </div>

                        {/* Activity Heatmap (Simplified) */}
                        <div className="card">
                            <h3 className="font-semibold mb-4">📅 Activity History (Last 30 Days)</h3>
                            <div className="grid grid-cols-7 gap-1">
                                {Array.from({ length: 30 }, (_, i) => {
                                    const date = new Date();
                                    date.setDate(date.getDate() - (29 - i));
                                    const dateStr = date.toISOString().split('T')[0];
                                    const activity = streak_history?.find(
                                        s => s.activity_date === dateStr
                                    );
                                    const intensity = activity
                                        ? Math.min(activity.tasks_completed, 4)
                                        : 0;
                                    const colors = [
                                        'bg-slate-700',
                                        'bg-indigo-900',
                                        'bg-indigo-700',
                                        'bg-indigo-500',
                                        'bg-indigo-400'
                                    ];
                                    return (
                                        <div
                                            key={i}
                                            className={`aspect-square rounded-sm ${colors[intensity]} transition-colors`}
                                            title={`${dateStr}: ${activity?.tasks_completed || 0} tasks`}
                                        />
                                    );
                                })}
                            </div>
                            <div className="flex items-center gap-2 mt-3 justify-end text-xs text-slate-500">
                                <span>Less</span>
                                {['bg-slate-700', 'bg-indigo-900', 'bg-indigo-700', 'bg-indigo-500', 'bg-indigo-400'].map((c, i) => (
                                    <div key={i} className={`w-3 h-3 rounded-sm ${c}`} />
                                ))}
                                <span>More</span>
                            </div>
                        </div>

                        {/* Circles */}
                        {circles.length > 0 && (
                            <div className="card">
                                <h3 className="font-semibold mb-3">🤝 Community Circles</h3>
                                <div className="space-y-3">
                                    {circles.map((membership, i) => (
                                        <div key={i} className="flex items-center gap-3 p-3 rounded-xl bg-slate-700/30">
                                            <Users size={18} className="text-green-400" />
                                            <div className="flex-1">
                                                <p className="text-sm font-medium">
                                                    {membership.circles?.name}
                                                </p>
                                                <p className="text-xs text-slate-400">
                                                    Role: {membership.role}{membership.circles?.wards?.ward_name}
                                                </p>
                                            </div>
                                        </div>
                                    ))}
                                </div>
                            </div>
                        )}
                    </div>
                )}

                {/* Tasks Tab */}
                {activeTab === 'tasks' && (
                    <div className="card space-y-3">
                        <h3 className="font-semibold mb-2">
                            Completed Tasks ({completed_tasks.length})
                        </h3>
                        {completed_tasks.length > 0 ? (
                            completed_tasks.map((task, i) => (
                                <div key={task.id} className="flex items-start gap-3 p-3 rounded-xl bg-slate-700/30">
                                    <span className="text-xl mt-0.5">
                                        {getCategoryIcon(task.micro_tasks?.category)}
                                    </span>
                                    <div className="flex-1 min-w-0">
                                        <p className="text-sm font-medium">
                                            {task.micro_tasks?.title}
                                        </p>
                                        <div className="flex items-center gap-3 mt-1 text-xs text-slate-400">
                                            <span>{task.micro_tasks?.category}</span>
                                            <span>+{task.xp_earned} XP</span>
                                            <span>{timeAgo(task.submitted_at)}</span>
                                        </div>
                                        {task.proof_text && (
                                            <p className="text-xs text-slate-500 mt-2 italic line-clamp-2">
                                                "{task.proof_text}"
                                            </p>
                                        )}
                                    </div>
                                    {task.proof_url && (
                                        <img
                                            src={task.proof_url}
                                            alt="proof"
                                            className="w-16 h-16 rounded-lg object-cover flex-shrink-0"
                                        />
                                    )}
                                </div>
                            ))
                        ) : (
                            <p className="text-center text-slate-500 py-8 text-sm">
                                No completed tasks yet.
                            </p>
                        )}
                    </div>
                )}

                {/* Issues Tab */}
                {activeTab === 'issues' && (
                    <div className="card space-y-3">
                        <h3 className="font-semibold mb-2">
                            Reported Issues ({reported_issues.length})
                        </h3>
                        {reported_issues.length > 0 ? (
                            reported_issues.map((issue, i) => (
                                <div key={issue.id} className="flex items-center gap-3 p-3 rounded-xl bg-slate-700/30">
                                    <div className="flex-1 min-w-0">
                                        <p className="text-sm font-medium">{issue.title}</p>
                                        <div className="flex items-center gap-2 mt-1">
                                            <span className={`badge text-[10px] ${getStatusStyle(issue.status)}`}>
                                                {issue.status.replace('_', ' ')}
                                            </span>
                                            <span className="text-[10px] text-slate-500">
                                                {issue.category}
                                            </span>
                                            <span className="text-[10px] text-slate-500">
                                                👍 {issue.upvotes}
                                            </span>
                                        </div>
                                    </div>
                                    <span className="text-xs text-slate-500">
                                        {timeAgo(issue.created_at)}
                                    </span>
                                </div>
                            ))
                        ) : (
                            <p className="text-center text-slate-500 py-8 text-sm">
                                No reported issues yet.
                            </p>
                        )}
                    </div>
                )}

                {/* Achievements Tab */}
                {activeTab === 'achievements' && (
                    <div className="card">
                        <h3 className="font-semibold mb-4">
                            Achievements ({achievements.length})
                        </h3>
                        <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
                            {achievements.length > 0 ? (
                                achievements.map((ua, i) => (
                                    <div key={i} className="p-4 rounded-xl bg-slate-700/30 text-center border border-slate-600/30">
                                        <span className="text-3xl block mb-2">
                                            {ua.achievements?.icon}
                                        </span>
                                        <p className="text-sm font-semibold">
                                            {ua.achievements?.name}
                                        </p>
                                        <p className="text-[10px] text-slate-400 mt-1">
                                            {ua.achievements?.description}
                                        </p>
                                        <p className="text-[10px] text-slate-500 mt-2">
                                            Earned {timeAgo(ua.earned_at)}
                                        </p>
                                    </div>
                                ))
                            ) : (
                                <div className="col-span-full text-center py-8">
                                    <Award size={48} className="mx-auto text-slate-600 mb-3" />
                                    <p className="text-sm text-slate-500">
                                        Complete tasks to earn achievements!
                                    </p>
                                </div>
                            )}
                        </div>
                    </div>
                )}
            </motion.div>
        </div>
    );
}

/**
 * Simple certificate generator (creates downloadable HTML)
 */
function generateCertificatePDF(data) {
    const html = `
<!DOCTYPE html>
<html>
<head><title>CivicStreak Certificate</title></head>
<body style="font-family: Georgia, serif; text-align: center; padding: 60px; border: 4px double #4f46e5; margin: 20px; min-height: 600px;">
    <div style="border: 2px solid #e5e7eb; padding: 40px;">
        <h1 style="color: #4f46e5; font-size: 36px; margin-bottom: 5px;">🏙️ CivicStreak</h1>
        <p style="color: #6b7280; font-size: 14px;">Certificate of Civic Engagement</p>
        <hr style="margin: 30px auto; width: 200px; border-color: #e5e7eb;">
        <p style="font-size: 16px; color: #6b7280;">This is to certify that</p>
        <h2 style="font-size: 32px; color: #1f2937; margin: 10px 0;">${data.name}</h2>
        <p style="font-size: 16px; color: #6b7280;">has demonstrated outstanding civic engagement as a</p>
        <h3 style="font-size: 24px; color: #4f46e5; margin: 10px 0;">${data.level}</h3>
        <p style="font-size: 14px; color: #6b7280;">Ward: ${data.ward} | City: ${data.city}</p>
        <div style="margin: 30px auto; max-width: 400px; text-align: left; font-size: 13px; color: #374151;">
            <p>⭐ XP Points: ${data.xp_points} | 🔥 Longest Streak: ${data.longest_streak} days</p>
            <p>✅ Tasks Completed: ${data.tasks_completed} | 📢 Issues Reported: ${data.issues_reported}</p>
            <p>✨ Issues Resolved: ${data.issues_resolved} | 📅 Days Active: ${data.days_active}</p>
        </div>
        <hr style="margin: 30px auto; width: 200px; border-color: #e5e7eb;">
        <p style="font-size: 12px; color: #9ca3af;">Certificate ID: ${data.certificate_id}</p>
        <p style="font-size: 12px; color: #9ca3af;">Issued: ${new Date(data.issued_date).toLocaleDateString()}</p>
        <p style="font-size: 11px; color: #9ca3af; margin-top: 20px;">Verify at: CivicStreak.vercel.app/verify/${data.certificate_id}</p>
    </div>
</body>
</html>`;

    const blob = new Blob([html], { type: 'text/html' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `CivicStreak_Certificate_${data.name.replace(/\s/g, '_')}.html`;
    a.click();
    URL.revokeObjectURL(url);
}

4.10 Leaderboard Page

client/src/app/leaderboard/page.js

'use client';
// ====================================
// Global Leaderboard Page
// ====================================
import { useState, useEffect } from 'react';
import { wardAPI } from '@/lib/api';
import { useAuth } from '@/context/AuthContext';
import { formatXP, getLevelEmoji } from '@/lib/utils';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Trophy, Flame, Crown, Medal } from 'lucide-react';

export default function LeaderboardPage() {
    const { profile } = useAuth();
    const [leaderboard, setLeaderboard] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        fetchLeaderboard();
    }, []);

    const fetchLeaderboard = async () => {
        try {
            const response = await wardAPI.getLeaderboard();
            if (response.success) {
                setLeaderboard(response.data);
            }
        } catch (error) {
            console.error('Fetch leaderboard error:', error);
        } finally {
            setLoading(false);
        }
    };

    const getRankIcon = (rank) => {
        if (rank === 1) return '🥇';
        if (rank === 2) return '🥈';
        if (rank === 3) return '🥉';
        return `${rank}.`;
    };

    return (
        <div className="space-y-6">
            <div>
                <h1 className="text-2xl font-bold flex items-center gap-2">
                    <Trophy className="text-amber-400" /> Leaderboard
                </h1>
                <p className="text-slate-400 mt-1">
                    Top CivicStreaks making a civic impact
                </p>
            </div>

            {/* Top 3 Podium */}
            {!loading && leaderboard.length >= 3 && (
                <motion.div
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    className="grid grid-cols-3 gap-4 items-end"
                >
                    {/* 2nd Place */}
                    <PodiumCard user={leaderboard[1]} rank={2} height="h-36" />
                    {/* 1st Place */}
                    <PodiumCard user={leaderboard[0]} rank={1} height="h-44" highlight />
                    {/* 3rd Place */}
                    <PodiumCard user={leaderboard[2]} rank={3} height="h-28" />
                </motion.div>
            )}

            {/* Full List */}
            <div className="card">
                <div className="flex items-center justify-between mb-4">
                    <h2 className="font-semibold">All Rankings</h2>
                    <span className="text-xs text-slate-400">
                        {leaderboard.length} CivicStreaks
                    </span>
                </div>

                {loading ? (
                    <div className="space-y-3">
                        {[...Array(10)].map((_, i) => (
                            <div key={i} className="h-14 bg-slate-700/50 rounded-xl animate-pulse"></div>
                        ))}
                    </div>
                ) : (
                    <div className="space-y-2">
                        {leaderboard.map((user, i) => {
                            const isCurrentUser = user.id === profile?.id;
                            return (
                                <motion.div
                                    key={user.id}
                                    initial={{ opacity: 0, x: -10 }}
                                    animate={{ opacity: 1, x: 0 }}
                                    transition={{ delay: i * 0.03 }}
                                >
                                    <Link
                                        href={`/portfolio/${user.id}`}
                                        className={`flex items-center gap-3 p-3 rounded-xl transition-all ${
                                            isCurrentUser
                                                ? 'bg-indigo-500/10 border border-indigo-500/30'
                                                : 'bg-slate-700/20 hover:bg-slate-700/40'
                                        }`}
                                    >
                                        {/* Rank */}
                                        <span className="text-lg font-bold w-8 text-center flex-shrink-0">
                                            {getRankIcon(user.rank)}
                                        </span>

                                        {/* Avatar */}
                                        <div className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0 ${
                                            isCurrentUser ? 'bg-indigo-600' : 'bg-slate-600'
                                        }`}>
                                            {user.avatar_url ? (
                                                <img src={user.avatar_url} alt="" className="w-full h-full rounded-full object-cover" />
                                            ) : (
                                                user.full_name?.charAt(0)?.toUpperCase()
                                            )}
                                        </div>

                                        {/* Info */}
                                        <div className="flex-1 min-w-0">
                                            <p className="text-sm font-medium truncate">
                                                {user.full_name}
                                                {isCurrentUser && (
                                                    <span className="text-indigo-400 ml-2 text-xs">(You)</span>
                                                )}
                                            </p>
                                            <p className="text-[10px] text-slate-400">
                                                {getLevelEmoji(user.level)} {user.level}
                                                {user.wards?.ward_name && ` • ${user.wards.ward_name}`}
                                            </p>
                                        </div>

                                        {/* Stats */}
                                        <div className="flex items-center gap-4 flex-shrink-0">
                                            <div className="text-center hidden md:block">
                                                <div className="flex items-center gap-1 text-amber-400">
                                                    <Flame size={12} />
                                                    <span className="text-xs font-bold">{user.current_streak}</span>
                                                </div>
                                                <p className="text-[9px] text-slate-500">streak</p>
                                            </div>
                                            <div className="text-center hidden md:block">
                                                <span className="text-xs font-medium text-slate-400">
                                                    {user.tasks_completed}
                                                </span>
                                                <p className="text-[9px] text-slate-500">tasks</p>
                                            </div>
                                            <div className="text-right">
                                                <span className="text-sm font-bold text-indigo-400">
                                                    {formatXP(user.xp_points)}
                                                </span>
                                                <p className="text-[9px] text-slate-500">XP</p>
                                            </div>
                                        </div>
                                    </Link>
                                </motion.div>
                            );
                        })}
                    </div>
                )}
            </div>
        </div>
    );
}

function PodiumCard({ user, rank, height, highlight }) {
    return (
        <div className={`card text-center ${height} flex flex-col justify-end items-center pb-4 ${
            highlight ? 'border-amber-500/30 bg-gradient-to-b from-amber-950/20 to-slate-800/50' : ''
        }`}>
            <div className="text-2xl mb-1">{rank === 1 ? '👑' : rank === 2 ? '🥈' : '🥉'}</div>
            <div className={`w-12 h-12 rounded-full flex items-center justify-center text-sm font-bold mb-2 ${
                highlight ? 'bg-amber-600' : 'bg-slate-600'
            }`}>
                {user?.full_name?.charAt(0)?.toUpperCase()}
            </div>
            <p className="text-sm font-semibold truncate w-full px-2">
                {user?.full_name}
            </p>
            <p className="text-sm font-bold text-indigo-400 mt-1">
                {formatXP(user?.xp_points)} XP
            </p>
            <p className="text-[10px] text-amber-400">
                🔥 {user?.current_streak} days
            </p>
        </div>
    );
}