diff --git a/.env.example b/.env.example index a66c50d..7ccb534 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,11 @@ NEXT_PUBLIC_SUPABASE_URL='your-supabase-project-url' NEXT_PUBLIC_SUPABASE_ANON_KEY='your-supabase-anon-key' GOOGLE_SITE_VERIFICATION='your-google-site-verification-code' + +# Notion Integration (Fork Dashboard) +NOTION_API_KEY='secret_your-notion-integration-token' +NOTION_FORKS_DB_ID='your-forks-database-id' +NOTION_EVENTS_DB_ID='your-events-database-id' +NOTION_MEMBERS_DB_ID='your-members-database-id' +NOTION_REPORTS_DB_ID='your-reports-database-id' +NOTION_USERS_DB_ID='your-users-database-id' diff --git a/TECHNICAL_DOCUMENTATION.md b/TECHNICAL_DOCUMENTATION.md index 25798c2..7092db3 100644 --- a/TECHNICAL_DOCUMENTATION.md +++ b/TECHNICAL_DOCUMENTATION.md @@ -29,6 +29,12 @@ │ │ │ ├── feedback/ # Chat feedback collection │ │ │ ├── image/ # Image generation trigger │ │ │ └── voice/ # Voice input (placeholder) +│ │ ├── forks/ # Fork Dashboard API +│ │ │ ├── route.ts # GET forks list + leaderboard +│ │ │ └── [id]/route.ts # GET single fork details +│ │ ├── events/route.ts # Fork events CRUD +│ │ ├── reports/route.ts # Fork reports CRUD +│ │ ├── team/route.ts # Team members list │ │ ├── join/route.ts # Submit join requests → Supabase │ │ ├── contact/ # Contact form submissions │ │ └── discord/ # Discord OAuth integration @@ -55,7 +61,11 @@ │ ├── sentiment.ts # Frustration detection regex │ ├── team-data.ts # Team members + role matching logic │ ├── theme.ts # Theme utilities (dark mode) -│ └── utils.ts # cn() classname merger +│ ├── utils.ts # cn() classname merger +│ ├── notion.ts # Notion API wrapper (Fork Dashboard) +│ ├── gamification.ts # Points & levels system (Fork Dashboard) +│ ├── healthScore.ts # Health score calculations (Fork Dashboard) +│ └── onboarding.ts # Onboarding tracking (Fork Dashboard) ├── public/ │ ├── images/ # Optimized AVIF/WebP │ ├── partners/ # Sponsor logos @@ -725,7 +735,128 @@ Next.js 16 --- -## 16. Future Roadmap Notes +## 16. Fork Dashboard Integration + +### 16.1 Overview + +The Fork Dashboard is a Notion-powered management system for Bits&Bytes regional chapters (forks). It provides real-time tracking of fork health, events, team members, reports, and gamification through a web dashboard. + +**Architecture:** Single database (Notion) shared between Discord bot and website, ensuring real-time sync without additional infrastructure. + +### 16.2 Notion Database Schema + +| Database | Purpose | Key Properties | +|----------|---------|----------------| +| **Forks** | Regional chapters | Name, City, Status, Points, Health Score, Level, Team Size, Events Count | +| **Events** | Fork events | Name, Fork (relation), Status, Type, Date, Points, Sponsors | +| **Team Members** | Fork membership | Name, Fork (relation), Role, Discord ID, Status, Onboarding Complete | +| **Reports** | Weekly/monthly reports | Title, Fork (relation), Type, Status, Content, Late flag | +| **Users** | Website authentication | Name, Email, Password Hash, Role, Fork (relation) | + +### 16.3 Environment Variables + +```bash +# Fork Dashboard (Notion Integration) +NOTION_API_KEY='secret_your-notion-integration-token' +NOTION_FORKS_DB_ID='your-forks-database-id' +NOTION_EVENTS_DB_ID='your-events-database-id' +NOTION_MEMBERS_DB_ID='your-members-database-id' +NOTION_REPORTS_DB_ID='your-reports-database-id' +NOTION_USERS_DB_ID='your-users-database-id' +``` + +### 16.4 Library Modules (`lib/`) + +| Module | Purpose | Key Functions | +|--------|---------|---------------| +| `notion.ts` | Notion API wrapper | `getForks()`, `getFork()`, `getEvents()`, `createEvent()`, `getTeamMembers()`, `getReports()`, `createReport()` | +| `gamification.ts` | Points & levels | `addPoints()`, `getLevelFromPoints()`, `getLeaderboard()`, `POINTS` constants | +| `healthScore.ts` | Health calculations | `calculateHealthScore()`, `getHealthStatus()`, `getHealthRecommendations()` | +| `onboarding.ts` | Onboarding tracking | `calculateOnboardingProgress()`, `getNextStep()`, `ONBOARDING_STEPS` | + +### 16.5 API Routes (`app/api/`) + +| Route | Methods | Purpose | +|-------|---------|---------| +| `/api/forks` | GET | List all forks with health scores and leaderboard | +| `/api/forks/[id]` | GET | Single fork details with events, members, reports | +| `/api/events` | GET, POST | List/create events (auto-awards points) | +| `/api/reports` | GET, POST | List/submit reports (on-time bonus logic) | +| `/api/team` | GET | List team members by fork | + +### 16.6 Gamification System + +**Points Allocation:** +```typescript +const POINTS = { + EVENT_CREATED: 10, + EVENT_APPROVED: 20, + EVENT_COMPLETED: 50, + PER_SPONSOR_SECURED: 10, + REPORT_SUBMITTED: 15, + WEEKLY_PULSE_UPDATE: 10, + ON_TIME_REPORT_BONUS: 10, + MISSED_REPORT_DEADLINE: -15, + INACTIVE_TWO_WEEKS: -25, +} +``` + +**Level Progression:** +| Level | Points Range | Badge | Color | +|-------|--------------|-------|-------| +| Seed Fork | 0-99 | 🌱 | #81ECEC | +| Active Fork | 100-299 | 🌿 | #00FF95 | +| High Impact Fork | 300-699 | 🌳 | #00F2FF | +| Elite Fork | 700+ | 🏆 | #FFD700 | + +### 16.7 Health Score Calculation + +**Components (each 0-20 points):** +1. **Pulse Recency** - Days since last weekly update +2. **Events Conducted** - Total events count +3. **Team Completeness** - Team size +4. **Report Submission** - Regular reporting +5. **Partnerships** - Sponsor/partner engagement + +**Status Thresholds:** +- Excellent: 80-100 (green) +- Good: 60-79 (yellow) +- Fair: 40-59 (orange) +- At Risk: 0-39 (red) + +### 16.8 Dashboard UI (`app/fork/dashboard/`) + +**Features:** +- Fork selector with status badges +- Stats overview (Total Forks, Active Forks, Events, Members) +- Health score visualization with progress bar +- Events/Team/Reports tabs +- Live leaderboard sidebar +- Configuration error handling (graceful degradation) + +**Access:** `/fork/dashboard` + +### 16.9 Error Handling + +The dashboard gracefully handles missing Notion configuration: +- Displays configuration required message with missing env vars +- Shows actionable instructions for setup +- Returns 503 status for API calls when not configured + +**Example Check:** +```typescript +const configStatus = getNotionConfigStatus() +if (!configStatus.configured) { + return NextResponse.json( + { error: "Notion integration not configured", missing: configStatus.missing }, + { status: 503 } + ) +} +``` + +--- + +## 17. Future Roadmap Notes - [ ] Global rate limiter (Redis integration) - [ ] Voice input support (Whisper API) @@ -733,4 +864,6 @@ Next.js 16 - [ ] Multi-language RAG (Hindi embeddings) - [ ] Analytics dashboard (Supabase dashboards) - [ ] Email notifications for leads (Resend.dev) +- [ ] Fork Dashboard authentication (Supabase Auth) +- [ ] Website-to-Discord sync webhooks diff --git a/app/api/events/route.ts b/app/api/events/route.ts new file mode 100644 index 0000000..6e1a1d1 --- /dev/null +++ b/app/api/events/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from "next/server" +import { getEvents, createEvent, getNotionConfigStatus, NotionConfigError, NotionDatabaseError } from "@/lib/notion" +import { POINTS, addPoints } from "@/lib/gamification" + +export async function GET(req: NextRequest) { + const configStatus = getNotionConfigStatus() + if (!configStatus.configured) { + return NextResponse.json( + { error: "Notion integration not configured", missing: configStatus.missing }, + { status: 503 } + ) + } + + try { + const { searchParams } = new URL(req.url) + const forkId = searchParams.get("forkId") || undefined + const status = searchParams.get("status") as any + const limit = searchParams.get("limit") ? parseInt(searchParams.get("limit")!) : undefined + + const events = await getEvents({ forkId, status, limit }) + + return NextResponse.json({ events, total: events.length }) + } catch (error) { + console.error("[API /events] Error:", error) + + if (error instanceof NotionConfigError) { + return NextResponse.json({ error: error.message }, { status: 503 }) + } + + if (error instanceof NotionDatabaseError) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + return NextResponse.json( + { error: "Failed to fetch events. Please try again later." }, + { status: 500 } + ) + } +} + +export async function POST(req: NextRequest) { + const configStatus = getNotionConfigStatus() + if (!configStatus.configured) { + return NextResponse.json( + { error: "Notion integration not configured", missing: configStatus.missing }, + { status: 503 } + ) + } + + try { + const body = await req.json() + const { name, forkId, type, date, description } = body + + if (!name || !forkId || !type) { + return NextResponse.json( + { error: "Missing required fields: name, forkId, type" }, + { status: 400 } + ) + } + + const event = await createEvent({ + name, + forkId, + type, + date: date ? new Date(date) : undefined, + description, + }) + + // Award points for event creation + await addPoints(forkId, POINTS.EVENT_CREATED, "Event Created") + + return NextResponse.json({ event, message: "Event created successfully" }, { status: 201 }) + } catch (error) { + console.error("[API /events POST] Error:", error) + + if (error instanceof NotionConfigError) { + return NextResponse.json({ error: error.message }, { status: 503 }) + } + + if (error instanceof NotionDatabaseError) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + return NextResponse.json( + { error: "Failed to create event. Please try again later." }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/forks/[id]/route.ts b/app/api/forks/[id]/route.ts new file mode 100644 index 0000000..b0cee3d --- /dev/null +++ b/app/api/forks/[id]/route.ts @@ -0,0 +1,75 @@ +import { NextRequest, NextResponse } from "next/server" +import { getFork, getEvents, getTeamMembers, getReports, getNotionConfigStatus, NotionConfigError, NotionDatabaseError } from "@/lib/notion" +import { calculateHealthScore, getHealthRecommendations } from "@/lib/healthScore" +import { getLevelInfo, getPointsToNextLevel } from "@/lib/gamification" + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const configStatus = getNotionConfigStatus() + if (!configStatus.configured) { + return NextResponse.json( + { error: "Notion integration not configured", missing: configStatus.missing }, + { status: 503 } + ) + } + + try { + const { id } = await params + const fork = await getFork(id) + + if (!fork) { + return NextResponse.json({ error: "Fork not found" }, { status: 404 }) + } + + // Fetch related data + const [events, members, reports] = await Promise.all([ + getEvents({ forkId: id }), + getTeamMembers({ forkId: id }), + getReports({ forkId: id, limit: 5 }), + ]) + + // Calculate health score + const healthResult = calculateHealthScore(fork) + const recommendations = getHealthRecommendations(healthResult.breakdown) + + // Get level info + const levelInfo = getLevelInfo(fork.level) + const nextLevel = getPointsToNextLevel(fork.points) + + return NextResponse.json({ + fork: { + ...fork, + healthScore: healthResult.score, + healthStatus: healthResult.status, + healthBreakdown: healthResult.breakdown, + }, + level: { + current: fork.level, + ...levelInfo, + pointsToNextLevel: nextLevel.pointsNeeded, + nextLevel: nextLevel.next, + }, + events, + members, + reports, + recommendations, + }) + } catch (error) { + console.error("[API /forks/[id]] Error:", error) + + if (error instanceof NotionConfigError) { + return NextResponse.json({ error: error.message }, { status: 503 }) + } + + if (error instanceof NotionDatabaseError) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + return NextResponse.json( + { error: "Failed to fetch fork details. Please try again later." }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/forks/route.ts b/app/api/forks/route.ts new file mode 100644 index 0000000..596dff0 --- /dev/null +++ b/app/api/forks/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from "next/server" +import { getForks, getNotionConfigStatus, NotionConfigError, NotionDatabaseError } from "@/lib/notion" +import { calculateHealthScore } from "@/lib/healthScore" +import { getLeaderboard } from "@/lib/gamification" + +export async function GET(req: NextRequest) { + // Check Notion configuration + const configStatus = getNotionConfigStatus() + if (!configStatus.configured) { + return NextResponse.json( + { + error: "Notion integration not configured", + missing: configStatus.missing, + message: `Missing environment variables: ${configStatus.missing.join(", ")}. Please configure these in your .env file.`, + }, + { status: 503 } + ) + } + + try { + const { searchParams } = new URL(req.url) + const status = searchParams.get("status") as "Active" | "Inactive" | "Pending" | null + const limit = searchParams.get("limit") ? parseInt(searchParams.get("limit")!) : undefined + + const forks = await getForks({ status: status || undefined, limit }) + + // Calculate health scores for each fork + const forksWithHealth = forks.map((fork) => { + const healthResult = calculateHealthScore(fork) + return { + ...fork, + healthScore: healthResult.score, + healthStatus: healthResult.status, + healthBreakdown: healthResult.breakdown, + } + }) + + // Generate leaderboard + const leaderboard = await getLeaderboard(forks) + + return NextResponse.json({ + forks: forksWithHealth, + leaderboard, + total: forks.length, + }) + } catch (error) { + console.error("[API /forks] Error:", error) + + if (error instanceof NotionConfigError) { + return NextResponse.json({ error: error.message }, { status: 503 }) + } + + if (error instanceof NotionDatabaseError) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + return NextResponse.json( + { error: "Failed to fetch forks. Please try again later." }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/reports/route.ts b/app/api/reports/route.ts new file mode 100644 index 0000000..9c4e92b --- /dev/null +++ b/app/api/reports/route.ts @@ -0,0 +1,94 @@ +import { NextRequest, NextResponse } from "next/server" +import { getReports, createReport, getNotionConfigStatus, NotionConfigError, NotionDatabaseError } from "@/lib/notion" +import { POINTS, addPoints } from "@/lib/gamification" + +export async function GET(req: NextRequest) { + const configStatus = getNotionConfigStatus() + if (!configStatus.configured) { + return NextResponse.json( + { error: "Notion integration not configured", missing: configStatus.missing }, + { status: 503 } + ) + } + + try { + const { searchParams } = new URL(req.url) + const forkId = searchParams.get("forkId") || undefined + const type = searchParams.get("type") as any + const status = searchParams.get("status") as any + const limit = searchParams.get("limit") ? parseInt(searchParams.get("limit")!) : undefined + + const reports = await getReports({ forkId, type, status, limit }) + + return NextResponse.json({ reports, total: reports.length }) + } catch (error) { + console.error("[API /reports] Error:", error) + + if (error instanceof NotionConfigError) { + return NextResponse.json({ error: error.message }, { status: 503 }) + } + + if (error instanceof NotionDatabaseError) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + return NextResponse.json( + { error: "Failed to fetch reports. Please try again later." }, + { status: 500 } + ) + } +} + +export async function POST(req: NextRequest) { + const configStatus = getNotionConfigStatus() + if (!configStatus.configured) { + return NextResponse.json( + { error: "Notion integration not configured", missing: configStatus.missing }, + { status: 503 } + ) + } + + try { + const body = await req.json() + const { title, forkId, type, content, isLate } = body + + if (!title || !forkId || !type || !content) { + return NextResponse.json( + { error: "Missing required fields: title, forkId, type, content" }, + { status: 400 } + ) + } + + const report = await createReport({ + title, + forkId, + type, + content, + isLate: isLate ?? false, + }) + + // Award points for report submission + let points = POINTS.REPORT_SUBMITTED + if (!isLate) { + points += POINTS.ON_TIME_REPORT_BONUS + } + await addPoints(forkId, points, `Report Submitted: ${title}`) + + return NextResponse.json({ report, message: "Report submitted successfully" }, { status: 201 }) + } catch (error) { + console.error("[API /reports POST] Error:", error) + + if (error instanceof NotionConfigError) { + return NextResponse.json({ error: error.message }, { status: 503 }) + } + + if (error instanceof NotionDatabaseError) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + return NextResponse.json( + { error: "Failed to submit report. Please try again later." }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/team/route.ts b/app/api/team/route.ts new file mode 100644 index 0000000..50d0be9 --- /dev/null +++ b/app/api/team/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from "next/server" +import { getTeamMembers, getNotionConfigStatus, NotionConfigError, NotionDatabaseError } from "@/lib/notion" + +export async function GET(req: NextRequest) { + const configStatus = getNotionConfigStatus() + if (!configStatus.configured) { + return NextResponse.json( + { error: "Notion integration not configured", missing: configStatus.missing }, + { status: 503 } + ) + } + + try { + const { searchParams } = new URL(req.url) + const forkId = searchParams.get("forkId") || undefined + const status = searchParams.get("status") as any + const limit = searchParams.get("limit") ? parseInt(searchParams.get("limit")!) : undefined + + const members = await getTeamMembers({ forkId, status, limit }) + + return NextResponse.json({ members, total: members.length }) + } catch (error) { + console.error("[API /team] Error:", error) + + if (error instanceof NotionConfigError) { + return NextResponse.json({ error: error.message }, { status: 503 }) + } + + if (error instanceof NotionDatabaseError) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + return NextResponse.json( + { error: "Failed to fetch team members. Please try again later." }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/fork/dashboard/fork-dashboard.tsx b/app/fork/dashboard/fork-dashboard.tsx new file mode 100644 index 0000000..95c9c9e --- /dev/null +++ b/app/fork/dashboard/fork-dashboard.tsx @@ -0,0 +1,535 @@ +"use client" + +import { useState, useEffect } from "react" +import Link from "next/link" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Progress } from "@/components/ui/progress" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { + Trophy, Users, Calendar, FileText, Activity, TrendingUp, + AlertTriangle, CheckCircle, Clock, MapPin, ArrowRight, + Plus, RefreshCw +} from "lucide-react" + +// Types +interface Fork { + id: string + name: string + city: string + status: string + points: number + healthScore: number + healthStatus: string + level: string + eventsCount: number + teamSize: number +} + +interface Event { + id: string + name: string + type: string + status: string + date?: string +} + +interface TeamMember { + id: string + name: string + role: string + status: string +} + +interface LeaderboardEntry { + rank: number + forkId: string + forkName: string + city: string + points: number + level: string + badge: string +} + +const LEVEL_COLORS: Record = { + "Seed Fork": "bg-cyan-100 text-cyan-800 border-cyan-200", + "Active Fork": "bg-green-100 text-green-800 border-green-200", + "High Impact Fork": "bg-blue-100 text-blue-800 border-blue-200", + "Elite Fork": "bg-yellow-100 text-yellow-800 border-yellow-200", +} + +const HEALTH_COLORS: Record = { + "Excellent": "text-green-600", + "Good": "text-yellow-600", + "Fair": "text-orange-600", + "At Risk": "text-red-600", +} + +const EVENT_STATUS_COLORS: Record = { + "Draft": "bg-gray-100 text-gray-800", + "Planning": "bg-blue-100 text-blue-800", + "Announced": "bg-purple-100 text-purple-800", + "Ongoing": "bg-green-100 text-green-800", + "Completed": "bg-gray-100 text-gray-600", + "Cancelled": "bg-red-100 text-red-800", +} + +export function ForkDashboard() { + const [forks, setForks] = useState([]) + const [leaderboard, setLeaderboard] = useState([]) + const [selectedFork, setSelectedFork] = useState(null) + const [events, setEvents] = useState([]) + const [members, setMembers] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [configMissing, setConfigMissing] = useState([]) + + useEffect(() => { + fetchForks() + }, []) + + async function fetchForks() { + try { + setLoading(true) + setError(null) + + const res = await fetch("/api/forks") + const data = await res.json() + + if (!res.ok) { + if (data.missing) { + setConfigMissing(data.missing) + } + throw new Error(data.error || "Failed to fetch forks") + } + + setForks(data.forks || []) + setLeaderboard(data.leaderboard || []) + + if (data.forks?.length > 0) { + setSelectedFork(data.forks[0]) + fetchForkDetails(data.forks[0].id) + } + } catch (err) { + console.error("Failed to fetch forks:", err) + setError(err instanceof Error ? err.message : "Failed to load dashboard") + } finally { + setLoading(false) + } + } + + async function fetchForkDetails(forkId: string) { + try { + const res = await fetch(`/api/forks/${forkId}`) + const data = await res.json() + + if (res.ok) { + setEvents(data.events || []) + setMembers(data.members || []) + } + } catch (err) { + console.error("Failed to fetch fork details:", err) + } + } + + if (loading) { + return ( +
+
+ +

Loading dashboard...

+
+
+ ) + } + + if (configMissing.length > 0) { + return ( +
+ + +
+ + Configuration Required +
+ + The Fork Dashboard requires Notion integration to be configured. + +
+ +

+ Missing environment variables: +

+
    + {configMissing.map((key) => ( +
  • {key}
  • + ))} +
+

+ Please add these to your .env.local file and restart the server. +

+ + + +
+
+
+ ) + } + + if (error) { + return ( +
+ + +
+ + Error Loading Dashboard +
+
+ +

{error}

+ +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+
+
+ + ← Back to Home + +

Fork Dashboard

+
+ +
+
+
+ +
+ {/* Stats Overview */} +
+ + +
+
+

Total Forks

+

{forks.length}

+
+
+ +
+
+
+
+ + + +
+
+

Active Forks

+

{forks.filter(f => f.status === "Active").length}

+
+
+ +
+
+
+
+ + + +
+
+

Total Events

+

{forks.reduce((sum, f) => sum + f.eventsCount, 0)}

+
+
+ +
+
+
+
+ + + +
+
+

Total Members

+

{forks.reduce((sum, f) => sum + f.teamSize, 0)}

+
+
+ +
+
+
+
+
+ +
+ {/* Main Content */} +
+ {/* Fork Selector */} + {forks.length > 0 && ( + + + Select Fork + + +
+ {forks.map((fork) => ( + + ))} +
+
+
+ )} + + {/* Selected Fork Details */} + {selectedFork && ( + + +
+
+ {selectedFork.name} + + + {selectedFork.city} + +
+ + {selectedFork.level} + +
+
+ +
+
+

Points

+

{selectedFork.points}

+
+
+

Health

+

+ {selectedFork.healthScore} +

+
+
+

Events

+

{selectedFork.eventsCount}

+
+
+

Team

+

{selectedFork.teamSize}

+
+
+ + {/* Health Score Progress */} +
+
+ Health Score + {selectedFork.healthStatus} +
+ +
+
+
+ )} + + {/* Tabs for Events/Team/Reports */} + + + + + Events + + + + Team + + + + Reports + + + + + + +
+ Events + +
+
+ + {events.length === 0 ? ( +

No events found

+ ) : ( +
+ {events.map((event) => ( +
+
+

{event.name}

+

{event.type}

+
+ + {event.status} + +
+ ))} +
+ )} +
+
+
+ + + + +
+ Team Members + +
+
+ + {members.length === 0 ? ( +

No team members found

+ ) : ( +
+ {members.map((member) => ( +
+
+

{member.name}

+

{member.role}

+
+ + {member.status} + +
+ ))} +
+ )} +
+
+
+ + + + +
+ Reports + +
+
+ +

+ Select a fork to view reports +

+
+
+
+
+
+ + {/* Sidebar - Leaderboard */} +
+ + + + + Leaderboard + + Fork rankings by points + + + {leaderboard.length === 0 ? ( +

No data available

+ ) : ( +
+ {leaderboard.slice(0, 10).map((entry) => ( +
+ + {entry.rank} + +
+

{entry.forkName}

+

{entry.city}

+
+
+

{entry.points}

+ {entry.badge} +
+
+ ))} +
+ )} +
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/fork/dashboard/page.tsx b/app/fork/dashboard/page.tsx new file mode 100644 index 0000000..0f6242d --- /dev/null +++ b/app/fork/dashboard/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next" +import { ForkDashboard } from "./fork-dashboard" + +export const metadata: Metadata = { + title: "Fork Dashboard | Bits&Bytes", + description: "Manage your Bits&Bytes fork - events, team, reports, and health scores.", +} + +export default function DashboardPage() { + return +} \ No newline at end of file diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..76d3397 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,35 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } \ No newline at end of file diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx new file mode 100644 index 0000000..1709c4d --- /dev/null +++ b/components/ui/progress.tsx @@ -0,0 +1,34 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" + +interface ProgressProps extends React.HTMLAttributes { + value?: number + max?: number +} + +const Progress = React.forwardRef( + ({ className, value = 0, max = 100, ...props }, ref) => { + const percentage = Math.min(100, Math.max(0, (value / max) * 100)) + + return ( +
+
+
+ ) + } +) +Progress.displayName = "Progress" + +export { Progress } \ No newline at end of file diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..dc5e9c2 --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,116 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" + +interface TabsContextValue { + value: string + onValueChange: (value: string) => void +} + +const TabsContext = React.createContext(undefined) + +function useTabsContext() { + const context = React.useContext(TabsContext) + if (!context) { + throw new Error("Tabs components must be used within a Tabs provider") + } + return context +} + +interface TabsProps extends React.HTMLAttributes { + defaultValue?: string + value?: string + onValueChange?: (value: string) => void +} + +const Tabs = React.forwardRef( + ({ defaultValue, value: controlledValue, onValueChange, className, children, ...props }, ref) => { + const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue || "") + const value = controlledValue ?? uncontrolledValue + + const handleValueChange = React.useCallback((newValue: string) => { + setUncontrolledValue(newValue) + onValueChange?.(newValue) + }, [onValueChange]) + + return ( + +
+ {children} +
+
+ ) + } +) +Tabs.displayName = "Tabs" + +const TabsList = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +TabsList.displayName = "TabsList" + +interface TabsTriggerProps extends React.ButtonHTMLAttributes { + value: string +} + +const TabsTrigger = React.forwardRef( + ({ className, value, ...props }, ref) => { + const { value: selectedValue, onValueChange } = useTabsContext() + const isSelected = selectedValue === value + + return ( +