diff --git a/drizzle/0010_partner_proposals.sql b/drizzle/0010_partner_proposals.sql new file mode 100644 index 0000000..2ba4392 --- /dev/null +++ b/drizzle/0010_partner_proposals.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS "partner_proposals" ( + "id" text PRIMARY KEY NOT NULL, + "organization_name" text NOT NULL, + "contact_name" text NOT NULL, + "contact_email" text NOT NULL, + "partner_type" text NOT NULL, + "proposal_type" text NOT NULL, + "summary" text NOT NULL, + "target_technologies" jsonb DEFAULT '[]'::jsonb NOT NULL, + "estimated_student_capacity" integer DEFAULT 10 NOT NULL, + "status" text DEFAULT 'submitted' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "partner_proposals_status_idx" ON "partner_proposals" ("status"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "partner_proposals_created_at_idx" ON "partner_proposals" ("created_at"); diff --git a/src/app/admin/AdminDashboardClient.tsx b/src/app/admin/AdminDashboardClient.tsx index 409fcf5..c899cf2 100644 --- a/src/app/admin/AdminDashboardClient.tsx +++ b/src/app/admin/AdminDashboardClient.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { Building2, Mail, Users } from "lucide-react"; interface ProjectItem { id: string; @@ -46,11 +47,26 @@ interface MentorItem { issueNumber?: number; } +export interface PartnerProposalItem { + id: string; + organizationName: string; + contactName: string; + contactEmail: string; + partnerType: string; + proposalType: string; + summary: string; + targetTechnologies: string[]; + estimatedStudentCapacity: number; + status: string; + createdAt: string; +} + interface AdminDashboardClientProps { projects: ProjectItem[]; issues: IssueItem[]; prs: PrItem[]; mentorRequests: MentorItem[]; + partnerProposals?: PartnerProposalItem[]; } export function AdminDashboardClient({ @@ -58,11 +74,12 @@ export function AdminDashboardClient({ issues: initialIssues, prs, mentorRequests, + partnerProposals = [], }: AdminDashboardClientProps) { const [projects, setProjects] = useState(initialProjects); const [issues, setIssues] = useState(initialIssues); const [activeTab, setActiveTab] = useState< - "projects" | "issues" | "prs" | "mentorship" + "projects" | "issues" | "prs" | "mentorship" | "partners" >("projects"); const [loadingAction, setLoadingAction] = useState(null); @@ -72,7 +89,10 @@ export function AdminDashboardClient({ const res = await fetch(`/api/admin/projects/${id}/approve`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ contributionEnabled: true, firstPrEnabled: true }), + body: JSON.stringify({ + contributionEnabled: true, + firstPrEnabled: true, + }), }); if (!res.ok) throw new Error("Failed to approve project"); setProjects((prev) => @@ -85,8 +105,8 @@ export function AdminDashboardClient({ firstPrEnabled: true, approvedAt: new Date().toISOString(), } - : p - ) + : p, + ), ); } catch (err: any) { alert(err.message); @@ -112,8 +132,8 @@ export function AdminDashboardClient({ firstPrEnabled: false, approvedAt: null, } - : p - ) + : p, + ), ); } catch (err: any) { alert(err.message); @@ -122,20 +142,23 @@ export function AdminDashboardClient({ } }; - const handleClassifyIssue = async ( + const handleUpdateDifficulty = async ( id: string, - difficulty: "beginner" | "intermediate" | "advanced" + difficulty: IssueItem["difficulty"], + isGoodFirstIssue: boolean, ) => { - setLoadingAction(`classify_${id}`); + setLoadingAction(`issue_${id}`); try { - const res = await fetch(`/api/admin/issues/${id}/classify`, { + const res = await fetch(`/api/admin/issues/${id}/triage`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ difficulty }), + body: JSON.stringify({ difficulty, isGoodFirstIssue }), }); - if (!res.ok) throw new Error("Failed to classify issue"); + if (!res.ok) throw new Error("Failed to triage issue"); setIssues((prev) => - prev.map((iss) => (iss.id === id ? { ...iss, difficulty } : iss)) + prev.map((i) => + i.id === id ? { ...i, difficulty, isGoodFirstIssue } : i, + ), ); } catch (err: any) { alert(err.message); @@ -147,168 +170,173 @@ export function AdminDashboardClient({ return (
{/* Navigation Tabs */} -
+
+
{/* Tab 1: Project Approvals */} {activeTab === "projects" && (
-
- {projects.map((proj) => { - const isApproved = proj.isOfficial && proj.contributionEnabled && !!proj.approvedAt; - return ( -
-
-
- - {proj.githubRepo} - - - {isApproved ? "Official & Approved" : "Unapproved / Disabled"} - -
-
- Open issues: {proj.openIssuesCount} • First PR eligible:{" "} - {proj.firstPrEnabled ? "Yes" : "No"} -
+
+ {projects.map((p) => ( +
+
+
+ + {p.name} + + + {p.isOfficial + ? "Official Ecosystem" + : "Pending Verification"} +
+

+ {p.githubRepo} • {p.openIssuesCount} issues +

+
-
- {isApproved ? ( - - ) : ( - - )} -
+
+ {p.isOfficial ? ( + + ) : ( + + )}
- ); - })} +
+ ))}
)} - {/* Tab 2: Issue Quality & Difficulty */} + {/* Tab 2: Issue Triage */} {activeTab === "issues" && (
-
- {issues.map((iss) => ( +
+ {issues.map((i) => (
-
+
- {iss.githubRepo} #{iss.githubIssueNumber} - - - {iss.difficulty} + {i.githubRepo} #{i.githubIssueNumber} + {i.isGoodFirstIssue && ( + + #FirstPR + + )}

- {iss.title} + {i.title}

-
- Set difficulty: -
@@ -365,13 +393,16 @@ export function AdminDashboardClient({
- From @{req.studentUsername || "contributor"} on Issue #{req.issueNumber || "—"} + From @{req.studentUsername || "contributor"} on Issue # + {req.issueNumber || "—"} {req.status}
-

{req.message}

+

+ {req.message} +

{req.reply && (

Reply: {req.reply} @@ -382,6 +413,77 @@ export function AdminDashboardClient({

)} + + {/* Tab 5: Partner Proposals */} + {activeTab === "partners" && ( +
+ {partnerProposals.length === 0 ? ( +
+ No institutional partner proposals submitted yet. +
+ ) : ( +
+ {partnerProposals.map((p) => ( +
+
+
+ + + {p.organizationName} + + + {p.partnerType.replace("_", " ")} + +
+
+ {new Date(p.createdAt).toLocaleDateString()} +
+
+ +
+ + + Contact: {p.contactName} + + + + + {p.contactEmail} + + + + Type: {p.proposalType.replace("_", " ")} + + + Capacity: {p.estimatedStudentCapacity} students + +
+ +

+ {p.summary} +

+ + {p.targetTechnologies && p.targetTechnologies.length > 0 && ( +
+ {p.targetTechnologies.map((t, idx) => ( + + {t} + + ))} +
+ )} +
+ ))} +
+ )} +
+ )}
); } diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 128dcd9..fb97142 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -8,7 +8,8 @@ import { AdminDashboardClient } from "./AdminDashboardClient"; export const metadata = { title: "Maintainer & Admin Operations — TechNexusOrg", - description: "Repository approvals, issue difficulty classification, and PR review triage.", + description: + "Repository approvals, issue difficulty classification, and PR review triage.", }; export const dynamic = "force-dynamic"; @@ -81,7 +82,10 @@ export default async function AdminPage() { openedAt: schema.pullRequests.openedAt, }) .from(schema.pullRequests) - .innerJoin(schema.projects, eq(schema.pullRequests.projectId, schema.projects.id)) + .innerJoin( + schema.projects, + eq(schema.pullRequests.projectId, schema.projects.id), + ) .where(eq(schema.pullRequests.state, "open")) .orderBy(desc(schema.pullRequests.openedAt)); @@ -97,11 +101,29 @@ export default async function AdminPage() { issueNumber: schema.issues.githubIssueNumber, }) .from(schema.mentorRequests) - .innerJoin(schema.users, eq(schema.mentorRequests.studentId, schema.users.id)) - .innerJoin(schema.issues, eq(schema.mentorRequests.issueId, schema.issues.id)) + .innerJoin( + schema.users, + eq(schema.mentorRequests.studentId, schema.users.id), + ) + .innerJoin( + schema.issues, + eq(schema.mentorRequests.issueId, schema.issues.id), + ) .orderBy(desc(schema.mentorRequests.createdAt)) .limit(50); + // 5. Fetch partner proposals + let partnerProposals: any[] = []; + try { + partnerProposals = await db + .select() + .from(schema.partnerProposals) + .orderBy(desc(schema.partnerProposals.createdAt)) + .limit(50); + } catch { + partnerProposals = []; + } + return (
@@ -109,14 +131,17 @@ export default async function AdminPage() { Maintainer Operations Center

- Review official repository trust boundaries, classify issue difficulty, and monitor PR review queues. + Review official repository trust boundaries, classify issue + difficulty, and monitor PR review queues.

({ ...p, - approvedAt: p.approvedAt ? (p.approvedAt as Date).toISOString() : null, + approvedAt: p.approvedAt + ? (p.approvedAt as Date).toISOString() + : null, }))} issues={issues as any} prs={prs.map((pr: any) => ({ @@ -124,6 +149,10 @@ export default async function AdminPage() { openedAt: (pr.openedAt as Date).toISOString(), }))} mentorRequests={mentorRequests as any} + partnerProposals={partnerProposals.map((prop: any) => ({ + ...prop, + createdAt: (prop.createdAt as Date).toISOString(), + }))} />
); diff --git a/src/app/api/partners/proposals/route.test.ts b/src/app/api/partners/proposals/route.test.ts new file mode 100644 index 0000000..98bb37f --- /dev/null +++ b/src/app/api/partners/proposals/route.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { POST } from "./route"; +import { getDb, schema } from "@/lib/db"; +import { runMigrations } from "@/lib/db/migrate"; +import { eq } from "drizzle-orm"; + +describe("POST /api/partners/proposals", () => { + beforeEach(async () => { + await runMigrations(); + }); + + it("should reject invalid proposal payloads", async () => { + const req = new Request("http://localhost:3000/api/partners/proposals", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + organizationName: "", + contactName: "", + contactEmail: "invalid-email", + }), + }); + + const res = await POST(req); + expect(res.status).toBe(400); + + const data = await res.json(); + expect(data.error).toBe("Validation failed"); + expect(data.details).toBeDefined(); + }); + + it("should successfully store a valid institutional proposal", async () => { + const payload = { + organizationName: "Indian Institute of Open Source", + contactName: "Prof. Vikram Sarabhai", + contactEmail: "vikram@iios.edu.in", + partnerType: "university", + proposalType: "project_partnership", + summary: + "Proposing a semester-long capstone track in distributed systems and POSIX kernels.", + targetTechnologies: ["Linux", "Go", "Rust", "PostgreSQL"], + estimatedStudentCapacity: 25, + }; + + const req = new Request("http://localhost:3000/api/partners/proposals", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + const res = await POST(req); + expect(res.status).toBe(201); + + const data = await res.json(); + expect(data.success).toBe(true); + expect(data.proposalId).toBeDefined(); + + // Verify record in database + const db = await getDb(); + const rows = await db + .select() + .from(schema.partnerProposals) + .where(eq(schema.partnerProposals.id, data.proposalId)); + + expect(rows).toHaveLength(1); + expect(rows[0].organizationName).toBe(payload.organizationName); + expect(rows[0].contactEmail).toBe(payload.contactEmail); + expect(rows[0].partnerType).toBe(payload.partnerType); + expect(rows[0].status).toBe("submitted"); + expect(rows[0].estimatedStudentCapacity).toBe(25); + }); +}); diff --git a/src/app/api/partners/proposals/route.ts b/src/app/api/partners/proposals/route.ts new file mode 100644 index 0000000..b9bac41 --- /dev/null +++ b/src/app/api/partners/proposals/route.ts @@ -0,0 +1,91 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import crypto from "crypto"; +import { getDb, schema } from "@/lib/db"; +import { runMigrations } from "@/lib/db/migrate"; + +const proposalSchema = z.object({ + organizationName: z + .string() + .min(2, "Organization name must be at least 2 characters"), + contactName: z.string().min(2, "Contact name must be at least 2 characters"), + contactEmail: z.string().email("Please provide a valid email address"), + partnerType: z.enum([ + "university", + "company", + "open_source_collective", + "other", + ]), + proposalType: z.enum([ + "project_partnership", + "cohort_sponsorship", + "mentorship_program", + "hiring_pipeline", + ]), + summary: z + .string() + .min(10, "Summary must be at least 10 characters") + .max(2000), + targetTechnologies: z.array(z.string()).default([]), + estimatedStudentCapacity: z.number().int().min(1).max(1000).default(10), +}); + +export async function POST(req: Request) { + try { + const body = await req.json(); + const result = proposalSchema.safeParse(body); + + if (!result.success) { + return NextResponse.json( + { error: "Validation failed", details: result.error.errors }, + { status: 400 }, + ); + } + + const { + organizationName, + contactName, + contactEmail, + partnerType, + proposalType, + summary, + targetTechnologies, + estimatedStudentCapacity, + } = result.data; + + // Ensure database migration has executed + await runMigrations(); + const db = await getDb(); + + const proposalId = `prop_${crypto.randomUUID()}`; + + await db.insert(schema.partnerProposals).values({ + id: proposalId, + organizationName, + contactName, + contactEmail, + partnerType, + proposalType, + summary, + targetTechnologies, + estimatedStudentCapacity, + status: "submitted", + }); + + return NextResponse.json( + { + success: true, + message: + "Partner proposal submitted successfully. Our team will review and contact you.", + proposalId, + }, + { status: 201 }, + ); + } catch (err: any) { + console.error("Failed to submit partner proposal:", err); + return NextResponse.json( + { error: "Internal server error submitting proposal" }, + { status: 500 }, + ); + } +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 9206e15..fca1b80 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -4,7 +4,10 @@ import Link from "next/link"; import { verifySessionToken, COOKIE_NAME } from "@/lib/auth/session"; import { getDb, schema } from "@/lib/db"; import { eq, and, or, desc } from "drizzle-orm"; -import { evaluateProgression, type ContributorLevel } from "@/lib/progression/rules"; +import { + evaluateProgression, + type ContributorLevel, +} from "@/lib/progression/rules"; import { recommendIssues } from "@/lib/recommendations/engine"; import { expireOverdueClaims } from "@/lib/issues/claims"; import { SyncContributionsButton } from "./SyncContributionsButton"; @@ -52,7 +55,10 @@ export default async function DashboardPage() { projectId: schema.projects.id, }) .from(schema.contributions) - .innerJoin(schema.projects, eq(schema.contributions.projectId, schema.projects.id)) + .innerJoin( + schema.projects, + eq(schema.contributions.projectId, schema.projects.id), + ) .where(eq(schema.contributions.userId, sessionUser.id)) .orderBy(desc(schema.contributions.createdAt)); @@ -80,8 +86,8 @@ export default async function DashboardPage() { .where( and( eq(schema.issueClaims.userId, sessionUser.id), - eq(schema.issueClaims.status, "active") - ) + eq(schema.issueClaims.status, "active"), + ), ); // Fetch live credentials @@ -91,6 +97,45 @@ export default async function DashboardPage() { .where(eq(schema.credentials.userId, sessionUser.id)) .orderBy(desc(schema.credentials.issuedAt)); + // Fetch active learning track enrollments + const userTrackEnrollments = await db + .select({ + id: schema.learningEnrollments.id, + status: schema.learningEnrollments.status, + enrolledAt: schema.learningEnrollments.enrolledAt, + completedAt: schema.learningEnrollments.completedAt, + trackTitle: schema.learningTracks.title, + trackSlug: schema.learningTracks.slug, + difficulty: schema.learningTracks.difficulty, + estimatedHours: schema.learningTracks.estimatedHours, + }) + .from(schema.learningEnrollments) + .innerJoin( + schema.learningTracks, + eq(schema.learningEnrollments.trackId, schema.learningTracks.id), + ) + .where(eq(schema.learningEnrollments.userId, sessionUser.id)); + + // Fetch active fellowship enrollment + const userFellowships = await db + .select({ + id: schema.internshipEnrollments.id, + status: schema.internshipEnrollments.status, + startDate: schema.internshipEnrollments.startDate, + programTitle: schema.internshipPrograms.title, + programTrack: schema.internshipPrograms.track, + durationWeeks: schema.internshipPrograms.durationWeeks, + }) + .from(schema.internshipEnrollments) + .innerJoin( + schema.internshipPrograms, + eq(schema.internshipEnrollments.programId, schema.internshipPrograms.id), + ) + .where(eq(schema.internshipEnrollments.userId, sessionUser.id)) + .limit(1); + + const activeFellowship = userFellowships[0] || null; + // Fetch user profile details const userProfiles = await db .select() @@ -99,7 +144,9 @@ export default async function DashboardPage() { .limit(1); const profile = userProfiles[0]; - const mergedCount = userContributions.filter((c: any) => c.state === "merged").length; + const mergedCount = userContributions.filter( + (c: any) => c.state === "merged", + ).length; // Real database metrics for progression calculation const completedClaims = await db @@ -108,8 +155,8 @@ export default async function DashboardPage() { .where( and( eq(schema.issueClaims.userId, sessionUser.id), - eq(schema.issueClaims.status, "completed") - ) + eq(schema.issueClaims.status, "completed"), + ), ); const userReviews = await db @@ -120,9 +167,9 @@ export default async function DashboardPage() { eq(schema.pullRequestReviews.reviewerGithubId, sessionUser.githubId), or( eq(schema.pullRequestReviews.reviewState, "approved"), - eq(schema.pullRequestReviews.reviewState, "changes_requested") - ) - ) + eq(schema.pullRequestReviews.reviewState, "changes_requested"), + ), + ), ); const userOpenedPrs = await db @@ -139,7 +186,9 @@ export default async function DashboardPage() { prsMerged: mergedCount, issuesResolved, reviewsCompleted, - projectsContributedCount: new Set(userContributions.map((c: any) => c.projectId)).size, + projectsContributedCount: new Set( + userContributions.map((c: any) => c.projectId), + ).size, isOnboarded: sessionUser.isOnboarded, }); @@ -169,7 +218,7 @@ export default async function DashboardPage() { const mappedExpLevel = profile?.experienceLevel === "complete_beginner" ? "beginner" - : ((profile?.experienceLevel as any) || "beginner"); + : (profile?.experienceLevel as any) || "beginner"; const recommended = profile ? recommendIssues( @@ -184,7 +233,7 @@ export default async function DashboardPage() { preferredLanguages: profile.preferredLanguages || [], contributionPreferences: profile.contributionPreferences || [], }, - 5 + 5, ) : []; @@ -213,7 +262,8 @@ export default async function DashboardPage() { {sessionUser.foundingNumber && ( - Founding Member #{String(sessionUser.foundingNumber).padStart(4, "0")} + Founding Member # + {String(sessionUser.foundingNumber).padStart(4, "0")} )}
@@ -248,6 +298,35 @@ export default async function DashboardPage() {
+ {/* Active Fellowship Banner */} + {activeFellowship && ( +
+
+
+ + + Active Fellowship:{" "} + {activeFellowship.programTrack.replace("_", " ")} + +
+

+ {activeFellowship.programTitle} +

+

+ Duration: {activeFellowship.durationWeeks} weeks • Supervised + open-source fellowship +

+
+ + Open Fellowship Workspace + → + +
+ )} + {/* 1. YOUR NEXT CONTRIBUTION (Top Action Hero) */}
@@ -274,7 +353,9 @@ export default async function DashboardPage() {
{topRecommendation.issue.githubRepo} • - {topRecommendation.issue.difficulty} + + {topRecommendation.issue.difficulty} + {topRecommendation.issue.estimatedEffort && ( <> • @@ -299,14 +380,16 @@ export default async function DashboardPage() { {/* Skills Tags */} {topRecommendation.issue.skillsRequired.length > 0 && (
- {topRecommendation.issue.skillsRequired.map((skill: string) => ( - - {skill} - - ))} + {topRecommendation.issue.skillsRequired.map( + (skill: string) => ( + + {skill} + + ), + )}
)}
@@ -318,12 +401,17 @@ export default async function DashboardPage() { Why this is recommended:
- {topRecommendation.matchReasons.map((reason: string, i: number) => ( -
- ✓ - {reason} -
- ))} + {topRecommendation.matchReasons.map( + (reason: string, i: number) => ( +
+ ✓ + {reason} +
+ ), + )}
@@ -344,7 +432,8 @@ export default async function DashboardPage() { No matching open issues found in official repositories right now.

- Check back shortly or browse all active issues across official TechNexusOrg repositories. + Check back shortly or browse all active issues across official + TechNexusOrg repositories.

- In Progress + + In Progress +
@@ -383,7 +474,10 @@ export default async function DashboardPage() {

- + {claim.issueTitle}

@@ -413,7 +507,8 @@ export default async function DashboardPage() { Contribution Progress & Pipeline

- Verified proof of work drives automatic rank elevation. No fake counts or self-awarded titles. + Verified proof of work drives automatic rank elevation. No fake + counts or self-awarded titles.

@@ -468,15 +563,17 @@ export default async function DashboardPage() { {matchReasons.length > 0 && (
- {matchReasons.slice(0, 2).map((reason: string, idx: number) => ( -
- ✓ - {reason} -
- ))} + {matchReasons + .slice(0, 2) + .map((reason: string, idx: number) => ( +
+ ✓ + {reason} +
+ ))}
)}
@@ -515,7 +612,8 @@ export default async function DashboardPage() { No contributions recorded yet.

- Claim an issue and submit your pull request on GitHub to see live contribution tracking. + Claim an issue and submit your pull request on GitHub to see live + contribution tracking.

) : ( @@ -536,7 +634,9 @@ export default async function DashboardPage() { > {contrib.state} - {contrib.githubRepo} + + {contrib.githubRepo} +

- {/* 6. VERIFIED CREDENTIALS */} + {/* 6. ENROLLED LEARNING TRACKS */} +
+
+
+

+ Enrolled Learning Tracks ({userTrackEnrollments.length}) +

+

+ Hands-on terminal curriculum, architecture deep-dives, and + verifiable capstones. +

+
+ + Browse All Tracks → + +
+ + {userTrackEnrollments.length === 0 ? ( +
+

+ Not enrolled in any structured learning tracks yet. +

+ + Explore Linux & Git Tracks → + +
+ ) : ( +
+ {userTrackEnrollments.map((enrollment: any) => ( +
+
+ + {enrollment.status.replace("_", " ")} + + + ⏱ {enrollment.estimatedHours} hrs + +
+

+ + {enrollment.trackTitle} + +

+
+ + {enrollment.difficulty} + + + Continue Track → + +
+
+ ))} +
+ )} +
+ + {/* 7. VERIFIED CREDENTIALS */}

@@ -588,7 +760,8 @@ export default async function DashboardPage() { No credentials issued yet.

- Merge your first pull request on an official project to earn the verified #FirstPR credential. + Merge your first pull request on an official project to earn the + verified #FirstPR credential.

) : ( @@ -606,7 +779,9 @@ export default async function DashboardPage() { {new Date(cred.issuedAt).toLocaleDateString()}
-

{cred.title}

+

+ {cred.title} +

{cred.description}

- {/* 7. SUPPORTING STATS */} + {/* 8. SUPPORTING STATS */}
-
Total PRs Merged
-
{mergedCount}
-
Official TechNexus repositories
+
+ Total PRs Merged +
+
+ {mergedCount} +
+
+ Official TechNexus repositories +
-
Verified Credentials
+
+ Verified Credentials +
{userCredentials.length}
-
Verifiable records
+
+ Verifiable records +
-
Experience Profile
+
+ Experience Profile +
{profile?.experienceLevel?.replace("_", " ") || "Beginner"}
diff --git a/src/app/globals.css b/src/app/globals.css index 13f23c2..1cce37c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,11 +3,17 @@ @tailwind utilities; :root { - --background: #090d16; - --foreground: #f1f5f9; - --border: #1e293b; - --card: #0f172a; - --card-foreground: #f8fafc; + --background: #07090e; + --foreground: #f8fafc; + --border: #1b2333; + --card: #0c1018; + --card-foreground: #f1f5f9; + --surface-raised: #121824; + --surface-overlay: #182234; + --accent-cyan: #38bdf8; + --accent-lime: #10b981; + --accent-violet: #a855f7; + --accent-amber: #f59e0b; } body { @@ -15,24 +21,98 @@ body { color: var(--foreground); font-feature-settings: "cv02", "cv03", "cv04", "cv11"; min-height: 100vh; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; } -/* Developer technical aesthetics */ +/* Micro-interaction & developer aesthetics */ .grid-bg { background-size: 32px 32px; - background-image: - linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px), - linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px); + background-image: + linear-gradient(to right, rgba(255, 255, 255, 0.035) 1px, transparent 1px), + linear-gradient(to bottom, rgba(255, 255, 255, 0.035) 1px, transparent 1px); +} + +.subtle-radial { + background: radial-gradient( + circle at 50% 0%, + rgba(56, 189, 248, 0.12) 0%, + rgba(16, 185, 129, 0.04) 40%, + transparent 70% + ); } .glow-border { position: relative; } .glow-border::after { - content: ''; + content: ""; position: absolute; inset: -1px; - background: linear-gradient(90deg, rgba(2, 132, 199, 0.4), rgba(16, 185, 129, 0.4)); + background: linear-gradient( + 90deg, + rgba(56, 189, 248, 0.4), + rgba(16, 185, 129, 0.4) + ); border-radius: inherit; z-index: -1; + pointer-events: none; +} + +/* Bento card layered surface */ +.bento-card { + position: relative; + background: linear-gradient( + 180deg, + rgba(16, 23, 38, 0.6) 0%, + rgba(11, 15, 25, 0.8) 100% + ); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 1rem; + backdrop-filter: blur(12px); + transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); +} + +.bento-card:hover { + border-color: rgba(56, 189, 248, 0.35); + box-shadow: + 0 12px 30px -10px rgba(0, 0, 0, 0.5), + 0 0 20px -5px rgba(56, 189, 248, 0.15); + transform: translateY(-2px); +} + +/* Glass panel */ +.glass-panel { + background: rgba(12, 16, 24, 0.75); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.07); +} + +/* Custom scrollbars */ +::-webkit-scrollbar { + width: 7px; + height: 7px; +} +::-webkit-scrollbar-track { + background: #07090e; +} +::-webkit-scrollbar-thumb { + background: #1e293b; + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: #334155; +} + +/* Accessibility: Safe reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, + ::before, + ::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } } diff --git a/src/app/internships/page.tsx b/src/app/internships/page.tsx index 1dfebee..fa1e190 100644 --- a/src/app/internships/page.tsx +++ b/src/app/internships/page.tsx @@ -1,8 +1,10 @@ import Link from "next/link"; import { cookies } from "next/headers"; import { verifySessionToken, COOKIE_NAME } from "@/lib/auth/session"; -import { listInternshipPrograms, getInternshipPortalData } from "@/lib/internships/service"; -import { Navbar } from "@/components/Navbar"; +import { + listInternshipPrograms, + getInternshipPortalData, +} from "@/lib/internships/service"; import { ApplyModal } from "./ApplyModal"; export const metadata = { @@ -23,8 +25,6 @@ export default async function InternshipsPage() { return (
- -
{/* Banner if enrolled */} {portalData?.enrollment && ( @@ -32,10 +32,13 @@ export default async function InternshipsPage() {
- Active Fellowship Enrollment: {portalData.program.title} + + Active Fellowship Enrollment: {portalData.program.title} +

- Access your milestones, submit weekly work logs, and track mentor evaluations in your Contributor Portal. + Access your milestones, submit weekly work logs, and track + mentor evaluations in your Contributor Portal.

- Move beyond certificates and tutorial clones. Work as an apprentice software engineer - under experienced open-source maintainers. Ship real pull requests, defend your architectural choices, - and build undeniable proof of work. + Move beyond certificates and tutorial clones. Work as an apprentice + software engineer under experienced open-source maintainers. Ship + real pull requests, defend your architectural choices, and build + undeniable proof of work.

@@ -73,7 +77,8 @@ export default async function InternshipsPage() { 01. Real Repositories

- Every deliverable is an actual pull request merged into official TechNexusOrg repositories. Zero fake metrics or toy projects. + Every deliverable is an actual pull request merged into official + TechNexusOrg repositories. Zero fake metrics or toy projects.

@@ -81,7 +86,8 @@ export default async function InternshipsPage() { 02. 1-on-1 Mentorship

- Assigned engineering maintainers review your PRs, provide weekly code critique, and unblock architectural bottlenecks. + Assigned engineering maintainers review your PRs, provide weekly + code critique, and unblock architectural bottlenecks.

@@ -89,7 +95,8 @@ export default async function InternshipsPage() { 03. Cryptographic Proof

- Offers and graduation credentials contain tamper-evident SHA-256 verification hashes verifiable by employers and universities. + Offers and graduation credentials contain tamper-evident SHA-256 + verification hashes verifiable by employers and universities.

@@ -141,12 +148,16 @@ export default async function InternshipsPage() { Target Learning Outcomes

    - {program.learningOutcomes.map((outcome: string, idx: number) => ( -
  • - • - {outcome} -
  • - ))} + {program.learningOutcomes.map( + (outcome: string, idx: number) => ( +
  • + + • + + {outcome} +
  • + ), + )}
@@ -155,12 +166,14 @@ export default async function InternshipsPage() { Eligibility & Prerequisites
    - {program.prerequisites.map((prereq: string, idx: number) => ( -
  • - • - {prereq} -
  • - ))} + {program.prerequisites.map( + (prereq: string, idx: number) => ( +
  • + • + {prereq} +
  • + ), + )}
diff --git a/src/app/internships/portal/page.tsx b/src/app/internships/portal/page.tsx index 14f8d05..2980e9e 100644 --- a/src/app/internships/portal/page.tsx +++ b/src/app/internships/portal/page.tsx @@ -3,12 +3,12 @@ import { redirect } from "next/navigation"; import { cookies } from "next/headers"; import { verifySessionToken, COOKIE_NAME } from "@/lib/auth/session"; import { getInternshipPortalData } from "@/lib/internships/service"; -import { Navbar } from "@/components/Navbar"; import { WorkLogForm } from "./WorkLogForm"; export const metadata = { title: "Fellowship Contributor Portal — TechNexusOrg", - description: "Track milestones, submit weekly work logs, and review mentor evaluations.", + description: + "Track milestones, submit weekly work logs, and review mentor evaluations.", }; export const dynamic = "force-dynamic"; @@ -31,7 +31,6 @@ export default async function InternshipPortalPage() { if (!portalData) { return (
-
📋 @@ -40,7 +39,8 @@ export default async function InternshipPortalPage() { No Active Fellowship Enrollment

- You do not currently have an active fellowship enrollment. Explore our supervised engineering tracks and apply to an upcoming cohort. + You do not currently have an active fellowship enrollment. Explore + our supervised engineering tracks and apply to an upcoming cohort.

- -
{/* Breadcrumb */}
- {mentor.displayName || mentor.githubUsername} (@{mentor.githubUsername}) + {mentor.displayName || mentor.githubUsername} (@ + {mentor.githubUsername})
@@ -143,7 +145,8 @@ export default async function InternshipPortalPage() { Official Graduation Evaluation - Evaluated on {new Date(evaluation.evaluatedAt).toLocaleDateString()} + Evaluated on{" "} + {new Date(evaluation.evaluatedAt).toLocaleDateString()} @@ -230,12 +233,14 @@ export default async function InternshipPortalPage() { Deliverables:
    - {((ms.requiredDeliverables as string[]) || []).map((d, i) => ( -
  • - • - {d} -
  • - ))} + {((ms.requiredDeliverables as string[]) || []).map( + (d, i) => ( +
  • + • + {d} +
  • + ), + )}
@@ -252,7 +257,8 @@ export default async function InternshipPortalPage() { Submit Weekly Work Log

- Record your hours and link merged or active GitHub PRs for maintainer review. + Record your hours and link merged or active GitHub PRs for + maintainer review.

@@ -270,7 +276,8 @@ export default async function InternshipPortalPage() { {workLogs.length === 0 ? (

- No work logs submitted yet. Submit your first weekly log using the form. + No work logs submitted yet. Submit your first weekly log using + the form.

) : (
@@ -297,24 +304,28 @@ export default async function InternshipPortalPage() {
Linked Pull Requests:
- {((log.pullRequestUrls as string[]) || []).map((pr, i) => ( -
- - {pr} - -
- ))} + {((log.pullRequestUrls as string[]) || []).map( + (pr, i) => ( +
+ + {pr} + +
+ ), + )}
)} {log.blockers && (
- Blockers: + + Blockers:{" "} + {log.blockers}
)} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 30194c0..6696ef9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter, JetBrains_Mono } from "next/font/google"; import "./globals.css"; import { Navbar } from "@/components/Navbar"; import { Footer } from "@/components/Footer"; +import { CommandPalette } from "@/components/CommandPalette"; const inter = Inter({ subsets: ["latin"], @@ -18,7 +19,9 @@ export const metadata: Metadata = { title: "TechNexusOrg — Build Real Software. Build Real Proof.", description: "TechNexusOrg helps students and early developers move from learning to real open-source engineering experience. Verified GitHub proof of work.", - metadataBase: new URL(process.env.APP_URL || "https://technexusorg.github.io"), + metadataBase: new URL( + process.env.APP_URL || "https://technexusorg.github.io", + ), openGraph: { title: "TechNexusOrg — Build Real Software. Build Real Proof.", description: @@ -34,11 +37,15 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - + +
{children}