From f7c8a07c1c4b87e7954bee6c99297bdb2147e207 Mon Sep 17 00:00:00 2001 From: Aditya948351 Date: Wed, 8 Jul 2026 22:44:07 +0530 Subject: [PATCH 1/2] feat: DevPath Constitution roles, Admin panel and Pathway leaderboards --- .gitignore | Bin 1052 -> 976 bytes firestore.rules | 6 + src/app/admin/page.tsx | 413 ++++++++++++++++++ src/app/pathway/page.tsx | 921 +++++++-------------------------------- src/app/team/page.tsx | 913 ++++++++------------------------------ src/data/team.ts | 183 ++++---- 6 files changed, 849 insertions(+), 1587 deletions(-) create mode 100644 src/app/admin/page.tsx diff --git a/.gitignore b/.gitignore index 33f5fcb79d4494e6ce997a6a90e8a941346b8687..c8ed8e3f34a1ad2b1344c9a37f6ad59ec4c494eb 100644 GIT binary patch literal 976 zcmZWn!E)Rn487|sl*!E#!@al8q=%k2_Z0CsyW#N=b79U*ucJ|eR zdOe*_EJi@5GR-aTXjn#wdG1%FLK-`0DXDDrHuhf)LPYVp`3XwkddSDtgbg~n zQb>8G$j5XW_!xg{KV-WdKQ(4WH#6t3I#a1!y_L0Eo0iD~|x%XWDwjMXsyEK3)>E?etBd+k3^! z3f;L`s`$nbeB`q%FE6|$gzfk1uPW@-%2Na79T{P*A9+1fQ*gRhmR-F~%RE%&hQk)6 z`bKjGp7Xl-pU(NVcN1{6UIw1?i{SAEO>{`LNw7vd+*jgbOkT nG2^o9Agc+0Ej2aQ-BCKDfii@;~{aaC}vb%f&7fwR7 literal 1052 zcmZWoJ#X7E5aldD|A9d^Bck?h1=69jvl|q7mf27wK~j$W@B7Y@V!K_;cVAC;@6NAs zPb5#Zrt+ijPvnytT|Lc%GI8p4uFiNWy&FT!E{xsLu_=!+%PD4(dWdtCTUg%0s%_fl zRa#0EECu7Jv`s%~^9j2T(b8dxb_NmL|ClO;)HeL9c6hHkhaTW%K}lD0;@`B{r6$9#woo4k8e^3E2(ft zM&rO`Uj7{0y**vV?4C^ZP{$pDuWgd>LW`^T5?MGgij%0K116C~#9Ku#TG8MoQNCkE K%&4pIEYZItut4Gf diff --git a/firestore.rules b/firestore.rules index a3247542..790bffc2 100644 --- a/firestore.rules +++ b/firestore.rules @@ -152,6 +152,12 @@ service cloud.firestore { allow write: if isSuperAdmin(); } + // ─── Team Members ──────────────────────────────────────────────────────── + match /team_members/{memberId} { + allow read: if true; + allow write: if true; + } + // ─── Projects (top-level) ──────────────────────────────────────────────── match /projects/{projectId} { allow read: if true; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 00000000..701be91d --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,413 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { db } from '@/lib/firebase'; +import { doc, getDoc, collection, getDocs, addDoc, updateDoc, deleteDoc } from 'firebase/firestore'; +import { Shield, LogOut, Users, Plus, Trash2, Award } from 'lucide-react'; + +const ROLES = [ + 'Founder', + 'Core Admin', + 'Technical Lead', + 'City Leads Manager', + 'Operations Lead', + 'Community Lead', + 'Learning Lead', + 'Marketing & Creative Lead', +]; + +interface TeamMember { + id: string; + name: string; + role: 'Technical Contributor' | 'City Lead'; + subRole: string; + points: number; + monthlyPoints: number; + lastUpdatedMonth: string; +} + +export default function AdminPage() { + const [session, setSession] = useState<{ role: string; key: string } | null>(null); + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + const stored = localStorage.getItem('devpath_admin_session'); + if (stored) setSession(JSON.parse(stored)); + }, []); + + if (!isMounted) return null; + + return ( +
+
+ {!session ? ( + { setSession(s); localStorage.setItem('devpath_admin_session', JSON.stringify(s)); }} /> + ) : ( + { setSession(null); localStorage.removeItem('devpath_admin_session'); }} + /> + )} +
+
+ ); +} + +function AdminLogin({ onLogin }: { onLogin: (s: { role: string; key: string }) => void }) { + const [selectedRole, setSelectedRole] = useState(ROLES[0]); + const [keyInput, setKeyInput] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + try { + const docRef = doc(db, 'admin_keys', selectedRole); + const docSnap = await getDoc(docRef); + + if (docSnap.exists() && docSnap.data().key === keyInput) { + onLogin({ role: selectedRole, key: keyInput }); + } else { + setError('Invalid role or access key.'); + } + } catch (err: any) { + console.error(err); + setError('Failed to verify key. Check connection and permissions.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+ +
+

Admin Access

+

Sign in using your role and secure key.

+
+ +
+
+ + +
+ +
+ + setKeyInput(e.target.value)} + required + className="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-indigo-500 transition-colors" + placeholder="Enter your key..." + /> +
+ + {error &&

{error}

} + + +
+
+ ); +} + +function AdminDashboard({ role, onLogout }: { role: string; onLogout: () => void }) { + const [members, setMembers] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchMembers = async () => { + setLoading(true); + try { + const snap = await getDocs(collection(db, 'team_members')); + const data = snap.docs.map(d => ({ id: d.id, ...d.data() } as TeamMember)); + setMembers(data); + } catch (err) { + console.error(err); + alert('Failed to load members.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchMembers(); + }, []); + + const canManageAll = role === 'Founder' || role === 'Core Admin'; + const canManageTech = canManageAll || role === 'Technical Lead'; + const canManageCity = canManageAll || role === 'City Leads Manager'; + + return ( +
+
+
+

Admin Dashboard

+

Logged in as: {role}

+
+ +
+ + {loading ? ( +
Loading directory...
+ ) : ( +
+ {(canManageAll) && ( +
+ +
+ )} + + {canManageTech && ( + m.role === 'Technical Contributor')} + onRefresh={fetchMembers} + /> + )} + + {canManageCity && ( + m.role === 'City Lead')} + onRefresh={fetchMembers} + /> + )} +
+ )} +
+ ); +} + +function PointsAssignmentPanel({ title, members, onRefresh }: { title: string, members: TeamMember[], onRefresh: () => void }) { + const [selectedMember, setSelectedMember] = useState(''); + const [points, setPoints] = useState(''); + const [assigning, setAssigning] = useState(false); + + const handleAssign = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedMember || !points) return; + + setAssigning(true); + try { + const member = members.find(m => m.id === selectedMember); + if (!member) throw new Error('Member not found'); + + const pts = parseInt(points, 10); + if (isNaN(pts)) throw new Error('Invalid points'); + + const currentMonthStr = new Date().toISOString().slice(0, 7); // YYYY-MM + let newMonthly = member.monthlyPoints || 0; + + // Monthly Reset Logic + if (member.lastUpdatedMonth !== currentMonthStr) { + newMonthly = 0; + } + + newMonthly += pts; + const newTotal = (member.points || 0) + pts; + + await updateDoc(doc(db, 'team_members', member.id), { + points: newTotal, + monthlyPoints: newMonthly, + lastUpdatedMonth: currentMonthStr + }); + + alert(`Successfully added ${pts} points to ${member.name}!`); + setPoints(''); + setSelectedMember(''); + onRefresh(); + } catch (err) { + console.error(err); + alert('Failed to assign points.'); + } finally { + setAssigning(false); + } + }; + + return ( +
+

+ Assign Points: {title} +

+ +
+
+ + +
+
+ + setPoints(e.target.value)} + placeholder="e.g. 50" + className="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white focus:outline-none" + required + /> +
+ +
+ +
+

Current Leaderboard

+
+ {[...members].sort((a,b) => (b.monthlyPoints || 0) - (a.monthlyPoints || 0)).map(m => ( +
+ {m.name} + {m.monthlyPoints || 0} pts (Mo) +
+ ))} +
+
+
+ ); +} + +function ManagementPanel({ members, onRefresh }: { members: TeamMember[], onRefresh: () => void }) { + const [adding, setAdding] = useState(false); + const [formData, setFormData] = useState({ name: '', role: 'Technical Contributor', subRole: '' }); + + const handleAdd = async (e: React.FormEvent) => { + e.preventDefault(); + setAdding(true); + try { + const currentMonthStr = new Date().toISOString().slice(0, 7); + await addDoc(collection(db, 'team_members'), { + name: formData.name, + role: formData.role, + subRole: formData.subRole, + points: 0, + monthlyPoints: 0, + lastUpdatedMonth: currentMonthStr + }); + setFormData({ name: '', role: 'Technical Contributor', subRole: '' }); + onRefresh(); + } catch (err) { + alert('Failed to add member.'); + } finally { + setAdding(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm('Are you sure you want to remove this member?')) return; + try { + await deleteDoc(doc(db, 'team_members', id)); + onRefresh(); + } catch (err) { + alert('Failed to delete member.'); + } + }; + + return ( +
+

+ Directory Management +

+ +
+ setFormData({...formData, name: e.target.value})} + className="bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white" + /> + + setFormData({...formData, subRole: e.target.value})} + className="bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white" + /> + +
+ +
+ + + + + + + + + + + + {members.map(m => ( + + + + + + + + ))} + {members.length === 0 && ( + + )} + +
NameRoleDomain/CityTotal PtsActions
{m.name} + + {m.role} + + {m.subRole}{m.points || 0} + +
No members found. Add one above.
+
+
+ ); +} diff --git a/src/app/pathway/page.tsx b/src/app/pathway/page.tsx index 4afe1ef2..9cdd98d5 100644 --- a/src/app/pathway/page.tsx +++ b/src/app/pathway/page.tsx @@ -1,811 +1,196 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; -import { useVirtualizer } from '@tanstack/react-virtual'; +import { useEffect, useState } from 'react'; 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, - CheckCircle2, - GitBranch, - GitPullRequest, - Heart, - MessageSquare, - Sparkles, - Upload, - UserCheck, -} from 'lucide-react'; -import { useAuth } from '@/context/AuthContext'; +import { collection, getDocs, query, where, orderBy } from 'firebase/firestore'; +import { Trophy, Code, MapPin, Loader2 } from 'lucide-react'; +import { motion } from 'framer-motion'; -interface LeaderboardEntry { +interface TeamMember { id: string; - email?: string; - name?: string; - photoURL?: string; - points?: number; + name: string; + role: 'Technical Contributor' | 'City Lead'; + subRole: string; + points: number; + monthlyPoints: number; + lastUpdatedMonth: string; } -const pointEarningActivities = [ - { - label: 'Daily Login', - value: `+${POINTS.DAILY_LOGIN} (+Streak)`, - Icon: Flame, - iconClassName: 'text-orange-500', - }, - { - label: '7-Day Streak', - value: `+${POINTS.WEEKLY_STREAK_BONUS}`, - Icon: Flame, - iconClassName: 'text-red-500', - }, - { - label: 'Follow Community', - value: `+${POINTS.FOLLOW_COMMUNITY}`, - Icon: Users, - iconClassName: 'text-blue-500', - }, - { - label: 'Gain Follower', - value: `+${POINTS.FOLLOWER_GAINED}`, - Icon: UserCheck, - iconClassName: 'text-green-500', - }, - { - label: 'Earn Badge', - value: 'Dynamic', - Icon: Award, - iconClassName: 'text-purple-500', - }, - { - label: 'Project Star', - value: `+${POINTS.PROJECT_STAR}`, - Icon: Star, - iconClassName: 'text-yellow-500', - }, - { - label: 'Event Participation', - value: `+${POINTS.EVENT_PARTICIPATION}`, - Icon: Calendar, - iconClassName: 'text-pink-500', - }, - { - label: 'Hackathon Win', - value: `+${POINTS.HACKATHON_WIN}`, - Icon: Trophy, - iconClassName: 'text-yellow-600', - }, - { - label: 'Profile Completion', - value: `+${POINTS.PROFILE_COMPLETION}`, - Icon: CheckCircle2, - iconClassName: 'text-emerald-500', - }, - { - label: 'First Project Upload', - value: `+${POINTS.FIRST_PROJECT_UPLOAD}`, - Icon: Upload, - iconClassName: 'text-cyan-500', - }, - { - label: 'Repository Contribution', - value: `+${POINTS.REPOSITORY_CONTRIBUTION}`, - Icon: GitBranch, - iconClassName: 'text-sky-500', - }, - { - label: 'Pull Request Merged', - value: `+${POINTS.PULL_REQUEST_MERGED}`, - Icon: GitPullRequest, - iconClassName: 'text-violet-500', - }, - { - label: 'Issue Resolution', - value: `+${POINTS.ISSUE_RESOLUTION}`, - Icon: CheckCircle2, - iconClassName: 'text-lime-500', - }, - { - label: 'Community Post Creation', - value: `+${POINTS.COMMUNITY_POST_CREATION}`, - Icon: MessageSquare, - iconClassName: 'text-teal-500', - }, - { - label: 'Helpful Comment Received', - value: `+${POINTS.HELPFUL_COMMENT_RECEIVED}`, - Icon: Heart, - iconClassName: 'text-rose-500', - }, - { - label: 'Consecutive Weekly Activity', - value: `+${POINTS.CONSECUTIVE_WEEKLY_ACTIVITY}`, - Icon: Flame, - iconClassName: 'text-amber-500', - }, - { - label: 'Open Source Contribution', - value: `+${POINTS.OPEN_SOURCE_CONTRIBUTION}`, - Icon: GitPullRequest, - iconClassName: 'text-indigo-500', - }, - { - label: 'Mentor Recognition', - value: `+${POINTS.MENTOR_RECOGNITION}`, - Icon: Sparkles, - iconClassName: 'text-fuchsia-500', - }, -]; +const getInitials = (name: string) => { + const parts = name.split(' '); + return parts.length > 1 ? `${parts[0][0]}${parts[1][0]}`.toUpperCase() : name.slice(0, 2).toUpperCase(); +}; export default function PathwayPage() { - const { user } = useAuth(); - const [leaderboard, setLeaderboard] = useState([]); + const [techContributors, setTechContributors] = useState([]); + const [cityLeads, setCityLeads] = 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 () => { + const fetchLeaderboards = async () => { + setLoading(true); 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); + const snap = await getDocs(collection(db, 'team_members')); + const data = snap.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as TeamMember); + + // Filter out placeholders + const validMembers = data.filter(m => m.name !== 'Application Pending' && m.name.trim() !== ''); + + // We sort by monthlyPoints descending + const tech = validMembers + .filter(m => m.role === 'Technical Contributor') + .sort((a, b) => (b.monthlyPoints || 0) - (a.monthlyPoints || 0)); + + const city = validMembers + .filter(m => m.role === 'City Lead') + .sort((a, b) => (b.monthlyPoints || 0) - (a.monthlyPoints || 0)); + + setTechContributors(tech); + setCityLeads(city); } catch (error) { - console.error('Error fetching leaderboard:', error); + console.error('Error fetching leaderboards:', error); } finally { setLoading(false); } }; - fetchLeaderboard(); + fetchLeaderboards(); }, []); - 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 ( -
-
- {/* 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 - ) : ( -
- {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

-
- -
- {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'; +
+ {/* Background Gradients */} +
+
+
+
+
- return ( -
-
- #{virtualRow.index + 1} -
-
-
-
- {entry.photoURL ? ( - {displayName} - ) : ( -
- {displayName[0]} -
- )} -
- - {displayName} - -
-
-
- - {level.name} - -
-
- {entry.points || 0} -
-
- ); - })} -
-
- )} -
+
+
+ +
+ + + Monthly Leaderboards + + + + Recognizing our
+ + Top Contributors + +
+ + + Celebrating the exceptional efforts of Technical Contributors and City Leads driving the DevPath Bharat community forward this month. +
- {/* Sidebar: Levels & Rules */} -
-
- -

Progression System

+ {loading ? ( +
+ +

Loading leaderboards...

- - {/* 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) => ( -
- ))} -
-
+ ) : ( +
+ +
+ )} - {/* How to Earn Points - Moved to bottom */} -
+
+
+ ); +} - {/* How to Earn Points - Full Width */} -
-

- - How to Earn Points -

-
- {pointEarningActivities.map( - ({ label, value, Icon, iconClassName }) => ( -
- - - {label} - - - {value} - -
- ) - )} -
+function LeaderboardPanel({ title, icon: Icon, iconColor, members }: { title: string; icon: any; iconColor: string; members: TeamMember[] }) { + return ( + +
+
+
+

{title}

+
- {/* 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 - - -
-
- ))} -
+ {members.length === 0 ? ( +
+ +

No participants ranked yet this month.

+
+ ) : ( +
+
+
Rank
+
Contributor
+
Monthly Pts
- - {/* 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 - - -
+ +
+ {members.map((member, index) => ( + +
+ {index + 1}
- ))} -
-
- - {/* 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} -

+ +
+
+ {getInitials(member.name)}
-
- - {reward.cost.toLocaleString()} pts - - +
+

{member.name}

+

{member.subRole}

- ))} -
-
- {/* 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 - - -
+
+

{member.monthlyPoints || 0}

+

Total: {member.points || 0}

- ))} -
+ + ))}
-
-
+ )} + ); } diff --git a/src/app/team/page.tsx b/src/app/team/page.tsx index afb47da1..981e10be 100644 --- a/src/app/team/page.tsx +++ b/src/app/team/page.tsx @@ -1,758 +1,227 @@ 'use client'; -import { - useCallback, - useEffect, - useRef, - useState, - type ReactNode, -} from 'react'; -import Link from 'next/link'; +import { useState, useEffect } from 'react'; import Image from 'next/image'; -import { motion, useReducedMotion } from 'framer-motion'; -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[]; -} - -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 BorderGlow: React.FC = ({ - 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); - } - }; - - 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}
-
- ); +import { motion } from 'framer-motion'; +import { Github, Linkedin, Instagram, Code, MapPin, Star, Shield, Users } from 'lucide-react'; +import { teamMembers, TeamMember, TeamCategory } from '@/data/team'; + +// Utility for creating initials +const getInitials = (name: string) => { + if (name === 'Application Pending') return '?'; + const parts = name.split(' '); + return parts.length > 1 ? `${parts[0][0]}${parts[1][0]}`.toUpperCase() : name.slice(0, 2).toUpperCase(); }; -const getInitials = (name: string): string => - name - .split(' ') - .map((part) => part[0]) - .join('') - .slice(0, 2) - .toUpperCase(); - -const rolePalette: Record = { - 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 && ( -