- Don't have an account?{" "}
+ Don't have an account?{' '}
;
+ return ;
}
diff --git a/src/app/notifications/page.tsx b/src/app/notifications/page.tsx
index ad536adb..b9bc127a 100644
--- a/src/app/notifications/page.tsx
+++ b/src/app/notifications/page.tsx
@@ -1,229 +1,281 @@
-"use client"
-import { useState, useEffect } from "react"
-import Image from "next/image"
-import { useAuth } from "@/context/AuthContext"
-import { db } from "@/lib/firebase"
-import { collection, query, orderBy, onSnapshot, doc, updateDoc, writeBatch, deleteDoc } from "firebase/firestore"
-import { motion, AnimatePresence } from "framer-motion"
-import { Bell, Check, CheckCheck, Trash2, Filter } from "lucide-react"
-import Navbar from "@/components/layout/Navbar"
+'use client';
+import { useState, useEffect } from 'react';
+import Image from 'next/image';
+import { useAuth } from '@/context/AuthContext';
+import { db } from '@/lib/firebase';
+import {
+ collection,
+ query,
+ orderBy,
+ onSnapshot,
+ doc,
+ updateDoc,
+ writeBatch,
+ deleteDoc,
+} from 'firebase/firestore';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Bell, Check, CheckCheck, Trash2, Filter } from 'lucide-react';
+import Navbar from '@/components/layout/Navbar';
interface Notification {
- id: string
- title: string
- message: string
- image?: string
- createdAt: any
- read: boolean
- type: 'achievement' | 'message' | 'event' | 'system' | 'event_reminder' | 'announcement' | 'wiki_update'
- link?: string
+ id: string;
+ title: string;
+ message: string;
+ image?: string;
+ createdAt: any;
+ read: boolean;
+ type:
+ | 'achievement'
+ | 'message'
+ | 'event'
+ | 'system'
+ | 'event_reminder'
+ | 'announcement'
+ | 'wiki_update';
+ link?: string;
}
export default function NotificationsPage() {
- const { user } = useAuth()
- const [notifications, setNotifications] = useState([])
- const [loading, setLoading] = useState(true)
- const [filter, setFilter] = useState<'all' | 'unread'>('all')
-
- useEffect(() => {
- if (!user) return;
-
- const q = query(
- collection(db, 'members', user.uid, 'notifications'),
- orderBy('createdAt', 'desc')
- );
-
- const unsubscribe = onSnapshot(q, (snapshot) => {
- setNotifications(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Notification)));
- setLoading(false);
- });
-
- return () => unsubscribe();
- }, [user]);
-
- const markAsRead = async (id: string) => {
- if (!user) return;
- try {
- await updateDoc(doc(db, 'members', user.uid, 'notifications', id), {
- read: true
- });
- } catch (error) {
- console.error("Error marking as read:", error);
- }
+ const { user } = useAuth();
+ const [notifications, setNotifications] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [filter, setFilter] = useState<'all' | 'unread'>('all');
+
+ useEffect(() => {
+ if (!user) return;
+
+ const q = query(
+ collection(db, 'members', user.uid, 'notifications'),
+ orderBy('createdAt', 'desc')
+ );
+
+ const unsubscribe = onSnapshot(q, (snapshot) => {
+ setNotifications(
+ snapshot.docs.map(
+ (doc) => ({ id: doc.id, ...doc.data() }) as Notification
+ )
+ );
+ setLoading(false);
+ });
+
+ return () => unsubscribe();
+ }, [user]);
+
+ const markAsRead = async (id: string) => {
+ if (!user) return;
+ try {
+ await updateDoc(doc(db, 'members', user.uid, 'notifications', id), {
+ read: true,
+ });
+ } catch (error) {
+ console.error('Error marking as read:', error);
}
+ };
+
+ const markAllAsRead = async () => {
+ if (!user) return;
+ try {
+ const batch = writeBatch(db);
+ const unread = notifications.filter((n) => !n.read);
+ if (unread.length === 0) return;
- const markAllAsRead = async () => {
- if (!user) return;
- try {
- const batch = writeBatch(db);
- const unread = notifications.filter(n => !n.read);
- if (unread.length === 0) return;
-
- unread.forEach(n => {
- const ref = doc(db, 'members', user.uid, 'notifications', n.id);
- batch.update(ref, { read: true });
- });
- await batch.commit();
- } catch (error) {
- console.error("Error marking all as read:", error);
- }
+ unread.forEach((n) => {
+ const ref = doc(db, 'members', user.uid, 'notifications', n.id);
+ batch.update(ref, { read: true });
+ });
+ await batch.commit();
+ } catch (error) {
+ console.error('Error marking all as read:', error);
}
+ };
- const deleteNotification = async (id: string) => {
- if (!user) return;
- if (!confirm("Delete this notification?")) return;
- try {
- await deleteDoc(doc(db, 'members', user.uid, 'notifications', id));
- } catch (error) {
- console.error("Error deleting notification:", error);
- }
+ const deleteNotification = async (id: string) => {
+ if (!user) return;
+ if (!confirm('Delete this notification?')) return;
+ try {
+ await deleteDoc(doc(db, 'members', user.uid, 'notifications', id));
+ } catch (error) {
+ console.error('Error deleting notification:', error);
}
+ };
- const filteredNotifications = filter === 'all'
- ? notifications
- : notifications.filter(n => !n.read);
-
- return (
-
-
-
-
-
-
-
- Notifications
-
-
- Stay updated with your latest activities and announcements.
-
-
+ const filteredNotifications =
+ filter === 'all' ? notifications : notifications.filter((n) => !n.read);
-
- n.read)}
- >
-
- Mark all read
-
-
-
+ return (
+
+
+
+
+
+
+
+ Notifications
+
+
+ Stay updated with your latest activities and announcements.
+
+
- {/* Filters */}
-
-
setFilter('all')}
- className={`pb-3 px-4 font-medium transition-colors relative ${filter === 'all' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'}`}
- >
- All
- {filter === 'all' && (
-
- )}
-
-
setFilter('unread')}
- className={`pb-3 px-4 font-medium transition-colors relative ${filter === 'unread' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'}`}
+
+ n.read)}
+ >
+
+ Mark all read
+
+
+
+
+ {/* Filters */}
+
+ setFilter('all')}
+ className={`pb-3 px-4 font-medium transition-colors relative ${filter === 'all' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'}`}
+ >
+ All
+ {filter === 'all' && (
+
+ )}
+
+ setFilter('unread')}
+ className={`pb-3 px-4 font-medium transition-colors relative ${filter === 'unread' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'}`}
+ >
+ Unread
+ {notifications.some((n) => !n.read) && (
+
+ {notifications.filter((n) => !n.read).length}
+
+ )}
+ {filter === 'unread' && (
+
+ )}
+
+
+
+ {/* List */}
+
+ {loading ? (
+
+ Loading notifications...
+
+ ) : filteredNotifications.length === 0 ? (
+
+
+
No notifications found
+
You're all caught up!
+
+ ) : (
+
+ {filteredNotifications.map((notif) => (
+
+
+
- Unread
- {notifications.some(n => !n.read) && (
-
- {notifications.filter(n => !n.read).length}
-
- )}
- {filter === 'unread' && (
-
- )}
-
-
-
- {/* List */}
-
- {loading ? (
-
Loading notifications...
- ) : filteredNotifications.length === 0 ? (
-
-
-
No notifications found
-
You're all caught up!
+ {notif.type === 'achievement' && '🏆'}
+ {notif.type === 'event' && '📅'}
+ {notif.type === 'message' && '💬'}
+ {(!notif.type || notif.type === 'system') && '🔔'}
+
+
+
+
+
+
+ {notif.title}
+
+
+ {notif.createdAt?.seconds
+ ? new Date(
+ notif.createdAt.seconds * 1000
+ ).toLocaleString()
+ : 'Just now'}
+
- ) : (
-
- {filteredNotifications.map((notif) => (
-
-
-
- {notif.type === 'achievement' && '🏆'}
- {notif.type === 'event' && '📅'}
- {notif.type === 'message' && '💬'}
- {(!notif.type || notif.type === 'system') && '🔔'}
-
-
-
-
-
-
- {notif.title}
-
-
- {notif.createdAt?.seconds ? new Date(notif.createdAt.seconds * 1000).toLocaleString() : 'Just now'}
-
-
-
- {!notif.read && (
- markAsRead(notif.id)}
- className="p-2 hover:bg-primary/10 text-primary rounded-lg transition-colors"
- title="Mark as read"
- >
-
-
- )}
- deleteNotification(notif.id)}
- className="p-2 hover:bg-red-500/10 text-red-500 rounded-lg transition-colors"
- title="Delete"
- >
-
-
-
-
-
-
- {notif.message}
-
-
- {notif.image && (
-
-
-
- )}
-
-
-
- {!notif.read && (
-
- )}
-
- ))}
-
- )}
-
-
+
+ {!notif.read && (
+ markAsRead(notif.id)}
+ className="p-2 hover:bg-primary/10 text-primary rounded-lg transition-colors"
+ title="Mark as read"
+ >
+
+
+ )}
+ deleteNotification(notif.id)}
+ className="p-2 hover:bg-red-500/10 text-red-500 rounded-lg transition-colors"
+ title="Delete"
+ >
+
+
+
+
+
+
+ {notif.message}
+
+
+ {notif.image && (
+
+
+
+ )}
+
+
+
+ {!notif.read && (
+
+ )}
+
+ ))}
+
+ )}
- )
+
+
+ );
}
diff --git a/src/app/opensource/error.tsx b/src/app/opensource/error.tsx
index c0a95f19..b8ac97d4 100644
--- a/src/app/opensource/error.tsx
+++ b/src/app/opensource/error.tsx
@@ -6,60 +6,61 @@ import { AlertTriangle, RotateCcw, Home } from 'lucide-react';
import Link from 'next/link';
export default function OpensourceError({
- error,
- reset,
+ error,
+ reset,
}: {
- error: Error & { digest?: string };
- reset: () => void;
+ error: Error & { digest?: string };
+ reset: () => void;
}) {
- useEffect(() => {
- console.error('Opensource page error:', error);
- }, [error]);
+ useEffect(() => {
+ console.error('Opensource page error:', error);
+ }, [error]);
- return (
-
-
+ return (
+
+
-
-
+
+
-
- Open Source Unavailable
-
+
+ Open Source Unavailable
+
-
+
-
- {error.message || "We couldn't load the open source dashboard. This may be a GitHub API or network issue — please try again."}
-
+
+ {error.message ||
+ "We couldn't load the open source dashboard. This may be a GitHub API or network issue — please try again."}
+
-
- }
- className="bg-destructive hover:bg-destructive/90 text-white rounded-2xl px-6 py-4"
- onClick={() => reset()}
- >
- Try Again
-
-
- }
- className="rounded-2xl px-6 py-4 border-white/20 hover:bg-white/5"
- >
- Return Home
-
-
-
-
+
+ }
+ className="bg-destructive hover:bg-destructive/90 text-white rounded-2xl px-6 py-4"
+ onClick={() => reset()}
+ >
+ Try Again
+
+
+ }
+ className="rounded-2xl px-6 py-4 border-white/20 hover:bg-white/5"
+ >
+ Return Home
+
+
- );
+
+
+ );
}
diff --git a/src/app/opensource/loading.tsx b/src/app/opensource/loading.tsx
index c36d7de7..fb40fa0e 100644
--- a/src/app/opensource/loading.tsx
+++ b/src/app/opensource/loading.tsx
@@ -1,47 +1,53 @@
export default function OpensourceLoading() {
- return (
-
- {/* Hero / header */}
-
+ return (
+
+ {/* Hero / header */}
+
- {/* Stats row */}
-
- {Array.from({ length: 4 }).map((_, i) => (
-
- ))}
-
+ {/* Stats row */}
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
- {/* Featured repos */}
-
-
- {Array.from({ length: 6 }).map((_, i) => (
-
- ))}
+ {/* Featured repos */}
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
- );
+
+
+
+ ))}
+
+
+ );
}
diff --git a/src/app/opensource/page.tsx b/src/app/opensource/page.tsx
index 4c9d7381..e7bbd41a 100644
--- a/src/app/opensource/page.tsx
+++ b/src/app/opensource/page.tsx
@@ -1,7 +1,20 @@
-"use client";
+'use client';
import { useState, useEffect } from 'react';
-import { Github, GitMerge, Globe, BookOpen, Code2, Users, ExternalLink, Star, Check, LayoutDashboard, Clock, GitFork } from 'lucide-react';
+import {
+ Github,
+ GitMerge,
+ Globe,
+ BookOpen,
+ Code2,
+ Users,
+ ExternalLink,
+ Star,
+ Check,
+ LayoutDashboard,
+ Clock,
+ GitFork,
+} from 'lucide-react';
import Link from 'next/link';
import { useAuth } from '@/context/AuthContext';
import { GithubAuthProvider, linkWithPopup } from 'firebase/auth';
@@ -10,525 +23,616 @@ import GitHubDashboard from '@/components/opensource/GitHubDashboard';
import { siteConfig } from '@/config/siteConfig';
type FeaturedRepoStats = {
- stars: number;
- forks: number;
- openIssues: number;
+ stars: number;
+ forks: number;
+ openIssues: number;
};
type FeaturedRepoStatsEntry = readonly [string, FeaturedRepoStats];
type GitHubRepoApiResponse = {
- stargazers_count?: number;
- forks_count?: number;
- open_issues_count?: number;
+ stargazers_count?: number;
+ forks_count?: number;
+ open_issues_count?: number;
};
type GitHubProfile = {
- login: string;
- avatar_url?: string;
- public_repos?: number;
- followers?: number;
- following?: number;
- bio?: string | null;
- company?: string | null;
- location?: string | null;
- created_at?: string;
+ login: string;
+ avatar_url?: string;
+ public_repos?: number;
+ followers?: number;
+ following?: number;
+ bio?: string | null;
+ company?: string | null;
+ location?: string | null;
+ created_at?: string;
};
type GitHubRepo = {
- id: number | string;
- stargazers_count?: number;
- language?: string | null;
+ id: number | string;
+ stargazers_count?: number;
+ language?: string | null;
};
type GitHubActivityEvent = {
- id: string;
- type: string;
- repo: {
- name: string;
- };
- created_at: string;
+ id: string;
+ type: string;
+ repo: {
+ name: string;
+ };
+ created_at: string;
};
const getGitHubRepoSlug = (url: string | null) => {
- if (!url) return null;
+ if (!url) return null;
- try {
- const { hostname, pathname } = new URL(url);
- if (hostname !== 'github.com') return null;
+ try {
+ const { hostname, pathname } = new URL(url);
+ if (hostname !== 'github.com') return null;
- const [owner, repo] = pathname.replace(/^\/|\/$/g, '').split('/');
- return owner && repo ? `${owner}/${repo}` : null;
- } catch {
- return null;
- }
+ const [owner, repo] = pathname.replace(/^\/|\/$/g, '').split('/');
+ return owner && repo ? `${owner}/${repo}` : null;
+ } catch {
+ return null;
+ }
};
const isFeaturedRepoStatsEntry = (
- entry: FeaturedRepoStatsEntry | null
+ entry: FeaturedRepoStatsEntry | null
): entry is FeaturedRepoStatsEntry => entry !== null;
export default function OpenSourcePage() {
- const { user, updateUserProfile } = useAuth();
- const [connecting, setConnecting] = useState(false);
- const [accessToken, setAccessToken] = useState
(null);
- const [repoStats, setRepoStats] = useState>({});
- const [statsLoading, setStatsLoading] = useState(false);
-
- useEffect(() => {
- const storedToken = localStorage.getItem('github_access_token');
- if (storedToken) setAccessToken(storedToken);
- }, []);
-
- useEffect(() => {
- const publicRepos = siteConfig.featuredRepos.filter(repo => repo.isPublic && getGitHubRepoSlug(repo.url));
- if (publicRepos.length === 0) return;
-
- let cancelled = false;
-
- const fetchFeaturedRepoStats = async () => {
- setStatsLoading(true);
- const statsEntries = await Promise.all(
- publicRepos.map(async (repo) => {
- const slug = getGitHubRepoSlug(repo.url);
- if (!slug) return null;
-
- try {
- const response = await fetch(`https://api.github.com/repos/${slug}`, {
- headers: { Accept: 'application/vnd.github+json' }
- });
-
- if (!response.ok) return null;
-
- const data = await response.json() as GitHubRepoApiResponse;
- return [
- repo.name,
- {
- stars: data.stargazers_count || 0,
- forks: data.forks_count || 0,
- openIssues: data.open_issues_count || 0
- }
- ] as FeaturedRepoStatsEntry;
- } catch {
- return null;
- }
- })
+ const { user, updateUserProfile } = useAuth();
+ const [connecting, setConnecting] = useState(false);
+ const [accessToken, setAccessToken] = useState(null);
+ const [repoStats, setRepoStats] = useState>(
+ {}
+ );
+ const [statsLoading, setStatsLoading] = useState(false);
+
+ useEffect(() => {
+ const storedToken = localStorage.getItem('github_access_token');
+ if (storedToken) setAccessToken(storedToken);
+ }, []);
+
+ useEffect(() => {
+ const publicRepos = siteConfig.featuredRepos.filter(
+ (repo) => repo.isPublic && getGitHubRepoSlug(repo.url)
+ );
+ if (publicRepos.length === 0) return;
+
+ let cancelled = false;
+
+ const fetchFeaturedRepoStats = async () => {
+ setStatsLoading(true);
+ const statsEntries = await Promise.all(
+ publicRepos.map(async (repo) => {
+ const slug = getGitHubRepoSlug(repo.url);
+ if (!slug) return null;
+
+ try {
+ const response = await fetch(
+ `https://api.github.com/repos/${slug}`,
+ {
+ headers: { Accept: 'application/vnd.github+json' },
+ }
);
- if (!cancelled) {
- setRepoStats(Object.fromEntries(statsEntries.filter(isFeaturedRepoStatsEntry)));
- setStatsLoading(false);
- }
- };
+ if (!response.ok) return null;
+
+ const data = (await response.json()) as GitHubRepoApiResponse;
+ return [
+ repo.name,
+ {
+ stars: data.stargazers_count || 0,
+ forks: data.forks_count || 0,
+ openIssues: data.open_issues_count || 0,
+ },
+ ] as FeaturedRepoStatsEntry;
+ } catch {
+ return null;
+ }
+ })
+ );
+
+ if (!cancelled) {
+ setRepoStats(
+ Object.fromEntries(statsEntries.filter(isFeaturedRepoStatsEntry))
+ );
+ setStatsLoading(false);
+ }
+ };
- fetchFeaturedRepoStats();
+ fetchFeaturedRepoStats();
- return () => {
- cancelled = true;
- };
- }, []);
+ return () => {
+ cancelled = true;
+ };
+ }, []);
- const handleConnectGitHub = async () => {
- if (!user) {
- alert("Please login to connect your GitHub account.");
- return;
- }
+ const handleConnectGitHub = async () => {
+ if (!user) {
+ alert('Please login to connect your GitHub account.');
+ return;
+ }
- setConnecting(true);
- try {
- const provider = new GithubAuthProvider();
- provider.addScope('read:user');
- provider.addScope('repo');
-
- let token;
-
- try {
- // Try linking first
- const result = await linkWithPopup(auth.currentUser!, provider);
- const credential = GithubAuthProvider.credentialFromResult(result);
- token = credential?.accessToken;
- } catch (linkError: unknown) {
- const authError = linkError as { code?: string };
- if (authError.code === 'auth/credential-already-in-use') {
- // Fallback: Connect for Data Only automatically
- // We don't merge accounts, just use the token for fetching data.
-
- // Try to retrieve credential from the error
- const credential = GithubAuthProvider.credentialFromError(
- linkError as Parameters[0]
- );
- if (credential) {
- token = credential.accessToken;
- // We don't have the user object here, but we can fetch profile with the token
- } else {
- // If we can't get it from error, we fail gracefully.
- throw new Error("This GitHub account is linked to another user, and we couldn't retrieve the credentials to fetch its data. Please try a different account.");
- }
- } else {
- throw linkError;
- }
- }
-
- if (token) {
- setAccessToken(token);
- localStorage.setItem('github_access_token', token); // Persist token
-
- // Fetch Extended Data
- const { fetchUserProfile, fetchUserRepos, fetchUserActivity, fetchRepoContributorStats, calculateUserLinesContributed } = await import('@/lib/github');
- const profile = await fetchUserProfile(token) as GitHubProfile;
- const repos = await fetchUserRepos(token) as GitHubRepo[];
- const activity = await fetchUserActivity(profile.login, token) as GitHubActivityEvent[];
-
- // Fetch contributor stats (lines and commits)
- const repoStats = await fetchRepoContributorStats(token);
- const userLineStats = calculateUserLinesContributed(repoStats, profile.login);
-
- // Calculate Total Stars
- const totalStars = repos.reduce((acc, repo) => acc + (repo.stargazers_count || 0), 0);
-
- // Calculate Top Languages
- const languageCounts: Record = {};
- repos.forEach((repo) => {
- if (repo.language) {
- languageCounts[repo.language] = (languageCounts[repo.language] || 0) + 1;
- }
- });
- const topLanguages = Object.entries(languageCounts)
- .sort(([, a], [, b]) => b - a)
- .slice(0, 5)
- .map(([language, count]) => ({ language, count }));
-
- const githubData = {
- githubStats: {
- connected: true,
- username: profile.login,
- photoURL: profile.avatar_url,
- repos: profile.public_repos,
- followers: profile.followers,
- following: profile.following,
- lastFetched: new Date().toISOString(),
- recentActivity: activity.slice(0, 5).map((event) => ({
- id: event.id,
- type: event.type,
- repo: { name: event.repo.name, url: `https://github.com/${event.repo.name}` },
- created_at: event.created_at
- })),
- totalStars,
- topLanguages,
- bio: profile.bio || undefined,
- company: profile.company || undefined,
- location: profile.location || undefined,
- createdAt: profile.created_at,
- linesAdded: userLineStats.additions,
- linesRemoved: userLineStats.deletions,
- linesContributed: userLineStats.additions,
- contributions: userLineStats.commits
- },
- github: profile.login, // Store username
- // Store detailed data in subcollection or just basic stats here?
- // Let's store basic stats in profile and maybe top repos if we want.
- };
-
- await updateUserProfile(githubData);
-
- // Save Repos to Subcollection (optional, but good for "more details")
- // We'll do this in the background to not block UI
- const { collection, writeBatch, doc } = await import('firebase/firestore');
- const batch = writeBatch(db);
-
- const collectionName = user.role === 'admin' ? 'admins' : 'members';
- const docId = user.role === 'admin' ? user.email! : user.uid;
-
- const reposRef = collection(db, collectionName, docId, 'github_repos');
-
- // Save top 10 repos for now to save writes
- repos.slice(0, 10).forEach((repo) => {
- const repoDoc = doc(reposRef, repo.id.toString());
- batch.set(repoDoc, repo);
- });
- await batch.commit();
-
- alert("GitHub account connected successfully!");
- }
-
- } catch (error: unknown) {
- console.error("Error connecting GitHub:", error);
- const message = error instanceof Error ? error.message : "Unknown error";
- alert("Failed to connect GitHub: " + message);
- } finally {
- setConnecting(false);
+ setConnecting(true);
+ try {
+ const provider = new GithubAuthProvider();
+ provider.addScope('read:user');
+ provider.addScope('repo');
+
+ let token;
+
+ try {
+ // Try linking first
+ const result = await linkWithPopup(auth.currentUser!, provider);
+ const credential = GithubAuthProvider.credentialFromResult(result);
+ token = credential?.accessToken;
+ } catch (linkError: unknown) {
+ const authError = linkError as { code?: string };
+ if (authError.code === 'auth/credential-already-in-use') {
+ // Fallback: Connect for Data Only automatically
+ // We don't merge accounts, just use the token for fetching data.
+
+ // Try to retrieve credential from the error
+ const credential = GithubAuthProvider.credentialFromError(
+ linkError as Parameters<
+ typeof GithubAuthProvider.credentialFromError
+ >[0]
+ );
+ if (credential) {
+ token = credential.accessToken;
+ // We don't have the user object here, but we can fetch profile with the token
+ } else {
+ // If we can't get it from error, we fail gracefully.
+ throw new Error(
+ "This GitHub account is linked to another user, and we couldn't retrieve the credentials to fetch its data. Please try a different account."
+ );
+ }
+ } else {
+ throw linkError;
}
- };
-
- return (
-
-
+ }
+
+ if (token) {
+ setAccessToken(token);
+ localStorage.setItem('github_access_token', token); // Persist token
+
+ // Fetch Extended Data
+ const {
+ fetchUserProfile,
+ fetchUserRepos,
+ fetchUserActivity,
+ fetchRepoContributorStats,
+ calculateUserLinesContributed,
+ } = await import('@/lib/github');
+ const profile = (await fetchUserProfile(token)) as GitHubProfile;
+ const repos = (await fetchUserRepos(token)) as GitHubRepo[];
+ const activity = (await fetchUserActivity(
+ profile.login,
+ token
+ )) as GitHubActivityEvent[];
+
+ // Fetch contributor stats (lines and commits)
+ const repoStats = await fetchRepoContributorStats(token);
+ const userLineStats = calculateUserLinesContributed(
+ repoStats,
+ profile.login
+ );
+
+ // Calculate Total Stars
+ const totalStars = repos.reduce(
+ (acc, repo) => acc + (repo.stargazers_count || 0),
+ 0
+ );
+
+ // Calculate Top Languages
+ const languageCounts: Record
= {};
+ repos.forEach((repo) => {
+ if (repo.language) {
+ languageCounts[repo.language] =
+ (languageCounts[repo.language] || 0) + 1;
+ }
+ });
+ const topLanguages = Object.entries(languageCounts)
+ .sort(([, a], [, b]) => b - a)
+ .slice(0, 5)
+ .map(([language, count]) => ({ language, count }));
+
+ const githubData = {
+ githubStats: {
+ connected: true,
+ username: profile.login,
+ photoURL: profile.avatar_url,
+ repos: profile.public_repos,
+ followers: profile.followers,
+ following: profile.following,
+ lastFetched: new Date().toISOString(),
+ recentActivity: activity.slice(0, 5).map((event) => ({
+ id: event.id,
+ type: event.type,
+ repo: {
+ name: event.repo.name,
+ url: `https://github.com/${event.repo.name}`,
+ },
+ created_at: event.created_at,
+ })),
+ totalStars,
+ topLanguages,
+ bio: profile.bio || undefined,
+ company: profile.company || undefined,
+ location: profile.location || undefined,
+ createdAt: profile.created_at,
+ linesAdded: userLineStats.additions,
+ linesRemoved: userLineStats.deletions,
+ linesContributed: userLineStats.additions,
+ contributions: userLineStats.commits,
+ },
+ github: profile.login, // Store username
+ // Store detailed data in subcollection or just basic stats here?
+ // Let's store basic stats in profile and maybe top repos if we want.
+ };
- {/* Hero Section */}
-
-
- Open Source Ecosystem
-
-
- Connect, Contribute, and Grow
-
-
- Open source is the heartbeat of modern software. Join the global community of developers building the future together.
-
-
- {/* GitHub Connect Button */}
-
- {user?.githubStats?.connected ? (
-
-
- GitHub Connected as {user.githubStats.username}
-
- {!accessToken && (
-
- Reconnect to Manage Repos
-
- )}
-
- ) : (
-
-
- {connecting ? 'Connecting...' : 'Connect GitHub Account'}
-
- )}
-
+ await updateUserProfile(githubData);
+
+ // Save Repos to Subcollection (optional, but good for "more details")
+ // We'll do this in the background to not block UI
+ const { collection, writeBatch, doc } =
+ await import('firebase/firestore');
+ const batch = writeBatch(db);
+
+ const collectionName = user.role === 'admin' ? 'admins' : 'members';
+ const docId = user.role === 'admin' ? user.email! : user.uid;
+
+ const reposRef = collection(db, collectionName, docId, 'github_repos');
+
+ // Save top 10 repos for now to save writes
+ repos.slice(0, 10).forEach((repo) => {
+ const repoDoc = doc(reposRef, repo.id.toString());
+ batch.set(repoDoc, repo);
+ });
+ await batch.commit();
+
+ alert('GitHub account connected successfully!');
+ }
+ } catch (error: unknown) {
+ console.error('Error connecting GitHub:', error);
+ const message = error instanceof Error ? error.message : 'Unknown error';
+ alert('Failed to connect GitHub: ' + message);
+ } finally {
+ setConnecting(false);
+ }
+ };
+
+ return (
+
+
+ {/* Hero Section */}
+
+
+ Open Source Ecosystem
+
+
+ Connect, Contribute, and Grow
+
+
+ Open source is the heartbeat of modern software. Join the global
+ community of developers building the future together.
+
+
+ {/* GitHub Connect Button */}
+
+ {user?.githubStats?.connected ? (
+
+
+ GitHub Connected as{' '}
+ {user.githubStats.username}
-
- {/* GitHub Dashboard (Only visible when connected and token available) */}
- {accessToken && (
-
-
-
- GitHub Dashboard
-
-
- Live Connection
-
-
-
-
+ {!accessToken && (
+
+ Reconnect to Manage Repos
+
)}
+
+ ) : (
+
+
+ {connecting ? 'Connecting...' : 'Connect GitHub Account'}
+
+ )}
+
+
- {/* Featured Repositories Section */}
-
-
- Featured Repositories
-
-
- {siteConfig.featuredRepos.map((repo) => {
- const IconComponent =
- repo.icon === 'BookOpen' ? BookOpen
- : repo.icon === 'Code2' ? Code2
- : Globe;
- const liveStats = repoStats[repo.name];
-
- return (
-
-
-
-
-
-
-
-
{repo.name}
-
{repo.description}
-
-
-
-
-
- {liveStats ? liveStats.stars.toLocaleString() : repo.stars}
-
- {liveStats && (
-
-
- {liveStats.forks.toLocaleString()}
-
- )}
-
-
-
-
- {repo.longDescription}
-
-
-
-
-
- {repo.language}
- {liveStats && (
-
- {liveStats.openIssues.toLocaleString()} open issues
-
- )}
- {!liveStats && statsLoading && repo.isPublic && (
- Updating stats...
- )}
-
-
- {repo.isPublic && repo.url ? (
-
- View Code
-
- ) : (
- /* Disabled state for repos not yet public */
-
-
-
- Coming Soon
-
- {/* Tooltip */}
-
- This repository is not yet public. Check back soon!
-
-
- )}
-
-
- );
- })}
+ {/* GitHub Dashboard (Only visible when connected and token available) */}
+ {accessToken && (
+
+
+
+ GitHub Dashboard
+
+
+ Live Connection
+
+
+
+
+ )}
+
+ {/* Featured Repositories Section */}
+
+
+ Featured Repositories
+
+
+ {siteConfig.featuredRepos.map((repo) => {
+ const IconComponent =
+ repo.icon === 'BookOpen'
+ ? BookOpen
+ : repo.icon === 'Code2'
+ ? Code2
+ : Globe;
+ const liveStats = repoStats[repo.name];
+
+ return (
+
+
+
+
+
+
+
+
{repo.name}
+
+ {repo.description}
+
+
+
+
+
+
+ {liveStats
+ ? liveStats.stars.toLocaleString()
+ : repo.stars}
+
+ {liveStats && (
+
+
+ {liveStats.forks.toLocaleString()}
+
+ )}
+
+
+
+
+ {repo.longDescription}
+
+
+
+
+
+ {repo.language}
+ {liveStats && (
+
+ {liveStats.openIssues.toLocaleString()} open issues
+
+ )}
+ {!liveStats && statsLoading && repo.isPublic && (
+ Updating stats...
+ )}
-
- {/* Platforms Section */}
-
-
- Major Platforms
-
-
- {/* GitHub */}
-
-
-
-
-
GitHub
-
- The world's largest platform for developer collaboration. Home to millions of open source projects.
-
-
- Visit Platform
-
+ {repo.isPublic && repo.url ? (
+
+ View Code
+
+ ) : (
+ /* Disabled state for repos not yet public */
+
+
+
+ Coming Soon
+
+ {/* Tooltip */}
+
+ This repository is not yet public. Check back soon!
+
+ )}
+
+
+ );
+ })}
+
+
- {/* GitLab */}
-
-
-
-
-
GitLab
-
- A complete DevOps platform delivered as a single application. Famous for its CI/CD capabilities.
-
-
- Visit Platform
-
-
+ {/* Platforms Section */}
+
+
+ Major Platforms
+
+
+ {/* GitHub */}
+
+
+
+
+
GitHub
+
+ The world's largest platform for developer collaboration.
+ Home to millions of open source projects.
+
+
+ Visit Platform
+
+
- {/* Bitbucket */}
-
-
-
-
-
Bitbucket
-
- Git solution for professional teams. Deeply integrated with Jira and Trello.
-
-
- Visit Platform
-
-
-
-
+ {/* GitLab */}
+
+
+
+
+
GitLab
+
+ A complete DevOps platform delivered as a single application.
+ Famous for its CI/CD capabilities.
+
+
+ Visit Platform
+
+
- {/* Getting Started Section */}
-
-
-
Start Your Journey
-
-
-
-
-
-
-
Learn the Basics
-
Understand Git, Pull Requests, and Issues. These are the fundamental tools of open source.
-
-
-
-
-
-
-
-
Find a Community
-
Look for projects with active maintainers and a welcoming community.
-
-
-
-
-
-
-
-
Make Your First Contribution
-
Start small. Fix a typo, update documentation, or tackle a "Good First Issue".
-
-
-
-
+ {/* Bitbucket */}
+
+
+
+
+
Bitbucket
+
+ Git solution for professional teams. Deeply integrated with Jira
+ and Trello.
+
+
+ Visit Platform
+
+
+
+
-
-
Resources
-
-
-
- How to Contribute to Open Source
-
-
-
-
-
- Good First Issues
-
-
-
-
-
- First Contributions Guide
-
-
-
-
-
+ {/* Getting Started Section */}
+
+
+
Start Your Journey
+
+
+
+
-
+
+
Learn the Basics
+
+ Understand Git, Pull Requests, and Issues. These are the
+ fundamental tools of open source.
+
+
+
+
+
+
+
+
+
Find a Community
+
+ Look for projects with active maintainers and a welcoming
+ community.
+
+
+
+
+
+
+
+
+
+ Make Your First Contribution
+
+
+ Start small. Fix a typo, update documentation, or tackle a
+ "Good First Issue".
+
+
+
+
+
+
+
Resources
+
+
+
+
+ How to Contribute to Open Source
+
+
+
+
+
+
+ Good First Issues
+
+
+
+
+
+ First Contributions Guide
+
+
+
+
+
- );
+
+
+ );
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 5fe6a105..16e9fac2 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -6,11 +6,12 @@ import dynamic from 'next/dynamic';
import BackToTop from '@/components/BackToTop';
import ErrorBoundary from '@/components/ErrorBoundary';
-
const Sponsors = dynamic(() => import('@/components/home/Sponsors'));
const Mission = dynamic(() => import('@/components/home/Mission'));
const CodingNews = dynamic(() => import('@/components/home/CodingNews'));
-const PastCollaborations = dynamic(() => import('@/components/home/PastCollaborations'));
+const PastCollaborations = dynamic(
+ () => import('@/components/home/PastCollaborations')
+);
export default function Home() {
return (
@@ -37,4 +38,4 @@ export default function Home() {
>
);
-}
\ No newline at end of file
+}
diff --git a/src/app/paths/error.tsx b/src/app/paths/error.tsx
index 845b6e92..0160e4ed 100644
--- a/src/app/paths/error.tsx
+++ b/src/app/paths/error.tsx
@@ -6,60 +6,61 @@ import { AlertTriangle, RotateCcw, Home } from 'lucide-react';
import Link from 'next/link';
export default function PathsError({
- error,
- reset,
+ error,
+ reset,
}: {
- error: Error & { digest?: string };
- reset: () => void;
+ error: Error & { digest?: string };
+ reset: () => void;
}) {
- useEffect(() => {
- console.error('Paths page error:', error);
- }, [error]);
+ useEffect(() => {
+ console.error('Paths page error:', error);
+ }, [error]);
- return (
-
-
+ return (
+
+
-
-
+
+
-
- Paths Unavailable
-
+
+ Paths Unavailable
+
-
+
-
- {error.message || "We couldn't load the learning paths. Please try again."}
-
+
+ {error.message ||
+ "We couldn't load the learning paths. Please try again."}
+
-
- }
- className="bg-destructive hover:bg-destructive/90 text-white rounded-2xl px-6 py-4"
- onClick={() => reset()}
- >
- Try Again
-
-
- }
- className="rounded-2xl px-6 py-4 border-white/20 hover:bg-white/5"
- >
- Return Home
-
-
-
-
+
+ }
+ className="bg-destructive hover:bg-destructive/90 text-white rounded-2xl px-6 py-4"
+ onClick={() => reset()}
+ >
+ Try Again
+
+
+ }
+ className="rounded-2xl px-6 py-4 border-white/20 hover:bg-white/5"
+ >
+ Return Home
+
+
- );
+
+
+ );
}
diff --git a/src/app/paths/loading.tsx b/src/app/paths/loading.tsx
index 75573a17..6e9f39ee 100644
--- a/src/app/paths/loading.tsx
+++ b/src/app/paths/loading.tsx
@@ -1,40 +1,43 @@
export default function PathsLoading() {
- return (
-
- {/* Header row */}
-
+ return (
+
+ {/* Header row */}
+
- {/* Path cards grid */}
-
- {Array.from({ length: 6 }).map((_, i) => (
-
- {/* Icon placeholder */}
-
-
-
- {/* Progress bar */}
-
-
-
- ))}
+ {/* Path cards grid */}
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ {/* Icon placeholder */}
+
+
+
+ {/* Progress bar */}
+
+
-
- );
+
+ ))}
+
+
+ );
}
diff --git a/src/app/paths/page.tsx b/src/app/paths/page.tsx
index a153255a..de470558 100644
--- a/src/app/paths/page.tsx
+++ b/src/app/paths/page.tsx
@@ -1,35 +1,37 @@
-"use client";
+'use client';
import React, { useState } from 'react';
import LearningPaths from '@/components/home/LearningPaths';
import SkillTreeVisualizer from '@/components/features/SkillTreeVisualizer';
export default function LearningPathsPage() {
- const [view, setView] = useState<'card' | 'tree'>('card');
+ const [view, setView] = useState<'card' | 'tree'>('card');
- return (
-
-
-
Learning Paths
-
- {/* Toggle Button */}
-
- setView('card')}
- className={`px-4 py-2 rounded-md ${view === 'card' ? 'bg-github-green text-white' : 'text-github-muted'}`}
- >
- Card View
-
- setView('tree')}
- className={`px-4 py-2 rounded-md ${view === 'tree' ? 'bg-github-green text-white' : 'text-github-muted'}`}
- >
- Tree View
-
-
-
+ return (
+
+
+
Learning Paths
- {view === 'card' ?
:
}
-
- );
-}
\ No newline at end of file
+ {/* Toggle Button */}
+
+ setView('card')}
+ className={`px-4 py-2 rounded-md ${view === 'card' ? 'bg-github-green text-white' : 'text-github-muted'}`}
+ >
+ Card View
+
+ setView('tree')}
+ className={`px-4 py-2 rounded-md ${view === 'tree' ? 'bg-github-green text-white' : 'text-github-muted'}`}
+ >
+ Tree View
+
+
+
+
+ {view === 'card' ? : }
+
+ );
+}
diff --git a/src/app/pathway/page.tsx b/src/app/pathway/page.tsx
index 02a8a828..729ef926 100644
--- a/src/app/pathway/page.tsx
+++ b/src/app/pathway/page.tsx
@@ -1,4 +1,4 @@
-"use client";
+'use client';
import { useEffect, useRef, useState } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
@@ -6,497 +6,745 @@ import { db } from '@/lib/firebase';
import { collection, getDocs, query, orderBy } from 'firebase/firestore';
import { LEVELS, POINTS, calculateLevel } from '@/lib/points';
import Image from 'next/image';
-import { Flame, Trophy, Star, Users, Award, Shield, Gift, Calendar, ChartNoAxesCombined } from 'lucide-react';
+import {
+ Flame,
+ Trophy,
+ Star,
+ Users,
+ Award,
+ Shield,
+ Gift,
+ Calendar,
+ ChartNoAxesCombined,
+} from 'lucide-react';
import { useAuth } from '@/context/AuthContext';
interface LeaderboardEntry {
- id: string;
- email?: string;
- name?: string;
- photoURL?: string;
- points?: number;
+ id: string;
+ email?: string;
+ name?: string;
+ photoURL?: string;
+ points?: number;
}
export default function PathwayPage() {
- const { user } = useAuth();
- const [leaderboard, setLeaderboard] = useState([]);
- const [loading, setLoading] = useState(true);
- const leaderboardScrollRef = useRef(null);
- const rowVirtualizer = useVirtualizer({
- count: leaderboard.length,
- getScrollElement: () => leaderboardScrollRef.current,
- estimateSize: () => 72,
- overscan: 8,
- });
- const chipsRef = useRef(null);
- const [activeDot, setActiveDot] = useState(0)
- const totalDots = LEVELS.slice(0, -1).length;
-
- useEffect(() => {
- const fetchLeaderboard = async () => {
- try {
- const q = query(collection(db, 'leaderboard'), orderBy('points', 'desc'));
- const snapshot = await getDocs(q);
- const data = snapshot.docs
- .map(doc => ({ id: doc.id, ...doc.data() } as LeaderboardEntry))
- .filter((entry) =>
- entry.id !== 'devpathind.community@gmail.com' &&
- entry.email !== 'devpathind.community@gmail.com' &&
- entry.name !== 'Super Admin'
- );
-
- // Fix missing names (e.g. Admins)
- const { doc, getDoc, where } = await import('firebase/firestore');
-
- const updatedData = await Promise.all(data.map(async (entry) => {
- if (!entry.name || entry.name.trim() === '') {
- try {
- // 1. Try Members (UID)
- const memberRef = doc(db, 'members', entry.id);
- const memberSnap = await getDoc(memberRef);
-
- if (memberSnap.exists() && memberSnap.data().name) {
- const newData = { name: memberSnap.data().name, photoURL: memberSnap.data().photoURL };
- // Only update local state, do not write to DB as it requires admin permissions
- return { ...entry, ...newData };
- }
-
- // 2. Try Admins (Query by UID)
- const adminsQuery = query(collection(db, 'admins'), where('uid', '==', entry.id));
- const adminsSnap = await getDocs(adminsQuery);
-
- if (!adminsSnap.empty) {
- const adminData = adminsSnap.docs[0].data();
- if (adminData.name) {
- const newData = { name: adminData.name, photoURL: adminData.photoURL || adminData.image };
- // Only update local state
- return { ...entry, ...newData };
- }
- }
- } catch (err) {
- console.error(`Error fixing user ${entry.id}:`, err);
- }
- }
- return entry;
- }));
-
- setLeaderboard(updatedData);
- } catch (error) {
- console.error("Error fetching leaderboard:", error);
- } finally {
- setLoading(false);
+ const { user } = useAuth();
+ const [leaderboard, setLeaderboard] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const leaderboardScrollRef = useRef(null);
+ const rowVirtualizer = useVirtualizer({
+ count: leaderboard.length,
+ getScrollElement: () => leaderboardScrollRef.current,
+ estimateSize: () => 72,
+ overscan: 8,
+ });
+ const chipsRef = useRef(null);
+ const [activeDot, setActiveDot] = useState(0);
+ const totalDots = LEVELS.slice(0, -1).length;
+
+ useEffect(() => {
+ const fetchLeaderboard = async () => {
+ try {
+ const q = query(
+ collection(db, 'leaderboard'),
+ orderBy('points', 'desc')
+ );
+ const snapshot = await getDocs(q);
+ const data = snapshot.docs
+ .map((doc) => ({ id: doc.id, ...doc.data() }) as LeaderboardEntry)
+ .filter(
+ (entry) =>
+ entry.id !== 'devpathind.community@gmail.com' &&
+ entry.email !== 'devpathind.community@gmail.com' &&
+ entry.name !== 'Super Admin'
+ );
+
+ // Fix missing names (e.g. Admins)
+ const { doc, getDoc, where } = await import('firebase/firestore');
+
+ const updatedData = await Promise.all(
+ data.map(async (entry) => {
+ if (!entry.name || entry.name.trim() === '') {
+ try {
+ // 1. Try Members (UID)
+ const memberRef = doc(db, 'members', entry.id);
+ const memberSnap = await getDoc(memberRef);
+
+ if (memberSnap.exists() && memberSnap.data().name) {
+ const newData = {
+ name: memberSnap.data().name,
+ photoURL: memberSnap.data().photoURL,
+ };
+ // Only update local state, do not write to DB as it requires admin permissions
+ return { ...entry, ...newData };
+ }
+
+ // 2. Try Admins (Query by UID)
+ const adminsQuery = query(
+ collection(db, 'admins'),
+ where('uid', '==', entry.id)
+ );
+ const adminsSnap = await getDocs(adminsQuery);
+
+ if (!adminsSnap.empty) {
+ const adminData = adminsSnap.docs[0].data();
+ if (adminData.name) {
+ const newData = {
+ name: adminData.name,
+ photoURL: adminData.photoURL || adminData.image,
+ };
+ // Only update local state
+ return { ...entry, ...newData };
+ }
+ }
+ } catch (err) {
+ console.error(`Error fixing user ${entry.id}:`, err);
+ }
}
- };
-
- fetchLeaderboard();
- }, []);
-
- const handleChipScroll = () => {
- const el = chipsRef.current
- if (!el) return
-
- const maxScroll = el.scrollWidth - el.clientWidth
-
- if (maxScroll <= 0) {
- setActiveDot(0)
- return
- }
-
- const progress = el.scrollLeft / maxScroll
- const index = Math.round(progress * (totalDots - 1))
-
- setActiveDot(index)
+ return entry;
+ })
+ );
+
+ setLeaderboard(updatedData);
+ } catch (error) {
+ console.error('Error fetching leaderboard:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchLeaderboard();
+ }, []);
+
+ const handleChipScroll = () => {
+ const el = chipsRef.current;
+ if (!el) return;
+
+ const maxScroll = el.scrollWidth - el.clientWidth;
+
+ if (maxScroll <= 0) {
+ setActiveDot(0);
+ return;
}
- return (
-
-
-
- {/* Header */}
-
-
- The DevPath Pathway
-
-
- Earn Dev Points, climb the ranks, and become a Pathfinder. Your journey from Shishya to Master starts here.
-
+ const progress = el.scrollLeft / maxScroll;
+ const index = Math.round(progress * (totalDots - 1));
+
+ setActiveDot(index);
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+ The DevPath Pathway
+
+
+ Earn Dev Points, climb the ranks, and become a Pathfinder. Your
+ journey from Shishya to Master starts here.
+
+
+
+ {/* User Stats (if logged in) */}
+ {user && (
+
+
+
+
+
+
+ {user.photoURL ? (
+
+ ) : (
+
+ {user.name?.[0]?.toUpperCase()}
+
+ )}
+
+
+
+
+
+
{user.name}
+
+ {calculateLevel(user.points || 0).currentLevel.name}
+
+
+
+
+
+ {user.points || 0}
+ {' '}
+ Dev Points
+
+
+
+
+ {user.streak || 0}
+ {' '}
+ Day Streak
+
+
+ {/* Progress Bar */}
+
+
+
+ Progress to{' '}
+ {LEVELS[
+ LEVELS.indexOf(
+ calculateLevel(user.points || 0).currentLevel
+ ) + 1
+ ]?.name || 'Max Level'}
+
+
+ {Math.round(calculateLevel(user.points || 0).progress)}%
+
+
+
+
+
+
+
+ )}
+
+
+ {/* Leaderboard */}
+
+
+
+
Leaderboard
+
- {/* User Stats (if logged in) */}
- {user && (
-
-
-
-
-
-
- {user.photoURL ? (
-
- ) : (
-
- {user.name?.[0]?.toUpperCase()}
-
- )}
-
-
-
-
-
-
{user.name}
-
- {calculateLevel(user.points || 0).currentLevel.name}
-
-
-
-
-
- {user.points || 0} Dev Points
-
-
-
- {user.streak || 0} Day Streak
-
-
- {/* Progress Bar */}
-
-
- Progress to {LEVELS[LEVELS.indexOf(calculateLevel(user.points || 0).currentLevel) + 1]?.name || 'Max Level'}
- {Math.round(calculateLevel(user.points || 0).progress)}%
-
-
-
+
+ {loading ? (
+
+ Loading leaderboard...
+
+ ) : (
+
+
+
Rank
+
Dev
+
Level
+
Points
+
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => {
+ const entry = leaderboard[virtualRow.index];
+ const level = calculateLevel(
+ entry.points || 0
+ ).currentLevel;
+ const displayName = entry.name?.trim() || 'Unknown Dev';
+
+ return (
+
+
+ #{virtualRow.index + 1}
+
+
+
+
+ {entry.photoURL ? (
+
+ ) : (
+
+ {displayName[0]}
+
+ )}
+
+
+ {displayName}
+
+
+
+
+ {level.name}
+
+
+
+ {entry.points || 0}
+
-
- )}
-
-
- {/* Leaderboard */}
-
-
-
-
Leaderboard
-
-
-
- {loading ? (
-
Loading leaderboard...
- ) : (
-
-
-
Rank
-
Dev
-
Level
-
Points
-
-
-
- {rowVirtualizer.getVirtualItems().map((virtualRow) => {
- const entry = leaderboard[virtualRow.index];
- const level = calculateLevel(entry.points || 0).currentLevel;
- const displayName = entry.name?.trim() || 'Unknown Dev';
-
- return (
-
-
- #{virtualRow.index + 1}
-
-
-
-
- {entry.photoURL ? (
-
- ) : (
-
- {displayName[0]}
-
- )}
-
-
{displayName}
-
-
-
-
- {level.name}
-
-
-
- {entry.points || 0}
-
-
- );
- })}
-
-
- )}
-
-
-
- {/* Sidebar: Levels & Rules */}
-
-
-
-
Progression System
-
-
- {/* Levels Guide */}
-
-
-
- Ranks & Levels
-
-
- {/* Sanrakshak Card */}
-
-
-
-
-
-
-
-
-
- Ultimate Stewardship Role
-
-
-
-
- Sanrakshak
-
-
-
- The Sanrakshak is the ultimate steward of the DevPath ecosystem.
- This role represents long-term ownership, trust, and responsibility for the platform's vision, governance, and continuity.
-
-
-
-
- 10,000,000+ Dev Points
-
-
-
-
- {/* Other Levels - Horizontal Scroll */}
-
-
- {LEVELS.slice(0, -1).map((lvl) => (
-
- {lvl.name}
-
- {lvl.max === Infinity ? `${lvl.min}+` : `${lvl.min} - ${lvl.max}`} pts
-
-
- ))}
-
-
- {Array.from({ length: totalDots }).map((_, i) => (
-
- ))}
-
+ );
+ })}
+
+
+ )}
+
+
-
-
+ {/* Sidebar: Levels & Rules */}
+
+
+
+
Progression System
+
- {/* How to Earn Points - Moved to bottom */}
-
+ {/* Levels Guide */}
+
+
+
+ Ranks & Levels
+
+
+ {/* Sanrakshak Card */}
+
+
+
+
- {/* How to Earn Points - Full Width */}
-
-
-
- How to Earn Points
-
-
-
- Daily Login
- +{POINTS.DAILY_LOGIN} (+Streak)
-
-
- 7-Day Streak
- +{POINTS.WEEKLY_STREAK_BONUS}
-
-
- Follow Community
- +{POINTS.FOLLOW_COMMUNITY}
-
-
-
Earn Badge
-
+{POINTS.BADGE_EARNED}
-
-
- Gain Follower
- +{POINTS.FOLLOWER_GAINED}
-
-
- Project Star
- +{POINTS.PROJECT_STAR}
-
-
- Event Participation
- +{POINTS.EVENT_PARTICIPATION}
-
-
- Hackathon Win
- +{POINTS.HACKATHON_WIN}
-
-
+
+
+
+ Ultimate Stewardship Role
+
+
+
+
+ Sanrakshak
+
+
+
+ The Sanrakshak is the ultimate steward of the DevPath
+ ecosystem. This role represents long-term ownership, trust,
+ and responsibility for the platform's vision,
+ governance, and continuity.
+
+
+
+
+ 10,000,000+ Dev Points
+
-
- {/* Community Rewards Section */}
-
-
-
- Community Rewards
-
-
Redeem your hard-earned Dev Points for exclusive perks and swag.
-
-
- {/* PHASE 1 */}
-
-
PHASE 1 — RESOURCES & GUIDED LEARNING (FOUNDATION)
-
- {[
- { name: "DevPath Curated Fundamentals Notes", cost: 5000, icon: "📚", desc: "Clean, original notes for DSA, Web, Android, Backend, ML. Focus: concepts + mental models." },
- { name: "DevPath Practice Set (Domain-based)", cost: 8000, icon: "📝", desc: "Carefully selected problems, tasks, and mini-assignments mapped to one chosen domain." },
- { name: "DevPath Roadmap + Weekly Plan", cost: 12000, icon: "🗺️", desc: "A realistic roadmap: What to learn, what to build, in what order. Time-bound and outcome-focused." },
- { name: "Single Guided Project (Chosen Tech Stack)", cost: 20000, icon: "🏗️", desc: "User selects stack. Receives one clear project problem, scope, and expected output." },
- ].map((reward) => (
-
-
{reward.icon}
-
-
{reward.name}
-
{reward.desc}
-
-
- {reward.cost.toLocaleString()} pts
-
- Redeem
-
-
-
- ))}
-
+
+
+ {/* Other Levels - Horizontal Scroll */}
+
+
+ {LEVELS.slice(0, -1).map((lvl) => (
+
+
+ {lvl.name}
+
+
+ {lvl.max === Infinity
+ ? `${lvl.min}+`
+ : `${lvl.min} - ${lvl.max}`}{' '}
+ pts
+
+ ))}
+
+
+ {Array.from({ length: totalDots }).map((_, i) => (
+
+ ))}
+
+
+
- {/* PHASE 2 */}
-
-
PHASE 2 — PROJECTS, MENTORSHIP & CREDIBILITY
-
- {[
- { name: "Verified Learner Badge", cost: 30000, icon: "🎓", desc: "Awarded after roadmap + task completion. Signals discipline." },
- { name: "Placement & Interview Prep Resources", cost: 40000, icon: "💼", desc: "Domain-focused: Core concepts, interview traps, what actually matters." },
- { name: "Project Mentorship – DevPath", cost: 50000, icon: "👨🏫", desc: "Mentorship on one project: Direction, architecture decisions, review checkpoints." },
- { name: "Community Spotlight", cost: 65000, icon: "🚀", desc: "Featured for Project, Learnings, and Execution clarity." },
- { name: "Verified Builder Badge", cost: 100000, icon: "🛠️", desc: "Earned only after completed project and review approval." },
- ].map((reward) => (
-
-
{reward.icon}
-
-
{reward.name}
-
{reward.desc}
-
-
- {reward.cost.toLocaleString()} pts
-
- Redeem
-
-
-
- ))}
-
-
+ {/* How to Earn Points - Moved to bottom */}
+
+
- {/* PHASE 3 */}
-
-
PHASE 3 — PHYSICAL COMMUNITY REWARDS
-
- {[
- { name: "DevPath Sticker Pack", cost: 125000, icon: "🎨", desc: "Simple, symbolic, low cost." },
- { name: "DevPath Coffee Cup", cost: 150000, icon: "☕", desc: "Clean branding. Everyday utility." },
- { name: "DevPath Mouse Pad", cost: 200000, icon: "🖱️", desc: "Desk-level presence. Long-term use." },
- { name: "DevPath T-Shirt", cost: 300000, icon: "👕", desc: "Not merch. Identity. Limited batches only." },
- { name: "Laptop Cooling Pad", cost: 400000, icon: "❄️", desc: "Practical reward for people who actually build." },
- { name: "Free DevPath Event Ticket", cost: 500000, icon: "🎟️", desc: "Access to Workshop, Meetup, or DevPath-hosted event." },
- ].map((reward) => (
-
-
{reward.icon}
-
-
{reward.name}
-
{reward.desc}
-
-
- {reward.cost.toLocaleString()} pts
-
- Redeem
-
-
-
- ))}
-
-
+ {/* How to Earn Points - Full Width */}
+
+
+
+ How to Earn Points
+
+
+
+
+ Daily Login
+
+
+ +{POINTS.DAILY_LOGIN} (+Streak)
+
+
+
+
+ 7-Day Streak
+
+
+ +{POINTS.WEEKLY_STREAK_BONUS}
+
+
+
+
+ Follow Community
+
+
+ +{POINTS.FOLLOW_COMMUNITY}
+
+
+
+
+ Earn Badge
+
+
+ +{POINTS.BADGE_EARNED}
+
+
+
+
+ Gain Follower
+
+
+ +{POINTS.FOLLOWER_GAINED}
+
+
+
+
+ Project Star
+
+
+ +{POINTS.PROJECT_STAR}
+
+
+
+
+ Event
+ Participation
+
+
+ +{POINTS.EVENT_PARTICIPATION}
+
+
+
+
+ Hackathon Win
+
+
+ +{POINTS.HACKATHON_WIN}
+
+
+
+
- {/* PHASE 4 */}
-
-
PHASE 4 — PREMIUM PHYSICAL REWARDS (TOP TIER)
-
- {[
- { name: "DevPath Backpack (Premium)", cost: 650000, icon: "🎒", desc: "High-quality backpack. Very limited quantity." },
- { name: "Mechanical Keyboard / Headset", cost: 800000, icon: "⌨️", desc: "One premium productivity accessory. Utility-focused." },
- { name: "DevPath Flagship Hardware", cost: 1000000, icon: "🖥️", desc: "External Monitor, Tablet, or Premium accessory. Rare & Symbolic." },
- ].map((reward) => (
-
-
{reward.icon}
-
-
{reward.name}
-
{reward.desc}
-
-
- {reward.cost.toLocaleString()} pts
-
- Redeem
-
-
-
- ))}
-
-
+ {/* Community Rewards Section */}
+
+
+
+ Community Rewards
+
+
+ Redeem your hard-earned Dev Points for exclusive perks and swag.
+
+
+
+ {/* PHASE 1 */}
+
+
+ PHASE 1 — RESOURCES & GUIDED LEARNING (FOUNDATION)
+
+
+ {[
+ {
+ name: 'DevPath Curated Fundamentals Notes',
+ cost: 5000,
+ icon: '📚',
+ desc: 'Clean, original notes for DSA, Web, Android, Backend, ML. Focus: concepts + mental models.',
+ },
+ {
+ name: 'DevPath Practice Set (Domain-based)',
+ cost: 8000,
+ icon: '📝',
+ desc: 'Carefully selected problems, tasks, and mini-assignments mapped to one chosen domain.',
+ },
+ {
+ name: 'DevPath Roadmap + Weekly Plan',
+ cost: 12000,
+ icon: '🗺️',
+ desc: 'A realistic roadmap: What to learn, what to build, in what order. Time-bound and outcome-focused.',
+ },
+ {
+ name: 'Single Guided Project (Chosen Tech Stack)',
+ cost: 20000,
+ icon: '🏗️',
+ desc: 'User selects stack. Receives one clear project problem, scope, and expected output.',
+ },
+ ].map((reward) => (
+
+
{reward.icon}
+
+
{reward.name}
+
+ {reward.desc}
+
+
+
+
+ {reward.cost.toLocaleString()} pts
+
+
+ Redeem
+
+
+
+ ))}
+
+
+
+ {/* PHASE 2 */}
+
+
+ PHASE 2 — PROJECTS, MENTORSHIP & CREDIBILITY
+
+
+ {[
+ {
+ name: 'Verified Learner Badge',
+ cost: 30000,
+ icon: '🎓',
+ desc: 'Awarded after roadmap + task completion. Signals discipline.',
+ },
+ {
+ name: 'Placement & Interview Prep Resources',
+ cost: 40000,
+ icon: '💼',
+ desc: 'Domain-focused: Core concepts, interview traps, what actually matters.',
+ },
+ {
+ name: 'Project Mentorship – DevPath',
+ cost: 50000,
+ icon: '👨🏫',
+ desc: 'Mentorship on one project: Direction, architecture decisions, review checkpoints.',
+ },
+ {
+ name: 'Community Spotlight',
+ cost: 65000,
+ icon: '🚀',
+ desc: 'Featured for Project, Learnings, and Execution clarity.',
+ },
+ {
+ name: 'Verified Builder Badge',
+ cost: 100000,
+ icon: '🛠️',
+ desc: 'Earned only after completed project and review approval.',
+ },
+ ].map((reward) => (
+
+
{reward.icon}
+
+
{reward.name}
+
+ {reward.desc}
+
+
+
+
+ {reward.cost.toLocaleString()} pts
+
+
+ Redeem
+
+
+
+ ))}
+
+
+
+ {/* PHASE 3 */}
+
+
+ PHASE 3 — PHYSICAL COMMUNITY REWARDS
+
+
+ {[
+ {
+ name: 'DevPath Sticker Pack',
+ cost: 125000,
+ icon: '🎨',
+ desc: 'Simple, symbolic, low cost.',
+ },
+ {
+ name: 'DevPath Coffee Cup',
+ cost: 150000,
+ icon: '☕',
+ desc: 'Clean branding. Everyday utility.',
+ },
+ {
+ name: 'DevPath Mouse Pad',
+ cost: 200000,
+ icon: '🖱️',
+ desc: 'Desk-level presence. Long-term use.',
+ },
+ {
+ name: 'DevPath T-Shirt',
+ cost: 300000,
+ icon: '👕',
+ desc: 'Not merch. Identity. Limited batches only.',
+ },
+ {
+ name: 'Laptop Cooling Pad',
+ cost: 400000,
+ icon: '❄️',
+ desc: 'Practical reward for people who actually build.',
+ },
+ {
+ name: 'Free DevPath Event Ticket',
+ cost: 500000,
+ icon: '🎟️',
+ desc: 'Access to Workshop, Meetup, or DevPath-hosted event.',
+ },
+ ].map((reward) => (
+
+
{reward.icon}
+
+
{reward.name}
+
+ {reward.desc}
+
+
+
+
+ {reward.cost.toLocaleString()} pts
+
+
+ Redeem
+
+
+
+ ))}
+
+
+
+ {/* PHASE 4 */}
+
+
+ PHASE 4 — PREMIUM PHYSICAL REWARDS (TOP TIER)
+
+
+ {[
+ {
+ name: 'DevPath Backpack (Premium)',
+ cost: 650000,
+ icon: '🎒',
+ desc: 'High-quality backpack. Very limited quantity.',
+ },
+ {
+ name: 'Mechanical Keyboard / Headset',
+ cost: 800000,
+ icon: '⌨️',
+ desc: 'One premium productivity accessory. Utility-focused.',
+ },
+ {
+ name: 'DevPath Flagship Hardware',
+ cost: 1000000,
+ icon: '🖥️',
+ desc: 'External Monitor, Tablet, or Premium accessory. Rare & Symbolic.',
+ },
+ ].map((reward) => (
+
+
{reward.icon}
+
+
{reward.name}
+
+ {reward.desc}
+
+
+
+
+ {reward.cost.toLocaleString()} pts
+
+
+ Redeem
+
+
+ ))}
+
- );
+
+
+ );
}
diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx
index 97b941c7..dc5f9430 100644
--- a/src/app/privacy/page.tsx
+++ b/src/app/privacy/page.tsx
@@ -1,4 +1,4 @@
-import { siteConfig } from "@/config/siteConfig";
+import { siteConfig } from '@/config/siteConfig';
export const metadata = {
title: `Privacy Policy | ${siteConfig.name}`,
diff --git a/src/app/profile/[username]/page.tsx b/src/app/profile/[username]/page.tsx
new file mode 100644
index 00000000..ec4b3ee9
--- /dev/null
+++ b/src/app/profile/[username]/page.tsx
@@ -0,0 +1,58 @@
+// src/app/profile/[username]/page.tsx
+// Public-facing portfolio route: /profile/[username]
+
+import { Metadata } from 'next';
+import { notFound } from 'next/navigation';
+import { getPublicProfileByUsername } from '@/lib/portfolio-service';
+import { ProfileHeader } from '@/components/profile/ProfileHeader';
+import { PathProgressSection } from '@/components/profile/PathProgressSection';
+import { SkillBadgesSection } from '@/components/profile/SkillBadgesSection';
+import { ProjectShowcaseSection } from '@/components/profile/ProjectShowcaseSection';
+import { ExportBar } from '@/components/profile/ExportBar';
+
+interface Props {
+ params: { username: string };
+}
+
+// Generate OpenGraph metadata dynamically
+export async function generateMetadata({ params }: Props): Promise
{
+ const profile = await getPublicProfileByUsername(params.username);
+ if (!profile) return { title: 'Profile not found' };
+
+ return {
+ title: `${profile.displayName} · DevPath Portfolio`,
+ description: profile.tagline,
+ openGraph: {
+ title: `${profile.displayName} · DevPath`,
+ description: profile.tagline,
+ url: `https://devpath.app/profile/${profile.username}`,
+ },
+ };
+}
+
+export default async function PublicProfilePage({ params }: Props) {
+ const profile = await getPublicProfileByUsername(params.username);
+
+ if (!profile) notFound();
+
+ return (
+
+ {/* ── Profile header: avatar, name, tagline, socials ── */}
+
+
+
+ {/* ── Dev Progress Bars ── */}
+
+
+ {/* ── Verified Tech Stack Badges ── */}
+
+
+ {/* ── Project Cards ── */}
+
+
+
+ {/* ── Sticky export bar ── */}
+
+
+ );
+}
diff --git a/src/app/profile/error.tsx b/src/app/profile/error.tsx
index 0d73b610..7120d28a 100644
--- a/src/app/profile/error.tsx
+++ b/src/app/profile/error.tsx
@@ -6,60 +6,61 @@ import { AlertTriangle, RotateCcw, Home } from 'lucide-react';
import Link from 'next/link';
export default function ProfileError({
- error,
- reset,
+ error,
+ reset,
}: {
- error: Error & { digest?: string };
- reset: () => void;
+ error: Error & { digest?: string };
+ reset: () => void;
}) {
- useEffect(() => {
- console.error('Profile page error:', error);
- }, [error]);
+ useEffect(() => {
+ console.error('Profile page error:', error);
+ }, [error]);
- return (
-
-
+ return (
+
+
-
-
+
+
-
- Profile Unavailable
-
+
+ Profile Unavailable
+
-
+
-
- {error.message || "We couldn't load your profile. Please try again or return home."}
-
+
+ {error.message ||
+ "We couldn't load your profile. Please try again or return home."}
+
-
- }
- className="bg-destructive hover:bg-destructive/90 text-white rounded-2xl px-6 py-4"
- onClick={() => reset()}
- >
- Try Again
-
-
- }
- className="rounded-2xl px-6 py-4 border-white/20 hover:bg-white/5"
- >
- Return Home
-
-
-
-
+
+ }
+ className="bg-destructive hover:bg-destructive/90 text-white rounded-2xl px-6 py-4"
+ onClick={() => reset()}
+ >
+ Try Again
+
+
+ }
+ className="rounded-2xl px-6 py-4 border-white/20 hover:bg-white/5"
+ >
+ Return Home
+
+
- );
+
+
+ );
}
diff --git a/src/app/profile/loading.tsx b/src/app/profile/loading.tsx
index 50559c40..ce93f4eb 100644
--- a/src/app/profile/loading.tsx
+++ b/src/app/profile/loading.tsx
@@ -1,47 +1,56 @@
export default function ProfileLoading() {
- return (
-
- {/* Avatar + name block */}
-
+ return (
+
+ {/* Avatar + name block */}
+
- {/* Stats row */}
-
- {Array.from({ length: 4 }).map((_, i) => (
-
- ))}
-
+ {/* Stats row */}
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
- {/* Badges / Achievements */}
-
-
- {Array.from({ length: 5 }).map((_, i) => (
-
- ))}
-
+ {/* Badges / Achievements */}
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
- {/* Activity / content cards */}
-
- {Array.from({ length: 3 }).map((_, i) => (
-
- ))}
-
-
- );
+ {/* Activity / content cards */}
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+ );
}
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx
index 0af2687c..874b58b8 100644
--- a/src/app/profile/page.tsx
+++ b/src/app/profile/page.tsx
@@ -1,5 +1,5 @@
import UserProfile from '@/components/profile/UserProfile';
export default function ProfilePage() {
- return
;
+ return
;
}
diff --git a/src/app/progress/page.tsx b/src/app/progress/page.tsx
index 76de86d8..db027309 100644
--- a/src/app/progress/page.tsx
+++ b/src/app/progress/page.tsx
@@ -1,7 +1,7 @@
-import { StreakCalendar } from "@/components/gamification/StreakCalendar";
-import { BadgeGrid } from "@/components/gamification/BadgeGrid";
-import { XPBar } from "@/components/gamification/XPBar";
-import { Leaderboard } from "@/components/gamification/Leaderboard";
+import { StreakCalendar } from '@/components/gamification/StreakCalendar';
+import { BadgeGrid } from '@/components/gamification/BadgeGrid';
+import { XPBar } from '@/components/gamification/XPBar';
+import { Leaderboard } from '@/components/gamification/Leaderboard';
// This would get real data from auth + firestore
export default function ProgressPage() {
diff --git a/src/app/resources/page.tsx b/src/app/resources/page.tsx
index 8e500ce7..1e0de9e8 100644
--- a/src/app/resources/page.tsx
+++ b/src/app/resources/page.tsx
@@ -2,9 +2,9 @@ import React from 'react';
import Resources from '@/components/home/Resources';
export default function ResourcesPage() {
- return (
-
-
-
- );
+ return (
+
+
+
+ );
}
diff --git a/src/app/seed-resources/page.tsx b/src/app/seed-resources/page.tsx
index 085e69e1..7a71b343 100644
--- a/src/app/seed-resources/page.tsx
+++ b/src/app/seed-resources/page.tsx
@@ -1,4 +1,4 @@
-"use client";
+'use client';
import { useState } from 'react';
import { db } from '@/lib/firebase';
@@ -6,33 +6,34 @@ import { doc, setDoc } from 'firebase/firestore';
import { internshipData } from '@/data/internshipData';
export default function SeedResourcesPage() {
- const [status, setStatus] = useState('Idle');
+ const [status, setStatus] = useState('Idle');
- const seedInternships = async () => {
- setStatus('Seeding...');
- try {
- await setDoc(doc(db, 'resources', 'Internship_Calendar_2026'), {
- ...internshipData,
- starCount: 0,
- createdAt: new Date().toISOString()
- });
- setStatus('Success! Internship Calendar seeded.');
- } catch (error) {
- console.error(error);
- setStatus('Error: ' + (error as any).message);
- }
- };
+ const seedInternships = async () => {
+ setStatus('Seeding...');
+ try {
+ await setDoc(doc(db, 'resources', 'Internship_Calendar_2026'), {
+ ...internshipData,
+ starCount: 0,
+ createdAt: new Date().toISOString(),
+ });
+ setStatus('Success! Internship Calendar seeded.');
+ } catch (error) {
+ console.error(error);
+ setStatus('Error: ' + (error as any).message);
+ }
+ };
- return (
-
-
Seed Resources
-
Status: {status}
-
- Seed Internship Calendar
-
-
- );
+ return (
+
+
Seed Resources
+
Status: {status}
+
+ Seed Internship Calendar
+
+
+ );
}
diff --git a/src/app/source-code/SourceCode.module.css b/src/app/source-code/SourceCode.module.css
index 603d5969..89e996e8 100644
--- a/src/app/source-code/SourceCode.module.css
+++ b/src/app/source-code/SourceCode.module.css
@@ -1,263 +1,263 @@
.container {
- padding-top: 120px;
- padding-bottom: 80px;
- min-height: 100vh;
- background: var(--bg-primary);
- padding-left: 24px;
- padding-right: 24px;
+ padding-top: 120px;
+ padding-bottom: 80px;
+ min-height: 100vh;
+ background: var(--bg-primary);
+ padding-left: 24px;
+ padding-right: 24px;
}
.content {
- max-width: 1000px;
- margin: 0 auto;
+ max-width: 1000px;
+ margin: 0 auto;
}
.hero {
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- margin-bottom: 80px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ margin-bottom: 80px;
}
.heroIcon {
- margin-bottom: 24px;
- filter: drop-shadow(0 0 20px var(--icon-glow));
+ margin-bottom: 24px;
+ filter: drop-shadow(0 0 20px var(--icon-glow));
}
.title {
- font-size: 56px;
- font-weight: 800;
- margin-bottom: 24px;
- color: var(--text-primary);
+ font-size: 56px;
+ font-weight: 800;
+ margin-bottom: 24px;
+ color: var(--text-primary);
}
.subtitle {
- font-size: 20px;
- color: var(--text-secondary);
- line-height: 1.6;
- max-width: 700px;
- margin: 0 auto 40px;
+ font-size: 20px;
+ color: var(--text-secondary);
+ line-height: 1.6;
+ max-width: 700px;
+ margin: 0 auto 40px;
}
.stats {
- display: flex;
- justify-content: center;
- gap: 40px;
+ display: flex;
+ justify-content: center;
+ gap: 40px;
}
.statItem {
- display: flex;
- align-items: center;
- gap: 12px;
- background: var(--glass-highlight);
- padding: 12px 24px;
- border-radius: 30px;
- border: 1px solid var(--glass-border);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ background: var(--glass-highlight);
+ padding: 12px 24px;
+ border-radius: 30px;
+ border: 1px solid var(--glass-border);
}
.statValue {
- font-weight: 700;
- color: var(--text-primary);
+ font-weight: 700;
+ color: var(--text-primary);
}
.statLabel {
- color: var(--text-secondary);
- font-size: 14px;
+ color: var(--text-secondary);
+ font-size: 14px;
}
.repoGrid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 24px;
- margin-bottom: 60px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 24px;
+ margin-bottom: 60px;
}
.repoCard {
- background: var(--card);
- border: 1px solid var(--glass-border);
- border-radius: 20px;
- padding: 32px;
- transition: all 0.3s ease;
- position: relative;
- overflow: hidden;
+ background: var(--card);
+ border: 1px solid var(--glass-border);
+ border-radius: 20px;
+ padding: 32px;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
}
.repoCard:hover {
- transform: translateY(-8px);
- border-color: rgba(255, 255, 255, 0.2);
- box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.5);
+ transform: translateY(-8px);
+ border-color: rgba(255, 255, 255, 0.2);
+ box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.5);
}
.repoHeader {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 20px;
}
.repoIcon {
- width: 48px;
- height: 48px;
- border-radius: 12px;
- background: var(--glass-highlight);
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-primary);
+ width: 48px;
+ height: 48px;
+ border-radius: 12px;
+ background: var(--glass-highlight);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-primary);
}
.repoTitle {
- font-size: 20px;
- font-weight: 700;
- color: var(--text-primary);
- margin-bottom: 8px;
+ font-size: 20px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 8px;
}
.repoDesc {
- font-size: 14px;
- color: var(--text-secondary);
- line-height: 1.6;
- margin-bottom: 24px;
+ font-size: 14px;
+ color: var(--text-secondary);
+ line-height: 1.6;
+ margin-bottom: 24px;
}
.repoMeta {
- display: flex;
- gap: 16px;
- margin-bottom: 24px;
+ display: flex;
+ gap: 16px;
+ margin-bottom: 24px;
}
.badge {
- font-size: 12px;
- padding: 4px 10px;
- border-radius: 12px;
- background: rgba(59, 130, 246, 0.1);
- color: #3b82f6;
- font-weight: 600;
+ font-size: 12px;
+ padding: 4px 10px;
+ border-radius: 12px;
+ background: rgba(59, 130, 246, 0.1);
+ color: #3b82f6;
+ font-weight: 600;
}
.quickStart {
- background: var(--card);
- border: 1px solid var(--glass-border);
- border-radius: 20px;
- padding: 40px;
- margin-bottom: 60px;
+ background: var(--card);
+ border: 1px solid var(--glass-border);
+ border-radius: 20px;
+ padding: 40px;
+ margin-bottom: 60px;
}
.sectionTitle {
- font-size: 24px;
- font-weight: 700;
- color: var(--text-primary);
- margin-bottom: 24px;
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 24px;
}
.steps {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 24px;
- margin-bottom: 32px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 24px;
+ margin-bottom: 32px;
}
.step {
- position: relative;
+ position: relative;
}
.stepNumber {
- width: 32px;
- height: 32px;
- background: #00d4ff;
- color: #000;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 700;
- margin-bottom: 16px;
+ width: 32px;
+ height: 32px;
+ background: #00d4ff;
+ color: #000;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ margin-bottom: 16px;
}
.stepTitle {
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 8px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 8px;
}
.stepDesc {
- font-size: 14px;
- color: var(--text-secondary);
+ font-size: 14px;
+ color: var(--text-secondary);
}
.codeSnippet {
- background: var(--glass-highlight);
- border: 1px solid var(--glass-border);
- border-radius: 12px;
- padding: 20px;
- font-family: 'Fira Code', monospace;
- display: flex;
- justify-content: space-between;
- align-items: center;
+ background: var(--glass-highlight);
+ border: 1px solid var(--glass-border);
+ border-radius: 12px;
+ padding: 20px;
+ font-family: 'Fira Code', monospace;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
.code {
- color: #a5d6ff;
+ color: #a5d6ff;
}
.copyButton {
- background: var(--glass-highlight);
- border: none;
- border-radius: 6px;
- padding: 6px 12px;
- color: var(--text-secondary);
- cursor: pointer;
- font-size: 12px;
- transition: all 0.2s;
+ background: var(--glass-highlight);
+ border: none;
+ border-radius: 6px;
+ padding: 6px 12px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.2s;
}
.copyButton:hover {
- background: rgba(255, 255, 255, 0.2);
- color: white;
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
}
.techStack {
- text-align: center;
- margin-bottom: 60px;
+ text-align: center;
+ margin-bottom: 60px;
}
.techGrid {
- display: flex;
- justify-content: center;
- flex-wrap: wrap;
- gap: 24px;
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 24px;
}
.techItem {
- background: var(--glass-highlight);
- border: 1px solid var(--glass-border);
- padding: 16px 32px;
- border-radius: 12px;
- color: var(--text-secondary);
- font-weight: 600;
- transition: all 0.2s;
+ background: var(--glass-highlight);
+ border: 1px solid var(--glass-border);
+ padding: 16px 32px;
+ border-radius: 12px;
+ color: var(--text-secondary);
+ font-weight: 600;
+ transition: all 0.2s;
}
.techItem:hover {
- background: rgba(255, 255, 255, 0.08);
- color: var(--text-primary);
- transform: translateY(-2px);
+ background: rgba(255, 255, 255, 0.08);
+ color: var(--text-primary);
+ transform: translateY(-2px);
}
@media (max-width: 768px) {
- .title {
- font-size: 42px;
- }
-
- .stats {
- flex-direction: column;
- gap: 16px;
- }
-
- .codeSnippet {
- flex-direction: column;
- align-items: flex-start;
- gap: 16px;
- }
-}
\ No newline at end of file
+ .title {
+ font-size: 42px;
+ }
+
+ .stats {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .codeSnippet {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+ }
+}
diff --git a/src/app/source-code/page.tsx b/src/app/source-code/page.tsx
index 3d3bd241..20b8b810 100644
--- a/src/app/source-code/page.tsx
+++ b/src/app/source-code/page.tsx
@@ -1,7 +1,16 @@
-"use client";
+'use client';
import { useState, ReactNode } from 'react';
-import { Github, Star, GitFork, CircleDot, ExternalLink, Code, FileText, Smartphone } from 'lucide-react';
+import {
+ Github,
+ Star,
+ GitFork,
+ CircleDot,
+ ExternalLink,
+ Code,
+ FileText,
+ Smartphone,
+} from 'lucide-react';
import Button from '@/components/ui/Button';
import CodeBlock from '@/components/common/CodeBlock';
import InteractiveSteps from '@/components/source-code/InteractiveSteps';
@@ -9,158 +18,203 @@ import RepoModal from '@/components/source-code/RepoModal';
import styles from './SourceCode.module.css';
interface Repo {
- id: string;
- title: string;
- description: string;
- longDescription: string;
- icon: ReactNode;
- techStack: string[];
- features: string[];
- link: string;
- status: 'public' | 'private' | 'coming-soon';
+ id: string;
+ title: string;
+ description: string;
+ longDescription: string;
+ icon: ReactNode;
+ techStack: string[];
+ features: string[];
+ link: string;
+ status: 'public' | 'private' | 'coming-soon';
}
const repos: Repo[] = [
- {
- id: 'web',
- title: 'devpath-web',
- description: 'The main Next.js web application repository.',
- longDescription: 'The core of the DevPath platform. This repository houses the Next.js 14 application, including the interactive learning paths, community features, event management system, and user profiles. It features a modern, responsive UI built with Tailwind CSS and Framer Motion.',
- icon:
,
- techStack: ['Next.js 14', 'TypeScript', 'Firebase', 'Tailwind CSS', 'Framer Motion', 'Zustand'],
- features: ['Authentication & User Profiles', 'Interactive Learning Roadmaps', 'Event Management System', 'Community Wiki & Docs', 'Real-time Notifications'],
- link: 'https://github.com/devpathindcommunity-india/DevPath-Web',
- status: 'public' as const
- },
- {
- id: 'docs',
- title: 'devpath-docs',
- description: 'Documentation, guides, and learning path curriculum content.',
- longDescription: 'The central knowledge base for DevPath. This repository contains all the markdown content for our wiki, learning paths, and contributor guidelines. It is designed to be easily editable by the community.',
- icon:
,
- techStack: ['Markdown', 'MDX', 'Contentlayer'],
- features: ['Comprehensive Wiki', 'Learning Path Curriculums', 'Contributor Guidelines', 'API Documentation'],
- link: 'https://github.com/devpathindcommunity-india/DevPath-Web', // Placeholder if no separate docs repo
- status: 'public' as const
- },
- {
- id: 'mobile',
- title: 'devpath-mobile',
- description: 'React Native mobile application for iOS and Android.',
- longDescription: 'Our upcoming mobile application built with React Native. It will allow users to access learning content offline, receive push notifications for events, and engage with the community on the go.',
- icon:
,
- techStack: ['React Native', 'Expo', 'NativeWind'],
- features: ['Offline Access', 'Push Notifications', 'Mobile-first UI', 'Cross-platform Support'],
- link: '#',
- status: 'private' as const
- }
+ {
+ id: 'web',
+ title: 'devpath-web',
+ description: 'The main Next.js web application repository.',
+ longDescription:
+ 'The core of the DevPath platform. This repository houses the Next.js 14 application, including the interactive learning paths, community features, event management system, and user profiles. It features a modern, responsive UI built with Tailwind CSS and Framer Motion.',
+ icon:
,
+ techStack: [
+ 'Next.js 14',
+ 'TypeScript',
+ 'Firebase',
+ 'Tailwind CSS',
+ 'Framer Motion',
+ 'Zustand',
+ ],
+ features: [
+ 'Authentication & User Profiles',
+ 'Interactive Learning Roadmaps',
+ 'Event Management System',
+ 'Community Wiki & Docs',
+ 'Real-time Notifications',
+ ],
+ link: 'https://github.com/devpathindcommunity-india/DevPath-Web',
+ status: 'public' as const,
+ },
+ {
+ id: 'docs',
+ title: 'devpath-docs',
+ description: 'Documentation, guides, and learning path curriculum content.',
+ longDescription:
+ 'The central knowledge base for DevPath. This repository contains all the markdown content for our wiki, learning paths, and contributor guidelines. It is designed to be easily editable by the community.',
+ icon:
,
+ techStack: ['Markdown', 'MDX', 'Contentlayer'],
+ features: [
+ 'Comprehensive Wiki',
+ 'Learning Path Curriculums',
+ 'Contributor Guidelines',
+ 'API Documentation',
+ ],
+ link: 'https://github.com/devpathindcommunity-india/DevPath-Web', // Placeholder if no separate docs repo
+ status: 'public' as const,
+ },
+ {
+ id: 'mobile',
+ title: 'devpath-mobile',
+ description: 'React Native mobile application for iOS and Android.',
+ longDescription:
+ 'Our upcoming mobile application built with React Native. It will allow users to access learning content offline, receive push notifications for events, and engage with the community on the go.',
+ icon:
,
+ techStack: ['React Native', 'Expo', 'NativeWind'],
+ features: [
+ 'Offline Access',
+ 'Push Notifications',
+ 'Mobile-first UI',
+ 'Cross-platform Support',
+ ],
+ link: '#',
+ status: 'private' as const,
+ },
];
export default function SourceCodePage() {
- const [selectedRepo, setSelectedRepo] = useState
(null);
+ const [selectedRepo, setSelectedRepo] = useState<(typeof repos)[0] | null>(
+ null
+ );
- return (
-
-
setSelectedRepo(null)}
- repo={selectedRepo}
- />
+ return (
+
+
setSelectedRepo(null)}
+ repo={selectedRepo}
+ />
-
-
-
-
DevPath is Open Source
- Built in public. Contribute, learn from the code, and help shape the future of developer education.
-
-
-
-
- 1
- Star
-
-
-
- 1
- Fork
-
-
-
- 5
- Issues
-
-
-
+
+
+
+
DevPath is Open Source
+ Built in public. Contribute, learn from the code, and help shape the
+ future of developer education.
+
+
+
+ 1
+ Star
+
+
+
+ 1
+ Fork
+
+
+
+ 5
+ Issues
+
+
+
-
- {repos.map((repo) => (
-
-
-
- {repo.icon}
-
- {repo.status === 'coming-soon' || repo.status === 'private' ? (
-
- {repo.status === 'private' ? 'Private Alpha' : 'Coming Soon'}
-
- ) : (
-
- )}
-
-
{repo.title}
-
- {repo.description}
-
-
- {repo.techStack[0]}
- {repo.status === 'public' && (
- MIT License
- )}
-
-
setSelectedRepo(repo)}
- disabled={repo.status === 'private'}
- >
- {repo.status === 'private' ? 'Private Repo' : 'View Details'}
-
-
- ))}
-
+
+ {repos.map((repo) => (
+
+
+
{repo.icon}
+ {repo.status === 'coming-soon' || repo.status === 'private' ? (
+
+ {repo.status === 'private'
+ ? 'Private Alpha'
+ : 'Coming Soon'}
+
+ ) : (
+
+ )}
+
+
{repo.title}
+
{repo.description}
+
+ {repo.techStack[0]}
+ {repo.status === 'public' && (
+
+ MIT License
+
+ )}
+
+
setSelectedRepo(repo)}
+ disabled={repo.status === 'private'}
+ >
+ {repo.status === 'private' ? 'Private Repo' : 'View Details'}
+
+
+ ))}
+
-
-
How to Contribute
-
-
+
+
How to Contribute
+
+
-
-
Quick Start Snippet
-
- Use this snippet to get the DevPath web app running locally.
-
-
+ Quick Start Snippet
+
+ Use this snippet to get the DevPath web app running locally.
+
+
-
+ />
+
-
-
Built With
-
-
Next.js 14
-
React
-
Firebase
-
TypeScript
-
Tailwind CSS
-
Framer Motion
-
Lucide Icons
-
-
-
+
+
Built With
+
+
Next.js 14
+
React
+
Firebase
+
TypeScript
+
Tailwind CSS
+
Framer Motion
+
Lucide Icons
+
- );
+
+
+ );
}
diff --git a/src/app/submit/page.tsx b/src/app/submit/page.tsx
index eecce7f8..123d30a5 100644
--- a/src/app/submit/page.tsx
+++ b/src/app/submit/page.tsx
@@ -1,5 +1,5 @@
import SubmitWizard from '@/components/submit/SubmitWizard';
export default function SubmitPage() {
- return ;
+ return ;
}
diff --git a/src/app/team/page.tsx b/src/app/team/page.tsx
index 9b116507..afb47da1 100644
--- a/src/app/team/page.tsx
+++ b/src/app/team/page.tsx
@@ -1,6 +1,12 @@
'use client';
-import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
+import {
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+ type ReactNode,
+} from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { motion, useReducedMotion } from 'framer-motion';
@@ -8,538 +14,745 @@ import { Github, Linkedin, Instagram } from 'lucide-react';
import { teamMembers, TeamMember } from '@/data/team';
interface BorderGlowProps {
- children?: ReactNode;
- className?: string;
- edgeSensitivity?: number;
- glowColor?: string;
- borderRadius?: number;
- glowRadius?: number;
- glowIntensity?: number;
- animated?: boolean;
- colors?: string[];
+ children?: ReactNode;
+ className?: string;
+ edgeSensitivity?: number;
+ glowColor?: string;
+ borderRadius?: number;
+ glowRadius?: number;
+ glowIntensity?: number;
+ animated?: boolean;
+ colors?: string[];
}
-const GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%'];
+const GRADIENT_POSITIONS = [
+ '80% 55%',
+ '69% 34%',
+ '8% 6%',
+ '41% 38%',
+ '86% 85%',
+ '82% 18%',
+ '51% 4%',
+];
const COLOR_MAP = [0, 1, 2, 0, 1, 2, 1];
const buildMeshGradients = (colors: string[]): string[] => {
- const gradients: string[] = [];
- for (let i = 0; i < 7; i++) {
- const color = colors[Math.min(COLOR_MAP[i], colors.length - 1)];
- gradients.push(`radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${color} 0px, transparent 50%)`);
- }
- gradients.push(`linear-gradient(${colors[0]} 0%, ${colors[0]} 100%)`);
- return gradients;
+ const gradients: string[] = [];
+ for (let i = 0; i < 7; i++) {
+ const color = colors[Math.min(COLOR_MAP[i], colors.length - 1)];
+ gradients.push(
+ `radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${color} 0px, transparent 50%)`
+ );
+ }
+ gradients.push(`linear-gradient(${colors[0]} 0%, ${colors[0]} 100%)`);
+ return gradients;
};
const BorderGlow: React.FC = ({
- children,
- className = '',
- edgeSensitivity = 30,
- glowColor = '124 58 237',
- borderRadius = 0,
- glowRadius = 30,
- glowIntensity = 1,
- animated = false,
- colors = ['#c084fc', '#22d3ee', '#38bdf8']
+ children,
+ className = '',
+ edgeSensitivity = 30,
+ glowColor = '124 58 237',
+ borderRadius = 0,
+ glowRadius = 30,
+ glowIntensity = 1,
+ animated = false,
+ colors = ['#c084fc', '#22d3ee', '#38bdf8'],
}) => {
- const cardRef = useRef(null);
- const [isHovered, setIsHovered] = useState(false);
- const [cursorAngle, setCursorAngle] = useState(45);
- const [edgeProximity, setEdgeProximity] = useState(0);
- const [sweepActive, setSweepActive] = useState(false);
-
- const getCenterOfElement = useCallback((el: HTMLElement): [number, number] => {
- const { width, height } = el.getBoundingClientRect();
- return [width / 2, height / 2];
- }, []);
-
- const getEdgeProximity = useCallback((el: HTMLElement, x: number, y: number): number => {
- const [cx, cy] = getCenterOfElement(el);
- const dx = x - cx;
- const dy = y - cy;
- let kx = Infinity;
- let ky = Infinity;
- if (dx !== 0) kx = cx / Math.abs(dx);
- if (dy !== 0) ky = cy / Math.abs(dy);
- return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);
- }, [getCenterOfElement]);
-
- const getCursorAngle = useCallback((el: HTMLElement, x: number, y: number): number => {
- const [cx, cy] = getCenterOfElement(el);
- const dx = x - cx;
- const dy = y - cy;
- if (dx === 0 && dy === 0) return 0;
- const radians = Math.atan2(dy, dx);
- let degrees = radians * (180 / Math.PI) + 90;
- if (degrees < 0) degrees += 360;
- return degrees;
- }, [getCenterOfElement]);
-
- const pointerMoveRafRef = useRef(null);
- const pointerPositionRef = useRef<{ x: number; y: number } | null>(null);
-
- const handlePointerMove = useCallback((e: React.PointerEvent) => {
- const card = cardRef.current;
- if (!card) return;
- const rect = card.getBoundingClientRect();
- pointerPositionRef.current = {
- x: e.clientX - rect.left,
- y: e.clientY - rect.top,
- };
-
- if (pointerMoveRafRef.current !== null) return;
-
- pointerMoveRafRef.current = requestAnimationFrame(() => {
- pointerMoveRafRef.current = null;
- const nextCard = cardRef.current;
- const position = pointerPositionRef.current;
- if (!nextCard || !position) return;
- setEdgeProximity(getEdgeProximity(nextCard, position.x, position.y));
- setCursorAngle(getCursorAngle(nextCard, position.x, position.y));
- });
- }, [getEdgeProximity, getCursorAngle]);
-
- useEffect(() => {
- return () => {
- if (pointerMoveRafRef.current !== null) {
- cancelAnimationFrame(pointerMoveRafRef.current);
- }
- };
- }, []);
-
- useEffect(() => {
- if (!animated) return;
- const angleStart = 110;
- const angleEnd = 465;
- requestAnimationFrame(() => setSweepActive(true));
- requestAnimationFrame(() => setCursorAngle(angleStart));
-
- const t0 = performance.now();
- let raf = 0;
- const duration = 1400;
-
- const tick = () => {
- const t = Math.min((performance.now() - t0) / duration, 1);
- setCursorAngle((angleEnd - angleStart) * t + angleStart);
- setEdgeProximity(Math.max(0, Math.sin(t * Math.PI)));
- if (t < 1) {
- raf = requestAnimationFrame(tick);
- } else {
- setSweepActive(false);
- setEdgeProximity(0);
- }
- };
-
+ const cardRef = useRef(null);
+ const [isHovered, setIsHovered] = useState(false);
+ const [cursorAngle, setCursorAngle] = useState(45);
+ const [edgeProximity, setEdgeProximity] = useState(0);
+ const [sweepActive, setSweepActive] = useState(false);
+
+ const getCenterOfElement = useCallback(
+ (el: HTMLElement): [number, number] => {
+ const { width, height } = el.getBoundingClientRect();
+ return [width / 2, height / 2];
+ },
+ []
+ );
+
+ const getEdgeProximity = useCallback(
+ (el: HTMLElement, x: number, y: number): number => {
+ const [cx, cy] = getCenterOfElement(el);
+ const dx = x - cx;
+ const dy = y - cy;
+ let kx = Infinity;
+ let ky = Infinity;
+ if (dx !== 0) kx = cx / Math.abs(dx);
+ if (dy !== 0) ky = cy / Math.abs(dy);
+ return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);
+ },
+ [getCenterOfElement]
+ );
+
+ const getCursorAngle = useCallback(
+ (el: HTMLElement, x: number, y: number): number => {
+ const [cx, cy] = getCenterOfElement(el);
+ const dx = x - cx;
+ const dy = y - cy;
+ if (dx === 0 && dy === 0) return 0;
+ const radians = Math.atan2(dy, dx);
+ let degrees = radians * (180 / Math.PI) + 90;
+ if (degrees < 0) degrees += 360;
+ return degrees;
+ },
+ [getCenterOfElement]
+ );
+
+ const pointerMoveRafRef = useRef(null);
+ const pointerPositionRef = useRef<{ x: number; y: number } | null>(null);
+
+ const handlePointerMove = useCallback(
+ (e: React.PointerEvent) => {
+ const card = cardRef.current;
+ if (!card) return;
+ const rect = card.getBoundingClientRect();
+ pointerPositionRef.current = {
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ };
+
+ if (pointerMoveRafRef.current !== null) return;
+
+ pointerMoveRafRef.current = requestAnimationFrame(() => {
+ pointerMoveRafRef.current = null;
+ const nextCard = cardRef.current;
+ const position = pointerPositionRef.current;
+ if (!nextCard || !position) return;
+ setEdgeProximity(getEdgeProximity(nextCard, position.x, position.y));
+ setCursorAngle(getCursorAngle(nextCard, position.x, position.y));
+ });
+ },
+ [getEdgeProximity, getCursorAngle]
+ );
+
+ useEffect(() => {
+ return () => {
+ if (pointerMoveRafRef.current !== null) {
+ cancelAnimationFrame(pointerMoveRafRef.current);
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!animated) return;
+ const angleStart = 110;
+ const angleEnd = 465;
+ requestAnimationFrame(() => setSweepActive(true));
+ requestAnimationFrame(() => setCursorAngle(angleStart));
+
+ const t0 = performance.now();
+ let raf = 0;
+ const duration = 1400;
+
+ const tick = () => {
+ const t = Math.min((performance.now() - t0) / duration, 1);
+ setCursorAngle((angleEnd - angleStart) * t + angleStart);
+ setEdgeProximity(Math.max(0, Math.sin(t * Math.PI)));
+ if (t < 1) {
raf = requestAnimationFrame(tick);
- return () => cancelAnimationFrame(raf);
- }, [animated]);
-
- const colorSensitivity = edgeSensitivity + 20;
- const isVisible = isHovered || sweepActive;
- const borderOpacity = isVisible
- ? Math.max(0, (edgeProximity * 100 - colorSensitivity) / (100 - colorSensitivity))
- : 0;
- const glowOpacity = isVisible
- ? Math.max(0, (edgeProximity * 100 - edgeSensitivity) / (100 - edgeSensitivity))
- : 0;
-
- const meshGradients = buildMeshGradients(colors);
- const borderBg = meshGradients.map(gradient => `${gradient} border-box`);
-
- return (
- {
- setIsHovered(true);
- setEdgeProximity(0.85);
- }}
- onPointerLeave={() => {
- setIsHovered(false);
- setEdgeProximity(0);
- }}
- className={`relative isolate overflow-visible ${className}`}
- style={{
- borderRadius: `${borderRadius}px`,
- transform: 'translate3d(0, 0, 0.01px)'
- }}
- >
-
-
- {/* remove the fill layer to avoid visible rectangular bands behind cards */}
-
-
-
-
-
-
{children}
-
- );
+ } else {
+ setSweepActive(false);
+ setEdgeProximity(0);
+ }
+ };
+
+ raf = requestAnimationFrame(tick);
+ return () => cancelAnimationFrame(raf);
+ }, [animated]);
+
+ const colorSensitivity = edgeSensitivity + 20;
+ const isVisible = isHovered || sweepActive;
+ const borderOpacity = isVisible
+ ? Math.max(
+ 0,
+ (edgeProximity * 100 - colorSensitivity) / (100 - colorSensitivity)
+ )
+ : 0;
+ const glowOpacity = isVisible
+ ? Math.max(
+ 0,
+ (edgeProximity * 100 - edgeSensitivity) / (100 - edgeSensitivity)
+ )
+ : 0;
+
+ const meshGradients = buildMeshGradients(colors);
+ const borderBg = meshGradients.map((gradient) => `${gradient} border-box`);
+
+ return (
+ {
+ setIsHovered(true);
+ setEdgeProximity(0.85);
+ }}
+ onPointerLeave={() => {
+ setIsHovered(false);
+ setEdgeProximity(0);
+ }}
+ className={`relative isolate overflow-visible ${className}`}
+ style={{
+ borderRadius: `${borderRadius}px`,
+ transform: 'translate3d(0, 0, 0.01px)',
+ }}
+ >
+
+
+ {/* remove the fill layer to avoid visible rectangular bands behind cards */}
+
+
+
+
+
+
{children}
+
+ );
};
const getInitials = (name: string): string =>
- name
- .split(' ')
- .map(part => part[0])
- .join('')
- .slice(0, 2)
- .toUpperCase();
+ name
+ .split(' ')
+ .map((part) => part[0])
+ .join('')
+ .slice(0, 2)
+ .toUpperCase();
const rolePalette: Record = {
- Owner: 'bg-emerald-400/20 text-emerald-500 border-emerald-300/30 dark:text-emerald-400',
- 'Core Admin': 'bg-indigo-400/20 text-indigo-500 border-indigo-300/30 dark:text-indigo-400',
- Head: 'bg-fuchsia-400/20 text-fuchsia-500 border-fuchsia-300/30 dark:text-fuchsia-400',
- 'City Lead': 'bg-sky-400/20 text-sky-500 border-sky-300/30 dark:text-sky-400'
+ Owner: 'bg-emerald-400/20 text-emerald-200 border-emerald-300/30',
+ 'Core Admin': 'bg-indigo-400/20 text-indigo-200 border-indigo-300/30',
+ Head: 'bg-fuchsia-400/20 text-fuchsia-200 border-fuchsia-300/30',
+ 'City Lead': 'bg-sky-400/20 text-sky-200 border-sky-300/30',
};
-const TeamTile = ({ member, index, stepClass = '' }: { member: TeamMember; index: number; stepClass?: string }) => {
- const shouldReduceMotion = useReducedMotion();
- const [imageReady, setImageReady] = useState(!member.image);
-
- useEffect(() => {
- setImageReady(!member.image);
- }, [member.image]);
-
- return (
-
-
- {!imageReady && (
-
- )}
-
- {member.image ? (
- setImageReady(true)}
- onError={() => setImageReady(true)}
- />
- ) : (
-
- {getInitials(member.name)}
-
- )}
+const TeamTile = ({
+ member,
+ index,
+ stepClass = '',
+}: {
+ member: TeamMember;
+ index: number;
+ stepClass?: string;
+}) => {
+ const shouldReduceMotion = useReducedMotion();
+ const [imageReady, setImageReady] = useState(!member.image);
-
+ useEffect(() => {
+ setImageReady(!member.image);
+ }, [member.image]);
-
-
{member.name}
-
{member.subRole ?? member.role}
-
-
-
- );
+ return (
+
+
+ {!imageReady && (
+
+ )}
+
+ {member.image ? (
+ setImageReady(true)}
+ onError={() => setImageReady(true)}
+ />
+ ) : (
+
+ {getInitials(member.name)}
+
+ )}
+
+
+
+
+
+ {member.name}
+
+
+ {member.subRole ?? member.role}
+
+
+
+
+ );
};
-const ValueCard = ({
- title,
- body
-}: {
- title: string;
- body: string;
-}) => (
-
- {title}
- {body}
-
+const ValueCard = ({ title, body }: { title: string; body: string }) => (
+
+ {title}
+ {body}
+
);
-const CoreRoleCard = ({ member, index }: { member: TeamMember; index: number }) => {
- const shouldReduceMotion = useReducedMotion();
- const [imageReady, setImageReady] = useState(!member.image);
+const CoreRoleCard = ({
+ member,
+ index,
+}: {
+ member: TeamMember;
+ index: number;
+}) => {
+ const shouldReduceMotion = useReducedMotion();
+ const [imageReady, setImageReady] = useState(!member.image);
- useEffect(() => {
- setImageReady(!member.image);
- }, [member.image]);
+ useEffect(() => {
+ setImageReady(!member.image);
+ }, [member.image]);
- return (
+ return (
-
-
- {!imageReady && (
-
- )}
-
- {member.image ? (
-
setImageReady(true)}
- onError={() => setImageReady(true)}
- />
- ) : (
-
- {getInitials(member.name)}
-
- )}
-
+
+
+ {!imageReady && (
+
+ )}
+
+ {member.image ? (
+
setImageReady(true)}
+ onError={() => setImageReady(true)}
+ />
+ ) : (
+
+
+ {getInitials(member.name)}
+
+ )}
+
+
-
-
{member.name}
-
{member.role}
- {member.subRole && (
-
{member.subRole}
- )}
-
- {member.responsibilities && member.responsibilities.length > 0 && (
-
- {member.responsibilities.slice(0, 2).map((item, idx) => (
- • {item}
- ))}
-
- )}
-
-
+
+
+ {member.name}
+
+
+ {member.role}
+
+ {member.subRole && (
+
+ {member.subRole}
+
+ )}
+
+ {member.responsibilities && member.responsibilities.length > 0 && (
+
+ {member.responsibilities.slice(0, 2).map((item, idx) => (
+
+ • {item}
+
+ ))}
+
+ )}
+
+
- );
+ );
};
export default function TeamPage() {
- const owner = teamMembers.find(member => member.category === 'Owner');
- const cityLeads = teamMembers.filter(member => member.category === 'City Lead');
-
- const coreAdmins = teamMembers.filter(member => member.category === 'Core Admin');
- const heads = teamMembers.filter(member => member.category === 'Head');
- const [ownerImageReady, setOwnerImageReady] = useState(!owner?.image);
-
- useEffect(() => {
- setOwnerImageReady(!owner?.image);
- }, [owner?.image]);
-
- // keep all left tiles on the same top baseline (no staircase offsets)
- const leftStepPattern = ['mt-0', 'mt-0', 'mt-0', 'mt-0'];
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
+ const owner = teamMembers.find((member) => member.category === 'Owner');
+ const cityLeads = teamMembers.filter(
+ (member) => member.category === 'City Lead'
+ );
+
+ const coreAdmins = teamMembers.filter(
+ (member) => member.category === 'Core Admin'
+ );
+ const heads = teamMembers.filter((member) => member.category === 'Head');
+ const [ownerImageReady, setOwnerImageReady] = useState(!owner?.image);
+
+ useEffect(() => {
+ setOwnerImageReady(!owner?.image);
+ }, [owner?.image]);
+
+ // keep all left tiles on the same top baseline (no staircase offsets)
+ const leftStepPattern = ['mt-0', 'mt-0', 'mt-0', 'mt-0'];
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ DevPath Community
+
+
+ Meet our team
+
+
+ A mission-focused group of builders, mentors, and organizers
+ creating accessible learning pathways for everyone.
+
+
+
+
+ {teamMembers.length} contributors
+
+
+ {coreAdmins.length} core admins
+
+
+ {heads.length} heads
+
+
+ {cityLeads.length} city leads
+
-
-
-
-
DevPath Community
-
- Meet our team
-
-
- A mission-focused group of builders, mentors, and organizers creating accessible learning pathways for everyone.
-
-
-
- {teamMembers.length} contributors
- {coreAdmins.length} core admins
- {heads.length} heads
- {cityLeads.length} city leads
-
-
-
- {cityLeads.map((member, index) => (
-
- ))}
-
-
-
-
- {owner ? (
- <>
-
-
- {!ownerImageReady && (
-
- )}
-
- {owner.image ? (
-
setOwnerImageReady(true)}
- onError={() => setOwnerImageReady(true)}
- />
- ) : (
- {getInitials(owner.name)}
- )}
-
-
-
-
-
{owner.name}
-
{owner.subRole ?? owner.role}
-
- Guiding the DevPath vision with clarity, collaboration, and an open-source-first mindset.
-
-
-
-
- {owner.socials?.github && (
-
-
-
- )}
- {owner.socials?.linkedin && (
-
-
-
- )}
- {owner.socials?.instagram && (
-
-
-
- )}
-
-
-
-
Core Admins
-
- {coreAdmins.map((member, index) => (
-
- ))}
-
-
-
- {heads.length > 0 && (
-
-
Community Heads & Leads
-
- {heads.map((member, index) => (
-
- ))}
-
-
- )}
- >
- ) : (
- Owner profile will appear here.
- )}
-
-
-
+
+ {cityLeads.map((member, index) => (
+
+ ))}
-
-
-
-
-
-
Our Mission
-
- We make community-driven learning practical, collaborative, and inclusive.
-
+
+
+
+ {owner ? (
+ <>
+
+
+ {!ownerImageReady && (
+
+ )}
+
+ {owner.image ? (
+
setOwnerImageReady(true)}
+ onError={() => setOwnerImageReady(true)}
+ />
+ ) : (
+
+ {getInitials(owner.name)}
+
+ )}
+
+
+
+
+
+ {owner.name}
+
+
+ {owner.subRole ?? owner.role}
+
+
+ Guiding the DevPath vision with clarity, collaboration,
+ and an open-source-first mindset.
+
-
-
-
-
-
+
+ {owner.socials?.github && (
+
+
+
+ )}
+ {owner.socials?.linkedin && (
+
+
+
+ )}
+ {owner.socials?.instagram && (
+
+
+
+ )}
-
-
- Explore
- Learn more about the team values
- Understand how our team collaborates across mentorship, content, and technical initiatives.
- Read more
-
-
-
- Opportunities
- Join the team
- We are continuously expanding with volunteer and leadership roles in multiple cities.
- View openings
-
+
+
+ Core Admins
+
+
+ {coreAdmins.map((member, index) => (
+
+ ))}
+
-
-
- >
- );
+
+ {heads.length > 0 && (
+
+
+ Community Heads & Leads
+
+
+ {heads.map((member, index) => (
+
+ ))}
+
+
+ )}
+ >
+ ) : (
+
+ Owner profile will appear here.
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ Our Mission
+
+
+ We make community-driven learning practical, collaborative, and
+ inclusive.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Explore
+
+
+ Learn more about the team values
+
+
+ Understand how our team collaborates across mentorship, content,
+ and technical initiatives.
+
+
+ Read more
+
+
+
+
+
+ Opportunities
+
+ Join the team
+
+ We are continuously expanding with volunteer and leadership
+ roles in multiple cities.
+
+
+ View openings
+
+
+
+
+
+ >
+ );
}
diff --git a/src/app/team/team.module.css b/src/app/team/team.module.css
index 0a82d253..2ce0f583 100644
--- a/src/app/team/team.module.css
+++ b/src/app/team/team.module.css
@@ -1,219 +1,219 @@
/* Team Section - Same animation for Desktop & Mobile */
.team {
- position: relative;
- width: 100vw;
- height: 100svh;
- background-color: var(--bg-primary);
- color: var(--text-primary);
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 2.5em;
- overflow: hidden;
- font-family: var(--font-barlow), 'Barlow Condensed', sans-serif;
- padding: 80px 20px 40px;
+ position: relative;
+ width: 100vw;
+ height: 100svh;
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 2.5em;
+ overflow: hidden;
+ font-family: var(--font-barlow), 'Barlow Condensed', sans-serif;
+ padding: 80px 20px 40px;
}
.loading {
- font-size: 1.5rem;
- color: var(--text-secondary);
+ font-size: 1.5rem;
+ color: var(--text-secondary);
}
.profileImages {
- width: max-content;
- max-width: 100%;
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: center;
- gap: 0;
+ width: max-content;
+ max-width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+ gap: 0;
}
.img {
- position: relative;
- width: 70px;
- height: 70px;
- padding: 5px;
- cursor: pointer;
- will-change: width, height;
- transition: transform 0.2s ease;
+ position: relative;
+ width: 70px;
+ height: 70px;
+ padding: 5px;
+ cursor: pointer;
+ will-change: width, height;
+ transition: transform 0.2s ease;
}
.img:active {
- transform: scale(0.95);
+ transform: scale(0.95);
}
.img.active {
- z-index: 10;
+ z-index: 10;
}
.imgInner {
- width: 100%;
- height: 100%;
- object-fit: cover;
- border-radius: 0.5rem;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- transition: box-shadow 0.3s ease;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 0.5rem;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ transition: box-shadow 0.3s ease;
}
.img:hover .imgInner,
.img.active .imgInner {
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
/* Profile Names Container - Hard clip to hide ALL overflow including Q descender */
.profileNames {
- width: 100%;
- height: 12rem;
- position: relative;
- /* CRITICAL: clip-path creates a hard clipping boundary - nothing outside is visible */
- clip-path: inset(0 0 0 0);
- overflow: hidden;
+ width: 100%;
+ height: 12rem;
+ position: relative;
+ /* CRITICAL: clip-path creates a hard clipping boundary - nothing outside is visible */
+ clip-path: inset(0 0 0 0);
+ overflow: hidden;
}
/* Each name container - stacked on top of each other */
.name {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- /* Each name also clips its content */
- clip-path: inset(0 0 0 0);
- overflow: hidden;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ /* Each name also clips its content */
+ clip-path: inset(0 0 0 0);
+ overflow: hidden;
}
/* Default text sits behind other names */
.default {
- z-index: 1;
+ z-index: 1;
}
/* Active names sit on top */
.name:not(.default) {
- z-index: 2;
+ z-index: 2;
}
.name h1 {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- text-transform: uppercase;
- font-family: var(--font-barlow), 'Barlow Condensed', sans-serif;
- font-size: 12rem;
- font-weight: 900;
- letter-spacing: -0.2rem;
- line-height: 1;
- color: hsl(var(--primary));
- user-select: none;
- transform: translateY(100%);
- margin: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-transform: uppercase;
+ font-family: var(--font-barlow), 'Barlow Condensed', sans-serif;
+ font-size: 12rem;
+ font-weight: 900;
+ letter-spacing: -0.2rem;
+ line-height: 1;
+ color: hsl(var(--primary));
+ user-select: none;
+ transform: translateY(100%);
+ margin: 0;
}
.default h1 {
- color: var(--text-secondary);
- transform: translateY(-100%);
+ color: var(--text-secondary);
+ transform: translateY(-100%);
}
/* Each letter also clips descenders */
.name h1 .letter {
- display: inline-block;
- transform: translateY(0%);
- will-change: transform;
- /* Clip descenders like Q tail */
- overflow: hidden;
- line-height: 0.85;
- padding-bottom: 0.1em;
- clip-path: inset(0 0 0 0);
+ display: inline-block;
+ transform: translateY(0%);
+ will-change: transform;
+ /* Clip descenders like Q tail */
+ overflow: hidden;
+ line-height: 0.85;
+ padding-bottom: 0.1em;
+ clip-path: inset(0 0 0 0);
}
/* Dark mode */
:global(.dark) .name:not(.default) h1 {
- text-shadow: 0 0 40px rgba(0, 212, 255, 0.3);
+ text-shadow: 0 0 40px rgba(0, 212, 255, 0.3);
}
:global(.dark) .default h1 {
- color: #e3e3db;
+ color: #e3e3db;
}
/* Light mode */
:global(:root:not(.dark)) .name:not(.default) h1 {
- color: hsl(var(--primary));
+ color: hsl(var(--primary));
}
:global(:root:not(.dark)) .default h1 {
- color: #374151;
+ color: #374151;
}
/* ===== MOBILE STYLES ===== */
@media screen and (max-width: 900px) {
- .team {
- padding: 100px 16px 40px;
- gap: 2em;
- height: auto;
- min-height: 100svh;
- }
+ .team {
+ padding: 100px 16px 40px;
+ gap: 2em;
+ height: auto;
+ min-height: 100svh;
+ }
- .profileImages {
- gap: 4px;
- }
+ .profileImages {
+ gap: 4px;
+ }
- .img {
- width: 55px;
- height: 55px;
- padding: 3px;
- }
+ .img {
+ width: 55px;
+ height: 55px;
+ padding: 3px;
+ }
- .profileNames {
- height: 6rem;
- }
+ .profileNames {
+ height: 6rem;
+ }
- .name h1 {
- font-size: 5rem;
- letter-spacing: -0.1rem;
- }
+ .name h1 {
+ font-size: 5rem;
+ letter-spacing: -0.1rem;
+ }
}
/* Small Mobile */
@media screen and (max-width: 600px) {
- .team {
- padding: 100px 12px 30px;
- }
+ .team {
+ padding: 100px 12px 30px;
+ }
- .img {
- width: 50px;
- height: 50px;
- padding: 2px;
- }
+ .img {
+ width: 50px;
+ height: 50px;
+ padding: 2px;
+ }
- .profileNames {
- height: 5rem;
- }
+ .profileNames {
+ height: 5rem;
+ }
- .name h1 {
- font-size: 4rem;
- letter-spacing: 0;
- }
+ .name h1 {
+ font-size: 4rem;
+ letter-spacing: 0;
+ }
}
/* Very Small Mobile */
@media screen and (max-width: 400px) {
- .img {
- width: 45px;
- height: 45px;
- }
-
- .profileNames {
- height: 4rem;
- }
-
- .name h1 {
- font-size: 3rem;
- }
-}
\ No newline at end of file
+ .img {
+ width: 45px;
+ height: 45px;
+ }
+
+ .profileNames {
+ height: 4rem;
+ }
+
+ .name h1 {
+ font-size: 3rem;
+ }
+}
diff --git a/src/app/template.tsx b/src/app/template.tsx
index e27f8640..8091d6a9 100644
--- a/src/app/template.tsx
+++ b/src/app/template.tsx
@@ -1,7 +1,7 @@
-"use client"
+'use client';
-import PageTransition from "@/components/layout/PageTransition"
+import PageTransition from '@/components/layout/PageTransition';
export default function Template({ children }: { children: React.ReactNode }) {
- return
{children}
+ return
{children} ;
}
diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx
index e6ecf789..0b3914fe 100644
--- a/src/app/terms/page.tsx
+++ b/src/app/terms/page.tsx
@@ -1,11 +1,11 @@
// app/terms-and-conditions/page.tsx
-import { Metadata } from "next";
+import { Metadata } from 'next';
export const metadata: Metadata = {
- title: "Terms & Conditions | DevPath",
+ title: 'Terms & Conditions | DevPath',
description:
- "Terms and Conditions governing the use of DevPath and its services.",
+ 'Terms and Conditions governing the use of DevPath and its services.',
};
export default function TermsAndConditionsPage() {
diff --git a/src/app/translate/Translate.module.css b/src/app/translate/Translate.module.css
index 3f244b63..3205b557 100644
--- a/src/app/translate/Translate.module.css
+++ b/src/app/translate/Translate.module.css
@@ -1,238 +1,238 @@
.container {
- padding-top: 120px;
- padding-bottom: 80px;
- min-height: 100vh;
- background: var(--bg-primary);
- padding-left: 24px;
- padding-right: 24px;
+ padding-top: 120px;
+ padding-bottom: 80px;
+ min-height: 100vh;
+ background: var(--bg-primary);
+ padding-left: 24px;
+ padding-right: 24px;
}
.content {
- max-width: 1200px;
- margin: 0 auto;
+ max-width: 1200px;
+ margin: 0 auto;
}
.hero {
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- margin-bottom: 80px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ margin-bottom: 80px;
}
.globeIcon {
- margin-bottom: 24px;
- animation: float 6s ease-in-out infinite;
- color: var(--primary);
- filter: drop-shadow(0 0 20px var(--icon-glow));
+ margin-bottom: 24px;
+ animation: float 6s ease-in-out infinite;
+ color: var(--primary);
+ filter: drop-shadow(0 0 20px var(--icon-glow));
}
.title {
- font-size: 56px;
- font-weight: 800;
- margin-bottom: 24px;
- background: linear-gradient(135deg, #00d4ff 0%, #9d4edd 100%);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
+ font-size: 56px;
+ font-weight: 800;
+ margin-bottom: 24px;
+ background: linear-gradient(135deg, #00d4ff 0%, #9d4edd 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
}
.subtitle {
- font-size: 20px;
- color: var(--text-secondary);
- line-height: 1.6;
- max-width: 700px;
- margin: 0 auto 40px;
+ font-size: 20px;
+ color: var(--text-secondary);
+ line-height: 1.6;
+ max-width: 700px;
+ margin: 0 auto 40px;
}
.grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: 24px;
- margin-bottom: 80px;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 24px;
+ margin-bottom: 80px;
}
.card {
- background: var(--card);
- border: 1px solid var(--glass-border);
- border-radius: 20px;
- padding: 24px;
- transition: all 0.3s ease;
- position: relative;
- overflow: hidden;
+ background: var(--card);
+ border: 1px solid var(--glass-border);
+ border-radius: 20px;
+ padding: 24px;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
}
.card:hover {
- transform: translateY(-8px);
- border-color: rgba(255, 255, 255, 0.2);
- box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.5);
+ transform: translateY(-8px);
+ border-color: rgba(255, 255, 255, 0.2);
+ box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.5);
}
.cardHeader {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
}
.flag {
- font-size: 32px;
+ font-size: 32px;
}
.statusBadge {
- font-size: 12px;
- padding: 4px 10px;
- border-radius: 12px;
- font-weight: 600;
+ font-size: 12px;
+ padding: 4px 10px;
+ border-radius: 12px;
+ font-weight: 600;
}
.statusBadge.complete {
- background: rgba(16, 185, 129, 0.1);
- color: #10b981;
+ background: rgba(16, 185, 129, 0.1);
+ color: #10b981;
}
.statusBadge.progress {
- background: rgba(245, 158, 11, 0.1);
- color: #f59e0b;
+ background: rgba(245, 158, 11, 0.1);
+ color: #f59e0b;
}
.statusBadge.start {
- background: rgba(255, 255, 255, 0.1);
- color: var(--text-secondary);
+ background: rgba(255, 255, 255, 0.1);
+ color: var(--text-secondary);
}
.langName {
- font-size: 20px;
- font-weight: 700;
- color: var(--text-primary);
- margin-bottom: 4px;
+ font-size: 20px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 4px;
}
.langNative {
- font-size: 14px;
- color: var(--text-secondary);
- margin-bottom: 20px;
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-bottom: 20px;
}
.progressContainer {
- margin-bottom: 24px;
+ margin-bottom: 24px;
}
.progressBar {
- height: 6px;
- background: var(--glass-border);
- border-radius: 3px;
- overflow: hidden;
- margin-bottom: 8px;
+ height: 6px;
+ background: var(--glass-border);
+ border-radius: 3px;
+ overflow: hidden;
+ margin-bottom: 8px;
}
.progressFill {
- height: 100%;
- background: linear-gradient(90deg, #00d4ff, #9d4edd);
- border-radius: 3px;
+ height: 100%;
+ background: linear-gradient(90deg, #00d4ff, #9d4edd);
+ border-radius: 3px;
}
.progressLabel {
- display: flex;
- justify-content: space-between;
- font-size: 12px;
- color: var(--text-secondary);
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ color: var(--text-secondary);
}
.section {
- background: var(--glass-highlight);
- border: 1px solid var(--glass-border);
- border-radius: 24px;
- padding: 40px;
- margin-bottom: 60px;
+ background: var(--glass-highlight);
+ border: 1px solid var(--glass-border);
+ border-radius: 24px;
+ padding: 40px;
+ margin-bottom: 60px;
}
.sectionTitle {
- font-size: 28px;
- font-weight: 700;
- color: var(--text-primary);
- margin-bottom: 32px;
- text-align: center;
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 32px;
+ text-align: center;
}
.steps {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 32px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 32px;
}
.step {
- text-align: center;
+ text-align: center;
}
.stepIcon {
- width: 64px;
- height: 64px;
- background: rgba(0, 212, 255, 0.1);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 0 auto 20px;
- color: #00d4ff;
+ width: 64px;
+ height: 64px;
+ background: rgba(0, 212, 255, 0.1);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 20px;
+ color: #00d4ff;
}
.stepTitle {
- font-size: 18px;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 8px;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 8px;
}
.stepDesc {
- font-size: 14px;
- color: var(--text-secondary);
- line-height: 1.5;
+ font-size: 14px;
+ color: var(--text-secondary);
+ line-height: 1.5;
}
.requestForm {
- max-width: 600px;
- margin: 0 auto;
- display: flex;
- gap: 16px;
+ max-width: 600px;
+ margin: 0 auto;
+ display: flex;
+ gap: 16px;
}
.input {
- flex: 1;
- background: var(--glass-highlight);
- border: 1px solid var(--glass-border);
- border-radius: 12px;
- padding: 12px 20px;
- color: var(--text-primary);
- outline: none;
- transition: all 0.2s;
+ flex: 1;
+ background: var(--glass-highlight);
+ border: 1px solid var(--glass-border);
+ border-radius: 12px;
+ padding: 12px 20px;
+ color: var(--text-primary);
+ outline: none;
+ transition: all 0.2s;
}
.input:focus {
- border-color: #00d4ff;
- background: var(--glass-highlight);
+ border-color: #00d4ff;
+ background: var(--glass-highlight);
}
@keyframes float {
- 0% {
- transform: translateY(0px);
- }
+ 0% {
+ transform: translateY(0px);
+ }
- 50% {
- transform: translateY(-20px);
- }
+ 50% {
+ transform: translateY(-20px);
+ }
- 100% {
- transform: translateY(0px);
- }
+ 100% {
+ transform: translateY(0px);
+ }
}
@media (max-width: 768px) {
- .title {
- font-size: 42px;
- }
-
- .requestForm {
- flex-direction: column;
- }
-}
\ No newline at end of file
+ .title {
+ font-size: 42px;
+ }
+
+ .requestForm {
+ flex-direction: column;
+ }
+}
diff --git a/src/app/translate/page.tsx b/src/app/translate/page.tsx
index a6ce18b3..18857c02 100644
--- a/src/app/translate/page.tsx
+++ b/src/app/translate/page.tsx
@@ -1,107 +1,205 @@
-"use client";
+'use client';
-import { Globe, Languages, MessageSquare, CheckCircle, Plus } from 'lucide-react';
+import {
+ Globe,
+ Languages,
+ MessageSquare,
+ CheckCircle,
+ Plus,
+} from 'lucide-react';
import Button from '@/components/ui/Button';
import styles from './Translate.module.css';
const languages = [
- { code: "es", name: "Spanish", native: "Español", progress: 100, contributors: 12, flag: "🇪🇸" },
- { code: "fr", name: "French", native: "Français", progress: 85, contributors: 8, flag: "🇫🇷" },
- { code: "de", name: "German", native: "Deutsch", progress: 92, contributors: 15, flag: "🇩🇪" },
- { code: "ja", name: "Japanese", native: "日本語", progress: 45, contributors: 6, flag: "🇯🇵" },
- { code: "pt", name: "Portuguese", native: "Português", progress: 78, contributors: 9, flag: "🇧🇷" },
- { code: "zh", name: "Chinese", native: "中文", progress: 30, contributors: 4, flag: "🇨🇳" },
- { code: "ru", name: "Russian", native: "Русский", progress: 60, contributors: 7, flag: "🇷🇺" },
- { code: "hi", name: "Hindi", native: "हिन्दी", progress: 15, contributors: 3, flag: "🇮🇳" },
+ {
+ code: 'es',
+ name: 'Spanish',
+ native: 'Español',
+ progress: 100,
+ contributors: 12,
+ flag: '🇪🇸',
+ },
+ {
+ code: 'fr',
+ name: 'French',
+ native: 'Français',
+ progress: 85,
+ contributors: 8,
+ flag: '🇫🇷',
+ },
+ {
+ code: 'de',
+ name: 'German',
+ native: 'Deutsch',
+ progress: 92,
+ contributors: 15,
+ flag: '🇩🇪',
+ },
+ {
+ code: 'ja',
+ name: 'Japanese',
+ native: '日本語',
+ progress: 45,
+ contributors: 6,
+ flag: '🇯🇵',
+ },
+ {
+ code: 'pt',
+ name: 'Portuguese',
+ native: 'Português',
+ progress: 78,
+ contributors: 9,
+ flag: '🇧🇷',
+ },
+ {
+ code: 'zh',
+ name: 'Chinese',
+ native: '中文',
+ progress: 30,
+ contributors: 4,
+ flag: '🇨🇳',
+ },
+ {
+ code: 'ru',
+ name: 'Russian',
+ native: 'Русский',
+ progress: 60,
+ contributors: 7,
+ flag: '🇷🇺',
+ },
+ {
+ code: 'hi',
+ name: 'Hindi',
+ native: 'हिन्दी',
+ progress: 15,
+ contributors: 3,
+ flag: '🇮🇳',
+ },
];
export default function TranslatePage() {
- return (
-
-
-
-
-
Help DevPath Speak Your Language
-
- Join our translation community and make DevPath accessible to developers worldwide.
-
-
}>
- Start Translating
-
-
-
-
- {languages.map((lang) => (
-
-
- {lang.flag}
- 50 ? styles.progress : styles.start
- }`}>
- {lang.progress === 100 ? 'Complete' :
- lang.progress > 0 ? 'In Progress' : 'Not Started'}
-
-
+ return (
+
+
+
+
+
Help DevPath Speak Your Language
+
+ Join our translation community and make DevPath accessible to
+ developers worldwide.
+
+
}
+ >
+ Start Translating
+
+
-
{lang.name}
-
{lang.native}
+
+ {languages.map((lang) => (
+
+
+ {lang.flag}
+ 50
+ ? styles.progress
+ : styles.start
+ }`}
+ >
+ {lang.progress === 100
+ ? 'Complete'
+ : lang.progress > 0
+ ? 'In Progress'
+ : 'Not Started'}
+
+
-
-
-
- {lang.progress}% translated
- {lang.contributors} contributors
-
-
+
{lang.name}
+
{lang.native}
-
Contribute
-
- ))}
+
+
-
-
-
How to Contribute
-
-
-
-
-
-
1. Choose Language
-
Select your native language from the list or request a new one.
-
-
-
-
-
-
2. Translate
-
Translate strings using our easy-to-use web interface.
-
-
-
-
-
-
3. Review
-
Vote on translations from others to ensure quality.
-
-
+
+ {lang.progress}% translated
+ {lang.contributors} contributors
+
-
-
Request a Language
-
- Don't see your language? Request it and we'll set it up!
-
-
-
- }>Request Language
-
-
+
+ Contribute
+
+
+ ))}
+
+
+
+
How to Contribute
+
+
+
+
+
+
1. Choose Language
+
+ Select your native language from the list or request a new one.
+
+
+
+
+
+
+
2. Translate
+
+ Translate strings using our easy-to-use web interface.
+
+
+
+
+
+
3. Review
+
+ Vote on translations from others to ensure quality.
+
+
+
+
+
+
+
Request a Language
+
+ Don't see your language? Request it and we'll set it up!
+
+
+
+ }
+ >
+ Request Language
+
+
- );
+
+
+ );
}
diff --git a/src/app/u/[uid]/page.tsx b/src/app/u/[uid]/page.tsx
index 1a32f521..5baeaa50 100644
--- a/src/app/u/[uid]/page.tsx
+++ b/src/app/u/[uid]/page.tsx
@@ -1,97 +1,107 @@
import type { Metadata } from 'next';
import ProfileClient from '../client';
import { db } from '@/lib/firebase';
-import { doc, getDoc, collection, query, where, getDocs } from 'firebase/firestore';
+import {
+ doc,
+ getDoc,
+ collection,
+ query,
+ where,
+ getDocs,
+} from 'firebase/firestore';
export const dynamicParams = false;
export async function generateStaticParams() {
- return [
- { uid: 'dummy' },
- { uid: 'kew7p1pbj7WoX66uGH2ZMcg79RB3' }
- ];
+ return [{ uid: 'dummy' }, { uid: 'kew7p1pbj7WoX66uGH2ZMcg79RB3' }];
}
-export async function generateMetadata(
- { params }: { params: Promise<{ uid: string }> }
-): Promise
{
- const { uid } = await params;
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ uid: string }>;
+}): Promise {
+ const { uid } = await params;
- let title = 'DevPath User Profile';
- let description = 'View a public DevPath community profile.';
- let imageUrl = '/DevPath-logo.webp';
+ let title = 'DevPath User Profile';
+ let description = 'View a public DevPath community profile.';
+ let imageUrl = '/DevPath-logo.webp';
- if (db) {
- try {
- let userDoc = await getDoc(doc(db, 'members', uid));
+ if (db) {
+ try {
+ let userDoc = await getDoc(doc(db, 'members', uid));
- if (!userDoc.exists()) {
- const q = query(collection(db, 'members'), where('uid', '==', uid));
- const querySnapshot = await getDocs(q);
- if (!querySnapshot.empty) {
- userDoc = querySnapshot.docs[0];
- }
- }
+ if (!userDoc.exists()) {
+ const q = query(collection(db, 'members'), where('uid', '==', uid));
+ const querySnapshot = await getDocs(q);
+ if (!querySnapshot.empty) {
+ userDoc = querySnapshot.docs[0];
+ }
+ }
- if (!userDoc.exists()) {
- userDoc = await getDoc(doc(db, 'admins', uid));
- if (!userDoc.exists()) {
- const q = query(collection(db, 'admins'), where('uid', '==', uid));
- const querySnapshot = await getDocs(q);
- if (!querySnapshot.empty) {
- userDoc = querySnapshot.docs[0];
- }
- }
- }
+ if (!userDoc.exists()) {
+ userDoc = await getDoc(doc(db, 'admins', uid));
+ if (!userDoc.exists()) {
+ const q = query(collection(db, 'admins'), where('uid', '==', uid));
+ const querySnapshot = await getDocs(q);
+ if (!querySnapshot.empty) {
+ userDoc = querySnapshot.docs[0];
+ }
+ }
+ }
- if (userDoc.exists()) {
- const data = userDoc.data();
- if (data?.privacySettings?.isPublic !== false) {
- title = data.name ? `${data.name} | DevPath Profile` : title;
- description = data.bio || `Check out ${data.name || 'this user'}'s profile on DevPath.`;
- if (data.photoURL) {
- imageUrl = data.photoURL;
- }
- }
- }
- } catch (error) {
- console.error("Error fetching user metadata:", error);
+ if (userDoc.exists()) {
+ const data = userDoc.data();
+ if (data?.privacySettings?.isPublic !== false) {
+ title = data.name ? `${data.name} | DevPath Profile` : title;
+ description =
+ data.bio ||
+ `Check out ${data.name || 'this user'}'s profile on DevPath.`;
+ if (data.photoURL) {
+ imageUrl = data.photoURL;
+ }
}
+ }
+ } catch (error) {
+ console.error('Error fetching user metadata:', error);
}
+ }
- return {
- title,
- description,
- alternates: {
- canonical: `/u/${uid}`,
- },
- openGraph: {
- title,
- description,
- url: `/u/${uid}`,
- images: [
- {
- url: imageUrl,
- width: 800,
- height: 600,
- alt: title,
- }
- ],
- type: 'profile',
- },
- twitter: {
- card: 'summary_large_image',
- title,
- description,
- images: [imageUrl],
+ return {
+ title,
+ description,
+ alternates: {
+ canonical: `/u/${uid}`,
+ },
+ openGraph: {
+ title,
+ description,
+ url: `/u/${uid}`,
+ images: [
+ {
+ url: imageUrl,
+ width: 800,
+ height: 600,
+ alt: title,
},
- };
+ ],
+ type: 'profile',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title,
+ description,
+ images: [imageUrl],
+ },
+ };
}
-export default async function UserProfilePage(
- { params }: { params: Promise<{ uid: string }> }
-) {
- const { uid } = await params;
+export default async function UserProfilePage({
+ params,
+}: {
+ params: Promise<{ uid: string }>;
+}) {
+ const { uid } = await params;
- return ;
+ return ;
}
diff --git a/src/app/u/client.tsx b/src/app/u/client.tsx
index 9e62e19b..bd60ade4 100644
--- a/src/app/u/client.tsx
+++ b/src/app/u/client.tsx
@@ -1,13 +1,55 @@
-"use client";
-const STATS_URL = process.env.NEXT_PUBLIC_GITHUB_STATS_URL ?? 'https://github-readme-stats-salesp07.vercel.app';
-const STREAK_URL = process.env.NEXT_PUBLIC_GITHUB_STREAK_URL ?? 'https://github-readme-streak-stats-salesp07.vercel.app';
+'use client';
+const STATS_URL =
+ process.env.NEXT_PUBLIC_GITHUB_STATS_URL ??
+ 'https://github-readme-stats-salesp07.vercel.app';
+const STREAK_URL =
+ process.env.NEXT_PUBLIC_GITHUB_STREAK_URL ??
+ 'https://github-readme-streak-stats-salesp07.vercel.app';
import { useEffect, useState, Suspense } from 'react';
import Image from 'next/image';
-import { doc, getDoc, collection, query, where, getDocs, orderBy, updateDoc, arrayUnion, arrayRemove } from 'firebase/firestore';
+import {
+ doc,
+ getDoc,
+ collection,
+ query,
+ where,
+ getDocs,
+ orderBy,
+ updateDoc,
+ arrayUnion,
+ arrayRemove,
+} from 'firebase/firestore';
import { db } from '@/lib/firebase';
import { getEmbedUrl } from '@/lib/utils';
import { teamMembers } from '@/data/team';
-import { Trophy, Flame, Star, Target, MapPin, Link as LinkIcon, Calendar, Phone, Github, Instagram, Linkedin, CheckCircle, User as UserIcon, Heart, Share2, Video, Image as ImageIcon, Globe, Check, Users, Shield, X, GitMerge, BookOpen, Plus, Code2 } from 'lucide-react';
+import {
+ Trophy,
+ Flame,
+ Star,
+ Target,
+ MapPin,
+ Link as LinkIcon,
+ Calendar,
+ Phone,
+ Github,
+ Instagram,
+ Linkedin,
+ CheckCircle,
+ User as UserIcon,
+ Heart,
+ Share2,
+ Video,
+ Image as ImageIcon,
+ Globe,
+ Check,
+ Users,
+ Shield,
+ X,
+ GitMerge,
+ BookOpen,
+ Plus,
+ Code2,
+} from 'lucide-react';
import styles from '@/components/profile/Profile.module.css';
import Rewards from '@/components/profile/Rewards';
import FollowButton from '@/components/profile/FollowButton';
@@ -23,953 +65,1203 @@ import { useNotification } from '@/context/NotificationContext';
import NotFoundView from '@/components/layout/NotFoundView';
interface PublicUser {
- id?: string;
- name: string;
- photoURL?: string;
+ id?: string;
+ name: string;
+ photoURL?: string;
+ bio?: string;
+ role?: string;
+ communityRoles?: string[];
+ displayRole?: string;
+ roleTasks?: string[];
+ city?: string;
+ state?: string;
+ district?: string;
+ mobile?: string;
+ email?: string;
+ github?: string;
+ linkedin?: string;
+ instagram?: string;
+ privacySettings?: {
+ showMobile: boolean;
+ showLocation: boolean;
+ showEmail: boolean;
+ showProjects?: boolean;
+ showRewards?: boolean;
+ isPublic?: boolean;
+ showInCommunity?: boolean;
+ };
+ createdAt?: any; // Can be string or Timestamp
+ followers?: string[];
+ following?: string[];
+ loginDates?: string[];
+ points?: number;
+ streak?: number;
+ achievements?: string[];
+ githubStats?: {
+ connected: boolean;
+ username?: string;
+ repos?: number;
+ totalStars?: number;
+ followers?: number;
+ following?: number;
bio?: string;
- role?: string;
- communityRoles?: string[];
- displayRole?: string;
- roleTasks?: string[];
- city?: string;
- state?: string;
- district?: string;
- mobile?: string;
- email?: string;
- github?: string;
- linkedin?: string;
- instagram?: string;
- privacySettings?: {
- showMobile: boolean;
- showLocation: boolean;
- showEmail: boolean;
- showProjects?: boolean;
- showRewards?: boolean;
- isPublic?: boolean;
- showInCommunity?: boolean;
- };
- createdAt?: any; // Can be string or Timestamp
- followers?: string[];
- following?: string[];
- loginDates?: string[];
- points?: number;
- streak?: number;
- achievements?: string[];
- githubStats?: {
- connected: boolean;
- username?: string;
- repos?: number;
- totalStars?: number;
- followers?: number;
- following?: number;
- bio?: string;
- company?: string;
- location?: string;
- createdAt?: string;
- recentActivity?: any[];
- linesAdded?: number;
- linesRemoved?: number;
- linesContributed?: number;
- contributions?: number;
- };
+ company?: string;
+ location?: string;
+ createdAt?: string;
+ recentActivity?: any[];
+ linesAdded?: number;
+ linesRemoved?: number;
+ linesContributed?: number;
+ contributions?: number;
+ };
}
interface Project {
- id: string;
- title: string;
- description: string;
- screenshots: string[];
- videoUrl?: string;
- websiteUrl?: string;
- skills?: string[];
- likes: string[];
- createdAt: any;
+ id: string;
+ title: string;
+ description: string;
+ screenshots: string[];
+ videoUrl?: string;
+ websiteUrl?: string;
+ skills?: string[];
+ likes: string[];
+ createdAt: any;
}
function ProfileContent({ uid }: { uid?: string }) {
- const { user: currentUser } = useAuth();
- const { showSuccess, showError } = useNotificationActions();
- const [user, setUser] = useState(null);
- const [projects, setProjects] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState('');
- const [showNotFound, setShowNotFound] = useState(false);
- const [activeTab, setActiveTab] = useState('Overview');
- const [copied, setCopied] = useState(false);
- const [selectedProject, setSelectedProject] = useState(null);
-
- // Helper to strip HTML for preview
- const stripHtml = (html: string) => {
- if (!html) return '';
- return html.replace(/<[^>]*>?/gm, '');
- };
+ const { user: currentUser } = useAuth();
+ const { showSuccess, showError } = useNotificationActions();
+ const [user, setUser] = useState(null);
+ const [projects, setProjects] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [showNotFound, setShowNotFound] = useState(false);
+ const [activeTab, setActiveTab] = useState('Overview');
+ const [copied, setCopied] = useState(false);
+ const [selectedProject, setSelectedProject] = useState(null);
+
+ // Helper to strip HTML for preview
+ const stripHtml = (html: string) => {
+ if (!html) return '';
+ return html.replace(/<[^>]*>?/gm, '');
+ };
+
+ useEffect(() => {
+ const fetchUserAndProjects = async () => {
+ // Extract uid from URL if not provided via prop
+ let currentUid = uid;
+ if (!currentUid) {
+ const parts = window.location.pathname.split('/');
+ currentUid = parts[parts.length - 1];
+ }
+
+ if (!currentUid || currentUid === 'u') {
+ setError('No user specified.');
+ setShowNotFound(false);
+ setLoading(false);
+ return;
+ }
+ if (
+ currentUid.length < 3 ||
+ currentUid.length > 128 ||
+ /[<>"']/.test(currentUid)
+ ) {
+ setError('Invalid user identifier.');
+ setShowNotFound(true);
+ setLoading(false);
+ return;
+ }
+ setLoading(true);
+ setError('');
+ setShowNotFound(false);
+
+ try {
+ let userData: PublicUser | undefined;
+ let userId = currentUid;
+
+ // 1. Try fetching by Document ID from 'members'
+ let userDoc = await getDoc(doc(db, 'members', currentUid));
+
+ // 2. If not found, try fetching by 'uid' field query in 'members'
+ if (!userDoc.exists()) {
+ const q = query(
+ collection(db, 'members'),
+ where('uid', '==', currentUid)
+ );
+ const querySnapshot = await getDocs(q);
+ if (!querySnapshot.empty) {
+ userDoc = querySnapshot.docs[0];
+ userId = userDoc.id;
+ }
+ }
- useEffect(() => {
- const fetchUserAndProjects = async () => {
- // Extract uid from URL if not provided via prop
- let currentUid = uid;
- if (!currentUid) {
- const parts = window.location.pathname.split('/');
- currentUid = parts[parts.length - 1];
+ let isFromAdminCollection = false;
+
+ // 3. If still not found, try 'admins' collection
+ if (!userDoc.exists()) {
+ userDoc = await getDoc(doc(db, 'admins', currentUid));
+ if (userDoc.exists()) {
+ isFromAdminCollection = true;
+ } else {
+ const q = query(
+ collection(db, 'admins'),
+ where('uid', '==', currentUid)
+ );
+ const querySnapshot = await getDocs(q);
+ if (!querySnapshot.empty) {
+ userDoc = querySnapshot.docs[0];
+ userId = userDoc.id;
+ isFromAdminCollection = true;
}
+ }
+ }
- if (!currentUid || currentUid === 'u') {
- setError('No user specified.');
- setShowNotFound(false);
- setLoading(false);
- return;
+ if (userDoc.exists()) {
+ userData = { id: userId, ...userDoc.data() } as PublicUser;
+
+ if (isFromAdminCollection) {
+ userData.role = 'admin';
+ }
+
+ // Privacy Check
+ if (
+ userData.privacySettings?.isPublic === false &&
+ currentUser?.uid !== userId
+ ) {
+ setError('This profile is private.');
+ setShowNotFound(true);
+ setLoading(false);
+ return;
+ }
+
+ // Check if user is an admin (if not already marked)
+ if (userData.role !== 'admin') {
+ // 1. Try by Email (if available)
+ if (userData.email) {
+ const adminCheck = await getDoc(
+ doc(db, 'admins', userData.email)
+ );
+ if (adminCheck.exists()) {
+ userData.role = 'admin';
+ }
}
- if (currentUid.length < 3 || currentUid.length > 128 || /[<>"']/.test(currentUid)) {
- setError('Invalid user identifier.');
- setShowNotFound(true);
- setLoading(false);
- return;
+ // 2. Try by UID field in admins collection (fallback)
+ if (userData.role !== 'admin') {
+ const q = query(
+ collection(db, 'admins'),
+ where('uid', '==', currentUid)
+ );
+ const querySnapshot = await getDocs(q);
+ if (!querySnapshot.empty) {
+ userData.role = 'admin';
+ }
}
- setLoading(true);
- setError('');
- setShowNotFound(false);
-
- try {
- let userData: PublicUser | undefined;
- let userId = currentUid;
-
- // 1. Try fetching by Document ID from 'members'
- let userDoc = await getDoc(doc(db, 'members', currentUid));
-
- // 2. If not found, try fetching by 'uid' field query in 'members'
- if (!userDoc.exists()) {
- const q = query(collection(db, 'members'), where('uid', '==', currentUid));
- const querySnapshot = await getDocs(q);
- if (!querySnapshot.empty) {
- userDoc = querySnapshot.docs[0];
- userId = userDoc.id;
- }
- }
-
- let isFromAdminCollection = false;
-
- // 3. If still not found, try 'admins' collection
- if (!userDoc.exists()) {
- userDoc = await getDoc(doc(db, 'admins', currentUid));
- if (userDoc.exists()) {
- isFromAdminCollection = true;
- } else {
- const q = query(collection(db, 'admins'), where('uid', '==', currentUid));
- const querySnapshot = await getDocs(q);
- if (!querySnapshot.empty) {
- userDoc = querySnapshot.docs[0];
- userId = userDoc.id;
- isFromAdminCollection = true;
- }
- }
- }
-
- if (userDoc.exists()) {
- userData = { id: userId, ...userDoc.data() } as PublicUser;
-
- if (isFromAdminCollection) {
- userData.role = 'admin';
- }
-
- // Privacy Check
- if (userData.privacySettings?.isPublic === false && currentUser?.uid !== userId) {
- setError('This profile is private.');
- setShowNotFound(true);
- setLoading(false);
- return;
- }
-
- // Check if user is an admin (if not already marked)
- if (userData.role !== 'admin') {
- // 1. Try by Email (if available)
- if (userData.email) {
- const adminCheck = await getDoc(doc(db, 'admins', userData.email));
- if (adminCheck.exists()) {
- userData.role = 'admin';
- }
- }
- // 2. Try by UID field in admins collection (fallback)
- if (userData.role !== 'admin') {
- const q = query(collection(db, 'admins'), where('uid', '==', currentUid));
- const querySnapshot = await getDocs(q);
- if (!querySnapshot.empty) {
- userData.role = 'admin';
- }
- }
- }
-
- // Check Team Members for Community Role (Robust Matching)
- const normalize = (s: string | undefined) => s?.toLowerCase().trim() || '';
-
- // 1. Try Strict Matching by Socials first (High Confidence)
- let teamMatches = teamMembers.filter(m =>
- (m.socials?.github && userData?.github && normalize(m.socials.github) === normalize(userData.github)) ||
- (m.socials?.linkedin && userData?.linkedin && normalize(m.socials.linkedin) === normalize(userData.linkedin))
- );
-
- const isHighConfidence = teamMatches.length > 0;
-
- // 2. Fallback to Name Matching ONLY if no social match found
- if (teamMatches.length === 0) {
- // For name-only matches, we filter out sensitive roles (Owner, Core Admin) to prevent impersonation
- teamMatches = teamMembers.filter(m =>
- normalize(m.name) === normalize(userData?.name) &&
- !['Owner', 'Core Admin'].includes(m.category)
- );
- }
-
- if (teamMatches.length > 0) {
- const roles = teamMatches.map(m => m.subRole ? `${m.role} - ${m.subRole}` : m.role);
- // Deduplicate
- const uniqueRoles = Array.from(new Set(roles));
-
- userData = {
- ...userData,
- communityRoles: uniqueRoles
- };
-
- // Force Admin role ONLY if matched via Socials (High Confidence) AND category is Owner/Core Admin
- if (isHighConfidence) {
- if (teamMatches.some(m => ['Owner', 'Core Admin'].includes(m.category))) {
- userData.role = 'admin';
- }
- }
- }
-
- // Fetch Role Details
- if ((userData as any).roleId) {
- const roleDoc = await getDoc(doc(db, 'roles', (userData as any).roleId));
- if (roleDoc.exists()) {
- const roleData = roleDoc.data();
- userData = {
- ...userData,
- displayRole: roleData.title,
- roleTasks: roleData.tasks
- };
- }
- }
- setUser(userData);
-
- // Fetch Projects (Root Collection Query for consistency)
- const projectsQuery = query(
- collection(db, 'projects'),
- where('userId', '==', userId),
- orderBy('createdAt', 'desc')
- );
-
- try {
- const projectsSnapshot = await getDocs(projectsQuery);
- const projectsData = projectsSnapshot.docs.map(doc => {
- const data = doc.data();
- return {
- id: doc.id,
- ...data,
- screenshots: data.screenshots || [],
- likes: data.likes || [],
- skills: data.skills || []
- } as Project;
- });
- setProjects(projectsData);
- } catch (err: any) {
- // Fallback for missing index
- if (err.message.includes("index")) {
- const q = query(collection(db, 'projects'), where('userId', '==', userId));
- const snapshot = await getDocs(q);
- const projectsData = snapshot.docs.map(doc => {
- const data = doc.data();
- return {
- id: doc.id,
- ...data,
- screenshots: data.screenshots || [],
- likes: data.likes || [],
- skills: data.skills || []
- } as Project;
- });
- projectsData.sort((a, b) => {
- const dateA = a.createdAt?.seconds ? a.createdAt.seconds : (new Date(a.createdAt || 0).getTime() / 1000);
- const dateB = b.createdAt?.seconds ? b.createdAt.seconds : (new Date(b.createdAt || 0).getTime() / 1000);
- return dateB - dateA;
- });
- setProjects(projectsData);
- }
- }
-
- } else {
- setError('User not found.');
- setShowNotFound(true);
- }
- } catch (err) {
- console.error("Error fetching user:", err);
- setError('Failed to load profile.');
- setShowNotFound(false);
- } finally {
- setLoading(false);
+ }
+
+ // Check Team Members for Community Role (Robust Matching)
+ const normalize = (s: string | undefined) =>
+ s?.toLowerCase().trim() || '';
+
+ // 1. Try Strict Matching by Socials first (High Confidence)
+ let teamMatches = teamMembers.filter(
+ (m) =>
+ (m.socials?.github &&
+ userData?.github &&
+ normalize(m.socials.github) === normalize(userData.github)) ||
+ (m.socials?.linkedin &&
+ userData?.linkedin &&
+ normalize(m.socials.linkedin) === normalize(userData.linkedin))
+ );
+
+ const isHighConfidence = teamMatches.length > 0;
+
+ // 2. Fallback to Name Matching ONLY if no social match found
+ if (teamMatches.length === 0) {
+ // For name-only matches, we filter out sensitive roles (Owner, Core Admin) to prevent impersonation
+ teamMatches = teamMembers.filter(
+ (m) =>
+ normalize(m.name) === normalize(userData?.name) &&
+ !['Owner', 'Core Admin'].includes(m.category)
+ );
+ }
+
+ if (teamMatches.length > 0) {
+ const roles = teamMatches.map((m) =>
+ m.subRole ? `${m.role} - ${m.subRole}` : m.role
+ );
+ // Deduplicate
+ const uniqueRoles = Array.from(new Set(roles));
+
+ userData = {
+ ...userData,
+ communityRoles: uniqueRoles,
+ };
+
+ // Force Admin role ONLY if matched via Socials (High Confidence) AND category is Owner/Core Admin
+ if (isHighConfidence) {
+ if (
+ teamMatches.some((m) =>
+ ['Owner', 'Core Admin'].includes(m.category)
+ )
+ ) {
+ userData.role = 'admin';
+ }
}
- };
-
- fetchUserAndProjects();
- }, [uid]);
-
- useEffect(() => {
- if (!uid) return;
- const link = document.createElement('link');
- link.rel = 'canonical';
- link.href = `${window.location.origin}/u/${uid}`;
- document.head.appendChild(link);
- return () => { link.remove(); };
- }, [uid]);
-
- const handleShareProfile = async () => {
- const profileUrl = `${window.location.origin}/u/${uid}`;
- const copiedSuccessfully = await copyToClipboard(profileUrl);
-
- if (copiedSuccessfully) {
- setCopied(true);
- setTimeout(() => setCopied(false), 3000);
- showSuccess('Profile link copied to clipboard.');
- } else {
- showError('Copying the profile link is not supported in this browser.');
- }
- };
-
- const handleLikeProject = async (projectId: string, currentLikes: string[]) => {
- if (!currentUser) {
- alert("Please login to like projects.");
- return;
- }
-
- const isLiked = currentLikes.includes(currentUser.uid);
- // Update Root Collection
- const projectRef = doc(db, 'projects', projectId);
-
- try {
- if (isLiked) {
- await updateDoc(projectRef, {
- likes: arrayRemove(currentUser.uid)
- });
- setProjects(prev => prev.map(p =>
- p.id === projectId ? { ...p, likes: p.likes.filter(id => id !== currentUser.uid) } : p
- ));
- } else {
- await updateDoc(projectRef, {
- likes: arrayUnion(currentUser.uid)
- });
- setProjects(prev => prev.map(p =>
- p.id === projectId ? { ...p, likes: [...p.likes, currentUser.uid] } : p
- ));
+ }
+
+ // Fetch Role Details
+ if ((userData as any).roleId) {
+ const roleDoc = await getDoc(
+ doc(db, 'roles', (userData as any).roleId)
+ );
+ if (roleDoc.exists()) {
+ const roleData = roleDoc.data();
+ userData = {
+ ...userData,
+ displayRole: roleData.title,
+ roleTasks: roleData.tasks,
+ };
}
- } catch (error) {
- console.error("Error updating like:", error);
- }
- };
-
- const handleShareProject = (projectId: string) => {
- const url = window.location.href;
- copyToClipboard(url).then((copiedSuccessfully) => {
- if (copiedSuccessfully) {
- showSuccess('Project link copied to clipboard.');
- } else {
- showError('Copying the project link is not supported in this browser.');
+ }
+ setUser(userData);
+
+ // Fetch Projects (Root Collection Query for consistency)
+ const projectsQuery = query(
+ collection(db, 'projects'),
+ where('userId', '==', userId),
+ orderBy('createdAt', 'desc')
+ );
+
+ try {
+ const projectsSnapshot = await getDocs(projectsQuery);
+ const projectsData = projectsSnapshot.docs.map((doc) => {
+ const data = doc.data();
+ return {
+ id: doc.id,
+ ...data,
+ screenshots: data.screenshots || [],
+ likes: data.likes || [],
+ skills: data.skills || [],
+ } as Project;
+ });
+ setProjects(projectsData);
+ } catch (err: any) {
+ // Fallback for missing index
+ if (err.message.includes('index')) {
+ const q = query(
+ collection(db, 'projects'),
+ where('userId', '==', userId)
+ );
+ const snapshot = await getDocs(q);
+ const projectsData = snapshot.docs.map((doc) => {
+ const data = doc.data();
+ return {
+ id: doc.id,
+ ...data,
+ screenshots: data.screenshots || [],
+ likes: data.likes || [],
+ skills: data.skills || [],
+ } as Project;
+ });
+ projectsData.sort((a, b) => {
+ const dateA = a.createdAt?.seconds
+ ? a.createdAt.seconds
+ : new Date(a.createdAt || 0).getTime() / 1000;
+ const dateB = b.createdAt?.seconds
+ ? b.createdAt.seconds
+ : new Date(b.createdAt || 0).getTime() / 1000;
+ return dateB - dateA;
+ });
+ setProjects(projectsData);
}
- });
+ }
+ } else {
+ setError('User not found.');
+ setShowNotFound(true);
+ }
+ } catch (err) {
+ console.error('Error fetching user:', err);
+ setError('Failed to load profile.');
+ setShowNotFound(false);
+ } finally {
+ setLoading(false);
+ }
};
- // Helper to safely format date
- const formatDate = (dateValue: any) => {
- if (!dateValue) return 'Dec 2023';
- if (typeof dateValue === 'string') return new Date(dateValue).toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
- if (dateValue.seconds) return new Date(dateValue.seconds * 1000).toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
- return 'Dec 2023';
+ fetchUserAndProjects();
+ }, [uid]);
+
+ useEffect(() => {
+ if (!uid) return;
+ const link = document.createElement('link');
+ link.rel = 'canonical';
+ link.href = `${window.location.origin}/u/${uid}`;
+ document.head.appendChild(link);
+ return () => {
+ link.remove();
};
-
- if (loading) {
- return (
-
- );
+ }, [uid]);
+
+ const handleShareProfile = async () => {
+ const profileUrl = `${window.location.origin}/u/${uid}`;
+ const copiedSuccessfully = await copyToClipboard(profileUrl);
+
+ if (copiedSuccessfully) {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 3000);
+ showSuccess('Profile link copied to clipboard.');
+ } else {
+ showError('Copying the profile link is not supported in this browser.');
+ }
+ };
+
+ const handleLikeProject = async (
+ projectId: string,
+ currentLikes: string[]
+ ) => {
+ if (!currentUser) {
+ alert('Please login to like projects.');
+ return;
}
- if (error || !user) {
- if (showNotFound) {
- return ;
- }
+ const isLiked = currentLikes.includes(currentUser.uid);
+ // Update Root Collection
+ const projectRef = doc(db, 'projects', projectId);
- return (
-
-
-
Profile Not Found
-
{error || "The user you are looking for does not exist."}
-
+ try {
+ if (isLiked) {
+ await updateDoc(projectRef, {
+ likes: arrayRemove(currentUser.uid),
+ });
+ setProjects((prev) =>
+ prev.map((p) =>
+ p.id === projectId
+ ? { ...p, likes: p.likes.filter((id) => id !== currentUser.uid) }
+ : p
+ )
+ );
+ } else {
+ await updateDoc(projectRef, {
+ likes: arrayUnion(currentUser.uid),
+ });
+ setProjects((prev) =>
+ prev.map((p) =>
+ p.id === projectId
+ ? { ...p, likes: [...p.likes, currentUser.uid] }
+ : p
+ )
);
+ }
+ } catch (error) {
+ console.error('Error updating like:', error);
}
+ };
+
+ const handleShareProject = (projectId: string) => {
+ const url = window.location.href;
+ copyToClipboard(url).then((copiedSuccessfully) => {
+ if (copiedSuccessfully) {
+ showSuccess('Project link copied to clipboard.');
+ } else {
+ showError('Copying the project link is not supported in this browser.');
+ }
+ });
+ };
+
+ // Helper to safely format date
+ const formatDate = (dateValue: any) => {
+ if (!dateValue) return 'Dec 2023';
+ if (typeof dateValue === 'string')
+ return new Date(dateValue).toLocaleDateString('en-US', {
+ month: 'short',
+ year: 'numeric',
+ });
+ if (dateValue.seconds)
+ return new Date(dateValue.seconds * 1000).toLocaleDateString('en-US', {
+ month: 'short',
+ year: 'numeric',
+ });
+ return 'Dec 2023';
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
- const showMobile = user.privacySettings?.showMobile;
- const showLocation = user.privacySettings?.showLocation ?? true;
- const safeSocialLinks = {
- github: getSafeSocialUrl(user.github, 'github'),
- linkedin: getSafeSocialUrl(user.linkedin, 'linkedin'),
- instagram: getSafeSocialUrl(user.instagram, 'instagram')
- };
+ if (error || !user) {
+ if (showNotFound) {
+ return ;
+ }
return (
-
-
-
-
-
-
- {user.photoURL ? (
-
- ) : (
-
- {user.name?.charAt(0) || 'U'}
-
- )}
-
+
+
+
Profile Not Found
+
+ {error || 'The user you are looking for does not exist.'}
+
+
+ );
+ }
+
+ const showMobile = user.privacySettings?.showMobile;
+ const showLocation = user.privacySettings?.showLocation ?? true;
+ const safeSocialLinks = {
+ github: getSafeSocialUrl(user.github, 'github'),
+ linkedin: getSafeSocialUrl(user.linkedin, 'linkedin'),
+ instagram: getSafeSocialUrl(user.instagram, 'instagram'),
+ };
+
+ return (
+
+
+
+
+
+
+ {user.photoURL ? (
+
+ ) : (
+
+ {user.name?.charAt(0) || 'U'}
+
+ )}
+
-
-
{user.name || 'User'}
-
- {(user.role === 'admin' || (user.communityRoles && user.communityRoles.some(r => r.includes('Owner') || r.includes('Core Admin')))) && (
-
- Community Admin
-
- )}
-
- {user.communityRoles && user.communityRoles.map((role, index) => (
-
- {role}
-
- ))}
-
-
-
- {(user.displayRole || user.role || 'MEMBER').toUpperCase()}
- {user.bio && • {user.bio} }
-
+
+
{user.name || 'User'}
+
+ {(user.role === 'admin' ||
+ (user.communityRoles &&
+ user.communityRoles.some(
+ (r) => r.includes('Owner') || r.includes('Core Admin')
+ ))) && (
+
+ Community Admin
+
+ )}
-
- {showLocation && (user.city || user.state) && (
-
-
- {[user.city, user.district, user.state].filter(Boolean).join(', ')}
-
- )}
- {showMobile && user.mobile && (
-
- {user.mobile}
-
- )}
-
- Joined {formatDate(user.createdAt)}
-
-
-
-
-
-
- {user.id && }
-
- {copied ? (
- <>
-
- Copied!
- >
- ) : (
- <>
-
- Share
- >
- )}
-
-
-
-
+ {user.communityRoles &&
+ user.communityRoles.map((role, index) => (
+
+ {role}
+
+ ))}
+
+
+
+
+ {(user.displayRole || user.role || 'MEMBER').toUpperCase()}
+
+ {user.bio && (
+ • {user.bio}
+ )}
+
+
+
+ {showLocation && (user.city || user.state) && (
+
+
+ {[user.city, user.district, user.state]
+ .filter(Boolean)
+ .join(', ')}
+
+ )}
+ {showMobile && user.mobile && (
+
+ {user.mobile}
+
+ )}
+
+ Joined {formatDate(user.createdAt)}
+
+
+
+
+
+
+ {user.id && (
+
+ )}
+
+ {copied ? (
+ <>
+
+ Copied!
+ >
+ ) : (
+ <>
+
+ Share
+ >
+ )}
+
+
+
+
+
+
+ {/* Stats Row */}
+
+
+
{user.points || 0}
+
+ Total XP
+
+
+
+
{user.streak || 0}
+
+ Day Streak
+
+
+
+
+ {user.followers?.length || 0}
+
+
+ Followers
+
+
+
+
+ {user.following?.length || 0}
+
+
+ Following
+
+
+
+
+ {/* GitHub Stats Section */}
+
+ {user.githubStats?.connected ? (
+
+
+ GitHub Activity
+
+
+
+
+
+ {user.githubStats.repos || 0}
+
+
+ Repositories
+
-
- {/* Stats Row */}
-
-
-
{user.points || 0}
-
Total XP
-
-
-
{user.streak || 0}
-
Day Streak
-
-
-
{user.followers?.length || 0}
-
Followers
-
-
-
{user.following?.length || 0}
-
Following
-
+
+
+
+ {user.githubStats.totalStars || 0}
+
+
+ Total Stars
+
-
- {/* GitHub Stats Section */}
-
- {user.githubStats?.connected ? (
-
-
- GitHub Activity
-
-
-
-
- {user.githubStats.repos || 0}
- Repositories
-
-
-
- {user.githubStats.totalStars || 0}
- Total Stars
-
-
-
- {user.githubStats.followers || 0}
- Followers
-
-
-
- {user.githubStats.following || 0}
- Following
-
-
-
- {(user.githubStats.linesContributed ?? GIT_FALLBACK_STATS[(user.githubStats.username || '').toLowerCase()]?.additions ?? 0).toLocaleString()}
- Lines Contributed
-
-
-
- {user.githubStats.contributions ?? GIT_FALLBACK_STATS[(user.githubStats.username || '').toLowerCase()]?.commits ?? 0}
- Commits Contributed
-
-
-
- {/* GitHub Readme Stats & Streak */}
- {user.githubStats.username && (
-
+
+
+
+ {user.githubStats.followers || 0}
+
+
+ Followers
+
+
+
+
+
+ {user.githubStats.following || 0}
+
+
+ Following
+
+
+
+
+
+ {(
+ user.githubStats.linesContributed ??
+ GIT_FALLBACK_STATS[
+ (user.githubStats.username || '').toLowerCase()
+ ]?.additions ??
+ 0
+ ).toLocaleString()}
+
+
+ Lines Contributed
+
+
+
+
+
+ {user.githubStats.contributions ??
+ GIT_FALLBACK_STATS[
+ (user.githubStats.username || '').toLowerCase()
+ ]?.commits ??
+ 0}
+
+
+ Commits Contributed
+
+
+
+
+ {/* GitHub Readme Stats & Streak */}
+ {user.githubStats.username && (
+
+ )}
+
+ {/* Recent Activity */}
+ {user.githubStats.recentActivity &&
+ user.githubStats.recentActivity.length > 0 && (
+
+
+ Recent Contributions
+
+
+ {user.githubStats.recentActivity.map((event: any) => (
+
+
+ {event.type === 'PushEvent' && (
+
)}
-
- {/* Recent Activity */}
- {user.githubStats.recentActivity && user.githubStats.recentActivity.length > 0 && (
-
-
Recent Contributions
-
- {user.githubStats.recentActivity.map((event: any) => (
-
-
- {event.type === 'PushEvent' &&
}
- {event.type === 'PullRequestEvent' &&
}
- {event.type === 'IssuesEvent' &&
}
- {event.type === 'CreateEvent' &&
}
- {event.type === 'WatchEvent' &&
}
-
-
-
-
- {event.type.replace('Event', '').replace(/([A-Z])/g, ' $1').trim()}
-
- {' '}on{' '}
-
- {event.repo.name}
-
-
-
- {new Date(event.created_at).toLocaleDateString()}
-
-
-
- ))}
-
-
+ {event.type === 'PullRequestEvent' && (
+
)}
+ {event.type === 'IssuesEvent' && (
+
+ )}
+ {event.type === 'CreateEvent' && (
+
+ )}
+ {event.type === 'WatchEvent' && (
+
+ )}
+
+
+
+
+ {event.type
+ .replace('Event', '')
+ .replace(/([A-Z])/g, ' $1')
+ .trim()}
+ {' '}
+ on{' '}
+
+ {event.repo.name}
+
+
+
+ {new Date(event.created_at).toLocaleDateString()}
+
+
- ) : (
-
-
-
GitHub Not Connected
-
This user hasn't connected their GitHub account yet.
-
- )}
-
-
- {user.roleTasks && user.roleTasks.length > 0 && (
-
-
-
- Role Responsibilities
-
-
- {user.roleTasks.map((task, index) => (
-
-
- {task}
-
- ))}
-
+ ))}
+
)}
-
-
-
setActiveTab('Overview')}
- >
- Overview
-
-
setActiveTab('Projects')}
- style={{ display: user.privacySettings?.showProjects === false ? 'none' : 'block' }}
- >
- Projects
-
-
setActiveTab('Achievements')}
- >
- Achievements
-
-
setActiveTab('Rewards')}
- style={{ display: user.privacySettings?.showRewards === false ? 'none' : 'block' }}
- >
- Rewards
-
+
+ ) : (
+
+
+
GitHub Not Connected
+
+ This user hasn't connected their GitHub account yet.
+
+
+ )}
+
+
+ {user.roleTasks && user.roleTasks.length > 0 && (
+
+
+
+ Role Responsibilities
+
+
+ {user.roleTasks.map((task, index) => (
+
+
+ {task}
-
- {activeTab === 'Overview' && (
-
- {/* Contribution Heatmap */}
-
-
Contribution Activity
-
-
-
-
-
- )}
-
- {activeTab === 'Projects' && (
-
- {projects.length === 0 ? (
-
-
-
No projects showcased yet.
-
+ ))}
+
+
+ )}
+
+
+
setActiveTab('Overview')}
+ >
+ Overview
+
+
setActiveTab('Projects')}
+ style={{
+ display:
+ user.privacySettings?.showProjects === false ? 'none' : 'block',
+ }}
+ >
+ Projects
+
+
setActiveTab('Achievements')}
+ >
+ Achievements
+
+
setActiveTab('Rewards')}
+ style={{
+ display:
+ user.privacySettings?.showRewards === false ? 'none' : 'block',
+ }}
+ >
+ Rewards
+
+
+
+ {activeTab === 'Overview' && (
+
+ {/* Contribution Heatmap */}
+
+
Contribution Activity
+
+
+
+ )}
+
+ {activeTab === 'Projects' && (
+
+ {projects.length === 0 ? (
+
+
+
No projects showcased yet.
+
+ ) : (
+ projects.map((project) => (
+
+ {/* Media Display: Screenshots OR Video */}
+
+ {project.videoUrl ? (
+
+ ) : (
+ <>
+ {project.screenshots.length > 0 ? (
+
) : (
- projects.map(project => (
-
- {/* Media Display: Screenshots OR Video */}
-
- {project.videoUrl ? (
-
- ) : (
- <>
- {project.screenshots.length > 0 ? (
-
- ) : (
-
-
-
- )}
- {project.screenshots.length > 1 && (
-
- +{project.screenshots.length - 1} more
-
- )}
- >
- )}
-
-
-
-
-
{project.title}
-
- {project.websiteUrl && (
-
-
-
- )}
-
-
-
-
-
{stripHtml(project.description)}
-
setSelectedProject(project)}
- className="text-primary text-xs font-medium hover:underline hover:opacity-80 transition-all duration-200 mt-1"
- >
- Read More
-
-
-
- {project.skills && project.skills.length > 0 && (
-
- {project.skills.slice(0, 3).map(skill => (
-
- {skill}
-
- ))}
- {project.skills.length > 3 && (
-
- +{project.skills.length - 3}
-
- )}
-
- )}
-
-
-
- handleLikeProject(project.id, project.likes)}
- className={`flex items-center gap-1.5 text-sm transition-all duration-200 ${currentUser && project.likes.includes(currentUser.uid) ? 'text-red-500' : 'text-muted-foreground hover:text-red-500 hover:scale-105'}`}
- >
-
- {project.likes.length}
-
- handleShareProject(project.id)}
- className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-primary transition-colors"
- >
-
- Share
-
-
-
- {formatDate(project.createdAt)}
-
-
-
-
- ))
+
+
+
)}
+ {project.screenshots.length > 1 && (
+
+ +{project.screenshots.length - 1} more
+
+ )}
+ >
+ )}
+
+
+
+
+
+ {project.title}
+
+
+ {project.websiteUrl && (
+
+
+
+ )}
+
- )}
- {activeTab === 'Achievements' && (
-
-
- {user.achievements && user.achievements.length > 0 ? (
- user.achievements.map((badgeId, index) => (
-
-
-
-
-
-
{badgeId.replace(/-/g, ' ')}
-
Earned Badge
-
-
- ))
- ) : (
-
-
-
No achievements yet.
-
- )}
-
-
- )}
- {activeTab === 'Rewards' && (
-
-
+
+
+
+ {stripHtml(project.description)}
+
+
setSelectedProject(project)}
+ className="text-primary text-xs font-medium hover:underline hover:opacity-80 transition-all duration-200 mt-1"
+ >
+ Read More
+
- )}
-
+ {project.skills && project.skills.length > 0 && (
+
+ {project.skills.slice(0, 3).map((skill) => (
+
+ {skill}
+
+ ))}
+ {project.skills.length > 3 && (
+
+ +{project.skills.length - 3}
+
+ )}
+
+ )}
- {/* Project Details Modal */}
- {
- selectedProject && (
-
setSelectedProject(null)}
- >
-
e.stopPropagation()}
+
+
+
+ handleLikeProject(project.id, project.likes)
+ }
+ className={`flex items-center gap-1.5 text-sm transition-all duration-200 ${currentUser && project.likes.includes(currentUser.uid) ? 'text-red-500' : 'text-muted-foreground hover:text-red-500 hover:scale-105'}`}
>
-
-
{selectedProject.title}
- setSelectedProject(null)}
- className="p-2 hover:bg-muted hover:scale-110 rounded-full transition-all duration-200"
- >
-
-
-
-
-
- {/* Media */}
- {selectedProject.videoUrl ? (
-
-
-
- ) : selectedProject.screenshots.length > 0 && (
-
-
-
- )}
-
- {/* Description */}
-
-
- {/* Links & Skills */}
-
- {selectedProject.websiteUrl && (
-
- Visit Website
-
- )}
-
- {selectedProject.skills?.map(skill => (
-
- {skill}
-
- ))}
-
-
-
-
+
+
{project.likes.length}
+
+
handleShareProject(project.id)}
+ className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-primary transition-colors"
+ >
+
+ Share
+
+
+
+ {formatDate(project.createdAt)}
+
+
+
+
+ ))
+ )}
+
+ )}
+ {activeTab === 'Achievements' && (
+
+
+ {user.achievements && user.achievements.length > 0 ? (
+ user.achievements.map((badgeId, index) => (
+
+
+
+
+
+
+ {badgeId.replace(/-/g, ' ')}
+
+
+ Earned Badge
+
+
+ ))
+ ) : (
+
+
+
No achievements yet.
+
+ )}
+
+
+ )}
+ {activeTab === 'Rewards' && (
+
+
+
+ )}
+
+
+ {/* Project Details Modal */}
+ {selectedProject && (
+
setSelectedProject(null)}
+ >
+
e.stopPropagation()}
+ >
+
+
+ {selectedProject.title}
+
+ setSelectedProject(null)}
+ className="p-2 hover:bg-muted hover:scale-110 rounded-full transition-all duration-200"
+ >
+
+
+
+
+
+ {/* Media */}
+ {selectedProject.videoUrl ? (
+
+
+
+ ) : (
+ selectedProject.screenshots.length > 0 && (
+
+
+
)
- }
-
- );
+ )}
+
+ {/* Description */}
+
+
+ {/* Links & Skills */}
+
+ {selectedProject.websiteUrl && (
+
+ Visit Website
+
+ )}
+
+ {selectedProject.skills?.map((skill) => (
+
+ {skill}
+
+ ))}
+
+
+
+
+
+ )}
+
+ );
}
function isValidUid(value: string): boolean {
- const trimmed = value.trim();
- if (!trimmed || trimmed.length < 3 || trimmed.length > 128) return false;
- if (/[<>"']/.test(trimmed)) return false;
- return true;
+ const trimmed = value.trim();
+ if (!trimmed || trimmed.length < 3 || trimmed.length > 128) return false;
+ if (/[<>"']/.test(trimmed)) return false;
+ return true;
}
function SearchParamsFallback() {
- const searchParams = useSearchParams();
- const pathname = usePathname();
- const rawUid = searchParams.get('uid') || (pathname.startsWith('/u/') ? pathname.slice(3).split('/')[0].split('?')[0] : null);
- const uid = rawUid?.trim() || null;
-
- if (!uid) {
- return (
-
-
-
No User Specified
-
Please provide a user ID to view a profile.
-
- );
- }
+ const searchParams = useSearchParams();
+ const pathname = usePathname();
+ const rawUid =
+ searchParams.get('uid') ||
+ (pathname.startsWith('/u/')
+ ? pathname.slice(3).split('/')[0].split('?')[0]
+ : null);
+ const uid = rawUid?.trim() || null;
+
+ if (!uid) {
+ return (
+
+
+
No User Specified
+
+ Please provide a user ID to view a profile.
+
+
+ );
+ }
- if (!isValidUid(uid)) {
- return
;
- }
+ if (!isValidUid(uid)) {
+ return
;
+ }
- return
;
+ return
;
}
export default function ProfileClient({ uid }: { uid?: string }) {
- if (uid) {
- return
;
- }
- return (
-
}>
-
-
- );
+ if (uid) {
+ return
;
+ }
+ return (
+
+
+
+ }
+ >
+
+
+ );
}
diff --git a/src/app/u/page.tsx b/src/app/u/page.tsx
index ea741ae1..ef05f5a8 100644
--- a/src/app/u/page.tsx
+++ b/src/app/u/page.tsx
@@ -1,5 +1,5 @@
import ProfileClient from './client';
export default function PublicProfilePage() {
- return
;
+ return
;
}
diff --git a/src/app/updater/Updater.module.css b/src/app/updater/Updater.module.css
index d11ddbd1..4385bee2 100644
--- a/src/app/updater/Updater.module.css
+++ b/src/app/updater/Updater.module.css
@@ -1,217 +1,217 @@
.container {
- padding-top: 120px;
- padding-bottom: 80px;
- min-height: 100vh;
- background: var(--bg-primary);
- padding-left: 24px;
- padding-right: 24px;
+ padding-top: 120px;
+ padding-bottom: 80px;
+ min-height: 100vh;
+ background: var(--bg-primary);
+ padding-left: 24px;
+ padding-right: 24px;
}
.content {
- max-width: 800px;
- margin: 0 auto;
+ max-width: 800px;
+ margin: 0 auto;
}
.statusCard {
- background: var(--card);
- border: 1px solid var(--glass-border);
- border-radius: 24px;
- padding: 40px;
- text-align: center;
- margin-bottom: 40px;
- position: relative;
- overflow: hidden;
+ background: var(--card);
+ border: 1px solid var(--glass-border);
+ border-radius: 24px;
+ padding: 40px;
+ text-align: center;
+ margin-bottom: 40px;
+ position: relative;
+ overflow: hidden;
}
.statusCard::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 4px;
- background: linear-gradient(90deg, #00d4ff, #9d4edd);
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: linear-gradient(90deg, #00d4ff, #9d4edd);
}
.version {
- font-size: 64px;
- font-weight: 800;
- color: var(--text-primary);
- margin-bottom: 8px;
- letter-spacing: -2px;
+ font-size: 64px;
+ font-weight: 800;
+ color: var(--text-primary);
+ margin-bottom: 8px;
+ letter-spacing: -2px;
}
.channel {
- display: inline-block;
- padding: 4px 12px;
- background: rgba(16, 185, 129, 0.1);
- color: #10b981;
- border-radius: 20px;
- font-size: 14px;
- font-weight: 600;
- margin-bottom: 24px;
- border: 1px solid rgba(16, 185, 129, 0.2);
+ display: inline-block;
+ padding: 4px 12px;
+ background: rgba(16, 185, 129, 0.1);
+ color: #10b981;
+ border-radius: 20px;
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 24px;
+ border: 1px solid rgba(16, 185, 129, 0.2);
}
.message {
- font-size: 18px;
- color: var(--text-secondary);
- margin-bottom: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
+ font-size: 18px;
+ color: var(--text-secondary);
+ margin-bottom: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
}
.settings {
- background: var(--glass-highlight);
- border-radius: 16px;
- padding: 24px;
- margin-top: 32px;
- text-align: left;
+ background: var(--glass-highlight);
+ border-radius: 16px;
+ padding: 24px;
+ margin-top: 32px;
+ text-align: left;
}
.settingRow {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 0;
- border-bottom: 1px solid var(--glass-border);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 0;
+ border-bottom: 1px solid var(--glass-border);
}
.settingRow:last-child {
- border-bottom: none;
+ border-bottom: none;
}
.settingLabel {
- font-weight: 500;
- color: var(--text-primary);
+ font-weight: 500;
+ color: var(--text-primary);
}
.timeline {
- position: relative;
- padding-left: 32px;
+ position: relative;
+ padding-left: 32px;
}
.timeline::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 2px;
- background: var(--glass-border);
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ background: var(--glass-border);
}
.release {
- margin-bottom: 40px;
- position: relative;
+ margin-bottom: 40px;
+ position: relative;
}
.releaseDot {
- position: absolute;
- left: -37px;
- top: 6px;
- width: 12px;
- height: 12px;
- background: var(--bg-primary);
- border: 2px solid #00d4ff;
- border-radius: 50%;
- z-index: 1;
+ position: absolute;
+ left: -37px;
+ top: 6px;
+ width: 12px;
+ height: 12px;
+ background: var(--bg-primary);
+ border: 2px solid #00d4ff;
+ border-radius: 50%;
+ z-index: 1;
}
.releaseHeader {
- display: flex;
- justify-content: space-between;
- align-items: baseline;
- margin-bottom: 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ margin-bottom: 12px;
}
.releaseVersion {
- font-size: 20px;
- font-weight: 700;
- color: var(--text-primary);
+ font-size: 20px;
+ font-weight: 700;
+ color: var(--text-primary);
}
.releaseDate {
- font-size: 14px;
- color: var(--text-secondary);
+ font-size: 14px;
+ color: var(--text-secondary);
}
.releaseNotes {
- background: var(--glass-highlight);
- border: 1px solid var(--glass-border);
- border-radius: 12px;
- padding: 20px;
+ background: var(--glass-highlight);
+ border: 1px solid var(--glass-border);
+ border-radius: 12px;
+ padding: 20px;
}
.noteList {
- list-style: none;
- padding: 0;
- margin: 0;
+ list-style: none;
+ padding: 0;
+ margin: 0;
}
.noteItem {
- margin-bottom: 8px;
- color: var(--text-secondary);
- font-size: 14px;
- display: flex;
- gap: 8px;
+ margin-bottom: 8px;
+ color: var(--text-secondary);
+ font-size: 14px;
+ display: flex;
+ gap: 8px;
}
.noteItem::before {
- content: '•';
- color: #00d4ff;
+ content: '•';
+ color: #00d4ff;
}
/* Toggle Switch */
.switch {
- position: relative;
- display: inline-block;
- width: 44px;
- height: 24px;
+ position: relative;
+ display: inline-block;
+ width: 44px;
+ height: 24px;
}
.switch input {
- opacity: 0;
- width: 0;
- height: 0;
+ opacity: 0;
+ width: 0;
+ height: 0;
}
.slider {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: var(--glass-highlight);
- transition: .4s;
- border-radius: 34px;
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: var(--glass-highlight);
+ transition: 0.4s;
+ border-radius: 34px;
}
.slider:before {
- position: absolute;
- content: "";
- height: 18px;
- width: 18px;
- left: 3px;
- bottom: 3px;
- background-color: white;
- transition: .4s;
- border-radius: 50%;
+ position: absolute;
+ content: '';
+ height: 18px;
+ width: 18px;
+ left: 3px;
+ bottom: 3px;
+ background-color: white;
+ transition: 0.4s;
+ border-radius: 50%;
}
-input:checked+.slider {
- background-color: #00d4ff;
+input:checked + .slider {
+ background-color: #00d4ff;
}
-input:checked+.slider:before {
- transform: translateX(20px);
+input:checked + .slider:before {
+ transform: translateX(20px);
}
@media (max-width: 768px) {
- .version {
- font-size: 48px;
- }
-}
\ No newline at end of file
+ .version {
+ font-size: 48px;
+ }
+}
diff --git a/src/app/updater/page.tsx b/src/app/updater/page.tsx
index 9685f64d..29b8e6df 100644
--- a/src/app/updater/page.tsx
+++ b/src/app/updater/page.tsx
@@ -1,6 +1,6 @@
-"use client";
+'use client';
-import { useEffect, useState } from "react";
+import { useEffect, useState } from 'react';
// Types
interface GitHubRelease {
@@ -26,41 +26,41 @@ interface ParsedRelease {
// ── Static fallback data ──────────────────────────────────────────────────────
const FALLBACK_RELEASES: ParsedRelease[] = [
{
- version: "v2.4.1",
- name: "v2.4.1",
- date: "December 14, 2025",
+ version: 'v2.4.1',
+ name: 'v2.4.1',
+ date: 'December 14, 2025',
changes: [
- "Added new Wiki documentation system",
- "Implemented Coming Soon badges for learning paths",
- "Performance improvements for dashboard rendering",
- "Fixed layout issues on mobile devices",
+ 'Added new Wiki documentation system',
+ 'Implemented Coming Soon badges for learning paths',
+ 'Performance improvements for dashboard rendering',
+ 'Fixed layout issues on mobile devices',
],
- url: "#",
+ url: '#',
prerelease: false,
},
{
- version: "v2.4.0",
- name: "v2.4.0",
- date: "December 10, 2025",
+ version: 'v2.4.0',
+ name: 'v2.4.0',
+ date: 'December 10, 2025',
changes: [
- "Launched new Gamification engine",
- "Added Real-time activity feed",
- "Redesigned User Profile page",
- "Introduced Dark Mode support",
+ 'Launched new Gamification engine',
+ 'Added Real-time activity feed',
+ 'Redesigned User Profile page',
+ 'Introduced Dark Mode support',
],
- url: "#",
+ url: '#',
prerelease: false,
},
{
- version: "v2.3.5",
- name: "v2.3.5",
- date: "November 28, 2025",
+ version: 'v2.3.5',
+ name: 'v2.3.5',
+ date: 'November 28, 2025',
changes: [
- "Hotfix for login authentication flow",
- "Updated dependency packages",
- "Minor UI tweaks to Navbar",
+ 'Hotfix for login authentication flow',
+ 'Updated dependency packages',
+ 'Minor UI tweaks to Navbar',
],
- url: "#",
+ url: '#',
prerelease: false,
},
];
@@ -74,8 +74,8 @@ async function fetchGitHubReleases(
const response = await fetch(url, {
headers: {
- Accept: "application/vnd.github+json",
- "X-GitHub-Api-Version": "2022-11-28",
+ Accept: 'application/vnd.github+json',
+ 'X-GitHub-Api-Version': '2022-11-28',
},
next: { revalidate: 3600 }, // cache for 1 hour (Next.js)
});
@@ -93,10 +93,10 @@ async function fetchGitHubReleases(
.map((r) => ({
version: r.tag_name,
name: r.name || r.tag_name,
- date: new Date(r.published_at).toLocaleDateString("en-US", {
- year: "numeric",
- month: "long",
- day: "numeric",
+ date: new Date(r.published_at).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
}),
// Parse markdown bullet list from release body
changes: parseReleaseBody(r.body),
@@ -107,9 +107,9 @@ async function fetchGitHubReleases(
// ── Parse markdown bullet points from release body ────────────────────────────
function parseReleaseBody(body: string | null): string[] {
- if (!body) return ["No release notes provided."];
+ if (!body) return ['No release notes provided.'];
- const lines = body.split("\n");
+ const lines = body.split('\n');
const bullets: string[] = [];
for (const line of lines) {
@@ -125,9 +125,9 @@ function parseReleaseBody(body: string | null): string[] {
if (bullets.length === 0) {
const nonEmpty = lines
.map((l) => l.trim())
- .filter((l) => l.length > 0 && !l.startsWith("#"))
+ .filter((l) => l.length > 0 && !l.startsWith('#'))
.slice(0, 3);
- return nonEmpty.length > 0 ? nonEmpty : ["See release notes on GitHub."];
+ return nonEmpty.length > 0 ? nonEmpty : ['See release notes on GitHub.'];
}
return bullets;
@@ -146,7 +146,11 @@ function ReleaseSkeleton() {
{[1, 2, 3].map((j) => (
-
+
))}
@@ -164,10 +168,7 @@ function ReleaseCard({
index: number;
}) {
return (
-
+
@@ -207,8 +208,8 @@ export default function UpdaterPage() {
const [isLiveData, setIsLiveData] = useState(false);
// ── Change these to your actual repo ────────────────────────────────────────
- const GITHUB_OWNER = "devpathindcommunity-india";
- const GITHUB_REPO = "DevPath-Web";
+ const GITHUB_OWNER = 'devpathindcommunity-india';
+ const GITHUB_REPO = 'DevPath-Web';
// ────────────────────────────────────────────────────────────────────────────
useEffect(() => {
@@ -234,12 +235,12 @@ export default function UpdaterPage() {
} catch (err) {
if (!cancelled) {
const message =
- err instanceof Error ? err.message : "Unknown error occurred";
+ err instanceof Error ? err.message : 'Unknown error occurred';
// Check if it's a rate limit error
- if (message.includes("403") || message.includes("429")) {
+ if (message.includes('403') || message.includes('429')) {
setError(
- "GitHub API rate limit reached. Showing cached release data."
+ 'GitHub API rate limit reached. Showing cached release data.'
);
} else {
setError(`Could not fetch live data: ${message}`);
@@ -685,7 +686,6 @@ export default function UpdaterPage() {
-
{/* ── Header ── */}
Release History
@@ -695,9 +695,11 @@ export default function UpdaterPage() {
{!loading && (
-
+
- {isLiveData ? "Live from GitHub" : "Showing cached data"}
+ {isLiveData ? 'Live from GitHub' : 'Showing cached data'}
)}
@@ -707,7 +709,12 @@ export default function UpdaterPage() {
rel="noopener noreferrer"
className="github-link"
>
-
+
View on GitHub
@@ -737,7 +744,11 @@ export default function UpdaterPage() {
) : (
{releases.map((release, i) => (
-
+
))}
)}
@@ -745,7 +756,7 @@ export default function UpdaterPage() {
{/* ── Footer ── */}
{!loading && releases.length > 0 && (
- {releases.length} release{releases.length !== 1 ? "s" : ""} •{" "}
+ {releases.length} release{releases.length !== 1 ? 's' : ''} •{' '}
)}
-
>
diff --git a/src/app/wiki/Wiki.module.css b/src/app/wiki/Wiki.module.css
index 09eadc7a..0d1718cb 100644
--- a/src/app/wiki/Wiki.module.css
+++ b/src/app/wiki/Wiki.module.css
@@ -1,240 +1,240 @@
.container {
- padding-top: 96px;
- min-height: 100vh;
- background: var(--bg-primary);
- display: flex;
+ padding-top: 96px;
+ min-height: 100vh;
+ background: var(--bg-primary);
+ display: flex;
}
.sidebar {
- width: 300px;
- background: var(--glass-bg);
- border-right: 1px solid var(--glass-border);
- padding: 24px;
- position: fixed;
- top: 96px;
- bottom: 0;
- overflow-y: auto;
- backdrop-filter: blur(10px);
+ width: 300px;
+ background: var(--glass-bg);
+ border-right: 1px solid var(--glass-border);
+ padding: 24px;
+ position: fixed;
+ top: 96px;
+ bottom: 0;
+ overflow-y: auto;
+ backdrop-filter: blur(10px);
}
.searchContainer {
- margin-bottom: 24px;
- position: relative;
+ margin-bottom: 24px;
+ position: relative;
}
.searchInput {
- width: 100%;
- background: var(--glass-highlight);
- border: 1px solid var(--glass-border);
- border-radius: 8px;
- padding: 10px 36px 10px 40px;
- color: var(--text-primary);
- font-size: 14px;
- outline: none;
- transition: all 0.2s;
+ width: 100%;
+ background: var(--glass-highlight);
+ border: 1px solid var(--glass-border);
+ border-radius: 8px;
+ padding: 10px 36px 10px 40px;
+ color: var(--text-primary);
+ font-size: 14px;
+ outline: none;
+ transition: all 0.2s;
}
.searchInput:focus {
- border-color: #00d4ff;
- background: var(--glass-highlight);
+ border-color: #00d4ff;
+ background: var(--glass-highlight);
}
.searchIcon {
- position: absolute;
- left: 12px;
- top: 50%;
- transform: translateY(-50%);
- color: var(--text-secondary);
- pointer-events: none;
+ position: absolute;
+ left: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-secondary);
+ pointer-events: none;
}
/* Clear (X) button inside the search input */
.clearSearch {
- position: absolute;
- right: 10px;
- top: 50%;
- transform: translateY(-50%);
- background: none;
- border: none;
- color: var(--text-secondary);
- cursor: pointer;
- padding: 2px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- transition: color 0.15s;
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 2px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ transition: color 0.15s;
}
.clearSearch:hover {
- color: var(--text-primary);
+ color: var(--text-primary);
}
.category {
- margin-bottom: 24px;
+ margin-bottom: 24px;
}
.categoryTitle {
- font-size: 12px;
- text-transform: uppercase;
- color: var(--text-secondary);
- letter-spacing: 1px;
- margin-bottom: 12px;
- font-weight: 600;
+ font-size: 12px;
+ text-transform: uppercase;
+ color: var(--text-secondary);
+ letter-spacing: 1px;
+ margin-bottom: 12px;
+ font-weight: 600;
}
.navLink {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 12px;
- color: var(--text-secondary);
- text-decoration: none;
- border-radius: 6px;
- transition: all 0.2s;
- font-size: 14px;
- cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ color: var(--text-secondary);
+ text-decoration: none;
+ border-radius: 6px;
+ transition: all 0.2s;
+ font-size: 14px;
+ cursor: pointer;
}
.navLink:hover {
- color: var(--text-primary);
- background: var(--glass-highlight);
+ color: var(--text-primary);
+ background: var(--glass-highlight);
}
.navLink.active {
- color: white;
- background: linear-gradient(90deg, rgba(0, 212, 255, 0.1), transparent);
- border-left: 2px solid #00d4ff;
+ color: white;
+ background: linear-gradient(90deg, rgba(0, 212, 255, 0.1), transparent);
+ border-left: 2px solid #00d4ff;
}
.content {
- margin-left: 300px;
- flex: 1;
- padding: 40px 60px;
- max-width: 1000px;
+ margin-left: 300px;
+ flex: 1;
+ padding: 40px 60px;
+ max-width: 1000px;
}
.breadcrumb {
- display: flex;
- gap: 8px;
- color: var(--text-secondary);
- font-size: 14px;
- margin-bottom: 24px;
+ display: flex;
+ gap: 8px;
+ color: var(--text-secondary);
+ font-size: 14px;
+ margin-bottom: 24px;
}
.articleHeader {
- margin-bottom: 40px;
- border-bottom: 1px solid var(--glass-border);
- padding-bottom: 24px;
+ margin-bottom: 40px;
+ border-bottom: 1px solid var(--glass-border);
+ padding-bottom: 24px;
}
.title {
- font-size: 42px;
- font-weight: 700;
- color: var(--text-primary);
- margin-bottom: 16px;
+ font-size: 42px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 16px;
}
.meta {
- display: flex;
- gap: 24px;
- color: var(--text-secondary);
- font-size: 14px;
+ display: flex;
+ gap: 24px;
+ color: var(--text-secondary);
+ font-size: 14px;
}
.articleBody {
- color: var(--text-secondary);
- line-height: 1.8;
- font-size: 16px;
+ color: var(--text-secondary);
+ line-height: 1.8;
+ font-size: 16px;
}
.articleBody h2 {
- font-size: 24px;
- color: var(--text-primary);
- margin: 40px 0 20px;
+ font-size: 24px;
+ color: var(--text-primary);
+ margin: 40px 0 20px;
}
.articleBody h3 {
- font-size: 20px;
- color: var(--text-primary);
- margin: 30px 0 16px;
+ font-size: 20px;
+ color: var(--text-primary);
+ margin: 30px 0 16px;
}
.articleBody p {
- margin-bottom: 20px;
+ margin-bottom: 20px;
}
.articleBody ul {
- margin-bottom: 20px;
- padding-left: 24px;
+ margin-bottom: 20px;
+ padding-left: 24px;
}
.articleBody li {
- margin-bottom: 8px;
+ margin-bottom: 8px;
}
.codeBlock {
- background: #0d1117;
- border: 1px solid var(--glass-border);
- border-radius: 8px;
- padding: 20px;
- margin: 24px 0;
- font-family: 'Fira Code', monospace;
- overflow-x: auto;
- position: relative;
+ background: #0d1117;
+ border: 1px solid var(--glass-border);
+ border-radius: 8px;
+ padding: 20px;
+ margin: 24px 0;
+ font-family: 'Fira Code', monospace;
+ overflow-x: auto;
+ position: relative;
}
.copyButton {
- position: absolute;
- top: 12px;
- right: 12px;
- background: rgba(255, 255, 255, 0.1);
- border: none;
- border-radius: 4px;
- padding: 4px 8px;
- color: var(--text-secondary);
- cursor: pointer;
- font-size: 12px;
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ background: rgba(255, 255, 255, 0.1);
+ border: none;
+ border-radius: 4px;
+ padding: 4px 8px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 12px;
}
.feedback {
- margin-top: 60px;
- padding-top: 40px;
- border-top: 1px solid var(--glass-border);
- display: flex;
- justify-content: space-between;
- align-items: center;
+ margin-top: 60px;
+ padding-top: 40px;
+ border-top: 1px solid var(--glass-border);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
.feedbackButtons {
- display: flex;
- gap: 12px;
+ display: flex;
+ gap: 12px;
}
@media (max-width: 1024px) {
- .sidebar {
- display: none;
- }
+ .sidebar {
+ display: none;
+ }
- .content {
- margin-left: 0;
- padding: 24px;
- }
+ .content {
+ margin-left: 0;
+ padding: 24px;
+ }
}
.highlight {
- background-color: transparent;
- color: #00f5ff;
- text-shadow: 0 0 8px #00f5ff;
- border-radius: 2px;
- padding: 0 1px;
- display: inline;
- font-style: normal;
+ background-color: transparent;
+ color: #00f5ff;
+ text-shadow: 0 0 8px #00f5ff;
+ border-radius: 2px;
+ padding: 0 1px;
+ display: inline;
+ font-style: normal;
}
.noResults {
- color: var(--text-secondary);
- font-size: 14px;
- text-align: center;
- padding: 24px 0;
-}
\ No newline at end of file
+ color: var(--text-secondary);
+ font-size: 14px;
+ text-align: center;
+ padding: 24px 0;
+}
diff --git a/src/app/wiki/WikiSearchResults.module.css b/src/app/wiki/WikiSearchResults.module.css
index 8de68848..3f07934d 100644
--- a/src/app/wiki/WikiSearchResults.module.css
+++ b/src/app/wiki/WikiSearchResults.module.css
@@ -1,163 +1,163 @@
.wrapper {
- padding: 8px 0;
+ padding: 8px 0;
}
.resultCount {
- font-size: 13px;
- color: var(--text-secondary);
- margin-bottom: 16px;
- padding: 0 4px;
+ font-size: 13px;
+ color: var(--text-secondary);
+ margin-bottom: 16px;
+ padding: 0 4px;
}
.resultCount strong {
- color: var(--text-primary);
+ color: var(--text-primary);
}
.list {
- display: flex;
- flex-direction: column;
- gap: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
}
.card {
- width: 100%;
- text-align: left;
- background: var(--glass-highlight);
- border: 1px solid var(--glass-border);
- border-radius: 10px;
- padding: 16px;
- cursor: pointer;
- transition: all 0.2s ease;
- position: relative;
- overflow: hidden;
+ width: 100%;
+ text-align: left;
+ background: var(--glass-highlight);
+ border: 1px solid var(--glass-border);
+ border-radius: 10px;
+ padding: 16px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ position: relative;
+ overflow: hidden;
}
.card::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 3px;
- background: transparent;
- transition: background 0.2s ease;
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background: transparent;
+ transition: background 0.2s ease;
}
.card:hover {
- border-color: rgba(0, 212, 255, 0.35);
- transform: translateY(-1px);
- box-shadow: 0 4px 20px rgba(0, 212, 255, 0.08);
+ border-color: rgba(0, 212, 255, 0.35);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 20px rgba(0, 212, 255, 0.08);
}
.card:hover::before {
- background: #00d4ff;
+ background: #00d4ff;
}
.cardTop {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 6px;
}
.category {
- font-size: 11px;
- text-transform: uppercase;
- letter-spacing: 0.8px;
- color: #00d4ff;
- font-weight: 600;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+ color: #00d4ff;
+ font-weight: 600;
}
.arrow {
- color: var(--text-secondary);
- opacity: 0;
- transform: translateX(-4px);
- transition: all 0.2s ease;
+ color: var(--text-secondary);
+ opacity: 0;
+ transform: translateX(-4px);
+ transition: all 0.2s ease;
}
.card:hover .arrow {
- opacity: 1;
- transform: translateX(0);
+ opacity: 1;
+ transform: translateX(0);
}
.cardTitle {
- font-size: 15px;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 6px;
- line-height: 1.4;
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 6px;
+ line-height: 1.4;
}
.cardDesc {
- font-size: 13px;
- color: var(--text-secondary);
- line-height: 1.6;
- margin-bottom: 10px;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
+ font-size: 13px;
+ color: var(--text-secondary);
+ line-height: 1.6;
+ margin-bottom: 10px;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
}
.tags {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 6px;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px;
}
.tagIcon {
- color: var(--text-secondary);
- opacity: 0.6;
- flex-shrink: 0;
+ color: var(--text-secondary);
+ opacity: 0.6;
+ flex-shrink: 0;
}
.tag {
- font-size: 11px;
- background: rgba(0, 212, 255, 0.08);
- border: 1px solid rgba(0, 212, 255, 0.18);
- color: var(--text-secondary);
- border-radius: 4px;
- padding: 2px 7px;
+ font-size: 11px;
+ background: rgba(0, 212, 255, 0.08);
+ border: 1px solid rgba(0, 212, 255, 0.18);
+ color: var(--text-secondary);
+ border-radius: 4px;
+ padding: 2px 7px;
}
/* The highlight mark */
.highlight {
- background: rgba(0, 212, 255, 0.22);
- color: #00d4ff;
- border-radius: 2px;
- padding: 0 1px;
- font-style: normal;
+ background: rgba(0, 212, 255, 0.22);
+ color: #00d4ff;
+ border-radius: 2px;
+ padding: 0 1px;
+ font-style: normal;
}
/* Empty state */
.empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 16px;
- gap: 10px;
- text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 40px 16px;
+ gap: 10px;
+ text-align: center;
}
.emptyIcon {
- color: var(--text-secondary);
- opacity: 0.4;
- margin-bottom: 4px;
+ color: var(--text-secondary);
+ opacity: 0.4;
+ margin-bottom: 4px;
}
.empty p {
- font-size: 14px;
- color: var(--text-primary);
- margin: 0;
+ font-size: 14px;
+ color: var(--text-primary);
+ margin: 0;
}
.empty strong {
- color: #00d4ff;
+ color: #00d4ff;
}
.empty span {
- font-size: 13px;
- color: var(--text-secondary);
+ font-size: 13px;
+ color: var(--text-secondary);
}
diff --git a/src/app/wiki/WikiSearchResults.tsx b/src/app/wiki/WikiSearchResults.tsx
index a4e7d704..4abb56e9 100644
--- a/src/app/wiki/WikiSearchResults.tsx
+++ b/src/app/wiki/WikiSearchResults.tsx
@@ -1,84 +1,87 @@
-"use client";
+'use client';
-import React from "react";
-import { SearchResult, highlightText } from "@/utils/wikiSearch";
-import { FileText, Tag, ArrowRight } from "lucide-react";
-import styles from "./WikiSearchResults.module.css";
+import React from 'react';
+import { SearchResult, highlightText } from '@/utils/wikiSearch';
+import { FileText, Tag, ArrowRight } from 'lucide-react';
+import styles from './WikiSearchResults.module.css';
type Props = {
- results: SearchResult[];
- query: string;
- onSelect: (id: string) => void;
+ results: SearchResult[];
+ query: string;
+ onSelect: (id: string) => void;
};
function Highlight({ text, query }: { text: string; query: string }) {
- const parts = highlightText(text, query);
- return (
- <>
- {parts.map((part, i) =>
- part.highlight ? (
-
- {part.text}
-
- ) : (
-
{part.text}
- )
- )}
- >
- );
+ const parts = highlightText(text, query);
+ return (
+ <>
+ {parts.map((part, i) =>
+ part.highlight ? (
+
+ {part.text}
+
+ ) : (
+
{part.text}
+ )
+ )}
+ >
+ );
}
export default function WikiSearchResults({ results, query, onSelect }: Props) {
- if (results.length === 0) {
- return (
-
-
-
No articles found for “{query}”
-
Try different keywords or browse the sidebar.
-
- );
- }
-
+ if (results.length === 0) {
return (
-
-
- {results.length} article{results.length !== 1 ? "s" : ""} found for{" "}
- “{query}”
-
+
+
+
+ No articles found for “{query}”
+
+
Try different keywords or browse the sidebar.
+
+ );
+ }
-
- {results.map(result => (
-
onSelect(result.id)}
- >
-
+ return (
+
+
+ {results.length} article{results.length !== 1 ? 's' : ''} found for{' '}
+ “{query}”
+
-
-
-
+
+ {results.map((result) => (
+
onSelect(result.id)}
+ >
+
+
+
+
+
-
-
-
+
+
+
- {result.keywordMatches.length > 0 && (
-
-
- {result.keywordMatches.slice(0, 4).map(kw => (
-
-
-
- ))}
-
- )}
-
+ {result.keywordMatches.length > 0 && (
+
+
+ {result.keywordMatches.slice(0, 4).map((kw) => (
+
+
+
))}
-
-
- );
+
+ )}
+
+ ))}
+
+
+ );
}
diff --git a/src/app/wiki/error.tsx b/src/app/wiki/error.tsx
index 5fa8c5df..b0006be9 100644
--- a/src/app/wiki/error.tsx
+++ b/src/app/wiki/error.tsx
@@ -6,60 +6,61 @@ import { AlertTriangle, RotateCcw, Home } from 'lucide-react';
import Link from 'next/link';
export default function WikiError({
- error,
- reset,
+ error,
+ reset,
}: {
- error: Error & { digest?: string };
- reset: () => void;
+ error: Error & { digest?: string };
+ reset: () => void;
}) {
- useEffect(() => {
- console.error('Wiki page error:', error);
- }, [error]);
+ useEffect(() => {
+ console.error('Wiki page error:', error);
+ }, [error]);
- return (
-
-
+ return (
+
+
-
-
+
+
-
- Wiki Unavailable
-
+
+ Wiki Unavailable
+
-
+
-
- {error.message || "We couldn't load the wiki content. Please try again."}
-
+
+ {error.message ||
+ "We couldn't load the wiki content. Please try again."}
+
-
- }
- className="bg-destructive hover:bg-destructive/90 text-white rounded-2xl px-6 py-4"
- onClick={() => reset()}
- >
- Try Again
-
-
- }
- className="rounded-2xl px-6 py-4 border-white/20 hover:bg-white/5"
- >
- Return Home
-
-
-
-
+
+ }
+ className="bg-destructive hover:bg-destructive/90 text-white rounded-2xl px-6 py-4"
+ onClick={() => reset()}
+ >
+ Try Again
+
+
+ }
+ className="rounded-2xl px-6 py-4 border-white/20 hover:bg-white/5"
+ >
+ Return Home
+
+
- );
+
+
+ );
}
diff --git a/src/app/wiki/loading.tsx b/src/app/wiki/loading.tsx
index 99fce93b..a512d07b 100644
--- a/src/app/wiki/loading.tsx
+++ b/src/app/wiki/loading.tsx
@@ -1,41 +1,49 @@
export default function WikiLoading() {
- return (
-
- {/* Sidebar */}
-
+ return (
+
+ {/* Sidebar */}
+
- {/* Article content */}
-
- {/* Search bar */}
-
+ {/* Article content */}
+
+ {/* Search bar */}
+
- {/* Article title */}
-
-
+ {/* Article title */}
+
+
- {/* Article body paragraphs */}
-
- {[100, 90, 95, 70, 85].map((w, i) => (
-
- ))}
-
-
-
- {[88, 75, 93, 60].map((w, i) => (
-
- ))}
-
-
+ {/* Article body paragraphs */}
+
+ {[100, 90, 95, 70, 85].map((w, i) => (
+
+ ))}
- );
+
+
+ {[88, 75, 93, 60].map((w, i) => (
+
+ ))}
+
+
+
+ );
}
diff --git a/src/app/wiki/page.tsx b/src/app/wiki/page.tsx
index 5363aabb..4d00cd1d 100644
--- a/src/app/wiki/page.tsx
+++ b/src/app/wiki/page.tsx
@@ -1,22 +1,22 @@
-"use client";
+'use client';
import React, { Suspense, useEffect, useMemo, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import {
- Search,
- ChevronRight,
- Book,
- Code,
- FileText,
- HelpCircle,
- ThumbsUp,
- ThumbsDown,
- Github,
- Users,
- MapPin,
- MessageCircle,
- Calendar,
- X,
+ Search,
+ ChevronRight,
+ Book,
+ Code,
+ FileText,
+ HelpCircle,
+ ThumbsUp,
+ ThumbsDown,
+ Github,
+ Users,
+ MapPin,
+ MessageCircle,
+ Calendar,
+ X,
} from 'lucide-react';
import Fuse from 'fuse.js';
import Button from '@/components/ui/Button';
@@ -29,279 +29,367 @@ import { copyToClipboard } from '@/lib/clipboard';
import { useNotificationActions } from '@/stores/ui-store';
const categories = [
- {
- title: 'Getting Started',
- items: [
- { id: 'intro', title: 'Introduction to DevPath', icon:
},
- { id: 'setup', title: 'Setting Up Your Profile', icon:
},
- { id: 'xp', title: 'Understanding XP System', icon:
},
- ],
- },
- {
- title: 'Learning Paths',
- items: [
- { id: 'react', title: 'Full Stack React Guide', icon:
},
- { id: 'python', title: 'Python for AI Roadmap', icon:
},
- ],
- },
- {
- title: 'Community',
- items: [
- { id: 'community-offerings', title: 'What Community Offers', icon:
},
- { id: 'city-leads', title: 'City Leads', icon:
},
- { id: 'technical-heads', title: 'Technical Heads', icon:
},
- { id: 'wp-community', title: 'WhatsApp Community', icon:
},
- { id: 'hackfiesta', title: 'HackFiesta', icon:
},
- { id: 'guidelines', title: 'Code of Conduct', icon:
},
- { id: 'contributing', title: 'How to Contribute', icon:
},
- { id: 'open-source', title: 'Open Source', icon:
},
- ],
- },
+ {
+ title: 'Getting Started',
+ items: [
+ {
+ id: 'intro',
+ title: 'Introduction to DevPath',
+ icon:
,
+ },
+ {
+ id: 'setup',
+ title: 'Setting Up Your Profile',
+ icon:
,
+ },
+ {
+ id: 'xp',
+ title: 'Understanding XP System',
+ icon:
,
+ },
+ ],
+ },
+ {
+ title: 'Learning Paths',
+ items: [
+ {
+ id: 'react',
+ title: 'Full Stack React Guide',
+ icon:
,
+ },
+ {
+ id: 'python',
+ title: 'Python for AI Roadmap',
+ icon:
,
+ },
+ ],
+ },
+ {
+ title: 'Community',
+ items: [
+ {
+ id: 'community-offerings',
+ title: 'What Community Offers',
+ icon:
,
+ },
+ { id: 'city-leads', title: 'City Leads', icon:
},
+ {
+ id: 'technical-heads',
+ title: 'Technical Heads',
+ icon:
,
+ },
+ {
+ id: 'wp-community',
+ title: 'WhatsApp Community',
+ icon:
,
+ },
+ { id: 'hackfiesta', title: 'HackFiesta', icon:
},
+ {
+ id: 'guidelines',
+ title: 'Code of Conduct',
+ icon:
,
+ },
+ {
+ id: 'contributing',
+ title: 'How to Contribute',
+ icon:
,
+ },
+ { id: 'open-source', title: 'Open Source', icon:
},
+ ],
+ },
];
function slugToLabel(slug: string): string {
- return slug
- .split('-')
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ');
+ return slug
+ .split('-')
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
}
function WikiPageContent() {
- const router = useRouter();
- const searchParams = useSearchParams();
- const { showSuccess, showError } = useNotificationActions();
- const [searchQuery, setSearchQuery] = useState('');
-
- const activeArticleParam = searchParams.get('article');
- const activeArticle =
- activeArticleParam && wikiContent[activeArticleParam as keyof typeof wikiContent]
- ? activeArticleParam
- : 'intro';
-
- const allItems = useMemo(
- () => categories.flatMap((category) => category.items.map((item) => ({ ...item, category: category.title }))),
- []
- );
-
- const fuse = useMemo(
- () => new Fuse(allItems, { keys: ['title'], threshold: 0.4, includeMatches: true }),
- [allItems]
- );
-
- const fuseResults = useMemo(() => fuse.search(searchQuery), [fuse, searchQuery]);
-
- const matchMap = useMemo(
- () =>
- new Map(
- fuseResults.map((result) => [
- result.item.id,
- result.matches?.find((match) => match.key === 'title')?.indices,
- ])
- ),
- [fuseResults]
- );
-
- const filteredCategories = useMemo(() => {
- if (!searchQuery.trim()) {
- return categories;
- }
-
- const matchedIds = new Set(fuseResults.map((result) => result.item.id));
-
- return categories
- .map((category) => ({
- ...category,
- items: category.items.filter((item) => matchedIds.has(item.id)),
- }))
- .filter((category) => category.items.length > 0);
- }, [searchQuery, fuseResults]);
-
- const searchResults = useMemo(() => searchArticles(wikiSearchIndex, searchQuery), [searchQuery]);
- const isSearching = searchQuery.trim().length > 0;
-
- const handleArticleChange = (id: string) => {
- router.push(`/wiki?article=${id}`, { scroll: false });
- };
-
- const handleResultSelect = (id: string) => {
- handleArticleChange(id);
- setSearchQuery('');
- };
-
- const highlightMatch = (text: string, indices?: readonly [number, number][]) => {
- if (!indices || indices.length === 0) return text;
- const result: (string | React.ReactNode)[] = [];
- let lastIndex = 0;
-
- for (const [start, end] of indices) {
- result.push(text.slice(lastIndex, start));
- result.push(
-
- {text.slice(start, end + 1)}
-
- );
- lastIndex = end + 1;
- }
-
- result.push(text.slice(lastIndex));
- return
{result} ;
- };
-
- useEffect(() => {
- const articleBody = document.querySelector(`.${styles.articleBody}`);
- if (!articleBody) return;
-
- const preElements = articleBody.querySelectorAll('pre');
-
- preElements.forEach((pre) => {
- if (pre.parentElement?.classList.contains('code-wrapper')) return;
-
- const wrapper = document.createElement('div');
- wrapper.className = 'code-wrapper relative group w-full';
- pre.parentNode?.insertBefore(wrapper, pre);
- wrapper.appendChild(pre);
-
- const button = document.createElement('button');
- button.className = 'absolute right-3 top-3 px-2 py-1 text-xs font-semibold bg-zinc-900/80 text-zinc-300 rounded border border-white/10 opacity-0 group-hover:opacity-100 hover:bg-zinc-800 hover:text-white transition-all duration-200 shadow-md cursor-pointer backdrop-blur-sm';
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const { showSuccess, showError } = useNotificationActions();
+ const [searchQuery, setSearchQuery] = useState('');
+
+ const activeArticleParam = searchParams.get('article');
+ const activeArticle =
+ activeArticleParam &&
+ wikiContent[activeArticleParam as keyof typeof wikiContent]
+ ? activeArticleParam
+ : 'intro';
+
+ const allItems = useMemo(
+ () =>
+ categories.flatMap((category) =>
+ category.items.map((item) => ({ ...item, category: category.title }))
+ ),
+ []
+ );
+
+ const fuse = useMemo(
+ () =>
+ new Fuse(allItems, {
+ keys: ['title'],
+ threshold: 0.4,
+ includeMatches: true,
+ }),
+ [allItems]
+ );
+
+ const fuseResults = useMemo(
+ () => fuse.search(searchQuery),
+ [fuse, searchQuery]
+ );
+
+ const matchMap = useMemo(
+ () =>
+ new Map(
+ fuseResults.map((result) => [
+ result.item.id,
+ result.matches?.find((match) => match.key === 'title')?.indices,
+ ])
+ ),
+ [fuseResults]
+ );
+
+ const filteredCategories = useMemo(() => {
+ if (!searchQuery.trim()) {
+ return categories;
+ }
+
+ const matchedIds = new Set(fuseResults.map((result) => result.item.id));
+
+ return categories
+ .map((category) => ({
+ ...category,
+ items: category.items.filter((item) => matchedIds.has(item.id)),
+ }))
+ .filter((category) => category.items.length > 0);
+ }, [searchQuery, fuseResults]);
+
+ const searchResults = useMemo(
+ () => searchArticles(wikiSearchIndex, searchQuery),
+ [searchQuery]
+ );
+ const isSearching = searchQuery.trim().length > 0;
+
+ const handleArticleChange = (id: string) => {
+ router.push(`/wiki?article=${id}`, { scroll: false });
+ };
+
+ const handleResultSelect = (id: string) => {
+ handleArticleChange(id);
+ setSearchQuery('');
+ };
+
+ const highlightMatch = (
+ text: string,
+ indices?: readonly [number, number][]
+ ) => {
+ if (!indices || indices.length === 0) return text;
+ const result: (string | React.ReactNode)[] = [];
+ let lastIndex = 0;
+
+ for (const [start, end] of indices) {
+ result.push(text.slice(lastIndex, start));
+ result.push(
+
+ {text.slice(start, end + 1)}
+
+ );
+ lastIndex = end + 1;
+ }
+
+ result.push(text.slice(lastIndex));
+ return
{result} ;
+ };
+
+ useEffect(() => {
+ const articleBody = document.querySelector(`.${styles.articleBody}`);
+ if (!articleBody) return;
+
+ const preElements = articleBody.querySelectorAll('pre');
+
+ preElements.forEach((pre) => {
+ if (pre.parentElement?.classList.contains('code-wrapper')) return;
+
+ const wrapper = document.createElement('div');
+ wrapper.className = 'code-wrapper relative group w-full';
+ pre.parentNode?.insertBefore(wrapper, pre);
+ wrapper.appendChild(pre);
+
+ const button = document.createElement('button');
+ button.className =
+ 'absolute right-3 top-3 px-2 py-1 text-xs font-semibold bg-zinc-900/80 text-zinc-300 rounded border border-white/10 opacity-0 group-hover:opacity-100 hover:bg-zinc-800 hover:text-white transition-all duration-200 shadow-md cursor-pointer backdrop-blur-sm';
+ button.innerHTML = 'Copy';
+ button.type = 'button';
+
+ button.addEventListener('click', async () => {
+ const codeText = pre.textContent || '';
+ const copiedSuccessfully = await copyToClipboard(codeText);
+
+ if (copiedSuccessfully) {
+ button.innerHTML = 'Copied!';
+ button.className =
+ 'absolute right-3 top-3 px-2 py-1 text-xs font-semibold bg-emerald-600 text-white rounded border border-emerald-500 transition-all duration-200 shadow-md';
+ showSuccess('Code copied to clipboard.');
+ setTimeout(() => {
button.innerHTML = 'Copy';
- button.type = 'button';
-
- button.addEventListener('click', async () => {
- const codeText = pre.textContent || '';
- const copiedSuccessfully = await copyToClipboard(codeText);
-
- if (copiedSuccessfully) {
- button.innerHTML = 'Copied!';
- button.className = 'absolute right-3 top-3 px-2 py-1 text-xs font-semibold bg-emerald-600 text-white rounded border border-emerald-500 transition-all duration-200 shadow-md';
- showSuccess('Code copied to clipboard.');
- setTimeout(() => {
- button.innerHTML = 'Copy';
- button.className = 'absolute right-3 top-3 px-2 py-1 text-xs font-semibold bg-zinc-900/80 text-zinc-300 rounded border border-white/10 opacity-0 group-hover:opacity-100 hover:bg-zinc-800 hover:text-white transition-all duration-200 shadow-md cursor-pointer backdrop-blur-sm';
- }, 2000);
- } else {
- showError('Copying code is not supported in this browser.');
- }
- });
-
- wrapper.appendChild(button);
- });
- }, [activeArticle, showError, showSuccess]);
-
- return (
-
-
-
-
- setSearchQuery(e.target.value)}
- aria-label="Search wiki articles"
- />
- {isSearching && (
- setSearchQuery('')}
- >
-
-
- )}
-
-
- {!isSearching && (
-
- {categories.map((category, index) => (
-
-
{category.title}
- {category.items.map((item) => (
-
handleArticleChange(item.id)}
- >
- {item.icon}
- {item.title}
-
- ))}
-
- ))}
-
- )}
-
- {isSearching && (
- <>
-
- {filteredCategories.map((category, index) => (
-
-
{category.title}
- {category.items.map((item) => (
-
handleArticleChange(item.id)}
- >
- {item.icon}
- {highlightMatch(item.title, matchMap.get(item.id))}
-
- ))}
-
- ))}
-
-
-
- >
- )}
-
-
-
-
- Docs
-
- {categories.find((category) => category.items.some((item) => item.id === activeArticle))?.title ?? slugToLabel(activeArticle)}
-
- {wikiContent[activeArticle as keyof typeof wikiContent]?.title ?? slugToLabel(activeArticle)}
-
-
-
-
-
{wikiContent[activeArticle as keyof typeof wikiContent]?.title ?? 'Introduction to DevPath'}
-
- Last updated: {wikiContent[activeArticle as keyof typeof wikiContent]?.lastUpdated ?? 'Dec 14, 2025'}
- Reading time: {wikiContent[activeArticle as keyof typeof wikiContent]?.readingTime ?? '5 min'}
-
-
-
-
- {wikiContent[activeArticle as keyof typeof wikiContent]?.content ||
Content coming soon...
}
-
+ button.className =
+ 'absolute right-3 top-3 px-2 py-1 text-xs font-semibold bg-zinc-900/80 text-zinc-300 rounded border border-white/10 opacity-0 group-hover:opacity-100 hover:bg-zinc-800 hover:text-white transition-all duration-200 shadow-md cursor-pointer backdrop-blur-sm';
+ }, 2000);
+ } else {
+ showError('Copying code is not supported in this browser.');
+ }
+ });
+
+ wrapper.appendChild(button);
+ });
+ }, [activeArticle, showError, showSuccess]);
+
+ return (
+
+
+
+
+ setSearchQuery(e.target.value)}
+ aria-label="Search wiki articles"
+ />
+ {isSearching && (
+ setSearchQuery('')}
+ >
+
+
+ )}
+
-
-
Was this article helpful?
-
- }>Yes
- }>No
-
+ {!isSearching && (
+
+ {categories.map((category, index) => (
+
+
{category.title}
+ {category.items.map((item) => (
+
handleArticleChange(item.id)}
+ >
+ {item.icon}
+ {item.title}
+
+ ))}
+
+ ))}
+
+ )}
+
+ {isSearching && (
+ <>
+
+ {filteredCategories.map((category, index) => (
+
+
{category.title}
+ {category.items.map((item) => (
+
handleArticleChange(item.id)}
+ >
+ {item.icon}
+ {highlightMatch(item.title, matchMap.get(item.id))}
-
-
+ ))}
+
+ ))}
+
+
+
+ >
+ )}
+
+
+
+
+ Docs
+
+
+ {categories.find((category) =>
+ category.items.some((item) => item.id === activeArticle)
+ )?.title ?? slugToLabel(activeArticle)}
+
+
+
+ {wikiContent[activeArticle as keyof typeof wikiContent]?.title ??
+ slugToLabel(activeArticle)}
+
- );
+
+
+
+
+ {wikiContent[activeArticle as keyof typeof wikiContent]?.title ??
+ 'Introduction to DevPath'}
+
+
+
+ Last updated:{' '}
+ {wikiContent[activeArticle as keyof typeof wikiContent]
+ ?.lastUpdated ?? 'Dec 14, 2025'}
+
+
+ Reading time:{' '}
+ {wikiContent[activeArticle as keyof typeof wikiContent]
+ ?.readingTime ?? '5 min'}
+
+
+
+
+
+ {wikiContent[activeArticle as keyof typeof wikiContent]
+ ?.content ||
Content coming soon...
}
+
+
+
+
Was this article helpful?
+
+ }>
+ Yes
+
+ }>
+ No
+
+
+
+
+
+
+ );
}
export default function WikiPage() {
- return (
-
- Loading Documentation...
-
- }
- >
-
-
- );
+ return (
+
+ Loading Documentation...
+
+ }
+ >
+
+
+ );
}
diff --git a/src/components/3d/HeaderScene.tsx b/src/components/3d/HeaderScene.tsx
index 284af5fe..aa88189b 100644
--- a/src/components/3d/HeaderScene.tsx
+++ b/src/components/3d/HeaderScene.tsx
@@ -1,245 +1,264 @@
// @ts-nocheck
-"use client";
+'use client';
import { Canvas, useFrame } from '@react-three/fiber';
import { useGLTF, Environment, Html } from '@react-three/drei';
-import { useRef, useEffect, Suspense, Component, ReactNode, useMemo } from 'react';
+import {
+ useRef,
+ useEffect,
+ Suspense,
+ Component,
+ ReactNode,
+ useMemo,
+} from 'react';
import * as THREE from 'three';
function Model({ color }: { color: string }) {
- const { scene } = useGLTF('/devpath3d.glb');
- const modelRef = useRef
(null);
+ const { scene } = useGLTF('/devpath3d.glb');
+ const modelRef = useRef(null);
- // Mouse tracking for rotation
- const mouse = useRef({ x: 0 });
+ // Mouse tracking for rotation
+ const mouse = useRef({ x: 0 });
- useEffect(() => {
- const handleMouseMove = (event: MouseEvent) => {
- // Normalize x to -1 to 1
- mouse.current.x = (event.clientX / window.innerWidth) * 2 - 1;
- };
- window.addEventListener('mousemove', handleMouseMove);
- return () => window.removeEventListener('mousemove', handleMouseMove);
- }, []);
+ useEffect(() => {
+ const handleMouseMove = (event: MouseEvent) => {
+ // Normalize x to -1 to 1
+ mouse.current.x = (event.clientX / window.innerWidth) * 2 - 1;
+ };
+ window.addEventListener('mousemove', handleMouseMove);
+ return () => window.removeEventListener('mousemove', handleMouseMove);
+ }, []);
+
+ useFrame((state, delta) => {
+ if (modelRef.current) {
+ // Target rotation based on mouse x (range: -60 to +60 degrees approx)
+ const targetRotation = mouse.current.x * (Math.PI / 3);
+
+ // Smooth interpolation
+ modelRef.current.rotation.y +=
+ (targetRotation - modelRef.current.rotation.y) * delta * 5;
+ }
+ });
+
+ useEffect(() => {
+ // First pass: find the largest mesh (background circle)
+ let maxVolume = 0;
+ let largestMeshId = '';
+
+ scene.traverse((child: any) => {
+ if ((child as THREE.Mesh).isMesh) {
+ const mesh = child as THREE.Mesh;
+ if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
- useFrame((state, delta) => {
- if (modelRef.current) {
- // Target rotation based on mouse x (range: -60 to +60 degrees approx)
- const targetRotation = mouse.current.x * (Math.PI / 3);
+ // Smooth the geometry
+ mesh.geometry.computeVertexNormals();
- // Smooth interpolation
- modelRef.current.rotation.y += (targetRotation - modelRef.current.rotation.y) * delta * 5;
+ const box = mesh.geometry.boundingBox!;
+ const size = new THREE.Vector3();
+ box.getSize(size);
+ const diagonal = size.length();
+
+ if (diagonal > maxVolume) {
+ maxVolume = diagonal;
+ largestMeshId = mesh.uuid;
}
+ }
});
- useEffect(() => {
- // First pass: find the largest mesh (background circle)
- let maxVolume = 0;
- let largestMeshId = '';
-
- scene.traverse((child: any) => {
- if ((child as THREE.Mesh).isMesh) {
- const mesh = child as THREE.Mesh;
- if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
-
- // Smooth the geometry
- mesh.geometry.computeVertexNormals();
-
- const box = mesh.geometry.boundingBox!;
- const size = new THREE.Vector3();
- box.getSize(size);
- const diagonal = size.length();
-
- if (diagonal > maxVolume) {
- maxVolume = diagonal;
- largestMeshId = mesh.uuid;
- }
- }
+ // Collect created materials so we can dispose on cleanup
+ const createdMaterials: THREE.MeshStandardMaterial[] = [];
+
+ // Second pass: apply materials
+ scene.traverse((child: any) => {
+ if ((child as THREE.Mesh).isMesh) {
+ const mesh = child as THREE.Mesh;
+ const isBackground = mesh.uuid === largestMeshId;
+
+ const frontColor = isBackground ? '#0B1120' : '#FFFFFF';
+ const sideColor = isBackground ? color : '#FFFFFF';
+
+ // Dispose old material before replacing to free GPU memory
+ if (mesh.material) {
+ const old = mesh.material as THREE.Material;
+ old.dispose();
+ }
+
+ const material = new THREE.MeshStandardMaterial({
+ color: new THREE.Color(sideColor),
+ roughness: 0.2,
+ metalness: 1.0,
+ emissive: new THREE.Color(sideColor),
+ emissiveIntensity: 0.2,
+ transparent: true,
+ opacity: 0.9,
});
- // Collect created materials so we can dispose on cleanup
- const createdMaterials: THREE.MeshStandardMaterial[] = [];
-
- // Second pass: apply materials
- scene.traverse((child: any) => {
- if ((child as THREE.Mesh).isMesh) {
- const mesh = child as THREE.Mesh;
- const isBackground = mesh.uuid === largestMeshId;
-
- const frontColor = isBackground ? '#0B1120' : '#FFFFFF';
- const sideColor = isBackground ? color : '#FFFFFF';
-
- // Dispose old material before replacing to free GPU memory
- if (mesh.material) {
- const old = mesh.material as THREE.Material;
- old.dispose();
- }
-
- const material = new THREE.MeshStandardMaterial({
- color: new THREE.Color(sideColor),
- roughness: 0.2,
- metalness: 1.0,
- emissive: new THREE.Color(sideColor),
- emissiveIntensity: 0.2,
- transparent: true,
- opacity: 0.9
- });
-
- material.onBeforeCompile = (shader: any) => {
- shader.uniforms.colorFront = { value: new THREE.Color(frontColor) };
- shader.uniforms.colorSide = { value: new THREE.Color(sideColor) };
-
- shader.vertexShader = `
+ material.onBeforeCompile = (shader: any) => {
+ shader.uniforms.colorFront = { value: new THREE.Color(frontColor) };
+ shader.uniforms.colorSide = { value: new THREE.Color(sideColor) };
+
+ shader.vertexShader = `
varying vec3 vObjectNormal;
${shader.vertexShader}
`.replace(
- '#include ',
- `
+ '#include ',
+ `
#include
vObjectNormal = normal;
`
- );
+ );
- shader.fragmentShader = `
+ shader.fragmentShader = `
uniform vec3 colorFront;
uniform vec3 colorSide;
varying vec3 vObjectNormal;
${shader.fragmentShader}
`.replace(
- '#include ',
- `
+ '#include ',
+ `
#include
float isFront = step(0.8, abs(vObjectNormal.z));
diffuseColor.rgb = mix(colorSide, colorFront, isFront);
`
- );
- };
-
- mesh.material = material;
- createdMaterials.push(material);
- }
- });
-
- // Cleanup: dispose all materials we created when color/scene changes or on unmount
- return () => {
- createdMaterials.forEach((mat) => mat.dispose());
+ );
};
- }, [scene, color]);
-
- return (
-
- {/* Uniform scale to avoid distortion */}
-
-
- );
-}
-
+ mesh.material = material;
+ createdMaterials.push(material);
+ }
+ });
+ // Cleanup: dispose all materials we created when color/scene changes or on unmount
+ return () => {
+ createdMaterials.forEach((mat) => mat.dispose());
+ };
+ }, [scene, color]);
+
+ return (
+
+ {/* Uniform scale to avoid distortion */}
+
+
+ );
+}
function FallbackGeometry({ color }: { color: string }) {
- const meshRef = useRef(null);
- const mouse = useRef({ x: 0, y: 0 });
- const geometry = useMemo(() => new THREE.TorusKnotGeometry(0.7, 0.22, 120, 16), []);
- const material = useMemo(() => new THREE.MeshStandardMaterial({
+ const meshRef = useRef(null);
+ const mouse = useRef({ x: 0, y: 0 });
+ const geometry = useMemo(
+ () => new THREE.TorusKnotGeometry(0.7, 0.22, 120, 16),
+ []
+ );
+ const material = useMemo(
+ () =>
+ new THREE.MeshStandardMaterial({
color,
roughness: 0.15,
metalness: 0.9,
emissive: new THREE.Color(color),
- emissiveIntensity: 0.25
- }), [color]);
-
- useEffect(() => {
- const handleMouseMove = (event: MouseEvent) => {
- // Normalize mouse position to range [-1, 1]
- mouse.current.x = (event.clientX / window.innerWidth) * 2 - 1;
- mouse.current.y = -(event.clientY / window.innerHeight) * 2 + 1;
- };
- window.addEventListener('mousemove', handleMouseMove);
- return () => window.removeEventListener('mousemove', handleMouseMove);
- }, []);
-
- useFrame((state, delta) => {
- if (meshRef.current) {
- // Slow idle rotation
- meshRef.current.rotation.y += delta * 0.4;
- meshRef.current.rotation.x += delta * 0.15;
-
- // Dynamically rotate based on mouse cursor coordinates with smooth damping
- const targetRotationY = mouse.current.x * (Math.PI / 4);
- const targetRotationX = mouse.current.y * (Math.PI / 4);
- meshRef.current.rotation.y += (targetRotationY - meshRef.current.rotation.y) * delta * 4;
- meshRef.current.rotation.x += (targetRotationX - meshRef.current.rotation.x) * delta * 4;
- }
- });
-
- useEffect(() => {
- return () => {
- geometry.dispose();
- material.dispose();
- };
- }, [geometry, material]);
-
- return (
-
-
- {/* Elegant futuristic floating metallic Torus Knot */}
- {/* @ts-ignore */}
-
-
-
- );
-}
-
-class ModelErrorBoundary extends Component<{ children: ReactNode; fallback: ReactNode }, { hasError: boolean }> {
- constructor(props: { children: ReactNode; fallback: ReactNode }) {
- super(props);
- this.state = { hasError: false };
- }
-
- static getDerivedStateFromError(_: Error) {
- return { hasError: true };
+ emissiveIntensity: 0.25,
+ }),
+ [color]
+ );
+
+ useEffect(() => {
+ const handleMouseMove = (event: MouseEvent) => {
+ // Normalize mouse position to range [-1, 1]
+ mouse.current.x = (event.clientX / window.innerWidth) * 2 - 1;
+ mouse.current.y = -(event.clientY / window.innerHeight) * 2 + 1;
+ };
+ window.addEventListener('mousemove', handleMouseMove);
+ return () => window.removeEventListener('mousemove', handleMouseMove);
+ }, []);
+
+ useFrame((state, delta) => {
+ if (meshRef.current) {
+ // Slow idle rotation
+ meshRef.current.rotation.y += delta * 0.4;
+ meshRef.current.rotation.x += delta * 0.15;
+
+ // Dynamically rotate based on mouse cursor coordinates with smooth damping
+ const targetRotationY = mouse.current.x * (Math.PI / 4);
+ const targetRotationX = mouse.current.y * (Math.PI / 4);
+ meshRef.current.rotation.y +=
+ (targetRotationY - meshRef.current.rotation.y) * delta * 4;
+ meshRef.current.rotation.x +=
+ (targetRotationX - meshRef.current.rotation.x) * delta * 4;
}
+ });
- handleRetry = () => {
- useGLTF.clear('/devpath3d.glb');
- this.setState({ hasError: false });
+ useEffect(() => {
+ return () => {
+ geometry.dispose();
+ material.dispose();
};
+ }, [geometry, material]);
+
+ return (
+
+ {/* Elegant futuristic floating metallic Torus Knot */}
+ {/* @ts-ignore */}
+
+
+
+ );
+}
- render() {
- if (this.state.hasError) {
- return this.props.fallback;
- }
-
- return this.props.children;
+class ModelErrorBoundary extends Component<
+ { children: ReactNode; fallback: ReactNode },
+ { hasError: boolean }
+> {
+ constructor(props: { children: ReactNode; fallback: ReactNode }) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(_: Error) {
+ return { hasError: true };
+ }
+
+ handleRetry = () => {
+ useGLTF.clear('/devpath3d.glb');
+ this.setState({ hasError: false });
+ };
+
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback;
}
+
+ return this.props.children;
+ }
}
export default function HeaderScene() {
- // Gold/Orange color from the logo
- const brandColor = '#FFB800';
-
- return (
-
-
-
-
-
- {/* Render the stunning interactive procedural 3D Torus Knot directly.
+ // Gold/Orange color from the logo
+ const brandColor = '#FFB800';
+
+ return (
+
+
+
+
+
+ {/* Render the stunning interactive procedural 3D Torus Knot directly.
Since devpath3d.glb does not exist in static assets, loading it is bypassed
to completely eliminate 404 network fetch errors and optimize load speed. */}
-
-
-
-
-
- );
+
+
+
+
+
+ );
}
// useGLTF.preload('/devpath3d.glb');
diff --git a/src/components/AnimatedBackground.tsx b/src/components/AnimatedBackground.tsx
index 8b13085e..2042eff2 100644
--- a/src/components/AnimatedBackground.tsx
+++ b/src/components/AnimatedBackground.tsx
@@ -1,84 +1,111 @@
-"use client"
-import { motion, useReducedMotion } from "framer-motion"
-import { useState, useEffect } from "react"
+'use client';
+import { motion, useReducedMotion } from 'framer-motion';
+import { useState, useEffect } from 'react';
-type EasingFunction = "easeInOut" | "easeIn" | "easeOut" | "linear"
+type EasingFunction = 'easeInOut' | 'easeIn' | 'easeOut' | 'linear';
export function AnimatedBackground() {
- const shouldReduceMotion = useReducedMotion()
- const [isMobile, setIsMobile] = useState(false)
+ const shouldReduceMotion = useReducedMotion();
+ const [isMobile, setIsMobile] = useState(false);
- useEffect(() => {
- const checkMobile = () => setIsMobile(window.innerWidth < 768)
- checkMobile()
- window.addEventListener('resize', checkMobile)
- return () => window.removeEventListener('resize', checkMobile)
- }, [])
+ useEffect(() => {
+ const checkMobile = () => setIsMobile(window.innerWidth < 768);
+ checkMobile();
+ window.addEventListener('resize', checkMobile);
+ return () => window.removeEventListener('resize', checkMobile);
+ }, []);
- // Reduced blur on mobile for performance
- const blurAmount = isMobile ? 40 : 80
- const easing: EasingFunction = "easeInOut"
+ // Reduced blur on mobile for performance
+ const blurAmount = isMobile ? 40 : 80;
+ const easing: EasingFunction = 'easeInOut';
- return (
-
- {/* Base gradient */}
-
+ return (
+
+ {/* Base gradient */}
+
- {/* Floating orbs - reduced blur on mobile */}
-
+ {/* Floating orbs - reduced blur on mobile */}
+
-
+
-
-
- )
+
+
+ );
}
diff --git a/src/components/BackToTop.tsx b/src/components/BackToTop.tsx
index d3a4f7de..9e3f0142 100644
--- a/src/components/BackToTop.tsx
+++ b/src/components/BackToTop.tsx
@@ -1,8 +1,8 @@
-"use client";
+'use client';
-import React from "react";
-import { useEffect, useState } from "react";
-import { motion, AnimatePresence } from "framer-motion";
+import React from 'react';
+import { useEffect, useState } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
const BackToTop = (): React.ReactElement | null => {
const [isVisible, setIsVisible] = useState(false);
@@ -16,14 +16,14 @@ const BackToTop = (): React.ReactElement | null => {
}
};
- window.addEventListener("scroll", toggleVisibility);
- return () => window.removeEventListener("scroll", toggleVisibility);
+ window.addEventListener('scroll', toggleVisibility);
+ return () => window.removeEventListener('scroll', toggleVisibility);
}, []);
const scrollToTop = (): void => {
window.scrollTo({
top: 0,
- behavior: "smooth",
+ behavior: 'smooth',
});
};
@@ -51,4 +51,4 @@ const BackToTop = (): React.ReactElement | null => {
);
};
-export default BackToTop;
\ No newline at end of file
+export default BackToTop;
diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx
index a7fc2184..6e946602 100644
--- a/src/components/ErrorBoundary.tsx
+++ b/src/components/ErrorBoundary.tsx
@@ -1,4 +1,4 @@
-"use client";
+'use client';
import React, { Component, ErrorInfo, ReactNode } from 'react';
@@ -14,7 +14,7 @@ interface State {
class ErrorBoundary extends Component {
public state: State = {
- hasError: false
+ hasError: false,
};
public static getDerivedStateFromError(error: Error): State {
@@ -22,7 +22,7 @@ class ErrorBoundary extends Component {
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
- console.error("Uncaught error:", error, errorInfo);
+ console.error('Uncaught error:', error, errorInfo);
}
public render() {
@@ -32,9 +32,12 @@ class ErrorBoundary extends Component {
}
return (
-
Something went wrong
+
+ Something went wrong
+
- {this.state.error?.message || "An unexpected error occurred while loading this component."}
+ {this.state.error?.message ||
+ 'An unexpected error occurred while loading this component.'}
this.setState({ hasError: false, error: undefined })}
diff --git a/src/components/FloatingParticles.tsx b/src/components/FloatingParticles.tsx
index 8eb953fa..4bd3550d 100644
--- a/src/components/FloatingParticles.tsx
+++ b/src/components/FloatingParticles.tsx
@@ -1,76 +1,87 @@
-"use client"
-import { motion, useReducedMotion } from "framer-motion"
-import { useState, useEffect } from "react"
+'use client';
+import { motion, useReducedMotion } from 'framer-motion';
+import { useState, useEffect } from 'react';
export function FloatingParticles() {
- const [mounted, setMounted] = useState(false)
- const [particles, setParticles] = useState>([])
- // const [isMobile, setIsMobile] = useState(false)
- const shouldReduceMotion = useReducedMotion()
+ const [mounted, setMounted] = useState(false);
+ const [particles, setParticles] = useState<
+ Array<{
+ symbol: string;
+ delay: number;
+ duration: number;
+ x: number;
+ y: number;
+ initialX: number;
+ initialY: number;
+ }>
+ >([]);
+ // const [isMobile, setIsMobile] = useState(false)
+ const shouldReduceMotion = useReducedMotion();
- useEffect(() => {
- setMounted(true)
+ useEffect(() => {
+ setMounted(true);
- // // Check if mobile
- // const checkMobile = () => setIsMobile(window.innerWidth < 768)
- // checkMobile()
- // // window.addEventListener('resize', checkMobile)
+ // // Check if mobile
+ // const checkMobile = () => setIsMobile(window.innerWidth < 768)
+ // checkMobile()
+ // // window.addEventListener('resize', checkMobile)
- const particleConfig = [
- { symbol: ">", delay: 0, duration: 15, x: 100, y: -100 },
- { symbol: "{ }", delay: 2, duration: 18, x: -80, y: -120 },
- { symbol: "[ ]", delay: 4, duration: 20, x: 120, y: 80 },
- { symbol: "( )", delay: 1, duration: 17, x: -100, y: 100 },
- { symbol: "=>", delay: 3, duration: 16, x: 80, y: -80 },
- { symbol: "&&", delay: 5, duration: 19, x: -120, y: -100 },
- ]
+ const particleConfig = [
+ { symbol: '>', delay: 0, duration: 15, x: 100, y: -100 },
+ { symbol: '{ }', delay: 2, duration: 18, x: -80, y: -120 },
+ { symbol: '[ ]', delay: 4, duration: 20, x: 120, y: 80 },
+ { symbol: '( )', delay: 1, duration: 17, x: -100, y: 100 },
+ { symbol: '=>', delay: 3, duration: 16, x: 80, y: -80 },
+ { symbol: '&&', delay: 5, duration: 19, x: -120, y: -100 },
+ ];
- const isMobileDevice = window.innerWidth < 768
- const configToUse = isMobileDevice
- ? particleConfig.slice(0, 3)
- : particleConfig
+ const isMobileDevice = window.innerWidth < 768;
+ const configToUse = isMobileDevice
+ ? particleConfig.slice(0, 3)
+ : particleConfig;
- setParticles(configToUse.map(p => ({
- ...p,
- initialX: Math.random() * 100,
- initialY: Math.random() * 100
- })))
+ setParticles(
+ configToUse.map((p) => ({
+ ...p,
+ initialX: Math.random() * 100,
+ initialY: Math.random() * 100,
+ }))
+ );
+ }, []);
- }, [])
+ // Don't render if user prefers reduced motion or not mounted
+ if (!mounted || shouldReduceMotion) return null;
- // Don't render if user prefers reduced motion or not mounted
- if (!mounted || shouldReduceMotion) return null
-
- return (
-
+ {particles.map((particle, i) => (
+
- {particles.map((particle, i) => (
-
- {particle.symbol}
-
- ))}
-
- )
+ {particle.symbol}
+
+ ))}
+
+ );
}
diff --git a/src/components/NextBestActionWidget.tsx b/src/components/NextBestActionWidget.tsx
index 20258ca5..222da9cd 100644
--- a/src/components/NextBestActionWidget.tsx
+++ b/src/components/NextBestActionWidget.tsx
@@ -1,13 +1,20 @@
-"use client";
+'use client';
-import { useEffect, useState } from "react";
-import Link from "next/link";
-import { getRecentCategories } from "@/utils/activityTracker";
-import { getRecommendations, Recommendation } from "@/utils/recommendations";
+import { useEffect, useState } from 'react';
+import Link from 'next/link';
+import { getRecentCategories } from '@/utils/activityTracker';
+import { getRecommendations, Recommendation } from '@/utils/recommendations';
const ICON_MAP: Record = {
- laptop: "💻", code: "🐙", file: "📄", calendar: "📅",
- ticket: "🎟", map: "🗺", users: "👋", chart: "📊", rocket: "🚀",
+ laptop: '💻',
+ code: '🐙',
+ file: '📄',
+ calendar: '📅',
+ ticket: '🎟',
+ map: '🗺',
+ users: '👋',
+ chart: '📊',
+ rocket: '🚀',
};
export default function NextBestActionWidget() {
@@ -23,29 +30,52 @@ export default function NextBestActionWidget() {
if (!loaded) return null;
return (
-
+
✨
-
Your next best action
-
Personalised to your recent activity
+
+ Your next best action
+
+
+ Personalised to your recent activity
+
{recs.length === 0 ? (
- Explore DevPath and we will suggest your next step here.
+
+ Explore DevPath and we will suggest your next step here.
+
) : (
{recs.map((rec, i) => (
-
+
- {ICON_MAP[rec.icon] ?? "📌"}
- {rec.tag}
+ {ICON_MAP[rec.icon] ?? '📌'}
+
+ {rec.tag}
+
- {rec.title}
- {rec.description}
-
+
+ {rec.title}
+
+
+ {rec.description}
+
+
{rec.cta} →
diff --git a/src/components/NotificationDropdown.tsx b/src/components/NotificationDropdown.tsx
index 63bb67d1..dccf3606 100644
--- a/src/components/NotificationDropdown.tsx
+++ b/src/components/NotificationDropdown.tsx
@@ -1,262 +1,305 @@
-"use client"
-import { useState, useEffect, useRef, useCallback } from "react"
-import { Bell } from "lucide-react"
-import { motion, AnimatePresence } from "framer-motion"
-import { useAuth } from "@/context/AuthContext"
-import { db } from "@/lib/firebase"
-import { collection, query, orderBy, onSnapshot, doc, updateDoc, writeBatch, limit, Timestamp } from "firebase/firestore"
-import Link from "next/link"
-import { cn } from "@/lib/utils"
+'use client';
+import { useState, useEffect, useRef, useCallback } from 'react';
+import { Bell } from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { useAuth } from '@/context/AuthContext';
+import { db } from '@/lib/firebase';
+import {
+ collection,
+ query,
+ orderBy,
+ onSnapshot,
+ doc,
+ updateDoc,
+ writeBatch,
+ limit,
+ Timestamp,
+} from 'firebase/firestore';
+import Link from 'next/link';
+import { cn } from '@/lib/utils';
interface Notification {
- id: string
- title: string
- message: string
- image?: string
- createdAt?: Timestamp
- read: boolean
- type: 'achievement' | 'message' | 'event' | 'system' | 'event_reminder' | 'announcement' | 'wiki_update'
-link?: string
+ id: string;
+ title: string;
+ message: string;
+ image?: string;
+ createdAt?: Timestamp;
+ read: boolean;
+ type:
+ | 'achievement'
+ | 'message'
+ | 'event'
+ | 'system'
+ | 'event_reminder'
+ | 'announcement'
+ | 'wiki_update';
+ link?: string;
}
const notificationTypeClasses: Record = {
- achievement: "bg-gradient-to-r from-yellow-500 to-orange-500",
- event: "bg-gradient-to-r from-cyan-500 to-blue-500",
- message: "bg-gradient-to-r from-purple-500 to-pink-500",
- system: "bg-gradient-to-r from-gray-500 to-gray-600",
- event_reminder: "bg-gradient-to-r from-green-500 to-teal-500",
-announcement: "bg-gradient-to-r from-blue-500 to-indigo-500",
-wiki_update: "bg-gradient-to-r from-violet-500 to-purple-500",
-}
+ achievement: 'bg-gradient-to-r from-yellow-500 to-orange-500',
+ event: 'bg-gradient-to-r from-cyan-500 to-blue-500',
+ message: 'bg-gradient-to-r from-purple-500 to-pink-500',
+ system: 'bg-gradient-to-r from-gray-500 to-gray-600',
+ event_reminder: 'bg-gradient-to-r from-green-500 to-teal-500',
+ announcement: 'bg-gradient-to-r from-blue-500 to-indigo-500',
+ wiki_update: 'bg-gradient-to-r from-violet-500 to-purple-500',
+};
export function NotificationDropdown() {
- const { user } = useAuth()
- const [isOpen, setIsOpen] = useState(false)
- const [notifications, setNotifications] = useState([])
- const triggerRef = useRef(null)
+ const { user } = useAuth();
+ const [isOpen, setIsOpen] = useState(false);
+ const [notifications, setNotifications] = useState([]);
+ const triggerRef = useRef(null);
- const closePanel = useCallback(() => {
- setIsOpen(false)
- // Return focus to trigger when panel closes
- triggerRef.current?.focus()
- }, [])
+ const closePanel = useCallback(() => {
+ setIsOpen(false);
+ // Return focus to trigger when panel closes
+ triggerRef.current?.focus();
+ }, []);
- // Close on Escape
- useEffect(() => {
- if (!isOpen) return
- const onKey = (e: KeyboardEvent) => {
- if (e.key === 'Escape') closePanel()
- }
- document.addEventListener('keydown', onKey)
- return () => document.removeEventListener('keydown', onKey)
- }, [isOpen, closePanel])
+ // Close on Escape
+ useEffect(() => {
+ if (!isOpen) return;
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') closePanel();
+ };
+ document.addEventListener('keydown', onKey);
+ return () => document.removeEventListener('keydown', onKey);
+ }, [isOpen, closePanel]);
- useEffect(() => {
- if (!user) {
- let cancelled = false
- window.queueMicrotask(() => {
- if (cancelled) return
- // Clear sensitive state on sign-out / user switch to avoid showing stale notifications.
- setNotifications([])
- setIsOpen(false)
- })
- return () => {
- cancelled = true
- }
- }
+ useEffect(() => {
+ if (!user) {
+ let cancelled = false;
+ window.queueMicrotask(() => {
+ if (cancelled) return;
+ // Clear sensitive state on sign-out / user switch to avoid showing stale notifications.
+ setNotifications([]);
+ setIsOpen(false);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }
- const q = query(
- collection(db, 'members', user.uid, 'notifications'),
- orderBy('createdAt', 'desc'),
- limit(10)
- );
+ const q = query(
+ collection(db, 'members', user.uid, 'notifications'),
+ orderBy('createdAt', 'desc'),
+ limit(10)
+ );
- const unsubscribe = onSnapshot(q,
- (snapshot) => {
- setNotifications(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Notification)));
- },
- (error) => {
- console.error("Notification subscription error:", error);
- // Gracefully handle permission errors (e.g. rules not yet deployed)
- }
+ const unsubscribe = onSnapshot(
+ q,
+ (snapshot) => {
+ setNotifications(
+ snapshot.docs.map(
+ (doc) => ({ id: doc.id, ...doc.data() }) as Notification
+ )
);
+ },
+ (error) => {
+ console.error('Notification subscription error:', error);
+ // Gracefully handle permission errors (e.g. rules not yet deployed)
+ }
+ );
- return () => unsubscribe();
- }, [user]);
+ return () => unsubscribe();
+ }, [user]);
- const unreadCount = notifications.filter(n => !n.read).length
+ const unreadCount = notifications.filter((n) => !n.read).length;
- const markAsRead = async (id: string) => {
- if (!user) return;
- // Optimistic update
- setNotifications(prev => prev.map(n => n.id === id ? { ...n, read: true } : n));
- try {
- await updateDoc(doc(db, 'members', user.uid, 'notifications', id), {
- read: true
- });
- } catch (error) {
- console.error("Error marking as read:", error);
- }
+ const markAsRead = async (id: string) => {
+ if (!user) return;
+ // Optimistic update
+ setNotifications((prev) =>
+ prev.map((n) => (n.id === id ? { ...n, read: true } : n))
+ );
+ try {
+ await updateDoc(doc(db, 'members', user.uid, 'notifications', id), {
+ read: true,
+ });
+ } catch (error) {
+ console.error('Error marking as read:', error);
}
+ };
- const markAllAsRead = async () => {
- if (!user) return;
- // Optimistic update
- setNotifications(prev => prev.map(n => ({ ...n, read: true })));
- try {
- const batch = writeBatch(db);
- const unread = notifications.filter(n => !n.read);
- if (unread.length === 0) return;
+ const markAllAsRead = async () => {
+ if (!user) return;
+ // Optimistic update
+ setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
+ try {
+ const batch = writeBatch(db);
+ const unread = notifications.filter((n) => !n.read);
+ if (unread.length === 0) return;
- unread.forEach(n => {
- const ref = doc(db, 'members', user.uid, 'notifications', n.id);
- batch.update(ref, { read: true });
- });
- await batch.commit();
- } catch (error) {
- console.error("Error marking all as read:", error);
- }
+ unread.forEach((n) => {
+ const ref = doc(db, 'members', user.uid, 'notifications', n.id);
+ batch.update(ref, { read: true });
+ });
+ await batch.commit();
+ } catch (error) {
+ console.error('Error marking all as read:', error);
}
+ };
+
+ return (
+
+ {/* Bell Button */}
+
setIsOpen(!isOpen)}
+ aria-label={`Notifications${unreadCount > 0 ? `, ${unreadCount} unread` : ''}`}
+ aria-expanded={isOpen}
+ aria-haspopup="true"
+ aria-controls="notification-panel"
+ className="relative p-2 rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent"
+ >
+
+ {unreadCount > 0 && (
+
+ {unreadCount}
+
+ )}
+
- return (
-
- {/* Bell Button */}
-
setIsOpen(!isOpen)}
- aria-label={`Notifications${unreadCount > 0 ? `, ${unreadCount} unread` : ''}`}
- aria-expanded={isOpen}
- aria-haspopup="true"
- aria-controls="notification-panel"
- className="relative p-2 rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent"
+ {/* Dropdown Panel */}
+
+ {isOpen && (
+ <>
+ {/* Backdrop */}
+
+
+ {/* Notification Panel */}
+
-
+ {/* Header */}
+
+
+ Notifications
+
{unreadCount > 0 && (
-
- {unreadCount}
-
+
+ Mark all as read
+
)}
-
+
- {/* Dropdown Panel */}
-
- {isOpen && (
- <>
- {/* Backdrop */}
+ {/* Notifications List */}
+
+ {notifications.length === 0 ? (
+
+
+
No notifications yet
+
+ ) : (
+ notifications.map((notif) => (
+
markAsRead(notif.id)}
+ role="button"
+ tabIndex={0}
+ aria-label={`${notif.title}: ${notif.message}${!notif.read ? ' (unread)' : ''}`}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ markAsRead(notif.id);
+ }
+ }}
+ >
+
+ {/* Type Icon */}
-
- {/* Notification Panel */}
-
- {/* Header */}
-
-
Notifications
- {unreadCount > 0 && (
-
- Mark all as read
-
- )}
-
-
- {/* Notifications List */}
-
- {notifications.length === 0 ? (
-
-
-
No notifications yet
-
- ) : (
- notifications.map((notif) => (
-
markAsRead(notif.id)}
- role="button"
- tabIndex={0}
- aria-label={`${notif.title}: ${notif.message}${!notif.read ? ' (unread)' : ''}`}
- onKeyDown={(e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault()
- markAsRead(notif.id)
- }
- }}
- >
-
- {/* Type Icon */}
-
- {notif.type === 'achievement' && '🏆'}
- {notif.type === 'event' && '📅'}
- {notif.type === 'message' && '💬'}
- {(!notif.type || notif.type === 'system') && '🔔'}
- {notif.type === 'event_reminder' && '⏰'}
-{notif.type === 'announcement' && '📢'}
-{notif.type === 'wiki_update' && '📝'}
-
+ {notif.type === 'achievement' && '🏆'}
+ {notif.type === 'event' && '📅'}
+ {notif.type === 'message' && '💬'}
+ {(!notif.type || notif.type === 'system') && '🔔'}
+ {notif.type === 'event_reminder' && '⏰'}
+ {notif.type === 'announcement' && '📢'}
+ {notif.type === 'wiki_update' && '📝'}
+
- {/* Content */}
-
-
{notif.title}
-
{notif.message}
-
- {notif.createdAt?.seconds ? new Date(notif.createdAt.seconds * 1000).toLocaleDateString() : 'Just now'}
-
-
+ {/* Content */}
+
+
+ {notif.title}
+
+
+ {notif.message}
+
+
+ {notif.createdAt?.seconds
+ ? new Date(
+ notif.createdAt.seconds * 1000
+ ).toLocaleDateString()
+ : 'Just now'}
+
+
- {/* Unread Indicator */}
- {!notif.read && (
-
- )}
-
-
- ))
- )}
-
-
- {/* Footer */}
-
-
- View All Notifications
-
-
-
- >
+ {/* Unread Indicator */}
+ {!notif.read && (
+
+ )}
+
+
+ ))
)}
-
-
- )
+
+
+ {/* Footer */}
+
+
+ View All Notifications
+
+
+
+ >
+ )}
+
+
+ );
}
diff --git a/src/components/PageTrackerInit.tsx b/src/components/PageTrackerInit.tsx
index 34aaef11..6ebb56fb 100644
--- a/src/components/PageTrackerInit.tsx
+++ b/src/components/PageTrackerInit.tsx
@@ -1,5 +1,5 @@
-"use client";
-import { usePageTracker } from "@/hooks/usePageTracker";
+'use client';
+import { usePageTracker } from '@/hooks/usePageTracker';
export default function PageTrackerInit() {
usePageTracker();
return null;
diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx
index b84f8334..bbe54184 100644
--- a/src/components/ProjectCard.tsx
+++ b/src/components/ProjectCard.tsx
@@ -1,192 +1,201 @@
-"use client"
-import { useState } from "react"
-import { motion, AnimatePresence } from "framer-motion"
-import { Star, GitFork, Eye, ExternalLink, Github } from "lucide-react"
-import { PremiumCard } from "./ui/PremiumCard"
+'use client';
+import { useState } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Star, GitFork, Eye, ExternalLink, Github } from 'lucide-react';
+import { PremiumCard } from './ui/PremiumCard';
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
-} from "./ui/tooltip";
+} from './ui/tooltip';
interface Project {
- author: string
- title: string
- technologies: string[]
- stats: {
- stars: number
- forks: number
- views: string
- }
- color: string
- image?: string
- description?: string
- liveUrl?: string
- githubUrl?: string
+ author: string;
+ title: string;
+ technologies: string[];
+ stats: {
+ stars: number;
+ forks: number;
+ views: string;
+ };
+ color: string;
+ image?: string;
+ description?: string;
+ liveUrl?: string;
+ githubUrl?: string;
}
export function ProjectCard({ project }: { project: Project }) {
- const [isModalOpen, setIsModalOpen] = useState(false)
- const [isStarred, setIsStarred] = useState(false)
-
- return (
- <>
-
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isStarred, setIsStarred] = useState(false);
+
+ return (
+ <>
+
+ setIsModalOpen(true)}
+ >
+ {/* Project Thumbnail/Gradient */}
+
+
+ {/* Author */}
+
+
+
+ {project.author}
+
+
+
+ {/* Title */}
+
+ {project.title}
+
+
+ {/* Tech Stack */}
+
+ {project.technologies.map((tech) => (
+
+
+
+
+ {tech}
+
+
+
+ {tech}
+
+
+
+ ))}
+
+
+ {/* Stats */}
+
+
{
+ e.stopPropagation();
+ setIsStarred(!isStarred);
+ }}
+ className={`flex items-center gap-2 transition-colors ${isStarred ? 'text-yellow-500' : 'hover:text-yellow-500'}`}
+ >
+
+ {project.stats.stars + (isStarred ? 1 : 0)}
+
+
+
+
+ {project.stats.forks}
+
+
+
+
+ {project.stats.views}
+
+
+
+
+
+ {/* Project Modal */}
+
+ {isModalOpen && (
+ <>
+ {/* Backdrop */}
+ setIsModalOpen(false)}
+ className="fixed inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm z-[2000]"
+ />
+
+ {/* Modal */}
+
+ {/* Close button */}
+ setIsModalOpen(false)}
+ className="absolute top-4 right-4 w-10 h-10 rounded-full bg-black/5 dark:bg-white/10 hover:bg-black/10 dark:hover:bg-white/20 flex items-center justify-center transition-colors text-gray-900 dark:text-white"
+ >
+ ✕
+
+
+ {/* Content */}
+
setIsModalOpen(true)}
- >
- {/* Project Thumbnail/Gradient */}
-
+
+
+
+ {project.title}
+
+
+ by {project.author}
+
+
+
+
+ {project.description ||
+ 'A detailed description of this amazing project would go here, explaining the problem it solves, technologies used, and implementation challenges overcome. This project demonstrates best practices in modern web development and has received significant community attention.'}
+
+
+
+ {project.technologies.map((tech) => (
+
-
-
-
- {/* Author */}
-
-
- {/* Title */}
-
- {project.title}
-
-
- {/* Tech Stack */}
-
- {project.technologies.map((tech) => (
-
-
-
-
- {tech}
-
-
-
- {tech}
-
-
-
- ))}
-
-
- {/* Stats */}
-
-
{
- e.stopPropagation()
- setIsStarred(!isStarred)
- }}
- className={`flex items-center gap-2 transition-colors ${isStarred ? 'text-yellow-500' : 'hover:text-yellow-500'}`}
- >
-
- {project.stats.stars + (isStarred ? 1 : 0)}
-
-
-
-
- {project.stats.forks}
-
-
-
-
- {project.stats.views}
-
-
+ {tech}
+
+ ))}
+
+
+
-
-
- {/* Project Modal */}
-
- {isModalOpen && (
- <>
- {/* Backdrop */}
- setIsModalOpen(false)}
- className="fixed inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm z-[2000]"
- />
-
- {/* Modal */}
-
- {/* Close button */}
- setIsModalOpen(false)}
- className="absolute top-4 right-4 w-10 h-10 rounded-full bg-black/5 dark:bg-white/10 hover:bg-black/10 dark:hover:bg-white/20 flex items-center justify-center transition-colors text-gray-900 dark:text-white"
- >
- ✕
-
-
- {/* Content */}
-
-
-
-
-
{project.title}
-
by {project.author}
-
-
-
- {project.description || "A detailed description of this amazing project would go here, explaining the problem it solves, technologies used, and implementation challenges overcome. This project demonstrates best practices in modern web development and has received significant community attention."}
-
-
-
- {project.technologies.map((tech) => (
-
- {tech}
-
- ))}
-
-
-
-
-
- >
- )}
-
- >
- )
+
+
+ >
+ )}
+
+ >
+ );
}
diff --git a/src/components/RSVPButton.tsx b/src/components/RSVPButton.tsx
index 328a8b84..49365822 100644
--- a/src/components/RSVPButton.tsx
+++ b/src/components/RSVPButton.tsx
@@ -1,194 +1,216 @@
-"use client"
-import { useState, useEffect, useRef } from "react"
-import { motion, AnimatePresence } from "framer-motion"
-import { Check, Calendar, Loader2, AlertCircle } from "lucide-react"
-import { db } from "@/lib/firebase"
-import { doc, getDoc, setDoc, serverTimestamp } from "firebase/firestore"
-import { useAuth } from "@/context/AuthContext"
+'use client';
+import { useState, useEffect, useRef } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Check, Calendar, Loader2, AlertCircle } from 'lucide-react';
+import { db } from '@/lib/firebase';
+import { doc, getDoc, setDoc, serverTimestamp } from 'firebase/firestore';
+import { useAuth } from '@/context/AuthContext';
interface RSVPButtonProps {
- /** Unique event identifier used as the Firestore document key */
- eventId: string
+ /** Unique event identifier used as the Firestore document key */
+ eventId: string;
}
-type RSVPState = "idle" | "loading" | "success" | "already_registered" | "error"
+type RSVPState =
+ | 'idle'
+ | 'loading'
+ | 'success'
+ | 'already_registered'
+ | 'error';
export function RSVPButton({ eventId }: RSVPButtonProps) {
- const { user } = useAuth()
- const [state, setState] = useState("idle")
- const [showConfetti, setShowConfetti] = useState(false)
- const [errorMessage, setErrorMessage] = useState("")
-
- /**
- * In-flight ref lock — synchronously set to true before the first await.
- * making duplicate Firestore writes structurally impossible.
- */
- const isSubmitting = useRef(false)
-
- // ── On mount: restore RSVP state for already-registered users ──────────────
- useEffect(() => {
- if (!user || !eventId) return
-
- let cancelled = false
- const rsvpRef = doc(db, "events", eventId, "rsvps", user.uid)
-
- getDoc(rsvpRef).then((snap) => {
- if (!cancelled && snap.exists()) {
- setState("already_registered")
- }
- }).catch(() => {
- // Silently ignore — user will just see the default idle state
- })
-
- return () => { cancelled = true }
- }, [user, eventId])
-
- // ── Main RSVP handler ───────────────────────────────────────────────────────
- const handleRSVP = async () => {
- // Guard: block if already in a terminal state
- if (state === "success" || state === "already_registered") return
-
- // Guard: synchronous in-flight lock — blocks rapid/double clicks
- if (isSubmitting.current) return
- isSubmitting.current = true
-
- if (!user) {
- setErrorMessage("Please sign in to RSVP.")
- setState("error")
- isSubmitting.current = false
- return
- }
-
- setState("loading")
- setErrorMessage("")
-
- try {
- /**
- * Use the user's UID as the document ID so a second write to the
- * same path is a no-op (setDoc with merge:true is idempotent).
- * Even if two requests race to Firestore, the last write wins and
- * the document content is identical — no duplicate counter increments.
- */
- const rsvpRef = doc(db, "events", eventId, "rsvps", user.uid)
- const existing = await getDoc(rsvpRef)
-
- if (existing.exists()) {
- setState("already_registered")
- } else {
- await setDoc(rsvpRef, {
- uid: user.uid,
- name: user.name ?? null,
- email: user.email ?? null,
- registeredAt: serverTimestamp(),
- eventId,
- })
- setState("success")
- setShowConfetti(true)
- setTimeout(() => setShowConfetti(false), 2000)
- }
- } catch (err) {
- console.error("[RSVPButton] Firestore write failed:", err)
- setErrorMessage("Something went wrong. Please try again.")
- setState("error")
- } finally {
- // Always release the lock so the user can retry after an error
- isSubmitting.current = false
+ const { user } = useAuth();
+ const [state, setState] = useState('idle');
+ const [showConfetti, setShowConfetti] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+
+ /**
+ * In-flight ref lock — synchronously set to true before the first await.
+ * making duplicate Firestore writes structurally impossible.
+ */
+ const isSubmitting = useRef(false);
+
+ // ── On mount: restore RSVP state for already-registered users ──────────────
+ useEffect(() => {
+ if (!user || !eventId) return;
+
+ let cancelled = false;
+ const rsvpRef = doc(db, 'events', eventId, 'rsvps', user.uid);
+
+ getDoc(rsvpRef)
+ .then((snap) => {
+ if (!cancelled && snap.exists()) {
+ setState('already_registered');
}
+ })
+ .catch(() => {
+ // Silently ignore — user will just see the default idle state
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [user, eventId]);
+
+ // ── Main RSVP handler ───────────────────────────────────────────────────────
+ const handleRSVP = async () => {
+ // Guard: block if already in a terminal state
+ if (state === 'success' || state === 'already_registered') return;
+
+ // Guard: synchronous in-flight lock — blocks rapid/double clicks
+ if (isSubmitting.current) return;
+ isSubmitting.current = true;
+
+ if (!user) {
+ setErrorMessage('Please sign in to RSVP.');
+ setState('error');
+ isSubmitting.current = false;
+ return;
}
- // ── Derived display helpers ─────────────────────────────────────────────────
- const isDisabled = state === "loading" || state === "success" || state === "already_registered"
-
- const buttonClass = [
- "px-6 py-3 rounded-xl font-semibold transition-all w-full md:w-auto flex items-center justify-center gap-2",
- state === "success" || state === "already_registered"
- ? "bg-green-500/20 border border-green-500/50 text-green-600 dark:text-green-400 cursor-not-allowed"
- : state === "error"
- ? "bg-red-500/20 border border-red-500/50 text-red-600 dark:text-red-400"
- : state === "loading"
- ? "bg-gradient-to-r from-cyan-500/70 to-purple-500/70 text-white cursor-wait"
- : "bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-400 hover:to-purple-400 text-white shadow-lg shadow-cyan-500/20",
- ].join(" ")
-
- return (
-
-
- {state === "loading" && (
- <>
-
- Registering…
- >
- )}
- {(state === "success" || state === "already_registered") && (
- <>
-
- {state === "already_registered" ? "Already Registered" : "Registered!"}
- >
- )}
- {(state === "idle" || state === "error") && (
- <>
-
- RSVP Now
- >
- )}
-
-
- {/* Inline error message */}
-
- {state === "error" && errorMessage && (
-
-
- {errorMessage}
-
- )}
-
-
- {/* Success confetti */}
- {showConfetti && (
-
- {[...Array(20)].map((_, i) => (
-
- ))}
-
- )}
+ setState('loading');
+ setErrorMessage('');
+
+ try {
+ /**
+ * Use the user's UID as the document ID so a second write to the
+ * same path is a no-op (setDoc with merge:true is idempotent).
+ * Even if two requests race to Firestore, the last write wins and
+ * the document content is identical — no duplicate counter increments.
+ */
+ const rsvpRef = doc(db, 'events', eventId, 'rsvps', user.uid);
+ const existing = await getDoc(rsvpRef);
+
+ if (existing.exists()) {
+ setState('already_registered');
+ } else {
+ await setDoc(rsvpRef, {
+ uid: user.uid,
+ name: user.name ?? null,
+ email: user.email ?? null,
+ registeredAt: serverTimestamp(),
+ eventId,
+ });
+ setState('success');
+ setShowConfetti(true);
+ setTimeout(() => setShowConfetti(false), 2000);
+ }
+ } catch (err) {
+ console.error('[RSVPButton] Firestore write failed:', err);
+ setErrorMessage('Something went wrong. Please try again.');
+ setState('error');
+ } finally {
+ // Always release the lock so the user can retry after an error
+ isSubmitting.current = false;
+ }
+ };
+
+ // ── Derived display helpers ─────────────────────────────────────────────────
+ const isDisabled =
+ state === 'loading' ||
+ state === 'success' ||
+ state === 'already_registered';
+
+ const buttonClass = [
+ 'px-6 py-3 rounded-xl font-semibold transition-all w-full md:w-auto flex items-center justify-center gap-2',
+ state === 'success' || state === 'already_registered'
+ ? 'bg-green-500/20 border border-green-500/50 text-green-600 dark:text-green-400 cursor-not-allowed'
+ : state === 'error'
+ ? 'bg-red-500/20 border border-red-500/50 text-red-600 dark:text-red-400'
+ : state === 'loading'
+ ? 'bg-gradient-to-r from-cyan-500/70 to-purple-500/70 text-white cursor-wait'
+ : 'bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-400 hover:to-purple-400 text-white shadow-lg shadow-cyan-500/20',
+ ].join(' ');
+
+ return (
+
+
+ {state === 'loading' && (
+ <>
+
+ Registering…
+ >
+ )}
+ {(state === 'success' || state === 'already_registered') && (
+ <>
+
+
+ {state === 'already_registered'
+ ? 'Already Registered'
+ : 'Registered!'}
+
+ >
+ )}
+ {(state === 'idle' || state === 'error') && (
+ <>
+
+ RSVP Now
+ >
+ )}
+
+
+ {/* Inline error message */}
+
+ {state === 'error' && errorMessage && (
+
+
+ {errorMessage}
+
+ )}
+
+
+ {/* Success confetti */}
+ {showConfetti && (
+
+ {[...Array(20)].map((_, i) => (
+
+ ))}
- )
+ )}
+
+ );
}
diff --git a/src/components/SectionDivider.tsx b/src/components/SectionDivider.tsx
index 9c3926aa..670ef164 100644
--- a/src/components/SectionDivider.tsx
+++ b/src/components/SectionDivider.tsx
@@ -1,17 +1,17 @@
export function SectionDivider() {
- return (
-
-
-
- {/* Animated glow */}
-
+ return (
+
+
+
+ {/* Animated glow */}
+
- {/* Center dot */}
-
-
-
+ {/* Center dot */}
+
- )
+
+
+ );
}
diff --git a/src/components/admin/AdminDashboard.tsx b/src/components/admin/AdminDashboard.tsx
index 01693556..15a19d39 100644
--- a/src/components/admin/AdminDashboard.tsx
+++ b/src/components/admin/AdminDashboard.tsx
@@ -1,716 +1,909 @@
-"use client";
+'use client';
import { useState, useEffect } from 'react';
import { useAuth } from '@/context/AuthContext';
import { Shield, Database, X } from 'lucide-react';
-import { doc, updateDoc, onSnapshot, collection, getDocs, query, orderBy, addDoc, serverTimestamp, deleteDoc, where, setDoc, arrayRemove, increment, arrayUnion, getDoc, limit, startAfter, QueryDocumentSnapshot, DocumentData, writeBatch } from 'firebase/firestore';
+import {
+ doc,
+ updateDoc,
+ onSnapshot,
+ collection,
+ getDocs,
+ query,
+ orderBy,
+ addDoc,
+ serverTimestamp,
+ deleteDoc,
+ where,
+ setDoc,
+ arrayRemove,
+ increment,
+ arrayUnion,
+ getDoc,
+ limit,
+ startAfter,
+ QueryDocumentSnapshot,
+ DocumentData,
+ writeBatch,
+} from 'firebase/firestore';
import { db } from '@/lib/firebase';
import Image from 'next/image';
import { determineBadges, getBadgeXp } from '@/lib/point-calculation';
const PAGE_SIZE = 50;
-export default function AdminDashboard({ initialAuth = false }: { initialAuth?: boolean }) {
- // === Pagination States ===
- const [lastVisible, setLastVisible] = useState
| null>(null);
- const [hasMore, setHasMore] = useState(true);
- const [loadingMore, setLoadingMore] = useState(false);
-
- // === Maintenance States ===
- const [maintenanceMode, setMaintenanceMode] = useState(false);
- const [maintenanceMsg, setMaintenanceMsg] = useState('');
- const [activeTab, setActiveTab] = useState('system');
-
- // === Missing States (Added for safety) ===
- const [users, setUsers] = useState([]);
- const [admins, setAdmins] = useState([]);
- const [projects, setProjects] = useState([]);
- const [discussions, setDiscussions] = useState([]);
- const [events, setEvents] = useState([]);
- const [userProjects, setUserProjects] = useState([]);
- const [migrationLog, setMigrationLog] = useState([]);
-
- const [searching, setSearching] = useState(false);
- const [loadingContent, setLoadingContent] = useState(false);
- const [loadingEvents, setLoadingEvents] = useState(false);
- const [loadingAdmins, setLoadingAdmins] = useState(false);
- const [loadingProjects, setLoadingProjects] = useState(false);
- const [creatingEvent, setCreatingEvent] = useState(false);
- const [migrating, setMigrating] = useState(false);
-
- const [showEventModal, setShowEventModal] = useState(false);
- const [selectedUser, setSelectedUser] = useState(null);
- const [badgeUser, setBadgeUser] = useState(null);
-
- const [newEvent, setNewEvent] = useState({
- title: '', description: '', date: '', location: '', image: '', registerLink: '',
- organisationName: '', opportunityType: 'Workshops & Webinar', opportunityCategory: '',
- websiteUrl: '', participationType: 'Individual', mode: 'Online', eligibility: 'Everyone', sponsors: []
+export default function AdminDashboard({
+ initialAuth = false,
+}: {
+ initialAuth?: boolean;
+}) {
+ // === Pagination States ===
+ const [lastVisible, setLastVisible] =
+ useState | null>(null);
+ const [hasMore, setHasMore] = useState(true);
+ const [loadingMore, setLoadingMore] = useState(false);
+
+ // === Maintenance States ===
+ const [maintenanceMode, setMaintenanceMode] = useState(false);
+ const [maintenanceMsg, setMaintenanceMsg] = useState('');
+ const [activeTab, setActiveTab] = useState('system');
+
+ // === Missing States (Added for safety) ===
+ const [users, setUsers] = useState([]);
+ const [admins, setAdmins] = useState([]);
+ const [projects, setProjects] = useState([]);
+ const [discussions, setDiscussions] = useState([]);
+ const [events, setEvents] = useState([]);
+ const [userProjects, setUserProjects] = useState([]);
+ const [migrationLog, setMigrationLog] = useState([]);
+
+ const [searching, setSearching] = useState(false);
+ const [loadingContent, setLoadingContent] = useState(false);
+ const [loadingEvents, setLoadingEvents] = useState(false);
+ const [loadingAdmins, setLoadingAdmins] = useState(false);
+ const [loadingProjects, setLoadingProjects] = useState(false);
+ const [creatingEvent, setCreatingEvent] = useState(false);
+ const [migrating, setMigrating] = useState(false);
+
+ const [showEventModal, setShowEventModal] = useState(false);
+ const [selectedUser, setSelectedUser] = useState(null);
+ const [badgeUser, setBadgeUser] = useState(null);
+
+ const [newEvent, setNewEvent] = useState({
+ title: '',
+ description: '',
+ date: '',
+ location: '',
+ image: '',
+ registerLink: '',
+ organisationName: '',
+ opportunityType: 'Workshops & Webinar',
+ opportunityCategory: '',
+ websiteUrl: '',
+ participationType: 'Individual',
+ mode: 'Online',
+ eligibility: 'Everyone',
+ sponsors: [],
+ });
+
+ const { user, isAdminAuthenticated } = useAuth() as any;
+ const SUPER_ADMIN_EMAIL = process.env.NEXT_PUBLIC_SUPER_ADMIN_EMAIL;
+
+ // ==========================================
+ // 1. YOUR MAINTENANCE MODE LISTENER
+ // ==========================================
+ useEffect(() => {
+ const unsub = onSnapshot(doc(db, 'settings', 'general'), (doc) => {
+ if (doc.exists()) {
+ const data = doc.data();
+ setMaintenanceMode(data.maintenanceMode || false);
+ setMaintenanceMsg(data.maintenanceMessage || '');
+ }
});
-
- const { user, isAdminAuthenticated } = useAuth() as any;
- const SUPER_ADMIN_EMAIL = process.env.NEXT_PUBLIC_SUPER_ADMIN_EMAIL;
-
- // ==========================================
- // 1. YOUR MAINTENANCE MODE LISTENER
- // ==========================================
- useEffect(() => {
- const unsub = onSnapshot(doc(db, 'settings', 'general'), (doc) => {
- if (doc.exists()) {
- const data = doc.data();
- setMaintenanceMode(data.maintenanceMode || false);
- setMaintenanceMsg(data.maintenanceMessage || '');
- }
- });
- return () => unsub();
- }, []);
-
- // ==========================================
- // 2. MASTER BRANCH FUNCTIONS
- // ==========================================
- const fetchUsers = async () => {
- setSearching(true);
- try {
- const membersRef = collection(db, 'members');
- const membersQuery = query(
- membersRef,
- orderBy('createdAt', 'desc'),
- limit(PAGE_SIZE)
- );
- const membersSnap = await getDocs(membersQuery);
- const membersList = membersSnap.docs.map(doc => ({ uid: doc.id, ...doc.data() } as any));
-
- const adminsRef = collection(db, 'admins');
- const adminsSnap = await getDocs(adminsRef);
- const adminsList = adminsSnap.docs.map(doc => {
- const data = doc.data();
- return {
- uid: doc.id,
- ...data,
- email: data.email || (doc.id.includes('@') ? doc.id : undefined),
- role: 'admin'
- };
- });
-
- const userMap = new Map();
- membersList.forEach(member => {
- if (member.uid) userMap.set(member.uid, member);
- });
-
- adminsList.forEach(admin => {
- let targetUid = admin.uid;
- if (!targetUid || targetUid.includes('@')) {
- const matchingMember = membersList.find(m => m.email === admin.email);
- if (matchingMember) {
- targetUid = matchingMember.uid;
- } else {
- targetUid = admin.uid || admin.email;
- }
- }
-
- if (targetUid) {
- const existing = userMap.get(targetUid);
- if (existing) {
- userMap.set(targetUid, { ...existing, ...admin, uid: targetUid, role: 'admin' });
- } else {
- userMap.set(targetUid, { ...admin, uid: targetUid, role: 'admin' });
- }
- }
- });
-
- const allUsers = Array.from(userMap.values());
- console.log(`Found ${allUsers.length} total users.`);
- setUsers(allUsers);
-
- const lastVisibleDoc = membersSnap.docs[membersSnap.docs.length - 1] || null;
- setLastVisible(lastVisibleDoc);
- setHasMore(membersSnap.docs.length === PAGE_SIZE);
- } catch (error) {
- console.error("Error fetching users:", error);
- } finally {
- setSearching(false);
- }
- };
-
- const loadMoreMembers = async () => {
- if (loadingMore || !hasMore || !lastVisible) return;
- setLoadingMore(true);
- try {
- const membersRef = collection(db, 'members');
- const membersQuery = query(
- membersRef,
- orderBy('createdAt', 'desc'),
- startAfter(lastVisible),
- limit(PAGE_SIZE)
- );
- const membersSnap = await getDocs(membersQuery);
- const membersList = membersSnap.docs.map(doc => ({ uid: doc.id, ...doc.data() } as any));
-
- if (membersList.length > 0) {
- const adminsRef = collection(db, 'admins');
- const adminsSnap = await getDocs(adminsRef);
- const adminsList = adminsSnap.docs.map(doc => {
- const data = doc.data();
- return {
- uid: doc.id,
- ...data,
- email: data.email || (doc.id.includes('@') ? doc.id : undefined),
- role: 'admin'
- };
- });
-
- const userMap = new Map();
- users.forEach(user => {
- if (user.uid) userMap.set(user.uid, user);
- });
- membersList.forEach(member => {
- if (member.uid) userMap.set(member.uid, member);
- });
-
- adminsList.forEach(admin => {
- let targetUid = admin.uid;
- if (!targetUid || targetUid.includes('@')) {
- const matchingMember = membersList.find(m => m.email === admin.email);
- if (matchingMember) {
- targetUid = matchingMember.uid;
- } else {
- targetUid = admin.uid || admin.email;
- }
- }
-
- if (targetUid) {
- const existing = userMap.get(targetUid);
- if (existing) {
- userMap.set(targetUid, { ...existing, ...admin, uid: targetUid, role: 'admin' });
- } else {
- userMap.set(targetUid, { ...admin, uid: targetUid, role: 'admin' });
- }
- }
- });
-
- const allUsers = Array.from(userMap.values());
- setUsers(allUsers);
- }
-
- const lastVisibleDoc = membersSnap.docs[membersSnap.docs.length - 1] || null;
- if (lastVisibleDoc) {
- setLastVisible(lastVisibleDoc);
- }
- setHasMore(membersSnap.docs.length === PAGE_SIZE);
- } catch (error) {
- console.error("Error loading more members:", error);
- } finally {
- setLoadingMore(false);
- }
- };
-
- const fetchContent = async () => {
- setLoadingContent(true);
- try {
- const projectsRef = collection(db, 'projects');
- const projectsSnap = await getDocs(projectsRef);
- const projectsList = projectsSnap.docs.map(doc => ({ id: doc.id, ...doc.data() }));
- setProjects(projectsList);
-
- const discussionsRef = collection(db, 'discussions');
- const discussionsSnap = await getDocs(discussionsRef);
- const discussionsList = discussionsSnap.docs.map(doc => ({ id: doc.id, ...doc.data() }));
- setDiscussions(discussionsList);
- } catch (error) {
- console.error("Error fetching content:", error);
- } finally {
- setLoadingContent(false);
- }
- };
-
- const fetchEvents = async () => {
- setLoadingEvents(true);
- try {
- const q = query(collection(db, 'events'), orderBy('date', 'asc'));
- const snapshot = await getDocs(q);
- const eventsList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
- setEvents(eventsList);
- } catch (error) {
- console.error("Error fetching events:", error);
- } finally {
- setLoadingEvents(false);
- }
- };
-
- const handleCreateEvent = async (e: React.FormEvent) => {
- e.preventDefault();
- setCreatingEvent(true);
- try {
- await addDoc(collection(db, 'events'), {
- ...newEvent,
- createdAt: serverTimestamp()
- });
-
- await addDoc(collection(db, 'admin_notifications'), {
- type: 'event',
- message: `New Event Created: ${newEvent.title}`,
- createdAt: serverTimestamp(),
- read: false
- });
-
- setShowEventModal(false);
- setNewEvent({
- title: '', description: '', date: '', location: '', image: '', registerLink: '',
- organisationName: '', opportunityType: 'Workshops & Webinar', opportunityCategory: '',
- websiteUrl: '', participationType: 'Individual', mode: 'Online', eligibility: 'Everyone', sponsors: []
- });
- fetchEvents();
- alert("Event created successfully!");
- } catch (error) {
- console.error("Error creating event:", error);
- alert("Failed to create event.");
- } finally {
- setCreatingEvent(false);
- }
- };
-
- const handleDeleteEvent = async (eventId: string) => {
- if (!confirm("Are you sure you want to delete this event?")) return;
- try {
- await deleteDoc(doc(db, 'events', eventId));
- setEvents(prev => prev.filter(e => e.id !== eventId));
- } catch (error) {
- console.error("Error deleting event:", error);
- alert("Failed to delete event.");
+ return () => unsub();
+ }, []);
+
+ // ==========================================
+ // 2. MASTER BRANCH FUNCTIONS
+ // ==========================================
+ const fetchUsers = async () => {
+ setSearching(true);
+ try {
+ const membersRef = collection(db, 'members');
+ const membersQuery = query(
+ membersRef,
+ orderBy('createdAt', 'desc'),
+ limit(PAGE_SIZE)
+ );
+ const membersSnap = await getDocs(membersQuery);
+ const membersList = membersSnap.docs.map(
+ (doc) => ({ uid: doc.id, ...doc.data() }) as any
+ );
+
+ const adminsRef = collection(db, 'admins');
+ const adminsSnap = await getDocs(adminsRef);
+ const adminsList = adminsSnap.docs.map((doc) => {
+ const data = doc.data();
+ return {
+ uid: doc.id,
+ ...data,
+ email: data.email || (doc.id.includes('@') ? doc.id : undefined),
+ role: 'admin',
+ };
+ });
+
+ const userMap = new Map();
+ membersList.forEach((member) => {
+ if (member.uid) userMap.set(member.uid, member);
+ });
+
+ adminsList.forEach((admin) => {
+ let targetUid = admin.uid;
+ if (!targetUid || targetUid.includes('@')) {
+ const matchingMember = membersList.find(
+ (m) => m.email === admin.email
+ );
+ if (matchingMember) {
+ targetUid = matchingMember.uid;
+ } else {
+ targetUid = admin.uid || admin.email;
+ }
}
- };
- const fetchAdmins = async () => {
- setLoadingAdmins(true);
- try {
- const q = query(collection(db, 'members'), where('role', '==', 'admin'));
- const snapshot = await getDocs(q);
- const membersAdmins: any[] = snapshot.docs.map(doc => ({ uid: doc.id, ...doc.data() }));
-
- const adminsColRef = collection(db, 'admins');
- const adminsColSnap = await getDocs(adminsColRef);
- const adminsColList: any[] = adminsColSnap.docs.map(doc => {
- const data = doc.data();
- return {
- uid: data.uid || doc.id,
- email: doc.id.includes('@') ? doc.id : data.email,
- name: data.name || data.displayName,
- ...data,
- role: 'admin'
- };
+ if (targetUid) {
+ const existing = userMap.get(targetUid);
+ if (existing) {
+ userMap.set(targetUid, {
+ ...existing,
+ ...admin,
+ uid: targetUid,
+ role: 'admin',
});
-
- const allAdmins = [...adminsColList];
- membersAdmins.forEach(memberAdmin => {
- const exists = allAdmins.find(a =>
- (a.email && a.email === memberAdmin.email) ||
- (a.uid && a.uid === memberAdmin.uid)
- );
- if (!exists) allAdmins.push(memberAdmin);
- });
-
- console.log("Fetched Admins:", allAdmins);
- setAdmins(allAdmins);
- } catch (error) {
- console.error("Error fetching admins:", error);
- } finally {
- setLoadingAdmins(false);
+ } else {
+ userMap.set(targetUid, { ...admin, uid: targetUid, role: 'admin' });
+ }
}
- };
+ });
+
+ const allUsers = Array.from(userMap.values());
+ console.log(`Found ${allUsers.length} total users.`);
+ setUsers(allUsers);
+
+ const lastVisibleDoc =
+ membersSnap.docs[membersSnap.docs.length - 1] || null;
+ setLastVisible(lastVisibleDoc);
+ setHasMore(membersSnap.docs.length === PAGE_SIZE);
+ } catch (error) {
+ console.error('Error fetching users:', error);
+ } finally {
+ setSearching(false);
+ }
+ };
+
+ const loadMoreMembers = async () => {
+ if (loadingMore || !hasMore || !lastVisible) return;
+ setLoadingMore(true);
+ try {
+ const membersRef = collection(db, 'members');
+ const membersQuery = query(
+ membersRef,
+ orderBy('createdAt', 'desc'),
+ startAfter(lastVisible),
+ limit(PAGE_SIZE)
+ );
+ const membersSnap = await getDocs(membersQuery);
+ const membersList = membersSnap.docs.map(
+ (doc) => ({ uid: doc.id, ...doc.data() }) as any
+ );
+
+ if (membersList.length > 0) {
+ const adminsRef = collection(db, 'admins');
+ const adminsSnap = await getDocs(adminsRef);
+ const adminsList = adminsSnap.docs.map((doc) => {
+ const data = doc.data();
+ return {
+ uid: doc.id,
+ ...data,
+ email: data.email || (doc.id.includes('@') ? doc.id : undefined),
+ role: 'admin',
+ };
+ });
- const handlePromoteToAdmin = async (targetUser: any) => {
- if (!confirm(`Promote ${targetUser.name} to Admin?`)) return;
- try {
- await updateDoc(doc(db, 'members', targetUser.uid), { role: 'admin' });
- if (targetUser.email) {
- await setDoc(doc(db, 'admins', targetUser.email), {
- ...targetUser,
- role: 'admin',
- promotedAt: serverTimestamp()
- }, { merge: true });
- }
- alert("User promoted to Admin.");
- fetchUsers();
- } catch (error) {
- console.error("Error promoting user:", error);
- alert("Failed to promote user.");
- }
- };
+ const userMap = new Map();
+ users.forEach((user) => {
+ if (user.uid) userMap.set(user.uid, user);
+ });
+ membersList.forEach((member) => {
+ if (member.uid) userMap.set(member.uid, member);
+ });
- const handleDemoteToMember = async (targetAdmin: any) => {
- if (targetAdmin.email === SUPER_ADMIN_EMAIL) {
- alert("Cannot remove Super Admin.");
- return;
- }
- if (!confirm(`Demote ${targetAdmin.name} to Member?`)) return;
- try {
- const batch = writeBatch(db);
- if (targetAdmin.uid) {
- batch.set(doc(db, 'members', targetAdmin.uid), {
- ...targetAdmin,
- role: 'member'
- }, { merge: true });
+ adminsList.forEach((admin) => {
+ let targetUid = admin.uid;
+ if (!targetUid || targetUid.includes('@')) {
+ const matchingMember = membersList.find(
+ (m) => m.email === admin.email
+ );
+ if (matchingMember) {
+ targetUid = matchingMember.uid;
+ } else {
+ targetUid = admin.uid || admin.email;
}
- if (targetAdmin.email) {
- batch.delete(doc(db, 'admins', targetAdmin.email));
+ }
+
+ if (targetUid) {
+ const existing = userMap.get(targetUid);
+ if (existing) {
+ userMap.set(targetUid, {
+ ...existing,
+ ...admin,
+ uid: targetUid,
+ role: 'admin',
+ });
+ } else {
+ userMap.set(targetUid, {
+ ...admin,
+ uid: targetUid,
+ role: 'admin',
+ });
}
- await batch.commit();
- alert("Admin demoted to Member.");
- fetchAdmins();
- } catch (error) {
- console.error("Error demoting admin:", error);
- alert("Failed to demote admin.");
- }
- };
+ }
+ });
- const handleDeleteUser = async (userId: string) => {
- if (!confirm("Are you sure you want to delete this user? This action cannot be undone.")) return;
- try {
- const targetUser = users.find(u => u.uid === userId);
- const batch = writeBatch(db);
-
- batch.delete(doc(db, 'members', userId));
-
- if (targetUser?.email) {
- batch.delete(doc(db, 'admins', targetUser.email));
+ const allUsers = Array.from(userMap.values());
+ setUsers(allUsers);
+ }
+
+ const lastVisibleDoc =
+ membersSnap.docs[membersSnap.docs.length - 1] || null;
+ if (lastVisibleDoc) {
+ setLastVisible(lastVisibleDoc);
+ }
+ setHasMore(membersSnap.docs.length === PAGE_SIZE);
+ } catch (error) {
+ console.error('Error loading more members:', error);
+ } finally {
+ setLoadingMore(false);
+ }
+ };
+
+ const fetchContent = async () => {
+ setLoadingContent(true);
+ try {
+ const projectsRef = collection(db, 'projects');
+ const projectsSnap = await getDocs(projectsRef);
+ const projectsList = projectsSnap.docs.map((doc) => ({
+ id: doc.id,
+ ...doc.data(),
+ }));
+ setProjects(projectsList);
+
+ const discussionsRef = collection(db, 'discussions');
+ const discussionsSnap = await getDocs(discussionsRef);
+ const discussionsList = discussionsSnap.docs.map((doc) => ({
+ id: doc.id,
+ ...doc.data(),
+ }));
+ setDiscussions(discussionsList);
+ } catch (error) {
+ console.error('Error fetching content:', error);
+ } finally {
+ setLoadingContent(false);
+ }
+ };
+
+ const fetchEvents = async () => {
+ setLoadingEvents(true);
+ try {
+ const q = query(collection(db, 'events'), orderBy('date', 'asc'));
+ const snapshot = await getDocs(q);
+ const eventsList = snapshot.docs.map((doc) => ({
+ id: doc.id,
+ ...doc.data(),
+ }));
+ setEvents(eventsList);
+ } catch (error) {
+ console.error('Error fetching events:', error);
+ } finally {
+ setLoadingEvents(false);
+ }
+ };
+
+ const handleCreateEvent = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setCreatingEvent(true);
+ try {
+ await addDoc(collection(db, 'events'), {
+ ...newEvent,
+ createdAt: serverTimestamp(),
+ });
+
+ await addDoc(collection(db, 'admin_notifications'), {
+ type: 'event',
+ message: `New Event Created: ${newEvent.title}`,
+ createdAt: serverTimestamp(),
+ read: false,
+ });
+
+ setShowEventModal(false);
+ setNewEvent({
+ title: '',
+ description: '',
+ date: '',
+ location: '',
+ image: '',
+ registerLink: '',
+ organisationName: '',
+ opportunityType: 'Workshops & Webinar',
+ opportunityCategory: '',
+ websiteUrl: '',
+ participationType: 'Individual',
+ mode: 'Online',
+ eligibility: 'Everyone',
+ sponsors: [],
+ });
+ fetchEvents();
+ alert('Event created successfully!');
+ } catch (error) {
+ console.error('Error creating event:', error);
+ alert('Failed to create event.');
+ } finally {
+ setCreatingEvent(false);
+ }
+ };
+
+ const handleDeleteEvent = async (eventId: string) => {
+ if (!confirm('Are you sure you want to delete this event?')) return;
+ try {
+ await deleteDoc(doc(db, 'events', eventId));
+ setEvents((prev) => prev.filter((e) => e.id !== eventId));
+ } catch (error) {
+ console.error('Error deleting event:', error);
+ alert('Failed to delete event.');
+ }
+ };
+
+ const fetchAdmins = async () => {
+ setLoadingAdmins(true);
+ try {
+ const q = query(collection(db, 'members'), where('role', '==', 'admin'));
+ const snapshot = await getDocs(q);
+ const membersAdmins: any[] = snapshot.docs.map((doc) => ({
+ uid: doc.id,
+ ...doc.data(),
+ }));
+
+ const adminsColRef = collection(db, 'admins');
+ const adminsColSnap = await getDocs(adminsColRef);
+ const adminsColList: any[] = adminsColSnap.docs.map((doc) => {
+ const data = doc.data();
+ return {
+ uid: data.uid || doc.id,
+ email: doc.id.includes('@') ? doc.id : data.email,
+ name: data.name || data.displayName,
+ ...data,
+ role: 'admin',
+ };
+ });
+
+ const allAdmins = [...adminsColList];
+ membersAdmins.forEach((memberAdmin) => {
+ const exists = allAdmins.find(
+ (a) =>
+ (a.email && a.email === memberAdmin.email) ||
+ (a.uid && a.uid === memberAdmin.uid)
+ );
+ if (!exists) allAdmins.push(memberAdmin);
+ });
+
+ console.log('Fetched Admins:', allAdmins);
+ setAdmins(allAdmins);
+ } catch (error) {
+ console.error('Error fetching admins:', error);
+ } finally {
+ setLoadingAdmins(false);
+ }
+ };
+
+ const handlePromoteToAdmin = async (targetUser: any) => {
+ if (!confirm(`Promote ${targetUser.name} to Admin?`)) return;
+ try {
+ await updateDoc(doc(db, 'members', targetUser.uid), { role: 'admin' });
+ if (targetUser.email) {
+ await setDoc(
+ doc(db, 'admins', targetUser.email),
+ {
+ ...targetUser,
+ role: 'admin',
+ promotedAt: serverTimestamp(),
+ },
+ { merge: true }
+ );
+ }
+ alert('User promoted to Admin.');
+ fetchUsers();
+ } catch (error) {
+ console.error('Error promoting user:', error);
+ alert('Failed to promote user.');
+ }
+ };
+
+ const handleDemoteToMember = async (targetAdmin: any) => {
+ if (targetAdmin.email === SUPER_ADMIN_EMAIL) {
+ alert('Cannot remove Super Admin.');
+ return;
+ }
+ if (!confirm(`Demote ${targetAdmin.name} to Member?`)) return;
+ try {
+ const batch = writeBatch(db);
+ if (targetAdmin.uid) {
+ batch.set(
+ doc(db, 'members', targetAdmin.uid),
+ {
+ ...targetAdmin,
+ role: 'member',
+ },
+ { merge: true }
+ );
+ }
+ if (targetAdmin.email) {
+ batch.delete(doc(db, 'admins', targetAdmin.email));
+ }
+ await batch.commit();
+ alert('Admin demoted to Member.');
+ fetchAdmins();
+ } catch (error) {
+ console.error('Error demoting admin:', error);
+ alert('Failed to demote admin.');
+ }
+ };
+
+ const handleDeleteUser = async (userId: string) => {
+ if (
+ !confirm(
+ 'Are you sure you want to delete this user? This action cannot be undone.'
+ )
+ )
+ return;
+ try {
+ const targetUser = users.find((u) => u.uid === userId);
+ const batch = writeBatch(db);
+
+ batch.delete(doc(db, 'members', userId));
+
+ if (targetUser?.email) {
+ batch.delete(doc(db, 'admins', targetUser.email));
+ }
+
+ await batch.commit();
+ setUsers((prev) => prev.filter((u) => u.uid !== userId));
+ if (selectedUser?.uid === userId) setSelectedUser(null);
+ alert('User deleted from all records.');
+ } catch (error) {
+ console.error('Error deleting user:', error);
+ alert('Failed to delete user.');
+ }
+ };
+
+ const handleUpdateUser = async (userId: string, data: any) => {
+ try {
+ const sanitizedData = { ...data };
+ Object.keys(sanitizedData).forEach((key) => {
+ if (sanitizedData[key] === undefined) delete sanitizedData[key];
+ });
+ if (data.communityRole === undefined) delete sanitizedData.communityRole;
+
+ let targetUser = users.find((u) => u.uid === userId);
+ if (!targetUser)
+ targetUser = admins.find((a) => a.uid === userId || a.email === userId);
+
+ if (targetUser && !targetUser.email) {
+ const otherRecord =
+ admins.find((a) => a.uid === targetUser?.uid) ||
+ users.find((u) => u.uid === targetUser?.uid);
+ if (otherRecord && otherRecord.email)
+ targetUser.email = otherRecord.email;
+ }
+
+ if (!targetUser) {
+ if (userId.includes('@')) {
+ const exactRef = doc(db, 'admins', userId);
+ if ((await getDoc(exactRef)).exists()) {
+ await updateDoc(exactRef, sanitizedData);
+ } else {
+ const lowerRef = doc(db, 'admins', userId.toLowerCase());
+ if ((await getDoc(lowerRef)).exists()) {
+ await updateDoc(lowerRef, sanitizedData);
+ } else {
+ throw new Error(`Admin document not found for email: ${userId}`);
}
-
- await batch.commit();
- setUsers(prev => prev.filter(u => u.uid !== userId));
- if (selectedUser?.uid === userId) setSelectedUser(null);
- alert("User deleted from all records.");
- } catch (error) {
- console.error("Error deleting user:", error);
- alert("Failed to delete user.");
+ }
+ } else {
+ await updateDoc(doc(db, 'members', userId), sanitizedData);
}
- };
-
- const handleUpdateUser = async (userId: string, data: any) => {
- try {
- const sanitizedData = { ...data };
- Object.keys(sanitizedData).forEach(key => {
- if (sanitizedData[key] === undefined) delete sanitizedData[key];
- });
- if (data.communityRole === undefined) delete sanitizedData.communityRole;
-
- let targetUser = users.find(u => u.uid === userId);
- if (!targetUser) targetUser = admins.find(a => a.uid === userId || a.email === userId);
-
- if (targetUser && !targetUser.email) {
- const otherRecord = admins.find(a => a.uid === targetUser?.uid) || users.find(u => u.uid === targetUser?.uid);
- if (otherRecord && otherRecord.email) targetUser.email = otherRecord.email;
+ } else {
+ if (targetUser.role === 'admin') {
+ const candidates = [
+ targetUser.email,
+ targetUser.email?.toLowerCase(),
+ userId.includes('@') ? userId : null,
+ userId.includes('@') ? userId.toLowerCase() : null,
+ targetUser.uid,
+ ].filter(Boolean) as string[];
+
+ const uniqueCandidates = Array.from(new Set(candidates));
+ let updated = false;
+ for (const id of uniqueCandidates) {
+ const docRef = doc(db, 'admins', id);
+ if ((await getDoc(docRef)).exists()) {
+ await updateDoc(docRef, sanitizedData);
+ updated = true;
+ break;
}
-
- if (!targetUser) {
- if (userId.includes('@')) {
- const exactRef = doc(db, 'admins', userId);
- if ((await getDoc(exactRef)).exists()) {
- await updateDoc(exactRef, sanitizedData);
- } else {
- const lowerRef = doc(db, 'admins', userId.toLowerCase());
- if ((await getDoc(lowerRef)).exists()) {
- await updateDoc(lowerRef, sanitizedData);
- } else {
- throw new Error(`Admin document not found for email: ${userId}`);
- }
- }
- } else {
- await updateDoc(doc(db, 'members', userId), sanitizedData);
- }
+ }
+ if (!updated) {
+ const memberRef = doc(db, 'members', targetUser.uid || userId);
+ if ((await getDoc(memberRef)).exists()) {
+ await updateDoc(memberRef, sanitizedData);
+ updated = true;
} else {
- if (targetUser.role === 'admin') {
- const candidates = [
- targetUser.email, targetUser.email?.toLowerCase(),
- userId.includes('@') ? userId : null,
- userId.includes('@') ? userId.toLowerCase() : null, targetUser.uid
- ].filter(Boolean) as string[];
-
- const uniqueCandidates = Array.from(new Set(candidates));
- let updated = false;
- for (const id of uniqueCandidates) {
- const docRef = doc(db, 'admins', id);
- if ((await getDoc(docRef)).exists()) {
- await updateDoc(docRef, sanitizedData);
- updated = true;
- break;
- }
- }
- if (!updated) {
- const memberRef = doc(db, 'members', targetUser.uid || userId);
- if ((await getDoc(memberRef)).exists()) {
- await updateDoc(memberRef, sanitizedData);
- updated = true;
- } else {
- throw new Error(`Could not find admin document.`);
- }
- }
- } else {
- await updateDoc(doc(db, 'members', userId), sanitizedData);
- }
+ throw new Error(`Could not find admin document.`);
}
-
- setUsers((prev: any[]) => prev.map(u => u.uid === userId ? { ...u, ...sanitizedData } : u));
- setAdmins((prev: any[]) => prev.map(a => (a.uid === userId || a.email === userId) ? { ...a, ...sanitizedData } : a));
- setSelectedUser((prev: any) => ({ ...prev, ...sanitizedData }));
- alert("User updated successfully.");
- } catch (error) {
- console.error("Error updating user:", error);
- alert("Failed to update user.");
+ }
+ } else {
+ await updateDoc(doc(db, 'members', userId), sanitizedData);
}
- };
-
- const fetchUserProjects = async (userId: string) => {
- setLoadingProjects(true);
- try {
- const projectsRef = collection(db, 'members', userId, 'projects');
- const snapshot = await getDocs(projectsRef);
- const projectsList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
- setUserProjects(projectsList);
- } catch (error) {
- console.error("Error fetching user projects:", error);
- } finally {
- setLoadingProjects(false);
- }
- };
-
- const handleDeleteProject = async (userId: string, projectId: string) => {
- if (!confirm("Delete this project?")) return;
- try {
- await deleteDoc(doc(db, 'members', userId, 'projects', projectId));
- setUserProjects(prev => prev.filter(p => p.id !== projectId));
- } catch (error) {
- console.error("Error deleting project:", error);
- }
- };
-
- const handleGlobalDeleteProject = async (projectId: string) => {
- if (!confirm("Delete this project globally?")) return;
- try {
- await deleteDoc(doc(db, 'projects', projectId));
- setProjects(prev => prev.filter(p => p.id !== projectId));
- } catch (error) {
- console.error("Error deleting project:", error);
- }
- };
-
- const handleDeleteDiscussion = async (discussionId: string) => {
- if (!confirm("Delete this discussion?")) return;
- try {
- await deleteDoc(doc(db, 'discussions', discussionId));
- setDiscussions(prev => prev.filter(d => d.id !== discussionId));
- } catch (error) {
- console.error("Error deleting discussion:", error);
+ }
+
+ setUsers((prev: any[]) =>
+ prev.map((u) => (u.uid === userId ? { ...u, ...sanitizedData } : u))
+ );
+ setAdmins((prev: any[]) =>
+ prev.map((a) =>
+ a.uid === userId || a.email === userId
+ ? { ...a, ...sanitizedData }
+ : a
+ )
+ );
+ setSelectedUser((prev: any) => ({ ...prev, ...sanitizedData }));
+ alert('User updated successfully.');
+ } catch (error) {
+ console.error('Error updating user:', error);
+ alert('Failed to update user.');
+ }
+ };
+
+ const fetchUserProjects = async (userId: string) => {
+ setLoadingProjects(true);
+ try {
+ const projectsRef = collection(db, 'members', userId, 'projects');
+ const snapshot = await getDocs(projectsRef);
+ const projectsList = snapshot.docs.map((doc) => ({
+ id: doc.id,
+ ...doc.data(),
+ }));
+ setUserProjects(projectsList);
+ } catch (error) {
+ console.error('Error fetching user projects:', error);
+ } finally {
+ setLoadingProjects(false);
+ }
+ };
+
+ const handleDeleteProject = async (userId: string, projectId: string) => {
+ if (!confirm('Delete this project?')) return;
+ try {
+ await deleteDoc(doc(db, 'members', userId, 'projects', projectId));
+ setUserProjects((prev) => prev.filter((p) => p.id !== projectId));
+ } catch (error) {
+ console.error('Error deleting project:', error);
+ }
+ };
+
+ const handleGlobalDeleteProject = async (projectId: string) => {
+ if (!confirm('Delete this project globally?')) return;
+ try {
+ await deleteDoc(doc(db, 'projects', projectId));
+ setProjects((prev) => prev.filter((p) => p.id !== projectId));
+ } catch (error) {
+ console.error('Error deleting project:', error);
+ }
+ };
+
+ const handleDeleteDiscussion = async (discussionId: string) => {
+ if (!confirm('Delete this discussion?')) return;
+ try {
+ await deleteDoc(doc(db, 'discussions', discussionId));
+ setDiscussions((prev) => prev.filter((d) => d.id !== discussionId));
+ } catch (error) {
+ console.error('Error deleting discussion:', error);
+ }
+ };
+
+ const openUserModal = (user: any) => {
+ setSelectedUser(user);
+ fetchUserProjects(user.uid);
+ };
+
+ const handleResetFollowers = async (userId: string) => {
+ if (
+ !confirm('Are you sure you want to remove all followers for this user?')
+ )
+ return;
+ try {
+ const targetUser = users.find((u) => u.uid === userId);
+ const collectionName =
+ targetUser?.role === 'admin' ? 'admins' : 'members';
+ await updateDoc(doc(db, collectionName, userId), { followers: [] });
+ setUsers((prev) =>
+ prev.map((u) => (u.uid === userId ? { ...u, followers: [] } : u))
+ );
+ setSelectedUser((prev: any) => ({ ...prev, followers: [] }));
+ alert('Followers reset successfully.');
+ } catch (error) {
+ console.error('Error resetting followers:', error);
+ }
+ };
+
+ const toggleBadge = async (
+ targetUser: any,
+ badgeId: string,
+ points: number
+ ) => {
+ if (!targetUser) return;
+ const hasBadge = targetUser.achievements?.includes(badgeId);
+ const collectionName = targetUser.role === 'admin' ? 'admins' : 'members';
+ const userDocId =
+ targetUser.role === 'admin' ? targetUser.email : targetUser.uid;
+ const userRefCorrect = doc(db, collectionName, userDocId);
+ const badgeRef = doc(db, 'earned_badges', `${targetUser.uid}_${badgeId}`);
+ const leaderboardRef = doc(db, 'leaderboard', targetUser.uid);
+
+ try {
+ if (hasBadge) {
+ await updateDoc(userRefCorrect, {
+ achievements: arrayRemove(badgeId),
+ points: increment(-points),
+ });
+ await deleteDoc(badgeRef);
+ await setDoc(
+ leaderboardRef,
+ { points: increment(-points) },
+ { merge: true }
+ );
+
+ setBadgeUser((prev: any) =>
+ prev
+ ? {
+ ...prev,
+ achievements: prev.achievements?.filter(
+ (id: string) => id !== badgeId
+ ),
+ points: (prev.points || 0) - points,
+ }
+ : null
+ );
+
+ setUsers((prev: any[]) =>
+ prev.map((u) =>
+ u.uid === targetUser.uid
+ ? {
+ ...u,
+ achievements: u.achievements?.filter(
+ (id: string) => id !== badgeId
+ ),
+ points: (u.points || 0) - points,
+ }
+ : u
+ )
+ );
+ } else {
+ await updateDoc(userRefCorrect, {
+ achievements: arrayUnion(badgeId),
+ points: increment(points),
+ });
+ await setDoc(badgeRef, {
+ userId: targetUser.uid,
+ badgeId,
+ awardedAt: serverTimestamp(),
+ awardedBy: 'admin',
+ });
+ await setDoc(
+ leaderboardRef,
+ { points: increment(points) },
+ { merge: true }
+ );
+
+ setBadgeUser((prev: any) =>
+ prev
+ ? {
+ ...prev,
+ achievements: [...(prev.achievements || []), badgeId],
+ points: (prev.points || 0) + points,
+ }
+ : null
+ );
+
+ setUsers((prev: any[]) =>
+ prev.map((u) =>
+ u.uid === targetUser.uid
+ ? {
+ ...u,
+ achievements: [...(u.achievements || []), badgeId],
+ points: (u.points || 0) + points,
+ }
+ : u
+ )
+ );
+ }
+ } catch (error) {
+ console.error('Error toggling badge:', error);
+ alert('Failed to update badge.');
+ }
+ };
+
+ const handleMigrateProjects = async () => {
+ if (!confirm('Start Project Migration?')) return;
+ setMigrating(true);
+ setMigrationLog(['Starting migration...']);
+ try {
+ const projectsRef = collection(db, 'projects');
+ const snapshot = await getDocs(projectsRef);
+ setMigrationLog((prev: string[]) => [
+ ...prev,
+ `Found ${snapshot.size} projects.`,
+ ]);
+
+ let success = 0,
+ skipped = 0,
+ errors = 0;
+ for (const docSnap of snapshot.docs) {
+ const data = docSnap.data();
+ const projectId = docSnap.id;
+ const userId = data.userId;
+
+ if (!userId) {
+ skipped++;
+ continue;
}
- };
-
- const openUserModal = (user: any) => {
- setSelectedUser(user);
- fetchUserProjects(user.uid);
- };
-
- const handleResetFollowers = async (userId: string) => {
- if (!confirm("Are you sure you want to remove all followers for this user?")) return;
try {
- const targetUser = users.find(u => u.uid === userId);
- const collectionName = targetUser?.role === 'admin' ? 'admins' : 'members';
- await updateDoc(doc(db, collectionName, userId), { followers: [] });
- setUsers(prev => prev.map(u => u.uid === userId ? { ...u, followers: [] } : u));
- setSelectedUser((prev: any) => ({ ...prev, followers: [] }));
- alert("Followers reset successfully.");
- } catch (error) {
- console.error("Error resetting followers:", error);
+ const targetRef = doc(db, 'members', userId, 'projects', projectId);
+ if ((await getDoc(targetRef)).exists()) {
+ skipped++;
+ } else {
+ await setDoc(targetRef, data);
+ success++;
+ }
+ } catch (err) {
+ errors++;
}
- };
-
- const toggleBadge = async (targetUser: any, badgeId: string, points: number) => {
- if (!targetUser) return;
- const hasBadge = targetUser.achievements?.includes(badgeId);
- const collectionName = targetUser.role === 'admin' ? 'admins' : 'members';
- const userDocId = targetUser.role === 'admin' ? targetUser.email : targetUser.uid;
- const userRefCorrect = doc(db, collectionName, userDocId);
- const badgeRef = doc(db, 'earned_badges', `${targetUser.uid}_${badgeId}`);
- const leaderboardRef = doc(db, 'leaderboard', targetUser.uid);
-
- try {
- if (hasBadge) {
- await updateDoc(userRefCorrect, {
- achievements: arrayRemove(badgeId),
- points: increment(-points)
- });
- await deleteDoc(badgeRef);
- await setDoc(leaderboardRef, { points: increment(-points) }, { merge: true });
-
- setBadgeUser((prev: any) => prev ? {
- ...prev, achievements: prev.achievements?.filter((id: string) => id !== badgeId), points: (prev.points || 0) - points
- } : null);
-
- setUsers((prev: any[]) => prev.map(u => u.uid === targetUser.uid ? {
- ...u, achievements: u.achievements?.filter((id: string) => id !== badgeId), points: (u.points || 0) - points
- } : u));
+ }
+ alert(
+ `Migration Complete!\nSuccess: ${success}\nSkipped: ${skipped}\nErrors: ${errors}`
+ );
+ } catch (error) {
+ console.error('Migration failed:', error);
+ } finally {
+ setMigrating(false);
+ }
+ };
+
+ const handleRecalculateAll = async () => {
+ if (!confirm('Are you sure you want to RECALCULATE ALL BADGES?')) return;
+ setMigrating(true);
+ try {
+ alert('Recalculation logic bypassed in safe merge mode.');
+ } finally {
+ setMigrating(false);
+ }
+ };
+
+ // Check for existing session
+ useEffect(() => {
+ const checkStatus = async () => {
+ if (!user) return;
+ if (isAdminAuthenticated) return;
+
+ if (user.email === SUPER_ADMIN_EMAIL) {
+ const sessionKey = sessionStorage.getItem('admin_session_key');
+ if (sessionKey) {
+ try {
+ const keyDoc = await getDoc(doc(db, 'superadmin_keys', 'config'));
+ if (keyDoc.exists() && keyDoc.data().value === sessionKey) {
+ // verifyAdminKey(sessionKey, true); // Uncomment if verifyAdminKey exists
} else {
- await updateDoc(userRefCorrect, {
- achievements: arrayUnion(badgeId), points: increment(points)
- });
- await setDoc(badgeRef, {
- userId: targetUser.uid, badgeId, awardedAt: serverTimestamp(), awardedBy: 'admin'
- });
- await setDoc(leaderboardRef, { points: increment(points) }, { merge: true });
-
- setBadgeUser((prev: any) => prev ? {
- ...prev, achievements: [...(prev.achievements || []), badgeId], points: (prev.points || 0) + points
- } : null);
-
- setUsers((prev: any[]) => prev.map(u => u.uid === targetUser.uid ? {
- ...u, achievements: [...(u.achievements || []), badgeId], points: (u.points || 0) + points
- } : u));
+ sessionStorage.removeItem('admin_session_key');
}
- } catch (error) {
- console.error("Error toggling badge:", error);
- alert("Failed to update badge.");
+ } catch (error) {
+ sessionStorage.removeItem('admin_session_key');
+ }
}
+ }
};
-
- const handleMigrateProjects = async () => {
- if (!confirm("Start Project Migration?")) return;
- setMigrating(true);
- setMigrationLog(['Starting migration...']);
- try {
- const projectsRef = collection(db, 'projects');
- const snapshot = await getDocs(projectsRef);
- setMigrationLog((prev: string[]) => [...prev, `Found ${snapshot.size} projects.`]);
-
- let success = 0, skipped = 0, errors = 0;
- for (const docSnap of snapshot.docs) {
- const data = docSnap.data();
- const projectId = docSnap.id;
- const userId = data.userId;
-
- if (!userId) { skipped++; continue; }
+ checkStatus();
+ }, [user, isAdminAuthenticated]);
+
+ // ==========================================
+ // 3. UI RENDER
+ // ==========================================
+ return (
+
+
+
Admin Dashboard
+
+ {/* Maintenance Section */}
+
+
+ Maintenance Mode
+
+
+
+
Global Maintenance Mode
+
+ Blocks all non-admin users.
+
+
+
{
try {
- const targetRef = doc(db, 'members', userId, 'projects', projectId);
- if ((await getDoc(targetRef)).exists()) {
- skipped++;
- } else {
- await setDoc(targetRef, data);
- success++;
- }
- } catch (err) { errors++; }
- }
- alert(`Migration Complete!\nSuccess: ${success}\nSkipped: ${skipped}\nErrors: ${errors}`);
- } catch (error) {
- console.error("Migration failed:", error);
- } finally {
- setMigrating(false);
- }
- };
-
- const handleRecalculateAll = async () => {
- if (!confirm("Are you sure you want to RECALCULATE ALL BADGES?")) return;
- setMigrating(true);
- try {
- alert("Recalculation logic bypassed in safe merge mode.");
- } finally {
- setMigrating(false);
- }
- };
-
- // Check for existing session
- useEffect(() => {
- const checkStatus = async () => {
- if (!user) return;
- if (isAdminAuthenticated) return;
-
- if (user.email === SUPER_ADMIN_EMAIL) {
- const sessionKey = sessionStorage.getItem('admin_session_key');
- if (sessionKey) {
- try {
- const keyDoc = await getDoc(doc(db, 'superadmin_keys', 'config'));
- if (keyDoc.exists() && keyDoc.data().value === sessionKey) {
- // verifyAdminKey(sessionKey, true); // Uncomment if verifyAdminKey exists
- } else {
- sessionStorage.removeItem('admin_session_key');
- }
- } catch (error) {
- sessionStorage.removeItem('admin_session_key');
- }
+ const newState = !maintenanceMode;
+ await updateDoc(doc(db, 'settings', 'general'), {
+ maintenanceMode: newState,
+ });
+ } catch (error) {
+ console.error('Error toggling maintenance mode:', error);
+ alert('Failed to toggle maintenance mode.');
}
- }
- };
- checkStatus();
- }, [user, isAdminAuthenticated]);
-
- // ==========================================
- // 3. UI RENDER
- // ==========================================
- return (
-
-
-
Admin Dashboard
-
- {/* Maintenance Section */}
-
-
- Maintenance Mode
-
-
-
-
Global Maintenance Mode
-
Blocks all non-admin users.
-
-
{
- try {
- const newState = !maintenanceMode;
- await updateDoc(doc(db, 'settings', 'general'), { maintenanceMode: newState });
- } catch (error) {
- console.error("Error toggling maintenance mode:", error);
- alert("Failed to toggle maintenance mode.");
- }
- }}
- className={`px-6 py-2 rounded-md font-bold ${maintenanceMode ? 'bg-red-500 text-white' : 'bg-green-600 text-white'}`}
- >
- {maintenanceMode ? 'Turn OFF' : 'Turn ON'}
-
-
-
- Maintenance Message
- setMaintenanceMsg(e.target.value)}
- className="w-full p-2 bg-muted border border-border rounded-md"
- />
- {
- try {
- await updateDoc(doc(db, 'settings', 'general'), { maintenanceMessage: maintenanceMsg });
- alert("Message saved!");
- } catch (error) {
- console.error("Error saving maintenance message:", error);
- alert("Failed to save maintenance message.");
- }
- }}
- className="mt-2 text-sm bg-primary text-white px-4 py-1 rounded"
- >
- Save Message
-
-
-
-
- {/* DO NOT DELETE THE REST OF THE UI BELOW THIS IN YOUR FILE */}
- {hasMore && (
-
-
- {loadingMore ? 'Loading...' : 'Load More'}
-
-
- )}
-
-
+ }}
+ className={`px-6 py-2 rounded-md font-bold ${maintenanceMode ? 'bg-red-500 text-white' : 'bg-green-600 text-white'}`}
+ >
+ {maintenanceMode ? 'Turn OFF' : 'Turn ON'}
+
+
+
+
+ Maintenance Message
+
+ setMaintenanceMsg(e.target.value)}
+ className="w-full p-2 bg-muted border border-border rounded-md"
+ />
+ {
+ try {
+ await updateDoc(doc(db, 'settings', 'general'), {
+ maintenanceMessage: maintenanceMsg,
+ });
+ alert('Message saved!');
+ } catch (error) {
+ console.error('Error saving maintenance message:', error);
+ alert('Failed to save maintenance message.');
+ }
+ }}
+ className="mt-2 text-sm bg-primary text-white px-4 py-1 rounded"
+ >
+ Save Message
+
+
- );
-}
\ No newline at end of file
+
+ {/* DO NOT DELETE THE REST OF THE UI BELOW THIS IN YOUR FILE */}
+ {hasMore && (
+
+
+ {loadingMore ? 'Loading...' : 'Load More'}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/admin/MaintenanceOverlay.tsx b/src/components/admin/MaintenanceOverlay.tsx
index 2745fcd2..569b8467 100644
--- a/src/components/admin/MaintenanceOverlay.tsx
+++ b/src/components/admin/MaintenanceOverlay.tsx
@@ -1,12 +1,22 @@
export default function MaintenanceOverlay({ message }: { message: string }) {
return (
-
-
Under Maintenance
+
+
+ Under Maintenance
+
{message || "We're updating our systems to serve you better."}
);
-}
\ No newline at end of file
+}
diff --git a/src/components/assistant/assistant-header.tsx b/src/components/assistant/assistant-header.tsx
index 26f4917a..78fc6ebf 100644
--- a/src/components/assistant/assistant-header.tsx
+++ b/src/components/assistant/assistant-header.tsx
@@ -1,58 +1,60 @@
-"use client";
+'use client';
-import { motion } from "framer-motion";
-import { X, Sparkles, Maximize2 } from "lucide-react";
-import { cn } from "@/lib/utils";
+import { motion } from 'framer-motion';
+import { X, Sparkles, Maximize2 } from 'lucide-react';
+import { cn } from '@/lib/utils';
interface AssistantHeaderProps {
- onClose?: () => void;
- onExpand?: () => void;
+ onClose?: () => void;
+ onExpand?: () => void;
}
export function AssistantHeader({ onClose, onExpand }: AssistantHeaderProps) {
- return (
-
-
-
-
-
-
-
DevPath Assistant
-
Your AI companion
-
-
+ return (
+
+
+
+
+
+
+
+ DevPath Assistant
+
+
Your AI companion
+
+
-
- );
+
+
+
+
+
+ );
}
diff --git a/src/components/assistant/assistant-input.tsx b/src/components/assistant/assistant-input.tsx
index 690cd0b6..be1f0a5e 100644
--- a/src/components/assistant/assistant-input.tsx
+++ b/src/components/assistant/assistant-input.tsx
@@ -1,119 +1,121 @@
-"use client";
+'use client';
-import { useRef, useState, useEffect } from "react";
-import { motion } from "framer-motion";
-import { Send, Paperclip } from "lucide-react";
-import { cn } from "@/lib/utils";
+import { useRef, useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+import { Send, Paperclip } from 'lucide-react';
+import { cn } from '@/lib/utils';
interface AssistantInputProps {
- onSend?: (message: string) => void;
- disabled?: boolean;
- placeholder?: string;
+ onSend?: (message: string) => void;
+ disabled?: boolean;
+ placeholder?: string;
}
export function AssistantInput({
- onSend,
- disabled = false,
- placeholder = "Ask me anything...",
+ onSend,
+ disabled = false,
+ placeholder = 'Ask me anything...',
}: AssistantInputProps) {
- const [value, setValue] = useState("");
- const [isFocused, setIsFocused] = useState(false);
- const textareaRef = useRef
(null);
+ const [value, setValue] = useState('');
+ const [isFocused, setIsFocused] = useState(false);
+ const textareaRef = useRef(null);
- useEffect(() => {
- const textarea = textareaRef.current;
- if (!textarea) return;
+ useEffect(() => {
+ const textarea = textareaRef.current;
+ if (!textarea) return;
- textarea.style.height = "0px";
- textarea.style.height = `${Math.min(textarea.scrollHeight, 100)}px`;
- }, [value]);
+ textarea.style.height = '0px';
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 100)}px`;
+ }, [value]);
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- if (!value.trim() || disabled) return;
- onSend?.(value.trim());
- setValue("");
- };
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- handleSubmit(e as unknown as React.FormEvent);
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!value.trim() || disabled) return;
+ onSend?.(value.trim());
+ setValue('');
+ };
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit(e as unknown as React.FormEvent);
}
-};
+ };
- return (
-
+ );
}
diff --git a/src/components/assistant/assistant-panel.tsx b/src/components/assistant/assistant-panel.tsx
index 9d8fd4ea..18f88172 100644
--- a/src/components/assistant/assistant-panel.tsx
+++ b/src/components/assistant/assistant-panel.tsx
@@ -1,72 +1,75 @@
-"use client";
+'use client';
-import { AnimatePresence, motion } from "framer-motion";
-import { AssistantHeader } from "./assistant-header";
-import { SuggestionCards } from "./suggestion-cards";
-import { AssistantInput } from "./assistant-input";
-import { cn } from "@/lib/utils";
+import { AnimatePresence, motion } from 'framer-motion';
+import { AssistantHeader } from './assistant-header';
+import { SuggestionCards } from './suggestion-cards';
+import { AssistantInput } from './assistant-input';
+import { cn } from '@/lib/utils';
interface AssistantPanelProps {
- isOpen: boolean;
- onClose?: () => void;
- onExpand?: () => void;
- onSend?: (message: string) => void;
- onSuggestionSelect?: (id: string) => void;
+ isOpen: boolean;
+ onClose?: () => void;
+ onExpand?: () => void;
+ onSend?: (message: string) => void;
+ onSuggestionSelect?: (id: string) => void;
}
export function AssistantPanel({
- isOpen,
- onClose,
- onExpand,
- onSend,
- onSuggestionSelect,
+ isOpen,
+ onClose,
+ onExpand,
+ onSend,
+ onSuggestionSelect,
}: AssistantPanelProps) {
- return (
-
- {isOpen && (
- <>
-
+ return (
+
+ {isOpen && (
+ <>
+
-
-
+
+
-
-
-
-
Hi there! 👋
-
- How can I help you today? Pick an action below or ask me anything.
-
-
+
+
+
+
+ Hi there! 👋
+
+
+ How can I help you today? Pick an action below or ask me
+ anything.
+
+
-
-
-
+
+
+
-
-
- >
- )}
-
- );
+
+
+ >
+ )}
+
+ );
}
diff --git a/src/components/assistant/floating-assistant-button.tsx b/src/components/assistant/floating-assistant-button.tsx
index becad073..5d74b454 100644
--- a/src/components/assistant/floating-assistant-button.tsx
+++ b/src/components/assistant/floating-assistant-button.tsx
@@ -1,67 +1,67 @@
-"use client";
+'use client';
-import { motion } from "framer-motion";
-import { Sparkles, MessageCircle } from "lucide-react";
-import { cn } from "@/lib/utils";
+import { motion } from 'framer-motion';
+import { Sparkles, MessageCircle } from 'lucide-react';
+import { cn } from '@/lib/utils';
interface FloatingAssistantButtonProps {
- isOpen?: boolean;
- onClick?: () => void;
- hasNotification?: boolean;
+ isOpen?: boolean;
+ onClick?: () => void;
+ hasNotification?: boolean;
}
export function FloatingAssistantButton({
- isOpen = false,
- onClick,
- hasNotification = false,
+ isOpen = false,
+ onClick,
+ hasNotification = false,
}: FloatingAssistantButtonProps) {
- return (
-
-
- {isOpen ? (
-
- ) : (
-
- )}
-
+ return (
+
+
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
- {hasNotification && !isOpen && (
-
- )}
+ {hasNotification && !isOpen && (
+
+ )}
-
-
- );
+
+
+ );
}
diff --git a/src/components/assistant/floating-assistant.tsx b/src/components/assistant/floating-assistant.tsx
index 44cbf6b3..9d00e539 100644
--- a/src/components/assistant/floating-assistant.tsx
+++ b/src/components/assistant/floating-assistant.tsx
@@ -1,46 +1,46 @@
-"use client";
+'use client';
-import { useState } from "react";
-import { AnimatePresence } from "framer-motion";
-import { FloatingAssistantButton } from "./floating-assistant-button";
-import { AssistantPanel } from "./assistant-panel";
+import { useState } from 'react';
+import { AnimatePresence } from 'framer-motion';
+import { FloatingAssistantButton } from './floating-assistant-button';
+import { AssistantPanel } from './assistant-panel';
interface FloatingAssistantProps {
- onSend?: (message: string) => void;
- onSuggestionSelect?: (id: string) => void;
- hasNotification?: boolean;
+ onSend?: (message: string) => void;
+ onSuggestionSelect?: (id: string) => void;
+ hasNotification?: boolean;
}
export function FloatingAssistant({
- onSend,
- onSuggestionSelect,
- hasNotification = false,
+ onSend,
+ onSuggestionSelect,
+ hasNotification = false,
}: FloatingAssistantProps) {
- const [isOpen, setIsOpen] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
- return (
- <>
-
- setIsOpen(!isOpen)}
- hasNotification={hasNotification}
- />
-
+ return (
+ <>
+
+ setIsOpen(!isOpen)}
+ hasNotification={hasNotification}
+ />
+
- setIsOpen(false)}
- onExpand={() => {}}
- onSend={(message) => {
- onSend?.(message);
- setIsOpen(false);
- }}
- onSuggestionSelect={(id) => {
- onSuggestionSelect?.(id);
- setIsOpen(false);
- }}
- />
- >
- );
+ setIsOpen(false)}
+ onExpand={() => {}}
+ onSend={(message) => {
+ onSend?.(message);
+ setIsOpen(false);
+ }}
+ onSuggestionSelect={(id) => {
+ onSuggestionSelect?.(id);
+ setIsOpen(false);
+ }}
+ />
+ >
+ );
}
diff --git a/src/components/assistant/suggestion-cards.tsx b/src/components/assistant/suggestion-cards.tsx
index 97b9615c..2c97bbd6 100644
--- a/src/components/assistant/suggestion-cards.tsx
+++ b/src/components/assistant/suggestion-cards.tsx
@@ -1,70 +1,72 @@
-"use client";
+'use client';
-import { motion } from "framer-motion";
+import { motion } from 'framer-motion';
import {
- Briefcase,
- FileText,
- Compass,
- BookOpen,
- Zap,
- Gift,
- type LucideIcon,
-} from "lucide-react";
-import { cn } from "@/lib/utils";
+ Briefcase,
+ FileText,
+ Compass,
+ BookOpen,
+ Zap,
+ Gift,
+ type LucideIcon,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
interface SuggestionCard {
- id: string;
- title: string;
- icon: LucideIcon;
+ id: string;
+ title: string;
+ icon: LucideIcon;
}
const suggestions: SuggestionCard[] = [
- { id: "internships", title: "Find internships", icon: Briefcase },
- { id: "resume", title: "Resume review", icon: FileText },
- { id: "roadmap", title: "Career roadmap", icon: Compass },
- { id: "interview", title: "Interview prep", icon: BookOpen },
- { id: "hackathons", title: "Hackathons", icon: Zap },
- { id: "resources", title: "Suggest resources", icon: Gift },
+ { id: 'internships', title: 'Find internships', icon: Briefcase },
+ { id: 'resume', title: 'Resume review', icon: FileText },
+ { id: 'roadmap', title: 'Career roadmap', icon: Compass },
+ { id: 'interview', title: 'Interview prep', icon: BookOpen },
+ { id: 'hackathons', title: 'Hackathons', icon: Zap },
+ { id: 'resources', title: 'Suggest resources', icon: Gift },
];
interface SuggestionCardsProps {
- onSelect?: (id: string) => void;
+ onSelect?: (id: string) => void;
}
export function SuggestionCards({ onSelect }: SuggestionCardsProps) {
- return (
-
- {suggestions.map((card, idx) => {
- const Icon = card.icon;
+ return (
+
+ {suggestions.map((card, idx) => {
+ const Icon = card.icon;
- return (
-
onSelect?.(card.id)}
- initial={{ opacity: 0, y: 8 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ delay: 0.05 * idx, duration: 0.25 }}
- whileHover={{ y: -2, borderColor: "rgba(34, 211, 238, 0.5)" }}
- whileTap={{ scale: 0.95 }}
- className={cn(
- "group relative overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]",
- "p-3 text-left transition-all",
- "hover:border-cyan-400/30 hover:bg-white/[0.06]",
- "active:bg-white/[0.08]"
- )}
- >
-
+ return (
+ onSelect?.(card.id)}
+ initial={{ opacity: 0, y: 8 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.05 * idx, duration: 0.25 }}
+ whileHover={{ y: -2, borderColor: 'rgba(34, 211, 238, 0.5)' }}
+ whileTap={{ scale: 0.95 }}
+ className={cn(
+ 'group relative overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]',
+ 'p-3 text-left transition-all',
+ 'hover:border-cyan-400/30 hover:bg-white/[0.06]',
+ 'active:bg-white/[0.08]'
+ )}
+ >
+
-
-
- );
- })}
-
- );
+
+
+
+
+
+ {card.title}
+
+
+
+ );
+ })}
+
+ );
}
diff --git a/src/components/auth/AdminKeyModal.tsx b/src/components/auth/AdminKeyModal.tsx
index b01ae5b5..30836b53 100644
--- a/src/components/auth/AdminKeyModal.tsx
+++ b/src/components/auth/AdminKeyModal.tsx
@@ -6,111 +6,122 @@ import { db } from '@/lib/firebase';
import { doc, getDoc } from 'firebase/firestore';
interface AdminKeyModalProps {
- isOpen: boolean;
- onVerified: () => void;
- onCancel: () => void;
+ isOpen: boolean;
+ onVerified: () => void;
+ onCancel: () => void;
}
-export default function AdminKeyModal({ isOpen, onVerified, onCancel }: AdminKeyModalProps) {
- const { verifyAdmin } = useAuth();
- const [key, setKey] = useState('');
- const [error, setError] = useState('');
- const [isLoading, setIsLoading] = useState(false);
+export default function AdminKeyModal({
+ isOpen,
+ onVerified,
+ onCancel,
+}: AdminKeyModalProps) {
+ const { verifyAdmin } = useAuth();
+ const [key, setKey] = useState('');
+ const [error, setError] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setError('');
- setIsLoading(true);
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setIsLoading(true);
- try {
- // Fetch the key securely on the client side since API routes are disabled in static export
- const docRef = doc(db, 'admin_keys', 'config');
- const docSnap = await getDoc(docRef);
+ try {
+ // Fetch the key securely on the client side since API routes are disabled in static export
+ const docRef = doc(db, 'admin_keys', 'config');
+ const docSnap = await getDoc(docRef);
- if (docSnap.exists() && docSnap.data().value === key) {
- verifyAdmin();
- onVerified();
- } else {
- setError('Invalid Admin Key. Please try again.');
- }
- } catch (err) {
- console.error('Verification error:', err);
- setError('Network error. Please try again.');
- } finally {
- setIsLoading(false);
- }
- };
+ if (docSnap.exists() && docSnap.data().value === key) {
+ verifyAdmin();
+ onVerified();
+ } else {
+ setError('Invalid Admin Key. Please try again.');
+ }
+ } catch (err) {
+ console.error('Verification error:', err);
+ setError('Network error. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
- return (
-
- {isOpen && (
-
-
-
-
-
-
-
-
Admin Verification
-
- Please enter the Admin Key to continue.
-
-
+ return (
+
+ {isOpen && (
+
+
+
+
+
+
+
+
Admin Verification
+
+ Please enter the Admin Key to continue.
+
+
-
-
-
Admin Key
-
-
- setKey(e.target.value)}
- className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
- placeholder="Enter key..."
- autoFocus
- required
- />
-
-
+
+
+
+ Admin Key
+
+
+
+ setKey(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
+ placeholder="Enter key..."
+ autoFocus
+ required
+ />
+
+
- {error && (
-
- )}
+ {error && (
+
+ )}
-
-
- Cancel
-
-
- {isLoading ? (
-
- ) : (
- 'Verify'
- )}
-
-
-
-
-
+
+
+ Cancel
+
+
+ {isLoading ? (
+
+ ) : (
+ 'Verify'
+ )}
+
- )}
-
- );
-}
\ No newline at end of file
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/certificate/RankingsTable.tsx b/src/components/certificate/RankingsTable.tsx
index c6dc9976..1732f201 100644
--- a/src/components/certificate/RankingsTable.tsx
+++ b/src/components/certificate/RankingsTable.tsx
@@ -1,131 +1,201 @@
-
-"use client";
+'use client';
import { useState, ReactNode } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
-import { ChevronDown, ChevronUp, Trophy, Award, Zap, Lightbulb, Monitor, Play, Info } from 'lucide-react';
+import {
+ ChevronDown,
+ ChevronUp,
+ Trophy,
+ Award,
+ Zap,
+ Lightbulb,
+ Monitor,
+ Play,
+ Info,
+} from 'lucide-react';
import rankingsData from '@/data/rankings.json';
interface Ranking {
- rank: number;
- project: string;
- total: number;
- details: {
- originality: number;
- technical: number;
- design: number;
- innovation: number;
- presentation: number;
- };
+ rank: number;
+ project: string;
+ total: number;
+ details: {
+ originality: number;
+ technical: number;
+ design: number;
+ innovation: number;
+ presentation: number;
+ };
}
export default function RankingsTable() {
- return (
-
-
-
-
- Hall of Fame
-
-
Top 20 Rankings
-
- Celebrating the most innovative and impactful projects from HackFiesta.
-
-
+ return (
+
+
+
+
+ Hall of Fame
+
+
+ Top 20 Rankings
+
+
+ Celebrating the most innovative and impactful projects from
+ HackFiesta.
+
+
-
- {/* Header */}
-
-
Rank
-
Project Name
-
Action
-
+
+ {/* Header */}
+
+
Rank
+
Project Name
+
Action
+
- {/* Rows */}
-
- {rankingsData.map((item: Ranking) => (
-
- ))}
-
-
-
-
-
- Ranking Adjustments & Bonus Points
-
-
- +2 points - Solo Participation
- -1 point - For having an Advanced commit on 20th January
- Team Ecosage and AidLedger got +2 points for their Exceptional Video and PPT based explantion from our Content Head of Community.
- +1 point to Tiya for having a Cross Platform app and for full Production grade App
-
-
+ {/* Rows */}
+
+ {rankingsData.map((item: Ranking) => (
+
+ ))}
+
+
+
+
+
+ Ranking Adjustments & Bonus Points
- );
+
+
+ +2 points - Solo
+ Participation
+
+
+ -1 point - For
+ having an Advanced commit on 20th January
+
+
+ Team Ecosage and{' '}
+ AidLedger got{' '}
+ +2 points for
+ their Exceptional Video and PPT based explantion from our Content
+ Head of Community.
+
+
+ +1 point to{' '}
+ Tiya for having a
+ Cross Platform app and for full Production grade App
+
+
+
+
+ );
}
function RankingRow({ item }: { item: Ranking }) {
- const [isOpen, setIsOpen] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
- // Colors for ranks
- let rankColor = "text-slate-400";
- let bgHighlight = "";
- if (item.rank === 1) { rankColor = "text-yellow-400"; bgHighlight = "bg-yellow-500/5"; }
- else if (item.rank === 2) { rankColor = "text-slate-300"; bgHighlight = "bg-slate-300/5"; }
- else if (item.rank === 3) { rankColor = "text-amber-600"; bgHighlight = "bg-amber-600/5"; }
+ // Colors for ranks
+ let rankColor = 'text-slate-400';
+ let bgHighlight = '';
+ if (item.rank === 1) {
+ rankColor = 'text-yellow-400';
+ bgHighlight = 'bg-yellow-500/5';
+ } else if (item.rank === 2) {
+ rankColor = 'text-slate-300';
+ bgHighlight = 'bg-slate-300/5';
+ } else if (item.rank === 3) {
+ rankColor = 'text-amber-600';
+ bgHighlight = 'bg-amber-600/5';
+ }
- return (
-
-
setIsOpen(!isOpen)}
- className="grid grid-cols-12 gap-4 p-4 items-center cursor-pointer"
- >
-
- #{item.rank}
-
-
- {item.project}
-
-
- {item.total} pts
-
- {isOpen ? 'Close' : 'View Feedback'}
- {isOpen ? : }
-
-
-
-
- {/* Expanded Details */}
-
- {isOpen && (
-
-
- } />
- } />
- } />
- } />
- } />
-
-
- )}
-
+ return (
+
+
setIsOpen(!isOpen)}
+ className="grid grid-cols-12 gap-4 p-4 items-center cursor-pointer"
+ >
+
+ #{item.rank}
+
+
+ {item.project}
+
+
+
+ {item.total} pts
+
+
+ {isOpen ? 'Close' : 'View Feedback'}
+ {isOpen ? : }
+
- );
+
+
+ {/* Expanded Details */}
+
+ {isOpen && (
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+ )}
+
+
+ );
}
-function ScoreCard({ label, score, icon }: { label: string, score: number, icon: ReactNode }) {
- return (
-
-
{icon}
-
{label}
-
{score}
-
- );
+function ScoreCard({
+ label,
+ score,
+ icon,
+}: {
+ label: string;
+ score: number;
+ icon: ReactNode;
+}) {
+ return (
+
+
{icon}
+
+ {label}
+
+
{score}
+
+ );
}
diff --git a/src/components/common/CodeBlock.tsx b/src/components/common/CodeBlock.tsx
index d16487c2..c52fccf9 100644
--- a/src/components/common/CodeBlock.tsx
+++ b/src/components/common/CodeBlock.tsx
@@ -1,59 +1,59 @@
-"use client";
+'use client';
import { useEffect, useRef, useState } from 'react';
import { Check, Copy } from 'lucide-react';
interface CodeBlockProps {
- code: string;
- language: string;
+ code: string;
+ language: string;
}
export default function CodeBlock({ code, language }: CodeBlockProps) {
- const [isCopied, setIsCopied] = useState(false);
- const resetTimerRef = useRef
(null);
-
- useEffect(() => {
- return () => {
- if (resetTimerRef.current !== null) {
- clearTimeout(resetTimerRef.current);
- }
- };
- }, []);
-
- const handleCopy = async () => {
- try {
- await navigator.clipboard.writeText(code);
- setIsCopied(true);
-
- if (resetTimerRef.current !== null) {
- clearTimeout(resetTimerRef.current);
- }
-
- resetTimerRef.current = window.setTimeout(() => {
- setIsCopied(false);
- }, 2000);
- } catch {
- // Clipboard access can fail in unsupported or insecure contexts.
- }
+ const [isCopied, setIsCopied] = useState(false);
+ const resetTimerRef = useRef(null);
+
+ useEffect(() => {
+ return () => {
+ if (resetTimerRef.current !== null) {
+ clearTimeout(resetTimerRef.current);
+ }
};
-
- return (
-
-
- {isCopied ? : }
- {isCopied ? 'Copied' : 'Copy'}
-
-
-
-
- {code}
-
-
-
- );
-}
\ No newline at end of file
+ }, []);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(code);
+ setIsCopied(true);
+
+ if (resetTimerRef.current !== null) {
+ clearTimeout(resetTimerRef.current);
+ }
+
+ resetTimerRef.current = window.setTimeout(() => {
+ setIsCopied(false);
+ }, 2000);
+ } catch {
+ // Clipboard access can fail in unsupported or insecure contexts.
+ }
+ };
+
+ return (
+
+
+ {isCopied ? : }
+ {isCopied ? 'Copied' : 'Copy'}
+
+
+
+
+ {code}
+
+
+
+ );
+}
diff --git a/src/components/community/CreateDiscussionModal.tsx b/src/components/community/CreateDiscussionModal.tsx
index 0647fff9..4f977058 100644
--- a/src/components/community/CreateDiscussionModal.tsx
+++ b/src/components/community/CreateDiscussionModal.tsx
@@ -1,9 +1,15 @@
-"use client";
+'use client';
import { useState } from 'react';
import { X, Loader2 } from 'lucide-react';
import { db } from '@/lib/firebase';
-import {writeBatch, doc, collection, serverTimestamp, increment } from 'firebase/firestore';
+import {
+ writeBatch,
+ doc,
+ collection,
+ serverTimestamp,
+ increment,
+} from 'firebase/firestore';
import { POINTS } from '@/lib/points';
import ReactMarkdown from 'react-markdown';
import DOMPurify from 'dompurify';
@@ -11,155 +17,179 @@ import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
interface CreateDiscussionModalProps {
- isOpen: boolean;
- onClose: () => void;
- userId: string;
- userName: string;
- onSuccess: () => void;
+ isOpen: boolean;
+ onClose: () => void;
+ userId: string;
+ userName: string;
+ onSuccess: () => void;
}
-export default function CreateDiscussionModal({ isOpen, onClose, userId, userName, onSuccess }: CreateDiscussionModalProps) {
- const [title, setTitle] = useState('');
- const [content, setContent] = useState('');
- const [contentTab, setContentTab] = useState<'write' | 'preview'>('write');
- const [tags, setTags] = useState('');
- const [loading, setLoading] = useState(false);
+export default function CreateDiscussionModal({
+ isOpen,
+ onClose,
+ userId,
+ userName,
+ onSuccess,
+}: CreateDiscussionModalProps) {
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [contentTab, setContentTab] = useState<'write' | 'preview'>('write');
+ const [tags, setTags] = useState('');
+ const [loading, setLoading] = useState(false);
- if (!isOpen) return null;
+ if (!isOpen) return null;
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setLoading(true);
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
- try {
- const tagsArray = tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
- const batch = writeBatch(db);
-
- const discussionRef = doc(collection(db, 'discussions'));
- const memberRef = doc(db, 'members', userId);
- batch.set(discussionRef,{
- authorId: userId,
- authorName: userName,
- title,
- content,
- tags: tagsArray,
- likes: [],
- replyCount: 0,
- createdAt: serverTimestamp()
- });
- batch.update(memberRef, {
- points: increment(POINTS.CREATE_DISCUSSION)
- });
- await batch.commit();
- setTitle('');
- setContent('');
- setContentTab('write');
- setTags('');
- onSuccess();
- onClose();
- } catch (error) {
- console.error("Error creating discussion:", error);
- alert("Failed to create discussion. Please try again.");
- } finally {
- setLoading(false);
- }
- };
+ try {
+ const tagsArray = tags
+ .split(',')
+ .map((tag) => tag.trim())
+ .filter((tag) => tag.length > 0);
+ const batch = writeBatch(db);
- return (
-
-
-
-
Start a Discussion
-
-
-
-
+ const discussionRef = doc(collection(db, 'discussions'));
+ const memberRef = doc(db, 'members', userId);
+ batch.set(discussionRef, {
+ authorId: userId,
+ authorName: userName,
+ title,
+ content,
+ tags: tagsArray,
+ likes: [],
+ replyCount: 0,
+ createdAt: serverTimestamp(),
+ });
+ batch.update(memberRef, {
+ points: increment(POINTS.CREATE_DISCUSSION),
+ });
+ await batch.commit();
+ setTitle('');
+ setContent('');
+ setContentTab('write');
+ setTags('');
+ onSuccess();
+ onClose();
+ } catch (error) {
+ console.error('Error creating discussion:', error);
+ alert('Failed to create discussion. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
-
-
- Title
- setTitle(e.target.value)}
- className="w-full bg-background border border-border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50"
- placeholder="What's on your mind?"
- required
- />
-
+ return (
+
+
+
+
Start a Discussion
+
+
+
+
-
-
-
Content
-
- setContentTab('write')}
- className={`px-3 py-1 text-xs rounded-md transition-all ${contentTab === 'write' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
- >
- Write
-
- setContentTab('preview')}
- className={`px-3 py-1 text-xs rounded-md transition-all ${contentTab === 'preview' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
- >
- Preview
-
-
-
+
+
+ Title
+ setTitle(e.target.value)}
+ className="w-full bg-background border border-border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50"
+ placeholder="What's on your mind?"
+ required
+ />
+
- {contentTab === 'write' ? (
- setContent(e.target.value)}
- className="w-full bg-background border border-border rounded-lg px-4 py-2 min-h-[200px] focus:outline-none focus:ring-2 focus:ring-primary/50 resize-y"
- placeholder="Share your thoughts, ask questions, or showcase your work... (Markdown supported)"
- required
- />
- ) : (
-
- {content ? (
-
- {DOMPurify.sanitize(content)}
-
- ) : (
- Nothing to preview
- )}
-
- )}
-
+
+
+
Content
+
+ setContentTab('write')}
+ className={`px-3 py-1 text-xs rounded-md transition-all ${contentTab === 'write' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
+ >
+ Write
+
+ setContentTab('preview')}
+ className={`px-3 py-1 text-xs rounded-md transition-all ${contentTab === 'preview' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
+ >
+ Preview
+
+
+
-
- Tags (comma separated)
- setTags(e.target.value)}
- className="w-full bg-background border border-border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50"
- placeholder="e.g., Help, Showcase, React, Firebase"
- />
-
+ {contentTab === 'write' ? (
+
setContent(e.target.value)}
+ className="w-full bg-background border border-border rounded-lg px-4 py-2 min-h-[200px] focus:outline-none focus:ring-2 focus:ring-primary/50 resize-y"
+ placeholder="Share your thoughts, ask questions, or showcase your work... (Markdown supported)"
+ required
+ />
+ ) : (
+
+ {content ? (
+
+ {DOMPurify.sanitize(content)}
+
+ ) : (
+
+ Nothing to preview
+
+ )}
+
+ )}
+
-
-
- Cancel
-
-
- {loading && }
- Post Discussion
-
-
-
-
-
- );
+
+
+ Tags (comma separated)
+
+ setTags(e.target.value)}
+ className="w-full bg-background border border-border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50"
+ placeholder="e.g., Help, Showcase, React, Firebase"
+ />
+
+
+
+
+ Cancel
+
+
+ {loading && }
+ Post Discussion
+
+
+
+
+
+ );
}
diff --git a/src/components/features/ComingSoonBadge.module.css b/src/components/features/ComingSoonBadge.module.css
index 0c94fb47..6e553a1a 100644
--- a/src/components/features/ComingSoonBadge.module.css
+++ b/src/components/features/ComingSoonBadge.module.css
@@ -1,32 +1,32 @@
.badge {
- position: absolute;
- top: 12px;
- right: 12px;
- background: linear-gradient(135deg, #00d4ff, #9d4edd);
- color: white;
- font-size: 10px;
- font-weight: 800;
- text-transform: uppercase;
- padding: 4px 8px;
- border-radius: 20px;
- border: 1px solid rgba(255, 255, 255, 0.3);
- transform: rotate(-5deg);
- box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
- z-index: 10;
- animation: pulse 2s infinite ease-in-out;
- opacity: 0.95;
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ background: linear-gradient(135deg, #00d4ff, #9d4edd);
+ color: white;
+ font-size: 10px;
+ font-weight: 800;
+ text-transform: uppercase;
+ padding: 4px 8px;
+ border-radius: 20px;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ transform: rotate(-5deg);
+ box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
+ z-index: 10;
+ animation: pulse 2s infinite ease-in-out;
+ opacity: 0.95;
}
@keyframes pulse {
- 0% {
- transform: rotate(-5deg) scale(1);
- }
+ 0% {
+ transform: rotate(-5deg) scale(1);
+ }
- 50% {
- transform: rotate(-5deg) scale(1.05);
- }
+ 50% {
+ transform: rotate(-5deg) scale(1.05);
+ }
- 100% {
- transform: rotate(-5deg) scale(1);
- }
-}
\ No newline at end of file
+ 100% {
+ transform: rotate(-5deg) scale(1);
+ }
+}
diff --git a/src/components/features/ComingSoonBadge.tsx b/src/components/features/ComingSoonBadge.tsx
index b928ddce..5c75c203 100644
--- a/src/components/features/ComingSoonBadge.tsx
+++ b/src/components/features/ComingSoonBadge.tsx
@@ -1,9 +1,5 @@
import styles from './ComingSoonBadge.module.css';
export default function ComingSoonBadge() {
- return (
-
- Coming Soon
-
- );
+ return Coming Soon
;
}
diff --git a/src/components/features/SkillTreeVisualizer.module.css b/src/components/features/SkillTreeVisualizer.module.css
index 8aa425e1..eb1944da 100644
--- a/src/components/features/SkillTreeVisualizer.module.css
+++ b/src/components/features/SkillTreeVisualizer.module.css
@@ -114,7 +114,7 @@
padding: 2rem;
transition: right 0.3s ease;
z-index: 10;
- box-shadow: -5px 0 25px rgba(0,0,0,0.5);
+ box-shadow: -5px 0 25px rgba(0, 0, 0, 0.5);
}
.drawer.open {
@@ -139,4 +139,4 @@
.drawerDesc {
color: #8b949e;
line-height: 1.6;
-}
\ No newline at end of file
+}
diff --git a/src/components/features/SkillTreeVisualizer.tsx b/src/components/features/SkillTreeVisualizer.tsx
index 4b6f9961..eea6014c 100644
--- a/src/components/features/SkillTreeVisualizer.tsx
+++ b/src/components/features/SkillTreeVisualizer.tsx
@@ -1,4 +1,4 @@
-"use client";
+'use client';
import React, { useState } from "react";
import styles from "./SkillTreeVisualizer.module.css";
@@ -18,17 +18,73 @@ type SkillNode = {
const pathsData: Record = {
Frontend: [
- { id: "1", label: "HTML/CSS", x: 50, y: 10, desc: "Building blocks of the web.", connections: ["2", "3"] },
- { id: "2", label: "JavaScript", x: 30, y: 35, desc: "Adding logic and interactivity.", connections: ["4"] },
- { id: "3", label: "Version Ctrl", x: 70, y: 35, desc: "Git and GitHub for collaboration.", connections: ["4"] },
- { id: "4", label: "React", x: 50, y: 65, desc: "Component based UI library.", connections: ["5"] },
- { id: "5", label: "Next.js", x: 50, y: 90, desc: "React framework for production.", connections: [] },
+ {
+ id: '1',
+ label: 'HTML/CSS',
+ x: 50,
+ y: 10,
+ desc: 'Building blocks of the web.',
+ connections: ['2', '3'],
+ },
+ {
+ id: '2',
+ label: 'JavaScript',
+ x: 30,
+ y: 35,
+ desc: 'Adding logic and interactivity.',
+ connections: ['4'],
+ },
+ {
+ id: '3',
+ label: 'Version Ctrl',
+ x: 70,
+ y: 35,
+ desc: 'Git and GitHub for collaboration.',
+ connections: ['4'],
+ },
+ {
+ id: '4',
+ label: 'React',
+ x: 50,
+ y: 65,
+ desc: 'Component based UI library.',
+ connections: ['5'],
+ },
+ {
+ id: '5',
+ label: 'Next.js',
+ x: 50,
+ y: 90,
+ desc: 'React framework for production.',
+ connections: [],
+ },
],
Backend: [
- { id: "1", label: "Databases", x: 50, y: 10, desc: "SQL vs NoSQL architectures.", connections: ["2"] },
- { id: "2", label: "Node.js", x: 50, y: 40, desc: "JavaScript runtime environment.", connections: ["3"] },
- { id: "3", label: "APIs", x: 50, y: 70, desc: "REST and GraphQL endpoint creation.", connections: [] },
- ]
+ {
+ id: '1',
+ label: 'Databases',
+ x: 50,
+ y: 10,
+ desc: 'SQL vs NoSQL architectures.',
+ connections: ['2'],
+ },
+ {
+ id: '2',
+ label: 'Node.js',
+ x: 50,
+ y: 40,
+ desc: 'JavaScript runtime environment.',
+ connections: ['3'],
+ },
+ {
+ id: '3',
+ label: 'APIs',
+ x: 50,
+ y: 70,
+ desc: 'REST and GraphQL endpoint creation.',
+ connections: [],
+ },
+ ],
};
export default function SkillTreeVisualizer() {
@@ -85,21 +141,17 @@ export default function SkillTreeVisualizer() {
{/* Path Selector Controls */}
-
{
- setActivePath("Frontend");
- setSelectedNode(null);
- }}
+ setActivePath('Frontend')}
>
Frontend Path
- {
- setActivePath("Backend");
- setSelectedNode(null);
- }}
+ setActivePath('Backend')}
>
Backend Path
@@ -109,9 +161,9 @@ export default function SkillTreeVisualizer() {
{/* SVG Lines connecting nodes */}
- {nodes.map(node =>
- node.connections.map(targetId => {
- const targetNode = nodes.find(n => n.id === targetId);
+ {nodes.map((node) =>
+ node.connections.map((targetId) => {
+ const targetNode = nodes.find((n) => n.id === targetId);
if (!targetNode) return null;
const isSourceCompleted = isNodeCompleted(activePath, node.id);
@@ -119,16 +171,13 @@ export default function SkillTreeVisualizer() {
const isActiveLine = isSourceCompleted && isTargetCompleted;
return (
-
);
})
@@ -136,6 +185,16 @@ export default function SkillTreeVisualizer() {
{/* Nodes */}
+ {nodes.map((node) => (
+
setSelectedNode(node)}
+ >
+ {node.label}
+
+ ))}
{nodes.map(node => {
const isCompleted = isNodeCompleted(activePath, node.id);
const isSelected = selectedNode?.id === node.id;
@@ -177,47 +236,22 @@ export default function SkillTreeVisualizer() {
})}
{/* Side Drawer */}
-
+
{selectedNode && (
-
-
-
setSelectedNode(null)}>✖
-
-
- {selectedNode.label}
-
-
{selectedNode.desc}
-
-
- {user && (
-
- toggleNode(activePath, selectedNode.id)}
- className={`w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-lg border font-semibold text-sm transition-all duration-200 ${
- isNodeCompleted(activePath, selectedNode.id)
- ? "bg-emerald-950/20 text-emerald-400 border-emerald-500/30 hover:bg-emerald-950/40"
- : "bg-slate-900 text-slate-300 border-slate-800 hover:bg-slate-800 hover:text-white"
- }`}
- >
- {isNodeCompleted(activePath, selectedNode.id) ? (
- <>
-
- Mark as Incomplete
- >
- ) : (
- <>
-
- Mark as Completed
- >
- )}
-
-
- )}
-
+ <>
+
setSelectedNode(null)}
+ >
+ ✖
+
+
{selectedNode.label}
+
{selectedNode.desc}
+ >
)}
);
-}
\ No newline at end of file
+}
diff --git a/src/components/gamification/BadgeGrid.tsx b/src/components/gamification/BadgeGrid.tsx
index 268be2ce..daa1d703 100644
--- a/src/components/gamification/BadgeGrid.tsx
+++ b/src/components/gamification/BadgeGrid.tsx
@@ -1,15 +1,15 @@
-import { BADGES } from "@/lib/gamification";
+import { BADGES } from '@/lib/gamification';
export function BadgeGrid({ earned }: { earned: string[] }) {
return (
- {BADGES.map(badge => {
+ {BADGES.map((badge) => {
const unlocked = earned.includes(badge.id);
return (
{badge.icon}
{badge.label}
diff --git a/src/components/gamification/Leaderboard.tsx b/src/components/gamification/Leaderboard.tsx
index c60c39fb..913e2248 100644
--- a/src/components/gamification/Leaderboard.tsx
+++ b/src/components/gamification/Leaderboard.tsx
@@ -1,27 +1,37 @@
-"use client";
-import { useEffect, useState } from "react";
-import { collection, query, orderBy, limit, getDocs } from "firebase/firestore";
-import { db } from "@/lib/firebase";
-import { getTier } from "@/lib/gamification";
-import Link from "next/link";
+'use client';
+import { useEffect, useState } from 'react';
+import { collection, query, orderBy, limit, getDocs } from 'firebase/firestore';
+import { db } from '@/lib/firebase';
+import { getTier } from '@/lib/gamification';
export function Leaderboard() {
const [users, setUsers] = useState
([]);
- const [filter, setFilter] = useState<"alltime" | "weekly" | "monthly">("alltime");
+ const [filter, setFilter] = useState<'alltime' | 'weekly' | 'monthly'>(
+ 'alltime'
+ );
+
useEffect(() => {
- const field = filter === "alltime" ? "xp" : filter === "weekly" ? "weeklyXP" : "monthlyXP";
- getDocs(query(collection(db, "users"), orderBy(field, "desc"), limit(20)))
- .then(snap => setUsers(snap.docs.map(d => ({ id: d.id, ...d.data() }))));
+ const field =
+ filter === 'alltime'
+ ? 'xp'
+ : filter === 'weekly'
+ ? 'weeklyXP'
+ : 'monthlyXP';
+ getDocs(
+ query(collection(db, 'users'), orderBy(field, 'desc'), limit(20))
+ ).then((snap) =>
+ setUsers(snap.docs.map((d) => ({ id: d.id, ...d.data() })))
+ );
}, [filter]);
return (
- {["alltime", "weekly", "monthly"].map(f => (
+ {['alltime', 'weekly', 'monthly'].map((f) => (
setFilter(f as any)}
className={`px-3 py-1 rounded-full text-sm capitalize
- ${filter === f ? "bg-blue-600 text-white" : "bg-gray-800 text-gray-400"}`}
+ ${filter === f ? 'bg-blue-600 text-white' : 'bg-gray-800 text-gray-400'}`}
>
{f}
@@ -31,20 +41,29 @@ export function Leaderboard() {
{users.map((u, i) => {
const tier = getTier(u.xp ?? 0);
return (
-
-
#{i + 1}
-
-
+
+ #{i + 1}
+
+
+
+ {u.displayName ?? 'Anonymous'}
+
+
- {u.displayName ?? "Anonymous"}
-
-
{tier.name}
- {u.xp?? 0} XP
+
+ {u.xp ?? 0} XP
+
);
})}
diff --git a/src/components/gamification/StreakCalendar.tsx b/src/components/gamification/StreakCalendar.tsx
index 05f25ff7..68df2ec7 100644
--- a/src/components/gamification/StreakCalendar.tsx
+++ b/src/components/gamification/StreakCalendar.tsx
@@ -1,9 +1,9 @@
-"use client";
-import { useMemo } from "react";
+'use client';
+import { useMemo } from 'react';
export function StreakCalendar({ activityDates }: { activityDates: string[] }) {
const dateSet = useMemo(() => new Set(activityDates), [activityDates]);
-
+
// Last 52 weeks
const weeks = useMemo(() => {
const cells: { date: string; active: boolean }[][] = [];
@@ -11,11 +11,14 @@ export function StreakCalendar({ activityDates }: { activityDates: string[] }) {
const start = new Date(today);
start.setDate(start.getDate() - 363);
- let week: typeof cells[0] = [];
+ let week: (typeof cells)[0] = [];
for (let d = new Date(start); d <= today; d.setDate(d.getDate() + 1)) {
- const str = d.toISOString().split("T")[0];
+ const str = d.toISOString().split('T')[0];
week.push({ date: str, active: dateSet.has(str) });
- if (week.length === 7) { cells.push(week); week = []; }
+ if (week.length === 7) {
+ cells.push(week);
+ week = [];
+ }
}
if (week.length) cells.push(week);
return cells;
@@ -29,7 +32,7 @@ export function StreakCalendar({ activityDates }: { activityDates: string[] }) {
))}
diff --git a/src/components/gamification/XPBar.tsx b/src/components/gamification/XPBar.tsx
index fa5b38d8..1c28a8f8 100644
--- a/src/components/gamification/XPBar.tsx
+++ b/src/components/gamification/XPBar.tsx
@@ -1,16 +1,18 @@
-import { getTier } from "@/lib/gamification";
+import { getTier } from '@/lib/gamification';
export function XPBar({ xp }: { xp: number }) {
const tier = getTier(xp);
const tiers = [0, 500, 1500, 3000, 6000];
- const next = tiers.find(t => t > xp) ?? xp;
- const prev = tiers.filter(t => t <= xp).at(-1) ?? 0;
+ const next = tiers.find((t) => t > xp) ?? xp;
+ const prev = tiers.filter((t) => t <= xp).at(-1) ?? 0;
const pct = Math.round(((xp - prev) / (next - prev)) * 100);
return (
- {tier.name}
+
+ {tier.name}
+
{xp} XP
diff --git a/src/components/home/CertificateCard.tsx b/src/components/home/CertificateCard.tsx
index 46cee416..a1b7cdc4 100644
--- a/src/components/home/CertificateCard.tsx
+++ b/src/components/home/CertificateCard.tsx
@@ -1,54 +1,57 @@
-"use client";
+'use client';
import { Award, ArrowRight, Download } from 'lucide-react';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
export default function CertificateCard() {
- const router = useRouter();
-
- const handleClick = () => {
- router.push('/certificate');
- };
-
- return (
-
- {/* Background Effects */}
-
-
-
-
-
-
- Now Available
-
-
-
- HackFiesta
- Certificate
-
-
-
- Official participation certificates are ready. Verify your identity and download your certificate instantly.
-
-
-
-
-
- {/* Decorative Icon */}
-
-
- );
+ const router = useRouter();
+
+ const handleClick = () => {
+ router.push('/certificate');
+ };
+
+ return (
+
+ {/* Background Effects */}
+
+
+
+
+
+
+ Now Available
+
+
+
+ HackFiesta
+
+ Certificate
+
+
+
+
+ Official participation certificates are ready. Verify your identity
+ and download your certificate instantly.
+
+
+
+
+
+ {/* Decorative Icon */}
+
+
+ );
}
diff --git a/src/components/home/CodingNews.module.css b/src/components/home/CodingNews.module.css
index f3b45b00..1c2a060f 100644
--- a/src/components/home/CodingNews.module.css
+++ b/src/components/home/CodingNews.module.css
@@ -1,257 +1,257 @@
.section {
- padding: 80px 24px;
- background: transparent;
- position: relative;
- z-index: 10;
+ padding: 80px 24px;
+ background: transparent;
+ position: relative;
+ z-index: 10;
}
.container {
- max-width: 100%;
- margin: 0;
- padding: 0 24px;
+ max-width: 100%;
+ margin: 0;
+ padding: 0 24px;
}
.header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 40px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 40px;
}
.iconWrapper {
- width: 48px;
- height: 48px;
- border-radius: 12px;
- background: rgba(139, 92, 246, 0.1);
- display: flex;
- align-items: center;
- justify-content: center;
- color: #8b5cf6;
- border: 1px solid rgba(139, 92, 246, 0.2);
+ width: 48px;
+ height: 48px;
+ border-radius: 12px;
+ background: rgba(139, 92, 246, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #8b5cf6;
+ border: 1px solid rgba(139, 92, 246, 0.2);
}
.title {
- font-size: 32px;
- font-weight: 700;
- color: var(--text-primary);
- letter-spacing: -0.02em;
+ font-size: 32px;
+ font-weight: 700;
+ color: var(--text-primary);
+ letter-spacing: -0.02em;
}
.refreshButton {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.1);
- color: var(--text-secondary);
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.2s;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: var(--text-secondary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s;
}
.refreshButton:hover:not(:disabled) {
- background: rgba(255, 255, 255, 0.1);
- color: white;
- transform: rotate(180deg);
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+ transform: rotate(180deg);
}
.refreshButton:disabled {
- opacity: 0.5;
- cursor: not-allowed;
+ opacity: 0.5;
+ cursor: not-allowed;
}
/* Scroll Container */
.scrollContainer {
- width: 100%;
- overflow-x: auto;
- /* Enable horizontal scrolling */
- position: relative;
- padding: 20px 0;
- /* Hide scrollbar */
- scrollbar-width: none;
- /* Firefox */
- -ms-overflow-style: none;
- /* IE/Edge */
+ width: 100%;
+ overflow-x: auto;
+ /* Enable horizontal scrolling */
+ position: relative;
+ padding: 20px 0;
+ /* Hide scrollbar */
+ scrollbar-width: none;
+ /* Firefox */
+ -ms-overflow-style: none;
+ /* IE/Edge */
}
.scrollContainer::-webkit-scrollbar {
- display: none;
- /* Chrome/Safari */
+ display: none;
+ /* Chrome/Safari */
}
.scrollTrack {
- display: grid;
- grid-template-rows: repeat(2, 1fr);
- /* 2 Rows */
- grid-auto-flow: column;
- /* Flow horizontally */
- gap: 24px;
- width: max-content;
- /* Allow growing horizontally */
- /* Animation removed, handled by JS */
+ display: grid;
+ grid-template-rows: repeat(2, 1fr);
+ /* 2 Rows */
+ grid-auto-flow: column;
+ /* Flow horizontally */
+ gap: 24px;
+ width: max-content;
+ /* Allow growing horizontally */
+ /* Animation removed, handled by JS */
}
.card {
- width: 320px;
- /* Fixed width for horizontal cards */
- background: rgba(20, 20, 22, 0.6);
- border: 1px solid rgba(255, 255, 255, 0.05);
- border-radius: 16px;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- transition: all 0.3s ease;
- height: 100%;
- backdrop-filter: blur(10px);
+ width: 320px;
+ /* Fixed width for horizontal cards */
+ background: rgba(20, 20, 22, 0.6);
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ border-radius: 16px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ transition: all 0.3s ease;
+ height: 100%;
+ backdrop-filter: blur(10px);
}
.card:hover {
- border-color: rgba(139, 92, 246, 0.3);
- box-shadow: 0 10px 30px -10px rgba(139, 92, 246, 0.2);
- transform: translateY(-5px);
+ border-color: rgba(139, 92, 246, 0.3);
+ box-shadow: 0 10px 30px -10px rgba(139, 92, 246, 0.2);
+ transform: translateY(-5px);
}
.imageWrapper {
- position: relative;
- height: 160px;
- overflow: hidden;
- background: #0f0f11;
+ position: relative;
+ height: 160px;
+ overflow: hidden;
+ background: #0f0f11;
}
.image {
- width: 100%;
- height: 100%;
- object-fit: cover;
- transition: transform 0.5s ease;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.5s ease;
}
.card:hover .image {
- transform: scale(1.05);
+ transform: scale(1.05);
}
.placeholderImage {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(255, 255, 255, 0.02);
- color: var(--text-secondary);
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.02);
+ color: var(--text-secondary);
}
.sourceBadge {
- position: absolute;
- top: 12px;
- right: 12px;
- padding: 4px 10px;
- background: rgba(0, 0, 0, 0.6);
- backdrop-filter: blur(4px);
- border-radius: 20px;
- font-size: 11px;
- font-weight: 600;
- color: white;
- border: 1px solid rgba(255, 255, 255, 0.1);
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ padding: 4px 10px;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(4px);
+ border-radius: 20px;
+ font-size: 11px;
+ font-weight: 600;
+ color: white;
+ border: 1px solid rgba(255, 255, 255, 0.1);
}
.content {
- padding: 20px;
- display: flex;
- flex-direction: column;
- flex: 1;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
}
.cardTitle {
- font-size: 16px;
- font-weight: 600;
- color: var(--text-primary);
- line-height: 1.5;
- margin-bottom: 16px;
- display: -webkit-box;
- -webkit-line-clamp: 3;
- line-clamp: 3;
- -webkit-box-orient: vertical;
- overflow: hidden;
- flex: 1;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+ line-height: 1.5;
+ margin-bottom: 16px;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ flex: 1;
}
.cardFooter {
- margin-top: auto;
- display: flex;
- align-items: center;
+ margin-top: auto;
+ display: flex;
+ align-items: center;
}
.readMore {
- font-size: 13px;
- font-weight: 500;
- color: #8b5cf6;
- display: flex;
- align-items: center;
- gap: 6px;
+ font-size: 13px;
+ font-weight: 500;
+ color: #8b5cf6;
+ display: flex;
+ align-items: center;
+ gap: 6px;
}
.error {
- padding: 16px;
- background: rgba(239, 68, 68, 0.1);
- border: 1px solid rgba(239, 68, 68, 0.2);
- border-radius: 12px;
- color: #ef4444;
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 24px;
+ padding: 16px;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ border-radius: 12px;
+ color: #ef4444;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 24px;
}
/* Skeleton Loading */
.skeletonCard {
- background: rgba(255, 255, 255, 0.02);
- border-radius: 16px;
- overflow: hidden;
- height: 300px;
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 16px;
+ overflow: hidden;
+ height: 300px;
}
.skeletonImage {
- height: 160px;
- background: rgba(255, 255, 255, 0.05);
- animation: pulse 1.5s infinite;
+ height: 160px;
+ background: rgba(255, 255, 255, 0.05);
+ animation: pulse 1.5s infinite;
}
.skeletonContent {
- padding: 20px;
+ padding: 20px;
}
.skeletonLine {
- height: 16px;
- background: rgba(255, 255, 255, 0.05);
- border-radius: 4px;
- margin-bottom: 12px;
- animation: pulse 1.5s infinite;
+ height: 16px;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 4px;
+ margin-bottom: 12px;
+ animation: pulse 1.5s infinite;
}
@keyframes pulse {
- 0% {
- opacity: 0.5;
- }
+ 0% {
+ opacity: 0.5;
+ }
- 50% {
- opacity: 0.8;
- }
+ 50% {
+ opacity: 0.8;
+ }
- 100% {
- opacity: 0.5;
- }
+ 100% {
+ opacity: 0.5;
+ }
}
@media (max-width: 640px) {
- .scrollTrack {
- grid-template-rows: 1fr;
- /* Single row on mobile */
- grid-auto-flow: column;
- }
+ .scrollTrack {
+ grid-template-rows: 1fr;
+ /* Single row on mobile */
+ grid-auto-flow: column;
+ }
- .card {
- width: 280px;
- /* Slightly smaller cards on mobile */
- }
-}
\ No newline at end of file
+ .card {
+ width: 280px;
+ /* Slightly smaller cards on mobile */
+ }
+}
diff --git a/src/components/home/CodingNews.tsx b/src/components/home/CodingNews.tsx
index 67ffe3b2..4856e508 100644
--- a/src/components/home/CodingNews.tsx
+++ b/src/components/home/CodingNews.tsx
@@ -1,4 +1,4 @@
-"use client";
+'use client';
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
@@ -7,259 +7,276 @@ import { ExternalLink, Newspaper, RefreshCw, AlertCircle } from 'lucide-react';
import styles from './CodingNews.module.css';
interface NewsItem {
- source: string;
- title: string;
- url: string;
- image: string | null;
+ source: string;
+ title: string;
+ url: string;
+ image: string | null;
}
const FALLBACK_NEWS: NewsItem[] = [
- {
- source: "DevPath Blog",
- title: "Optimizing Next.js Web Vitals: A Deep Dive into LCP, FID, and CLS for 2026",
- url: "https://devpath.in",
- image: "https://images.unsplash.com/photo-1555066931-4365d14bab8c?q=80&w=800"
- },
- {
- source: "Agentic Tech",
- title: "The Rise of Agentic AI: How Advanced Coding Assistants are Redefining Developer Workflows",
- url: "https://techcrunch.com",
- image: "https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?q=80&w=800"
- },
- {
- source: "Next.js Blog",
- title: "Next.js 16 Released: Turbopack for Faster Builds, React 19 Support, and Server Actions",
- url: "https://vercel.com/blog",
- image: "https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=800"
- },
- {
- source: "GitHub",
- title: "Open Source India: Empowering the Next Generation of Developers Nationwide",
- url: "https://github.blog",
- image: "https://images.unsplash.com/photo-1522071820081-009f0129c71c?q=80&w=800"
- }
+ {
+ source: 'DevPath Blog',
+ title:
+ 'Optimizing Next.js Web Vitals: A Deep Dive into LCP, FID, and CLS for 2026',
+ url: 'https://devpath.in',
+ image:
+ 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?q=80&w=800',
+ },
+ {
+ source: 'Agentic Tech',
+ title:
+ 'The Rise of Agentic AI: How Advanced Coding Assistants are Redefining Developer Workflows',
+ url: 'https://techcrunch.com',
+ image:
+ 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?q=80&w=800',
+ },
+ {
+ source: 'Next.js Blog',
+ title:
+ 'Next.js 16 Released: Turbopack for Faster Builds, React 19 Support, and Server Actions',
+ url: 'https://vercel.com/blog',
+ image:
+ 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=800',
+ },
+ {
+ source: 'GitHub',
+ title:
+ 'Open Source India: Empowering the Next Generation of Developers Nationwide',
+ url: 'https://github.blog',
+ image:
+ 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?q=80&w=800',
+ },
];
const FALLBACK_NEWS_IMAGE =
- 'https://images.unsplash.com/photo-1504639725590-34d0984388bd?q=80&w=1000&auto=format&fit=crop';
+ 'https://images.unsplash.com/photo-1504639725590-34d0984388bd?q=80&w=1000&auto=format&fit=crop';
interface NewsCardImageProps {
- src: string;
- alt: string;
- sizes?: string;
- priority?: boolean;
+ src: string;
+ alt: string;
+ sizes?: string;
+ priority?: boolean;
}
function NewsCardImage({ src, alt, sizes, priority }: NewsCardImageProps) {
- const [imgSrc, setImgSrc] = useState(src);
+ const [imgSrc, setImgSrc] = useState(src);
- useEffect(() => {
- setImgSrc(src);
- }, [src]);
+ useEffect(() => {
+ setImgSrc(src);
+ }, [src]);
- return (
-
setImgSrc(FALLBACK_NEWS_IMAGE)}
- />
- );
+ return (
+ setImgSrc(FALLBACK_NEWS_IMAGE)}
+ />
+ );
}
export default function CodingNews() {
- const [news, setNews] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const loopedNews = news.length > 0 ? [...news, ...news] : [];
+ const [news, setNews] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const loopedNews = news.length > 0 ? [...news, ...news] : [];
- // Scroll Logic Refs
- const scrollContainerRef = useRef(null);
- const [isHovering, setIsHovering] = useState(false);
- const mouseXRef = useRef(0);
+ // Scroll Logic Refs
+ const scrollContainerRef = useRef(null);
+ const [isHovering, setIsHovering] = useState(false);
+ const mouseXRef = useRef(0);
- const fetchNews = async () => {
- setLoading(true);
- setError(null);
- try {
- const apiUrl = process.env.NEXT_PUBLIC_NEWS_API_URL;
- if (!apiUrl) {
- // If API URL is not configured, load fallback news gracefully
- setNews(FALLBACK_NEWS);
- return;
- }
+ const fetchNews = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const apiUrl = process.env.NEXT_PUBLIC_NEWS_API_URL;
+ if (!apiUrl) {
+ // If API URL is not configured, load fallback news gracefully
+ setNews(FALLBACK_NEWS);
+ return;
+ }
- const response = await fetch(apiUrl);
- if (!response.ok) {
- throw new Error('Failed to fetch news');
- }
- const data = await response.json();
- setNews(data);
- } catch (err) {
- console.error("Error fetching news:", err);
- // Fall back gracefully even if fetch fails
- setNews(FALLBACK_NEWS);
- } finally {
- setLoading(false);
- }
- };
+ const response = await fetch(apiUrl);
+ if (!response.ok) {
+ throw new Error('Failed to fetch news');
+ }
+ const data = await response.json();
+ setNews(data);
+ } catch (err) {
+ console.error('Error fetching news:', err);
+ // Fall back gracefully even if fetch fails
+ setNews(FALLBACK_NEWS);
+ } finally {
+ setLoading(false);
+ }
+ };
- useEffect(() => {
- fetchNews();
- }, []);
+ useEffect(() => {
+ fetchNews();
+ }, []);
- // Mouse-driven scroll effect
- useEffect(() => {
- const container = scrollContainerRef.current;
- if (!container) return;
+ // Mouse-driven scroll effect
+ useEffect(() => {
+ const container = scrollContainerRef.current;
+ if (!container) return;
- let animationFrameId: number;
- const AUTO_SCROLL_EDGE_THRESHOLD = 2;
+ let animationFrameId: number;
+ const AUTO_SCROLL_EDGE_THRESHOLD = 2;
- const scroll = () => {
- if (container) {
- const loopBoundary = container.scrollWidth / 2;
+ const scroll = () => {
+ if (container) {
+ const loopBoundary = container.scrollWidth / 2;
- if (isHovering) {
- // Mouse movement scrolling logic
- const rect = container.getBoundingClientRect();
- const relativeX = mouseXRef.current - rect.left;
- const width = rect.width;
+ if (isHovering) {
+ // Mouse movement scrolling logic
+ const rect = container.getBoundingClientRect();
+ const relativeX = mouseXRef.current - rect.left;
+ const width = rect.width;
- // Define zones
- const leftZone = width * 0.3;
- const rightZone = width * 0.7;
+ // Define zones
+ const leftZone = width * 0.3;
+ const rightZone = width * 0.7;
- if (relativeX < leftZone) {
- // Scroll Left - speed increases as we get closer to edge
- const speed = Math.max(1, (leftZone - relativeX) / 10); // Adjusted speed divisor
- container.scrollLeft -= speed;
- } else if (relativeX > rightZone) {
- // Scroll Right - speed increases as we get closer to edge
- const speed = Math.max(1, (relativeX - rightZone) / 10); // Adjusted speed divisor
- container.scrollLeft += speed;
- }
+ if (relativeX < leftZone) {
+ // Scroll Left - speed increases as we get closer to edge
+ const speed = Math.max(1, (leftZone - relativeX) / 10); // Adjusted speed divisor
+ container.scrollLeft -= speed;
+ } else if (relativeX > rightZone) {
+ // Scroll Right - speed increases as we get closer to edge
+ const speed = Math.max(1, (relativeX - rightZone) / 10); // Adjusted speed divisor
+ container.scrollLeft += speed;
+ }
- if (loopBoundary > 0 && container.scrollLeft >= loopBoundary) {
- container.scrollLeft -= loopBoundary;
- } else if (loopBoundary > 0 && container.scrollLeft < 0) {
- container.scrollLeft += loopBoundary;
- }
- } else {
- // Auto-scroll when not hovering using a seamless loop
- if (loopBoundary <= 0) {
- container.scrollLeft = 0;
- } else if (container.scrollLeft >= loopBoundary - AUTO_SCROLL_EDGE_THRESHOLD) {
- container.scrollLeft -= loopBoundary;
- } else {
- container.scrollLeft += 0.5; // Slow auto-scroll speed
- }
- }
- }
- animationFrameId = requestAnimationFrame(scroll);
- };
+ if (loopBoundary > 0 && container.scrollLeft >= loopBoundary) {
+ container.scrollLeft -= loopBoundary;
+ } else if (loopBoundary > 0 && container.scrollLeft < 0) {
+ container.scrollLeft += loopBoundary;
+ }
+ } else {
+ // Auto-scroll when not hovering using a seamless loop
+ if (loopBoundary <= 0) {
+ container.scrollLeft = 0;
+ } else if (
+ container.scrollLeft >=
+ loopBoundary - AUTO_SCROLL_EDGE_THRESHOLD
+ ) {
+ container.scrollLeft -= loopBoundary;
+ } else {
+ container.scrollLeft += 0.5; // Slow auto-scroll speed
+ }
+ }
+ }
+ animationFrameId = requestAnimationFrame(scroll);
+ };
- animationFrameId = requestAnimationFrame(scroll);
+ animationFrameId = requestAnimationFrame(scroll);
- return () => {
- cancelAnimationFrame(animationFrameId);
- };
- }, [isHovering]);
+ return () => {
+ cancelAnimationFrame(animationFrameId);
+ };
+ }, [isHovering]);
- return (
-
-
-
-
-
-
-
-
Latest Tech News
-
-
-
-
-
+ return (
+
+
+
+
+
+
+
+
Latest Tech News
+
+
+
+
+
- {error && (
-
- )}
+ {error && (
+
+ )}
- {loading ? (
-
- {[...Array(4)].map((_, i) => (
-
- ))}
-
- ) : (
-
setIsHovering(true)}
- onMouseLeave={() => setIsHovering(false)}
- onMouseMove={(e) => {
- mouseXRef.current = e.clientX;
- }}
- >
-
- {loopedNews.map((item, index) => (
-
-
- {item.image ? (
-
- ) : (
-
-
-
- )}
-
{item.source}
-
-
-
{item.title}
-
-
- Read Article
-
-
-
-
- ))}
-
+ {loading ? (
+
+ {[...Array(4)].map((_, i) => (
+
+ ))}
+
+ ) : (
+
setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ onMouseMove={(e) => {
+ mouseXRef.current = e.clientX;
+ }}
+ >
+
+ {loopedNews.map((item, index) => (
+
+
+ {item.image ? (
+
+ ) : (
+
+
+
+ )}
+
{item.source}
+
+
+
{item.title}
+
+
+ Read Article
+
- )}
+
+
+ ))}
-
- );
+
+ )}
+
+
+ );
}
diff --git a/src/components/home/Community.module.css b/src/components/home/Community.module.css
index f6ae3907..cb2ddfab 100644
--- a/src/components/home/Community.module.css
+++ b/src/components/home/Community.module.css
@@ -1,185 +1,189 @@
.community {
- padding: 120px 24px;
- background: linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
- overflow: hidden;
+ padding: 120px 24px;
+ background: linear-gradient(
+ 180deg,
+ var(--bg-primary) 0%,
+ var(--bg-secondary) 100%
+ );
+ overflow: hidden;
}
.container {
- max-width: 1200px;
- margin: 0 auto;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 80px;
- align-items: center;
+ max-width: 1200px;
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 80px;
+ align-items: center;
}
.content {
- max-width: 500px;
+ max-width: 500px;
}
.title {
- font-size: 48px;
- font-weight: 700;
- margin-bottom: 24px;
- line-height: 1.1;
- color: var(--text-primary);
+ font-size: 48px;
+ font-weight: 700;
+ margin-bottom: 24px;
+ line-height: 1.1;
+ color: var(--text-primary);
}
.description {
- font-size: 18px;
- color: var(--text-secondary);
- line-height: 1.6;
- margin-bottom: 40px;
+ font-size: 18px;
+ color: var(--text-secondary);
+ line-height: 1.6;
+ margin-bottom: 40px;
}
.mockupWrapper {
- position: relative;
+ position: relative;
}
.chatCard {
- background: var(--card);
- border-radius: 24px;
- border: 1px solid var(--glass-border);
- box-shadow: 0 40px 80px -20px rgba(0, 0, 0, 0.5);
- overflow: hidden;
- transform: rotate(-2deg);
- transition: transform 0.3s ease;
+ background: var(--card);
+ border-radius: 24px;
+ border: 1px solid var(--glass-border);
+ box-shadow: 0 40px 80px -20px rgba(0, 0, 0, 0.5);
+ overflow: hidden;
+ transform: rotate(-2deg);
+ transition: transform 0.3s ease;
}
.chatCard:hover {
- transform: rotate(0deg) scale(1.02);
+ transform: rotate(0deg) scale(1.02);
}
.chatHeader {
- padding: 20px;
- background: rgba(255, 255, 255, 0.03);
- border-bottom: 1px solid var(--glass-border);
- display: flex;
- align-items: center;
- justify-content: space-between;
+ padding: 20px;
+ background: rgba(255, 255, 255, 0.03);
+ border-bottom: 1px solid var(--glass-border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
}
.serverInfo {
- display: flex;
- align-items: center;
- gap: 12px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
}
.serverIcon {
- width: 40px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.serverName {
- font-weight: 600;
- color: var(--text-primary);
- display: block;
+ font-weight: 600;
+ color: var(--text-primary);
+ display: block;
}
.serverStatus {
- font-size: 12px;
- color: #10b981;
- display: flex;
- align-items: center;
- gap: 4px;
+ font-size: 12px;
+ color: #10b981;
+ display: flex;
+ align-items: center;
+ gap: 4px;
}
.serverStatus::before {
- content: '';
- width: 6px;
- height: 6px;
- background: currentColor;
- border-radius: 50%;
+ content: '';
+ width: 6px;
+ height: 6px;
+ background: currentColor;
+ border-radius: 50%;
}
.chatBody {
- padding: 24px;
- display: flex;
- flex-direction: column;
- gap: 20px;
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
}
.message {
- display: flex;
- gap: 12px;
- animation: slideIn 0.5s ease-out backwards;
+ display: flex;
+ gap: 12px;
+ animation: slideIn 0.5s ease-out backwards;
}
.message:nth-child(1) {
- animation-delay: 0.1s;
+ animation-delay: 0.1s;
}
.message:nth-child(2) {
- animation-delay: 0.2s;
+ animation-delay: 0.2s;
}
.message:nth-child(3) {
- animation-delay: 0.3s;
+ animation-delay: 0.3s;
}
.avatar {
- width: 36px;
- height: 36px;
- border-radius: 50%;
- background: var(--glass-bg);
- border: 1px solid var(--glass-border);
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
}
.messageContent {
- flex: 1;
+ flex: 1;
}
.sender {
- display: flex;
- align-items: baseline;
- gap: 8px;
- margin-bottom: 4px;
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ margin-bottom: 4px;
}
.username {
- font-weight: 600;
- font-size: 14px;
- color: var(--text-primary);
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-primary);
}
.time {
- font-size: 12px;
- color: var(--text-secondary);
+ font-size: 12px;
+ color: var(--text-secondary);
}
.text {
- font-size: 14px;
- color: var(--text-secondary);
- line-height: 1.5;
+ font-size: 14px;
+ color: var(--text-secondary);
+ line-height: 1.5;
}
@keyframes slideIn {
- from {
- opacity: 0;
- transform: translateX(20px);
- }
+ from {
+ opacity: 0;
+ transform: translateX(20px);
+ }
- to {
- opacity: 1;
- transform: translateX(0);
- }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
}
@media (max-width: 1024px) {
- .container {
- grid-template-columns: 1fr;
- text-align: center;
- }
-
- .content {
- margin: 0 auto;
- }
-
- .mockupWrapper {
- max-width: 600px;
- margin: 0 auto;
- }
-}
\ No newline at end of file
+ .container {
+ grid-template-columns: 1fr;
+ text-align: center;
+ }
+
+ .content {
+ margin: 0 auto;
+ }
+
+ .mockupWrapper {
+ max-width: 600px;
+ margin: 0 auto;
+ }
+}
diff --git a/src/components/home/Community.tsx b/src/components/home/Community.tsx
index 512928b5..f31b4aed 100644
--- a/src/components/home/Community.tsx
+++ b/src/components/home/Community.tsx
@@ -1,4 +1,4 @@
-"use client"
+'use client';
import { MessageCircle } from 'lucide-react';
import { motion } from 'framer-motion';
import Image from 'next/image';
@@ -7,93 +7,133 @@ import Button from '../ui/Button';
import styles from './Community.module.css';
export default function Community() {
- return (
-
-
-
-
- Join Our Thriving
- Developer Community
-
-
- Connect with developers worldwide, share knowledge, and stay updated with the latest tech trends.
- Get help when you're stuck and celebrate your wins together.
-
-
- }>
- Join DevPath Community
-
-
+ return (
+
+
+
+
+ Join Our Thriving
+
+ Developer Community
+
+
+ Connect with developers worldwide, share knowledge, and stay updated
+ with the latest tech trends. Get help when you're stuck and
+ celebrate your wins together.
+
+
+ }
+ >
+ Join DevPath Community
+
+
+
+
+
+
+
+
+
+
+
+
+ DevPath Official
+
+
+ 325 online
+
+
+
+ 8,000+ members
+
+
-