Prettier format pending files

This commit is contained in:
Builder.io 2025-11-15 16:38:40 +00:00
parent 271b8b7ccd
commit 8a94eb1785
54 changed files with 2409 additions and 779 deletions

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: opportunities, error } = await supabase const { data: opportunities, error } = await supabase
.from("devlink_opportunities") .from("devlink_opportunities")
.select(` .select(
`
id, id,
title, title,
description, description,
@ -27,7 +28,8 @@ export default async (req: Request) => {
status, status,
skills_required, skills_required,
created_at created_at
`) `,
)
.eq("status", "open") .eq("status", "open")
.eq("type", "roblox") .eq("type", "roblox")
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: profile, error } = await supabase const { data: profile, error } = await supabase
.from("devlink_profiles") .from("devlink_profiles")
.select(` .select(
`
id, id,
user_id, user_id,
username, username,
@ -28,7 +29,8 @@ export default async (req: Request) => {
certifications, certifications,
created_at, created_at,
updated_at updated_at
`) `,
)
.eq("user_id", userData.user.id) .eq("user_id", userData.user.id)
.single(); .single();
@ -46,7 +48,9 @@ export default async (req: Request) => {
.insert([ .insert([
{ {
user_id: userData.user.id, user_id: userData.user.id,
username: userData.user.user_metadata?.username || userData.user.email?.split("@")[0], username:
userData.user.user_metadata?.username ||
userData.user.email?.split("@")[0],
profile_views: 0, profile_views: 0,
}, },
]) ])

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: teams, error } = await supabase const { data: teams, error } = await supabase
.from("devlink_teams") .from("devlink_teams")
.select(` .select(
`
id, id,
name, name,
description, description,
@ -31,7 +32,8 @@ export default async (req: Request) => {
full_name, full_name,
avatar_url avatar_url
) )
`) `,
)
.contains("members", [{ user_id: userData.user.id }]) .contains("members", [{ user_id: userData.user.id }])
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });

View file

@ -10,7 +10,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
if (authHeader) { if (authHeader) {
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (!authError && user) { if (!authError && user) {
userId = user.id; userId = user.id;
} }
@ -21,10 +24,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
// List all published courses // List all published courses
const { data: courses, error: coursesError } = await admin const { data: courses, error: coursesError } = await admin
.from("foundation_courses") .from("foundation_courses")
.select(` .select(
`
*, *,
instructor:user_profiles(id, full_name, avatar_url) instructor:user_profiles(id, full_name, avatar_url)
`) `,
)
.eq("is_published", true) .eq("is_published", true)
.order("order_index", { ascending: true }); .order("order_index", { ascending: true });
@ -42,7 +47,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
if (userEnrollments) { if (userEnrollments) {
enrollments = Object.fromEntries( enrollments = Object.fromEntries(
userEnrollments.map((e: any) => [e.course_id, e]) userEnrollments.map((e: any) => [e.course_id, e]),
); );
} }
} }
@ -76,7 +81,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
status: "in_progress", status: "in_progress",
enrolled_at: new Date().toISOString(), enrolled_at: new Date().toISOString(),
}, },
{ onConflict: "user_id,course_id" } { onConflict: "user_id,course_id" },
) )
.select() .select()
.single(); .single();

View file

@ -11,7 +11,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
} }
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) { if (authError || !user) {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
@ -26,20 +29,24 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
if (role === "mentor") { if (role === "mentor") {
const { data: m } = await admin const { data: m } = await admin
.from("foundation_mentorships") .from("foundation_mentorships")
.select(` .select(
`
*, *,
mentee:user_profiles!mentee_id(id, full_name, avatar_url, email) mentee:user_profiles!mentee_id(id, full_name, avatar_url, email)
`) `,
)
.eq("mentor_id", user.id) .eq("mentor_id", user.id)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
mentorships = m; mentorships = m;
} else if (role === "mentee") { } else if (role === "mentee") {
const { data: m } = await admin const { data: m } = await admin
.from("foundation_mentorships") .from("foundation_mentorships")
.select(` .select(
`
*, *,
mentor:user_profiles!mentor_id(id, full_name, avatar_url, email) mentor:user_profiles!mentor_id(id, full_name, avatar_url, email)
`) `,
)
.eq("mentee_id", user.id) .eq("mentee_id", user.id)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
mentorships = m; mentorships = m;
@ -49,7 +56,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
.from("foundation_mentorships") .from("foundation_mentorships")
.select( .select(
`*, `*,
mentee:user_profiles!mentee_id(id, full_name, avatar_url, email)` mentee:user_profiles!mentee_id(id, full_name, avatar_url, email)`,
) )
.eq("mentor_id", user.id); .eq("mentor_id", user.id);
@ -57,7 +64,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
.from("foundation_mentorships") .from("foundation_mentorships")
.select( .select(
`*, `*,
mentor:user_profiles!mentor_id(id, full_name, avatar_url, email)` mentor:user_profiles!mentor_id(id, full_name, avatar_url, email)`,
) )
.eq("mentee_id", user.id); .eq("mentee_id", user.id);

View file

@ -11,7 +11,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
} }
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) { if (authError || !user) {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
@ -41,18 +44,21 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
// Get lesson progress // Get lesson progress
const { data: lessonProgress } = await admin const { data: lessonProgress } = await admin
.from("foundation_lesson_progress") .from("foundation_lesson_progress")
.select(` .select(
`
*, *,
lesson:foundation_course_lessons(id, title, order_index) lesson:foundation_course_lessons(id, title, order_index)
`) `,
)
.eq("user_id", user.id) .eq("user_id", user.id)
.in("lesson_id", .in(
"lesson_id",
// Get lesson IDs for this course // Get lesson IDs for this course
(await admin await admin
.from("foundation_course_lessons") .from("foundation_course_lessons")
.select("id") .select("id")
.eq("course_id", courseId) .eq("course_id", courseId)
.then(r => r.data?.map((l: any) => l.id) || [])) .then((r) => r.data?.map((l: any) => l.id) || []),
); );
return res.status(200).json({ return res.status(200).json({
@ -66,7 +72,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
const { lesson_id, course_id, completed } = req.body; const { lesson_id, course_id, completed } = req.body;
if (!lesson_id || !course_id) { if (!lesson_id || !course_id) {
return res.status(400).json({ error: "lesson_id and course_id required" }); return res
.status(400)
.json({ error: "lesson_id and course_id required" });
} }
if (completed) { if (completed) {
@ -103,7 +111,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
.in("lesson_id", lessonsData?.map((l: any) => l.id) || []); .in("lesson_id", lessonsData?.map((l: any) => l.id) || []);
const completedCount = completedData?.length || 0; const completedCount = completedData?.length || 0;
const progressPercent = Math.round((completedCount / totalLessons) * 100); const progressPercent = Math.round(
(completedCount / totalLessons) * 100,
);
// Update enrollment progress // Update enrollment progress
await admin await admin

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: sprint, error } = await supabase const { data: sprint, error } = await supabase
.from("gameforge_sprints") .from("gameforge_sprints")
.select(` .select(
`
id, id,
project_id, project_id,
title, title,
@ -29,7 +30,8 @@ export default async (req: Request) => {
deadline, deadline,
gdd, gdd,
scope scope
`) `,
)
.eq("user_id", userData.user.id) .eq("user_id", userData.user.id)
.order("created_at", { ascending: false }) .order("created_at", { ascending: false })
.limit(1) .limit(1)

View file

@ -21,7 +21,8 @@ export default async (req: Request) => {
let query = supabase let query = supabase
.from("gameforge_tasks") .from("gameforge_tasks")
.select(` .select(
`
id, id,
title, title,
description, description,
@ -34,7 +35,8 @@ export default async (req: Request) => {
priority, priority,
due_date, due_date,
created_at created_at
`) `,
)
.eq("created_by_id", userData.user.id); .eq("created_by_id", userData.user.id);
if (sprintId) { if (sprintId) {

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: bounties, error } = await supabase const { data: bounties, error } = await supabase
.from("labs_bounties") .from("labs_bounties")
.select(` .select(
`
id, id,
title, title,
description, description,
@ -27,7 +28,8 @@ export default async (req: Request) => {
status, status,
research_track_id, research_track_id,
created_at created_at
`) `,
)
.eq("status", "available") .eq("status", "available")
.order("reward", { ascending: false }); .order("reward", { ascending: false });

View file

@ -32,7 +32,8 @@ export default async (req: Request) => {
// Fetch IP portfolio (all projects' IP counts) // Fetch IP portfolio (all projects' IP counts)
const { data: portfolio, error } = await supabase const { data: portfolio, error } = await supabase
.from("labs_ip_portfolio") .from("labs_ip_portfolio")
.select(` .select(
`
id, id,
patents_count, patents_count,
trademarks_count, trademarks_count,
@ -40,7 +41,8 @@ export default async (req: Request) => {
copyrights_count, copyrights_count,
created_at, created_at,
updated_at updated_at
`) `,
)
.single(); .single();
if (error && error.code !== "PGRST116") { if (error && error.code !== "PGRST116") {

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: publications, error } = await supabase const { data: publications, error } = await supabase
.from("labs_publications") .from("labs_publications")
.select(` .select(
`
id, id,
title, title,
description, description,
@ -27,7 +28,8 @@ export default async (req: Request) => {
published_date, published_date,
research_track_id, research_track_id,
created_at created_at
`) `,
)
.order("published_date", { ascending: false }); .order("published_date", { ascending: false });
if (error) { if (error) {

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: tracks, error } = await supabase const { data: tracks, error } = await supabase
.from("labs_research_tracks") .from("labs_research_tracks")
.select(` .select(
`
id, id,
title, title,
description, description,
@ -33,7 +34,8 @@ export default async (req: Request) => {
publications, publications,
whitepaper_url, whitepaper_url,
created_at created_at
`) `,
)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
if (error) { if (error) {

View file

@ -15,7 +15,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
} }
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) { if (authError || !user) {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
@ -49,7 +52,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
// Get applicants // Get applicants
let query = admin let query = admin
.from("nexus_applications") .from("nexus_applications")
.select(` .select(
`
*, *,
creator:user_profiles( creator:user_profiles(
id, id,
@ -64,7 +68,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
rating, rating,
review_count review_count
) )
`) `,
)
.eq("opportunity_id", opportunityId) .eq("opportunity_id", opportunityId)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
@ -74,7 +79,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
const { data: applications, error: appError } = await query.range( const { data: applications, error: appError } = await query.range(
offset, offset,
offset + limit - 1 offset + limit - 1,
); );
if (appError) { if (appError) {

View file

@ -15,7 +15,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
} }
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) { if (authError || !user) {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
@ -28,7 +31,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
let query = admin let query = admin
.from("nexus_contracts") .from("nexus_contracts")
.select(` .select(
`
*, *,
creator:user_profiles( creator:user_profiles(
id, id,
@ -38,7 +42,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
), ),
milestones:nexus_milestones(*), milestones:nexus_milestones(*),
payments:nexus_payments(*) payments:nexus_payments(*)
`) `,
)
.eq("client_id", user.id) .eq("client_id", user.id)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
@ -48,7 +53,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
const { data: contracts, error: contractsError } = await query.range( const { data: contracts, error: contractsError } = await query.range(
offset, offset,
offset + limit - 1 offset + limit - 1,
); );
if (contractsError) { if (contractsError) {
@ -63,11 +68,11 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
const totalSpent = (allContracts || []).reduce( const totalSpent = (allContracts || []).reduce(
(sum: number, c: any) => sum + (c.total_amount || 0), (sum: number, c: any) => sum + (c.total_amount || 0),
0 0,
); );
const activeContracts = (allContracts || []).filter( const activeContracts = (allContracts || []).filter(
(c: any) => c.status === "active" (c: any) => c.status === "active",
).length; ).length;
return res.status(200).json({ return res.status(200).json({

View file

@ -11,7 +11,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
} }
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) { if (authError || !user) {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
@ -26,10 +29,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
let query = admin let query = admin
.from("nexus_opportunities") .from("nexus_opportunities")
.select(` .select(
`
*, *,
applications:nexus_applications(id, status, creator_id) applications:nexus_applications(id, status, creator_id)
`) `,
)
.eq("posted_by", user.id) .eq("posted_by", user.id)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
@ -39,7 +44,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
const { data: opportunities, error: oppError } = await query.range( const { data: opportunities, error: oppError } = await query.range(
offset, offset,
offset + limit - 1 offset + limit - 1,
); );
if (oppError) { if (oppError) {
@ -72,7 +77,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
if (!title || !description || !category || !budget_type) { if (!title || !description || !category || !budget_type) {
return res.status(400).json({ return res.status(400).json({
error: "Missing required fields: title, description, category, budget_type", error:
"Missing required fields: title, description, category, budget_type",
}); });
} }
@ -83,7 +89,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
title, title,
description, description,
category, category,
required_skills: Array.isArray(required_skills) ? required_skills : [], required_skills: Array.isArray(required_skills)
? required_skills
: [],
budget_type, budget_type,
budget_min: budget_min || null, budget_min: budget_min || null,
budget_max: budget_max || null, budget_max: budget_max || null,

View file

@ -15,7 +15,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
} }
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) { if (authError || !user) {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
@ -28,7 +31,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
let query = admin let query = admin
.from("nexus_applications") .from("nexus_applications")
.select(` .select(
`
*, *,
opportunity:nexus_opportunities( opportunity:nexus_opportunities(
id, id,
@ -43,7 +47,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
posted_by, posted_by,
created_at created_at
) )
`) `,
)
.eq("creator_id", user.id) .eq("creator_id", user.id)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
@ -51,9 +56,13 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
query = query.eq("status", status); query = query.eq("status", status);
} }
const { data: applications, error: applicationsError, count } = await query const {
data: applications,
error: applicationsError,
count,
} = await query
.range(offset, offset + limit - 1) .range(offset, offset + limit - 1)
.then(result => ({ ...result, count: result.data?.length || 0 })); .then((result) => ({ ...result, count: result.data?.length || 0 }));
if (applicationsError) { if (applicationsError) {
return res.status(500).json({ error: applicationsError.message }); return res.status(500).json({ error: applicationsError.message });

View file

@ -15,7 +15,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
} }
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) { if (authError || !user) {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
@ -28,12 +31,14 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
let query = admin let query = admin
.from("nexus_contracts") .from("nexus_contracts")
.select(` .select(
`
*, *,
client:user_profiles(id, full_name, avatar_url), client:user_profiles(id, full_name, avatar_url),
milestones:nexus_milestones(*), milestones:nexus_milestones(*),
payments:nexus_payments(*) payments:nexus_payments(*)
`) `,
)
.eq("creator_id", user.id) .eq("creator_id", user.id)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
@ -41,9 +46,13 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
query = query.eq("status", status); query = query.eq("status", status);
} }
const { data: contracts, error: contractsError, count } = await query const {
data: contracts,
error: contractsError,
count,
} = await query
.range(offset, offset + limit - 1) .range(offset, offset + limit - 1)
.then(result => ({ ...result, count: result.data?.length || 0 })); .then((result) => ({ ...result, count: result.data?.length || 0 }));
if (contractsError) { if (contractsError) {
return res.status(500).json({ error: contractsError.message }); return res.status(500).json({ error: contractsError.message });

View file

@ -15,7 +15,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
} }
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) { if (authError || !user) {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
@ -29,7 +32,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
// Get payments for creator's contracts // Get payments for creator's contracts
let query = admin let query = admin
.from("nexus_payments") .from("nexus_payments")
.select(` .select(
`
*, *,
contract:nexus_contracts( contract:nexus_contracts(
id, id,
@ -40,7 +44,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
created_at created_at
), ),
milestone:nexus_milestones(id, description, amount, status) milestone:nexus_milestones(id, description, amount, status)
`) `,
)
.eq("contract.creator_id", user.id) .eq("contract.creator_id", user.id)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
@ -50,7 +55,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
const { data: payments, error: paymentsError } = await query.range( const { data: payments, error: paymentsError } = await query.range(
offset, offset,
offset + limit - 1 offset + limit - 1,
); );
if (paymentsError) { if (paymentsError) {
@ -63,11 +68,13 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
.select("total_amount, creator_payout_amount, status") .select("total_amount, creator_payout_amount, status")
.eq("creator_id", user.id); .eq("creator_id", user.id);
const totalEarnings = (contracts || []) const totalEarnings = (contracts || []).reduce(
.reduce((sum: number, c: any) => sum + (c.creator_payout_amount || 0), 0); (sum: number, c: any) => sum + (c.creator_payout_amount || 0),
0,
);
const completedContracts = (contracts || []).filter( const completedContracts = (contracts || []).filter(
(c: any) => c.status === "completed" (c: any) => c.status === "completed",
).length; ).length;
const pendingPayouts = (payments || []) const pendingPayouts = (payments || [])

View file

@ -11,7 +11,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
} }
const token = authHeader.replace("Bearer ", ""); const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await admin.auth.getUser(token); const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) { if (authError || !user) {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
@ -84,7 +87,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
availability_status: availability_status || "available", availability_status: availability_status || "available",
availability_hours_per_week: availability_hours_per_week || null, availability_hours_per_week: availability_hours_per_week || null,
}, },
{ onConflict: "user_id" } { onConflict: "user_id" },
) )
.select() .select()
.single(); .single();

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: directory, error } = await supabase const { data: directory, error } = await supabase
.from("staff_members") .from("staff_members")
.select(` .select(
`
id, id,
user_id, user_id,
full_name, full_name,
@ -31,7 +32,8 @@ export default async (req: Request) => {
location, location,
username, username,
created_at created_at
`) `,
)
.order("full_name", { ascending: true }); .order("full_name", { ascending: true });
if (error) { if (error) {

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: invoices, error } = await supabase const { data: invoices, error } = await supabase
.from("contractor_invoices") .from("contractor_invoices")
.select(` .select(
`
id, id,
user_id, user_id,
invoice_number, invoice_number,
@ -28,7 +29,8 @@ export default async (req: Request) => {
due_date, due_date,
description, description,
created_at created_at
`) `,
)
.eq("user_id", userData.user.id) .eq("user_id", userData.user.id)
.order("date", { ascending: false }); .order("date", { ascending: false });

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: staffMember, error } = await supabase const { data: staffMember, error } = await supabase
.from("staff_members") .from("staff_members")
.select(` .select(
`
id, id,
user_id, user_id,
full_name, full_name,
@ -30,7 +31,8 @@ export default async (req: Request) => {
salary, salary,
avatar_url, avatar_url,
created_at created_at
`) `,
)
.eq("user_id", userData.user.id) .eq("user_id", userData.user.id)
.single(); .single();

View file

@ -18,7 +18,8 @@ export default async (req: Request) => {
const { data: okrs, error } = await supabase const { data: okrs, error } = await supabase
.from("staff_okrs") .from("staff_okrs")
.select(` .select(
`
id, id,
user_id, user_id,
objective, objective,
@ -33,7 +34,8 @@ export default async (req: Request) => {
target_value target_value
), ),
created_at created_at
`) `,
)
.eq("user_id", userData.user.id) .eq("user_id", userData.user.id)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });

View file

@ -1,7 +1,13 @@
import { supabase } from "../_supabase"; import { supabase } from "../_supabase";
const VALID_ARMS = ["foundation", "gameforge", "labs", "corp", "devlink"]; const VALID_ARMS = ["foundation", "gameforge", "labs", "corp", "devlink"];
const VALID_TYPES = ["courses", "projects", "research", "opportunities", "manual"]; const VALID_TYPES = [
"courses",
"projects",
"research",
"opportunities",
"manual",
];
export default async (req: Request) => { export default async (req: Request) => {
try { try {
@ -28,7 +34,9 @@ export default async (req: Request) => {
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
if (error) { if (error) {
return new Response(JSON.stringify({ error: error.message }), { status: 500 }); return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
} }
return new Response(JSON.stringify(data), { return new Response(JSON.stringify(data), {
@ -42,7 +50,10 @@ export default async (req: Request) => {
const body = await req.json(); const body = await req.json();
const { arm, affiliation_type, affiliation_data, confirmed } = body; const { arm, affiliation_type, affiliation_data, confirmed } = body;
if (!VALID_ARMS.includes(arm) || !VALID_TYPES.includes(affiliation_type)) { if (
!VALID_ARMS.includes(arm) ||
!VALID_TYPES.includes(affiliation_type)
) {
return new Response("Invalid arm or affiliation type", { status: 400 }); return new Response("Invalid arm or affiliation type", { status: 400 });
} }
@ -57,13 +68,15 @@ export default async (req: Request) => {
affiliation_data: affiliation_data || {}, affiliation_data: affiliation_data || {},
confirmed: confirmed === true, confirmed: confirmed === true,
}, },
{ onConflict: "user_id,arm,affiliation_type" } { onConflict: "user_id,arm,affiliation_type" },
) )
.select() .select()
.single(); .single();
if (error) { if (error) {
return new Response(JSON.stringify({ error: error.message }), { status: 500 }); return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
} }
return new Response(JSON.stringify(data), { return new Response(JSON.stringify(data), {
@ -89,7 +102,9 @@ export default async (req: Request) => {
.eq("affiliation_type", affiliation_type || null); .eq("affiliation_type", affiliation_type || null);
if (error) { if (error) {
return new Response(JSON.stringify({ error: error.message }), { status: 500 }); return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
} }
return new Response(JSON.stringify({ success: true }), { return new Response(JSON.stringify({ success: true }), {
@ -101,6 +116,8 @@ export default async (req: Request) => {
return new Response("Method not allowed", { status: 405 }); return new Response("Method not allowed", { status: 405 });
} catch (error: any) { } catch (error: any) {
console.error("Arm affiliations error:", error); console.error("Arm affiliations error:", error);
return new Response(JSON.stringify({ error: error.message }), { status: 500 }); return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
} }
}; };

View file

@ -25,21 +25,30 @@ export default async (req: Request) => {
const updates: any = {}; const updates: any = {};
// Profile fields // Profile fields
if ("bio_detailed" in body) updates.bio_detailed = body.bio_detailed || null; if ("bio_detailed" in body)
updates.bio_detailed = body.bio_detailed || null;
if ("twitter_url" in body) updates.twitter_url = body.twitter_url || null; if ("twitter_url" in body) updates.twitter_url = body.twitter_url || null;
if ("linkedin_url" in body) updates.linkedin_url = body.linkedin_url || null; if ("linkedin_url" in body)
updates.linkedin_url = body.linkedin_url || null;
if ("github_url" in body) updates.github_url = body.github_url || null; if ("github_url" in body) updates.github_url = body.github_url || null;
if ("portfolio_url" in body) updates.portfolio_url = body.portfolio_url || null; if ("portfolio_url" in body)
updates.portfolio_url = body.portfolio_url || null;
if ("youtube_url" in body) updates.youtube_url = body.youtube_url || null; if ("youtube_url" in body) updates.youtube_url = body.youtube_url || null;
if ("twitch_url" in body) updates.twitch_url = body.twitch_url || null; if ("twitch_url" in body) updates.twitch_url = body.twitch_url || null;
// Professional info // Professional info
if ("hourly_rate" in body) if ("hourly_rate" in body)
updates.hourly_rate = body.hourly_rate ? parseFloat(body.hourly_rate) : null; updates.hourly_rate = body.hourly_rate
? parseFloat(body.hourly_rate)
: null;
if ("availability_status" in body) if ("availability_status" in body)
updates.availability_status = updates.availability_status = [
["available", "limited", "unavailable"].includes(body.availability_status) ? "available",
body.availability_status : "available"; "limited",
"unavailable",
].includes(body.availability_status)
? body.availability_status
: "available";
if ("timezone" in body) updates.timezone = body.timezone || null; if ("timezone" in body) updates.timezone = body.timezone || null;
if ("location" in body) updates.location = body.location || null; if ("location" in body) updates.location = body.location || null;
@ -48,25 +57,36 @@ export default async (req: Request) => {
updates.languages = Array.isArray(body.languages) ? body.languages : []; updates.languages = Array.isArray(body.languages) ? body.languages : [];
} }
if ("skills_detailed" in body) { if ("skills_detailed" in body) {
updates.skills_detailed = Array.isArray(body.skills_detailed) ? body.skills_detailed : []; updates.skills_detailed = Array.isArray(body.skills_detailed)
? body.skills_detailed
: [];
} }
if ("work_experience" in body) { if ("work_experience" in body) {
updates.work_experience = Array.isArray(body.work_experience) ? body.work_experience : []; updates.work_experience = Array.isArray(body.work_experience)
? body.work_experience
: [];
} }
if ("portfolio_items" in body) { if ("portfolio_items" in body) {
updates.portfolio_items = Array.isArray(body.portfolio_items) ? body.portfolio_items : []; updates.portfolio_items = Array.isArray(body.portfolio_items)
? body.portfolio_items
: [];
} }
if ("arm_affiliations" in body) { if ("arm_affiliations" in body) {
const validArms = ["foundation", "gameforge", "labs", "corp", "devlink"]; const validArms = ["foundation", "gameforge", "labs", "corp", "devlink"];
updates.arm_affiliations = Array.isArray(body.arm_affiliations) ? updates.arm_affiliations = Array.isArray(body.arm_affiliations)
body.arm_affiliations.filter((a: string) => validArms.includes(a)) : []; ? body.arm_affiliations.filter((a: string) => validArms.includes(a))
: [];
} }
// Nexus specific // Nexus specific
if ("nexus_profile_complete" in body) updates.nexus_profile_complete = body.nexus_profile_complete === true; if ("nexus_profile_complete" in body)
if ("nexus_headline" in body) updates.nexus_headline = body.nexus_headline || null; updates.nexus_profile_complete = body.nexus_profile_complete === true;
if ("nexus_headline" in body)
updates.nexus_headline = body.nexus_headline || null;
if ("nexus_categories" in body) { if ("nexus_categories" in body) {
updates.nexus_categories = Array.isArray(body.nexus_categories) ? body.nexus_categories : []; updates.nexus_categories = Array.isArray(body.nexus_categories)
? body.nexus_categories
: [];
} }
// Update the profile // Update the profile
@ -79,7 +99,9 @@ export default async (req: Request) => {
if (error) { if (error) {
console.error("Profile update error:", error); console.error("Profile update error:", error);
return new Response(JSON.stringify({ error: error.message }), { status: 500 }); return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
} }
return new Response(JSON.stringify(data), { return new Response(JSON.stringify(data), {
@ -88,6 +110,8 @@ export default async (req: Request) => {
}); });
} catch (error: any) { } catch (error: any) {
console.error("Profile update error:", error); console.error("Profile update error:", error);
return new Response(JSON.stringify({ error: error.message }), { status: 500 }); return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
} }
}; };

View file

@ -192,11 +192,26 @@ const App = () => (
<Route path="/" element={<SubdomainPassport />} /> <Route path="/" element={<SubdomainPassport />} />
<Route path="/onboarding" element={<Onboarding />} /> <Route path="/onboarding" element={<Onboarding />} />
<Route path="/dashboard" element={<Dashboard />} /> <Route path="/dashboard" element={<Dashboard />} />
<Route path="/dashboard/nexus" element={<NexusDashboard />} /> <Route
<Route path="/dashboard/foundation" element={<FoundationDashboard />} /> path="/dashboard/nexus"
<Route path="/dashboard/labs" element={<LabsDashboard />} /> element={<NexusDashboard />}
<Route path="/dashboard/gameforge" element={<GameForgeDashboard />} /> />
<Route path="/dashboard/dev-link" element={<DevLinkDashboard />} /> <Route
path="/dashboard/foundation"
element={<FoundationDashboard />}
/>
<Route
path="/dashboard/labs"
element={<LabsDashboard />}
/>
<Route
path="/dashboard/gameforge"
element={<GameForgeDashboard />}
/>
<Route
path="/dashboard/dev-link"
element={<DevLinkDashboard />}
/>
<Route <Route
path="/hub/client" path="/hub/client"
element={ element={

View file

@ -1,4 +1,10 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Trophy, Lock, Star, ArrowRight } from "lucide-react"; import { Trophy, Lock, Star, ArrowRight } from "lucide-react";
@ -25,11 +31,23 @@ interface AchievementsWidgetProps {
} }
const rarityMap = { const rarityMap = {
common: { color: "bg-gray-600/50 text-gray-100", border: "border-gray-500/30" }, common: {
uncommon: { color: "bg-green-600/50 text-green-100", border: "border-green-500/30" }, color: "bg-gray-600/50 text-gray-100",
border: "border-gray-500/30",
},
uncommon: {
color: "bg-green-600/50 text-green-100",
border: "border-green-500/30",
},
rare: { color: "bg-blue-600/50 text-blue-100", border: "border-blue-500/30" }, rare: { color: "bg-blue-600/50 text-blue-100", border: "border-blue-500/30" },
epic: { color: "bg-purple-600/50 text-purple-100", border: "border-purple-500/30" }, epic: {
legendary: { color: "bg-yellow-600/50 text-yellow-100", border: "border-yellow-500/30" }, color: "bg-purple-600/50 text-purple-100",
border: "border-purple-500/30",
},
legendary: {
color: "bg-yellow-600/50 text-yellow-100",
border: "border-yellow-500/30",
},
}; };
const colorMap = { const colorMap = {
@ -92,7 +110,9 @@ export function AchievementsWidget({
<div className="text-center py-12 space-y-4"> <div className="text-center py-12 space-y-4">
<Trophy className="h-12 w-12 mx-auto text-gray-500 opacity-50" /> <Trophy className="h-12 w-12 mx-auto text-gray-500 opacity-50" />
<p className="text-gray-400">No achievements yet</p> <p className="text-gray-400">No achievements yet</p>
<p className="text-sm text-gray-500">Complete courses and challenges to earn badges</p> <p className="text-sm text-gray-500">
Complete courses and challenges to earn badges
</p>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
@ -144,9 +164,7 @@ export function AchievementsWidget({
{/* Locked Achievements Placeholder */} {/* Locked Achievements Placeholder */}
{hasMore && ( {hasMore && (
<div <div className="p-3 rounded-lg border border-gray-600/30 bg-black/30 text-center space-y-2 opacity-50">
className="p-3 rounded-lg border border-gray-600/30 bg-black/30 text-center space-y-2 opacity-50"
>
<div className="flex justify-center"> <div className="flex justify-center">
<Lock className="w-6 h-6 text-gray-500" /> <Lock className="w-6 h-6 text-gray-500" />
</div> </div>

View file

@ -1,8 +1,20 @@
import { useState } from "react"; import { useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Users, MessageCircle, CheckCircle, Clock, ArrowRight } from "lucide-react"; import {
Users,
MessageCircle,
CheckCircle,
Clock,
ArrowRight,
} from "lucide-react";
export interface Applicant { export interface Applicant {
id: string; id: string;
@ -27,7 +39,10 @@ interface ApplicantTrackerWidgetProps {
description?: string; description?: string;
onViewProfile?: (applicantId: string) => void; onViewProfile?: (applicantId: string) => void;
onMessage?: (applicantId: string) => void; onMessage?: (applicantId: string) => void;
onUpdateStatus?: (applicantId: string, newStatus: "applied" | "interviewing" | "hired") => void; onUpdateStatus?: (
applicantId: string,
newStatus: "applied" | "interviewing" | "hired",
) => void;
accentColor?: "blue" | "purple" | "cyan" | "green"; accentColor?: "blue" | "purple" | "cyan" | "green";
} }
@ -73,12 +88,16 @@ export function ApplicantTrackerWidget({
const [draggedApplicant, setDraggedApplicant] = useState<string | null>(null); const [draggedApplicant, setDraggedApplicant] = useState<string | null>(null);
const statusCounts = { const statusCounts = {
applied: applicants.filter(a => a.status === "applied").length, applied: applicants.filter((a) => a.status === "applied").length,
interviewing: applicants.filter(a => a.status === "interviewing").length, interviewing: applicants.filter((a) => a.status === "interviewing").length,
hired: applicants.filter(a => a.status === "hired").length, hired: applicants.filter((a) => a.status === "hired").length,
}; };
const allStatuses: Array<"applied" | "interviewing" | "hired"> = ["applied", "interviewing", "hired"]; const allStatuses: Array<"applied" | "interviewing" | "hired"> = [
"applied",
"interviewing",
"hired",
];
const handleDragStart = (applicantId: string) => { const handleDragStart = (applicantId: string) => {
setDraggedApplicant(applicantId); setDraggedApplicant(applicantId);
@ -115,7 +134,9 @@ export function ApplicantTrackerWidget({
{allStatuses.map((status) => { {allStatuses.map((status) => {
const statusInfo = statusColors[status]; const statusInfo = statusColors[status];
const StatusIcon = statusInfo.icon; const StatusIcon = statusInfo.icon;
const statusApplicants = applicants.filter(a => a.status === status); const statusApplicants = applicants.filter(
(a) => a.status === status,
);
return ( return (
<div <div
@ -128,9 +149,13 @@ export function ApplicantTrackerWidget({
<div className="mb-4 pb-3 border-b border-gray-500/20"> <div className="mb-4 pb-3 border-b border-gray-500/20">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<StatusIcon className="h-4 w-4" /> <StatusIcon className="h-4 w-4" />
<span className="font-semibold text-white">{statusInfo.label}</span> <span className="font-semibold text-white">
{statusInfo.label}
</span>
</div> </div>
<p className="text-2xl font-bold text-gray-100">{statusApplicants.length}</p> <p className="text-2xl font-bold text-gray-100">
{statusApplicants.length}
</p>
</div> </div>
{/* Applicants List */} {/* Applicants List */}

View file

@ -1,6 +1,18 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Briefcase, AlertCircle, CheckCircle, Clock, ArrowRight } from "lucide-react"; import {
Briefcase,
AlertCircle,
CheckCircle,
Clock,
ArrowRight,
} from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
export interface Application { export interface Application {
@ -70,11 +82,31 @@ const colorMap = {
}; };
const statusMap = { const statusMap = {
submitted: { color: "bg-blue-600/50 text-blue-100", icon: Clock, label: "Submitted" }, submitted: {
pending: { color: "bg-yellow-600/50 text-yellow-100", icon: Clock, label: "Pending" }, color: "bg-blue-600/50 text-blue-100",
accepted: { color: "bg-green-600/50 text-green-100", icon: CheckCircle, label: "Accepted" }, icon: Clock,
rejected: { color: "bg-red-600/50 text-red-100", icon: AlertCircle, label: "Rejected" }, label: "Submitted",
interview: { color: "bg-purple-600/50 text-purple-100", icon: Briefcase, label: "Interview" }, },
pending: {
color: "bg-yellow-600/50 text-yellow-100",
icon: Clock,
label: "Pending",
},
accepted: {
color: "bg-green-600/50 text-green-100",
icon: CheckCircle,
label: "Accepted",
},
rejected: {
color: "bg-red-600/50 text-red-100",
icon: AlertCircle,
label: "Rejected",
},
interview: {
color: "bg-purple-600/50 text-purple-100",
icon: Briefcase,
label: "Interview",
},
}; };
export function ApplicationsWidget({ export function ApplicationsWidget({
@ -89,11 +121,11 @@ export function ApplicationsWidget({
}: ApplicationsWidgetProps) { }: ApplicationsWidgetProps) {
const colors = colorMap[accentColor]; const colors = colorMap[accentColor];
const statusCounts = { const statusCounts = {
submitted: applications.filter(a => a.status === "submitted").length, submitted: applications.filter((a) => a.status === "submitted").length,
pending: applications.filter(a => a.status === "pending").length, pending: applications.filter((a) => a.status === "pending").length,
accepted: applications.filter(a => a.status === "accepted").length, accepted: applications.filter((a) => a.status === "accepted").length,
rejected: applications.filter(a => a.status === "rejected").length, rejected: applications.filter((a) => a.status === "rejected").length,
interview: applications.filter(a => a.status === "interview").length, interview: applications.filter((a) => a.status === "interview").length,
}; };
return ( return (
@ -123,36 +155,47 @@ export function ApplicationsWidget({
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{/* Status Summary */} {/* Status Summary */}
{Object.entries(statusCounts).filter(([_, count]) => count > 0).length > 0 && ( {Object.entries(statusCounts).filter(([_, count]) => count > 0)
.length > 0 && (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mb-4 p-3 bg-black/20 rounded-lg"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mb-4 p-3 bg-black/20 rounded-lg">
{statusCounts.submitted > 0 && ( {statusCounts.submitted > 0 && (
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Submitted</p> <p className="text-xs text-gray-400">Submitted</p>
<p className="text-lg font-bold text-blue-400">{statusCounts.submitted}</p> <p className="text-lg font-bold text-blue-400">
{statusCounts.submitted}
</p>
</div> </div>
)} )}
{statusCounts.interview > 0 && ( {statusCounts.interview > 0 && (
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Interview</p> <p className="text-xs text-gray-400">Interview</p>
<p className="text-lg font-bold text-purple-400">{statusCounts.interview}</p> <p className="text-lg font-bold text-purple-400">
{statusCounts.interview}
</p>
</div> </div>
)} )}
{statusCounts.accepted > 0 && ( {statusCounts.accepted > 0 && (
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Accepted</p> <p className="text-xs text-gray-400">Accepted</p>
<p className="text-lg font-bold text-green-400">{statusCounts.accepted}</p> <p className="text-lg font-bold text-green-400">
{statusCounts.accepted}
</p>
</div> </div>
)} )}
{statusCounts.pending > 0 && ( {statusCounts.pending > 0 && (
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Pending</p> <p className="text-xs text-gray-400">Pending</p>
<p className="text-lg font-bold text-yellow-400">{statusCounts.pending}</p> <p className="text-lg font-bold text-yellow-400">
{statusCounts.pending}
</p>
</div> </div>
)} )}
{statusCounts.rejected > 0 && ( {statusCounts.rejected > 0 && (
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Rejected</p> <p className="text-xs text-gray-400">Rejected</p>
<p className="text-lg font-bold text-red-400">{statusCounts.rejected}</p> <p className="text-lg font-bold text-red-400">
{statusCounts.rejected}
</p>
</div> </div>
)} )}
</div> </div>
@ -176,10 +219,14 @@ export function ApplicationsWidget({
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h4 className="font-semibold text-white truncate"> <h4 className="font-semibold text-white truncate">
{app.opportunity?.title || app.title || "Untitled Opportunity"} {app.opportunity?.title ||
app.title ||
"Untitled Opportunity"}
</h4> </h4>
{app.opportunity?.category && ( {app.opportunity?.category && (
<p className="text-sm text-gray-400 mt-1">{app.opportunity.category}</p> <p className="text-sm text-gray-400 mt-1">
{app.opportunity.category}
</p>
)} )}
</div> </div>
<Badge className={statusInfo.color}> <Badge className={statusInfo.color}>
@ -216,11 +263,7 @@ export function ApplicationsWidget({
</div> </div>
{showCTA && onCTA && applications.length > 0 && ( {showCTA && onCTA && applications.length > 0 && (
<Button <Button onClick={onCTA} variant="outline" className="w-full mt-4">
onClick={onCTA}
variant="outline"
className="w-full mt-4"
>
<ArrowRight className="h-4 w-4 mr-2" /> <ArrowRight className="h-4 w-4 mr-2" />
{ctaText} {ctaText}
</Button> </Button>

View file

@ -46,15 +46,15 @@ export function CTAButtonGroup({
btn.size === "sm" btn.size === "sm"
? "text-sm px-3 py-1" ? "text-sm px-3 py-1"
: btn.size === "lg" : btn.size === "lg"
? "text-lg px-6 py-3" ? "text-lg px-6 py-3"
: "px-4 py-2"; : "px-4 py-2";
const variantClass = const variantClass =
btn.variant === "outline" btn.variant === "outline"
? "border border-opacity-30 text-opacity-75 hover:bg-opacity-10" ? "border border-opacity-30 text-opacity-75 hover:bg-opacity-10"
: btn.variant === "secondary" : btn.variant === "secondary"
? "bg-opacity-50 hover:bg-opacity-75" ? "bg-opacity-50 hover:bg-opacity-75"
: ""; : "";
const widthClass = btn.fullWidth ? "w-full" : ""; const widthClass = btn.fullWidth ? "w-full" : "";
@ -66,11 +66,18 @@ export function CTAButtonGroup({
key: idx, key: idx,
className: `${sizeClass} ${variantClass} ${widthClass}`, className: `${sizeClass} ${variantClass} ${widthClass}`,
onClick: btn.onClick, onClick: btn.onClick,
...(btn.href && { href: btn.href, target: "_blank", rel: "noopener noreferrer" }), ...(btn.href && {
href: btn.href,
target: "_blank",
rel: "noopener noreferrer",
}),
}; };
return ( return (
<Button {...(baseProps as any)} variant={btn.variant === "outline" ? "outline" : "default"}> <Button
{...(baseProps as any)}
variant={btn.variant === "outline" ? "outline" : "default"}
>
{Icon && <Icon className="h-4 w-4 mr-2" />} {Icon && <Icon className="h-4 w-4 mr-2" />}
{btn.label} {btn.label}
</Button> </Button>
@ -100,7 +107,9 @@ export function CTASection({
layout = "vertical", layout = "vertical",
}: CTASectionProps) { }: CTASectionProps) {
return ( return (
<div className={`bg-gradient-to-br ${gradient} rounded-lg border p-8 text-center space-y-4`}> <div
className={`bg-gradient-to-br ${gradient} rounded-lg border p-8 text-center space-y-4`}
>
<h3 className="text-2xl font-bold text-white">{title}</h3> <h3 className="text-2xl font-bold text-white">{title}</h3>
{subtitle && <p className="text-gray-300">{subtitle}</p>} {subtitle && <p className="text-gray-300">{subtitle}</p>}
{children && <div className="my-4">{children}</div>} {children && <div className="my-4">{children}</div>}

View file

@ -1,6 +1,18 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { FileText, AlertCircle, CheckCircle, Clock, DollarSign } from "lucide-react"; import {
FileText,
AlertCircle,
CheckCircle,
Clock,
DollarSign,
} from "lucide-react";
export interface Contract { export interface Contract {
id: string; id: string;
@ -79,7 +91,7 @@ export function ContractsWidget({
accentColor = "purple", accentColor = "purple",
}: ContractsWidgetProps) { }: ContractsWidgetProps) {
const colors = colorMap[accentColor]; const colors = colorMap[accentColor];
const activeContracts = contracts.filter(c => c.status === "active"); const activeContracts = contracts.filter((c) => c.status === "active");
return ( return (
<Card className={`${colors.bg} border ${colors.border}`}> <Card className={`${colors.bg} border ${colors.border}`}>
@ -100,9 +112,12 @@ export function ContractsWidget({
<div className="space-y-4"> <div className="space-y-4">
{contracts.map((contract) => { {contracts.map((contract) => {
const StatusIcon = statusMap[contract.status].icon; const StatusIcon = statusMap[contract.status].icon;
const progress = contract.paid_amount && contract.total_amount const progress =
? Math.round((contract.paid_amount / contract.total_amount) * 100) contract.paid_amount && contract.total_amount
: 0; ? Math.round(
(contract.paid_amount / contract.total_amount) * 100,
)
: 0;
return ( return (
<div <div
@ -128,7 +143,9 @@ export function ContractsWidget({
</div> </div>
{contract.description && ( {contract.description && (
<p className="text-sm text-gray-400">{contract.description}</p> <p className="text-sm text-gray-400">
{contract.description}
</p>
)} )}
<div className="grid grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-2 gap-4 text-sm">
@ -150,7 +167,9 @@ export function ContractsWidget({
{contract.milestones && contract.milestones.length > 0 && ( {contract.milestones && contract.milestones.length > 0 && (
<div className="space-y-2 pt-2 border-t border-gray-500/10"> <div className="space-y-2 pt-2 border-t border-gray-500/10">
<p className="text-xs font-semibold text-gray-300 uppercase">Milestones</p> <p className="text-xs font-semibold text-gray-300 uppercase">
Milestones
</p>
<div className="space-y-2"> <div className="space-y-2">
{contract.milestones.map((m) => ( {contract.milestones.map((m) => (
<div <div
@ -165,8 +184,8 @@ export function ContractsWidget({
m.status === "paid" m.status === "paid"
? "#22c55e" ? "#22c55e"
: m.status === "approved" : m.status === "approved"
? "#3b82f6" ? "#3b82f6"
: "#666", : "#666",
}} }}
/> />
<span className="text-gray-300 truncate"> <span className="text-gray-300 truncate">

View file

@ -1,4 +1,10 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { BookOpen, CheckCircle, Clock, Lock, ArrowRight } from "lucide-react"; import { BookOpen, CheckCircle, Clock, Lock, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -45,9 +51,21 @@ const colorMap = {
}; };
const statusMap = { const statusMap = {
not_started: { label: "Not Started", color: "bg-gray-600/50 text-gray-100", icon: Lock }, not_started: {
in_progress: { label: "In Progress", color: "bg-blue-600/50 text-blue-100", icon: Clock }, label: "Not Started",
completed: { label: "Completed", color: "bg-green-600/50 text-green-100", icon: CheckCircle }, color: "bg-gray-600/50 text-gray-100",
icon: Lock,
},
in_progress: {
label: "In Progress",
color: "bg-blue-600/50 text-blue-100",
icon: Clock,
},
completed: {
label: "Completed",
color: "bg-green-600/50 text-green-100",
icon: CheckCircle,
},
}; };
export function CoursesWidget({ export function CoursesWidget({
@ -58,8 +76,10 @@ export function CoursesWidget({
accentColor = "red", accentColor = "red",
}: CoursesWidgetProps) { }: CoursesWidgetProps) {
const colors = colorMap[accentColor]; const colors = colorMap[accentColor];
const completedCount = courses.filter(c => c.status === "completed").length; const completedCount = courses.filter((c) => c.status === "completed").length;
const inProgressCount = courses.filter(c => c.status === "in_progress").length; const inProgressCount = courses.filter(
(c) => c.status === "in_progress",
).length;
return ( return (
<Card className={`${colors.bg} border ${colors.border}`}> <Card className={`${colors.bg} border ${colors.border}`}>
@ -75,7 +95,9 @@ export function CoursesWidget({
<div className="text-center py-12"> <div className="text-center py-12">
<BookOpen className="h-12 w-12 mx-auto text-gray-500 opacity-50 mb-4" /> <BookOpen className="h-12 w-12 mx-auto text-gray-500 opacity-50 mb-4" />
<p className="text-gray-400 mb-4">No courses yet</p> <p className="text-gray-400 mb-4">No courses yet</p>
<p className="text-sm text-gray-500">Start your learning journey today</p> <p className="text-sm text-gray-500">
Start your learning journey today
</p>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
@ -87,11 +109,15 @@ export function CoursesWidget({
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">In Progress</p> <p className="text-xs text-gray-400">In Progress</p>
<p className="text-lg font-bold text-blue-400">{inProgressCount}</p> <p className="text-lg font-bold text-blue-400">
{inProgressCount}
</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Completed</p> <p className="text-xs text-gray-400">Completed</p>
<p className="text-lg font-bold text-green-400">{completedCount}</p> <p className="text-lg font-bold text-green-400">
{completedCount}
</p>
</div> </div>
</div> </div>
@ -111,25 +137,25 @@ export function CoursesWidget({
{/* Course Header */} {/* Course Header */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<h4 className="font-semibold text-white">{course.title}</h4> <h4 className="font-semibold text-white">
{course.title}
</h4>
<Badge className={statusInfo.color}> <Badge className={statusInfo.color}>
<StatusIcon className="h-3 w-3 mr-1" /> <StatusIcon className="h-3 w-3 mr-1" />
{statusInfo.label} {statusInfo.label}
</Badge> </Badge>
</div> </div>
{course.description && ( {course.description && (
<p className="text-xs text-gray-400">{course.description}</p> <p className="text-xs text-gray-400">
{course.description}
</p>
)} )}
</div> </div>
{/* Course Meta */} {/* Course Meta */}
<div className="flex items-center justify-between text-xs text-gray-400 py-2 border-y border-gray-500/10"> <div className="flex items-center justify-between text-xs text-gray-400 py-2 border-y border-gray-500/10">
{course.instructor && ( {course.instructor && <span>{course.instructor}</span>}
<span>{course.instructor}</span> {course.duration && <span>{course.duration}</span>}
)}
{course.duration && (
<span>{course.duration}</span>
)}
</div> </div>
{/* Progress */} {/* Progress */}
@ -138,16 +164,18 @@ export function CoursesWidget({
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-gray-400">Progress</span> <span className="text-gray-400">Progress</span>
<span className="text-gray-300 font-semibold"> <span className="text-gray-300 font-semibold">
{course.lessons_completed || 0}/{course.lessons_total} {course.lessons_completed || 0}/
{course.lessons_total}
</span> </span>
</div> </div>
<div className="w-full bg-black/50 rounded-full h-2"> <div className="w-full bg-black/50 rounded-full h-2">
<div <div
className="bg-gradient-to-r from-red-500 to-orange-500 h-2 rounded-full transition-all" className="bg-gradient-to-r from-red-500 to-orange-500 h-2 rounded-full transition-all"
style={{ style={{
width: course.lessons_total > 0 width:
? `${(((course.lessons_completed || 0) / course.lessons_total) * 100)}%` course.lessons_total > 0
: "0%" ? `${((course.lessons_completed || 0) / course.lessons_total) * 100}%`
: "0%",
}} }}
/> />
</div> </div>
@ -164,7 +192,9 @@ export function CoursesWidget({
onViewCourse?.(course.id); onViewCourse?.(course.id);
}} }}
> >
{course.status === "completed" ? "Review Course" : "Continue Learning"} {course.status === "completed"
? "Review Course"
: "Continue Learning"}
<ArrowRight className="h-3 w-3 ml-1" /> <ArrowRight className="h-3 w-3 ml-1" />
</Button> </Button>
</div> </div>

View file

@ -1,6 +1,13 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
export type ArmKey = "nexus" | "corp" | "foundation" | "gameforge" | "labs" | "devlink" | "staff"; export type ArmKey =
| "nexus"
| "corp"
| "foundation"
| "gameforge"
| "labs"
| "devlink"
| "staff";
interface ThemeConfig { interface ThemeConfig {
arm: ArmKey; arm: ArmKey;
@ -105,11 +112,16 @@ interface DashboardThemeProviderProps {
children: ReactNode; children: ReactNode;
} }
export function DashboardThemeProvider({ arm, children }: DashboardThemeProviderProps) { export function DashboardThemeProvider({
arm,
children,
}: DashboardThemeProviderProps) {
const theme = getTheme(arm); const theme = getTheme(arm);
return ( return (
<div className={`min-h-screen bg-gradient-to-b ${theme.bgGradient} ${theme.fontFamily}`}> <div
className={`min-h-screen bg-gradient-to-b ${theme.bgGradient} ${theme.fontFamily}`}
>
{children} {children}
</div> </div>
); );
@ -121,13 +133,19 @@ interface DashboardHeaderProps {
subtitle?: string; subtitle?: string;
} }
export function DashboardHeader({ arm, title, subtitle }: DashboardHeaderProps) { export function DashboardHeader({
arm,
title,
subtitle,
}: DashboardHeaderProps) {
const theme = getTheme(arm); const theme = getTheme(arm);
return ( return (
<div className="space-y-4 animate-slide-down"> <div className="space-y-4 animate-slide-down">
<div className="space-y-2"> <div className="space-y-2">
<h1 className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.headerGradient} bg-clip-text text-transparent`}> <h1
className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.headerGradient} bg-clip-text text-transparent`}
>
{title} {title}
</h1> </h1>
{subtitle && <p className="text-gray-400 text-lg">{subtitle}</p>} {subtitle && <p className="text-gray-400 text-lg">{subtitle}</p>}
@ -141,7 +159,10 @@ interface ColorPaletteConfig {
variant?: "default" | "alt"; variant?: "default" | "alt";
} }
export function getColorClasses(arm: ArmKey, variant: "default" | "alt" = "default") { export function getColorClasses(
arm: ArmKey,
variant: "default" | "alt" = "default",
) {
const configs: Record<ArmKey, Record<string, Record<string, string>>> = { const configs: Record<ArmKey, Record<string, Record<string, string>>> = {
nexus: { nexus: {
default: { default: {

View file

@ -1,5 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Users, Search, Phone, Mail, MapPin } from "lucide-react"; import { Users, Search, Phone, Mail, MapPin } from "lucide-react";
@ -48,8 +54,12 @@ export function DirectoryWidget({
return matchesSearch; return matchesSearch;
}); });
const employeeCount = members.filter(m => m.employment_type === "employee").length; const employeeCount = members.filter(
const contractorCount = members.filter(m => m.employment_type === "contractor").length; (m) => m.employment_type === "employee",
).length;
const contractorCount = members.filter(
(m) => m.employment_type === "contractor",
).length;
return ( return (
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
@ -69,7 +79,9 @@ export function DirectoryWidget({
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Contractors</p> <p className="text-xs text-gray-400">Contractors</p>
<p className="text-lg font-bold text-orange-400">{contractorCount}</p> <p className="text-lg font-bold text-orange-400">
{contractorCount}
</p>
</div> </div>
</div> </div>
@ -114,10 +126,14 @@ export function DirectoryWidget({
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h4 className="font-semibold text-white truncate">{member.name}</h4> <h4 className="font-semibold text-white truncate">
{member.name}
</h4>
<p className="text-xs text-gray-400">{member.role}</p> <p className="text-xs text-gray-400">{member.role}</p>
{member.department && ( {member.department && (
<p className="text-xs text-gray-500">{member.department}</p> <p className="text-xs text-gray-500">
{member.department}
</p>
)} )}
</div> </div>

View file

@ -1,8 +1,20 @@
import { useState } from "react"; import { useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Music, Toggle, ToggleLeft, ToggleRight, ExternalLink } from "lucide-react"; import {
Music,
Toggle,
ToggleLeft,
ToggleRight,
ExternalLink,
} from "lucide-react";
export interface EthosStorefrontData { export interface EthosStorefrontData {
for_hire: boolean; for_hire: boolean;
@ -92,7 +104,9 @@ export function EthosStorefrontWidget({
<Music className="h-5 w-5" /> <Music className="h-5 w-5" />
My ETHOS Storefront My ETHOS Storefront
</CardTitle> </CardTitle>
<CardDescription>Your marketplace presence for services and tracks</CardDescription> <CardDescription>
Your marketplace presence for services and tracks
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* Profile Header */} {/* Profile Header */}
@ -106,13 +120,17 @@ export function EthosStorefrontWidget({
)} )}
<div className="flex-1"> <div className="flex-1">
{data.headline && ( {data.headline && (
<h3 className="text-lg font-semibold text-white">{data.headline}</h3> <h3 className="text-lg font-semibold text-white">
{data.headline}
</h3>
)} )}
{data.bio && ( {data.bio && (
<p className="text-sm text-gray-400 mt-1">{data.bio}</p> <p className="text-sm text-gray-400 mt-1">{data.bio}</p>
)} )}
{data.verified && ( {data.verified && (
<Badge className="bg-green-600/50 text-green-100 mt-2"> Verified</Badge> <Badge className="bg-green-600/50 text-green-100 mt-2">
Verified
</Badge>
)} )}
</div> </div>
</div> </div>
@ -121,7 +139,9 @@ export function EthosStorefrontWidget({
<div className="p-4 bg-black/30 rounded-lg border border-gray-500/10 flex items-center justify-between"> <div className="p-4 bg-black/30 rounded-lg border border-gray-500/10 flex items-center justify-between">
<div> <div>
<p className="font-semibold text-white">Available for Work</p> <p className="font-semibold text-white">Available for Work</p>
<p className="text-sm text-gray-400">Is your profile visible to clients?</p> <p className="text-sm text-gray-400">
Is your profile visible to clients?
</p>
</div> </div>
<button <button
onClick={() => handleToggleForHire(!isForHire)} onClick={() => handleToggleForHire(!isForHire)}

View file

@ -5,7 +5,15 @@ import { Badge } from "@/components/ui/badge";
export interface KanbanColumn { export interface KanbanColumn {
id: string; id: string;
title: string; title: string;
color: "blue" | "yellow" | "green" | "red" | "purple" | "pink" | "cyan" | "amber"; color:
| "blue"
| "yellow"
| "green"
| "red"
| "purple"
| "pink"
| "cyan"
| "amber";
items: KanbanItem[]; items: KanbanItem[];
count?: number; count?: number;
} }
@ -59,17 +67,25 @@ interface KanbanBoardProps {
} }
export function KanbanBoard({ columns, gap = "medium" }: KanbanBoardProps) { export function KanbanBoard({ columns, gap = "medium" }: KanbanBoardProps) {
const gapClass = gap === "small" ? "gap-3" : gap === "large" ? "gap-6" : "gap-4"; const gapClass =
gap === "small" ? "gap-3" : gap === "large" ? "gap-6" : "gap-4";
return ( return (
<div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 ${gapClass}`}> <div
className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 ${gapClass}`}
>
{columns.map((column) => ( {columns.map((column) => (
<Card key={column.id} className={`bg-gradient-to-br ${colorMap[column.color]} border`}> <Card
key={column.id}
className={`bg-gradient-to-br ${colorMap[column.color]} border`}
>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center justify-between"> <CardTitle className="text-lg flex items-center justify-between">
<span>{column.title}</span> <span>{column.title}</span>
{column.count !== undefined && ( {column.count !== undefined && (
<span className="text-sm font-semibold text-gray-400">({column.count})</span> <span className="text-sm font-semibold text-gray-400">
({column.count})
</span>
)} )}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -86,15 +102,23 @@ export function KanbanBoard({ columns, gap = "medium" }: KanbanBoardProps) {
className={`p-3 bg-black/30 rounded-lg border transition cursor-move ${borderMap[column.color]} ${item.onClick ? "cursor-pointer" : ""}`} className={`p-3 bg-black/30 rounded-lg border transition cursor-move ${borderMap[column.color]} ${item.onClick ? "cursor-pointer" : ""}`}
> >
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
{item.icon && <div className="flex-shrink-0 mt-1">{item.icon}</div>} {item.icon && (
<div className="flex-shrink-0 mt-1">{item.icon}</div>
)}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-semibold text-white text-sm truncate">{item.title}</p> <p className="font-semibold text-white text-sm truncate">
{item.title}
</p>
{item.subtitle && ( {item.subtitle && (
<p className="text-xs text-gray-400 mt-1 truncate">{item.subtitle}</p> <p className="text-xs text-gray-400 mt-1 truncate">
{item.subtitle}
</p>
)} )}
{item.badge && ( {item.badge && (
<div className="mt-2"> <div className="mt-2">
<Badge className={`bg-${column.color}-600/50 ${textColorMap[column.color]} text-xs`}> <Badge
className={`bg-${column.color}-600/50 ${textColorMap[column.color]} text-xs`}
>
{item.badge} {item.badge}
</Badge> </Badge>
</div> </div>
@ -102,9 +126,14 @@ export function KanbanBoard({ columns, gap = "medium" }: KanbanBoardProps) {
{item.metadata && ( {item.metadata && (
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
{Object.entries(item.metadata).map(([key, value]) => ( {Object.entries(item.metadata).map(([key, value]) => (
<div key={key} className="flex justify-between text-xs text-gray-400"> <div
key={key}
className="flex justify-between text-xs text-gray-400"
>
<span>{key}:</span> <span>{key}:</span>
<span className="font-semibold text-white">{value}</span> <span className="font-semibold text-white">
{value}
</span>
</div> </div>
))} ))}
</div> </div>

View file

@ -1,7 +1,20 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Users, Calendar, CheckCircle, Clock, MessageCircle, ArrowRight } from "lucide-react"; import {
Users,
Calendar,
CheckCircle,
Clock,
MessageCircle,
ArrowRight,
} from "lucide-react";
export interface Mentor { export interface Mentor {
id: string; id: string;
@ -59,7 +72,8 @@ export function MentorshipWidget({
accentColor = "red", accentColor = "red",
}: MentorshipWidgetProps) { }: MentorshipWidgetProps) {
const colors = colorMap[accentColor]; const colors = colorMap[accentColor];
const person = mentorship?.type === "mentor" ? mentorship.mentor : mentorship?.mentee; const person =
mentorship?.type === "mentor" ? mentorship.mentor : mentorship?.mentee;
if (!mentorship) { if (!mentorship) {
return ( return (
@ -77,7 +91,7 @@ export function MentorshipWidget({
<div> <div>
<p className="text-gray-400 mb-2">No active mentorship</p> <p className="text-gray-400 mb-2">No active mentorship</p>
<p className="text-sm text-gray-500 mb-4"> <p className="text-sm text-gray-500 mb-4">
{title.includes("Mentorship") {title.includes("Mentorship")
? "Connect with an experienced mentor or become one yourself" ? "Connect with an experienced mentor or become one yourself"
: ""} : ""}
</p> </p>
@ -105,7 +119,9 @@ export function MentorshipWidget({
{title} {title}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
{mentorship.type === "mentor" ? "Mentoring someone" : "Being mentored by"} {mentorship.type === "mentor"
? "Mentoring someone"
: "Being mentored by"}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
@ -116,12 +132,13 @@ export function MentorshipWidget({
mentorship.status === "active" mentorship.status === "active"
? "bg-green-600/50 text-green-100" ? "bg-green-600/50 text-green-100"
: mentorship.status === "paused" : mentorship.status === "paused"
? "bg-yellow-600/50 text-yellow-100" ? "bg-yellow-600/50 text-yellow-100"
: "bg-gray-600/50 text-gray-100" : "bg-gray-600/50 text-gray-100"
} }
> >
{mentorship.status === "active" ? "🟢" : "⏸"} {mentorship.status === "active" ? "🟢" : "⏸"}{" "}
{" "}{mentorship.status.charAt(0).toUpperCase() + mentorship.status.slice(1)} {mentorship.status.charAt(0).toUpperCase() +
mentorship.status.slice(1)}
</Badge> </Badge>
</div> </div>
@ -151,10 +168,15 @@ export function MentorshipWidget({
{/* Specialties */} {/* Specialties */}
{person.specialties && person.specialties.length > 0 && ( {person.specialties && person.specialties.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs font-semibold text-gray-300 uppercase">Specialties</p> <p className="text-xs font-semibold text-gray-300 uppercase">
Specialties
</p>
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
{person.specialties.slice(0, 4).map((specialty) => ( {person.specialties.slice(0, 4).map((specialty) => (
<Badge key={specialty} className="bg-gray-600/30 text-gray-200 text-xs"> <Badge
key={specialty}
className="bg-gray-600/30 text-gray-200 text-xs"
>
{specialty} {specialty}
</Badge> </Badge>
))} ))}
@ -174,7 +196,9 @@ export function MentorshipWidget({
{mentorship.sessions_completed !== undefined && ( {mentorship.sessions_completed !== undefined && (
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Sessions</p> <p className="text-xs text-gray-400">Sessions</p>
<p className="text-lg font-bold text-white">{mentorship.sessions_completed}</p> <p className="text-lg font-bold text-white">
{mentorship.sessions_completed}
</p>
</div> </div>
)} )}
{mentorship.next_session && ( {mentorship.next_session && (

View file

@ -1,7 +1,20 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Briefcase, MapPin, DollarSign, Clock, ArrowRight, AlertCircle } from "lucide-react"; import {
Briefcase,
MapPin,
DollarSign,
Clock,
ArrowRight,
AlertCircle,
} from "lucide-react";
export interface Opportunity { export interface Opportunity {
id: string; id: string;
@ -112,7 +125,9 @@ export function OpportunitiesWidget({
{opp.title} {opp.title}
</h4> </h4>
{opp.category && ( {opp.category && (
<p className="text-xs text-gray-400 mt-1">{opp.category}</p> <p className="text-xs text-gray-400 mt-1">
{opp.category}
</p>
)} )}
</div> </div>
<Badge className={statusBadge[opp.status]}> <Badge className={statusBadge[opp.status]}>

View file

@ -1,8 +1,20 @@
import { useState } from "react"; import { useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { DollarSign, AlertCircle, CheckCircle, Clock, ExternalLink } from "lucide-react"; import {
DollarSign,
AlertCircle,
CheckCircle,
Clock,
ExternalLink,
} from "lucide-react";
export interface PayoutData { export interface PayoutData {
available_for_payout: number; available_for_payout: number;
@ -93,14 +105,12 @@ export function PayoutsWidget({
<div> <div>
<p className="font-semibold text-white">Connect Stripe Account</p> <p className="font-semibold text-white">Connect Stripe Account</p>
<p className="text-sm text-gray-400 mt-1"> <p className="text-sm text-gray-400 mt-1">
To receive payouts for completed contracts, you need to connect your Stripe account. To receive payouts for completed contracts, you need to connect
your Stripe account.
</p> </p>
</div> </div>
</div> </div>
<Button <Button onClick={onConnectStripe} className={colors.accent}>
onClick={onConnectStripe}
className={colors.accent}
>
<ExternalLink className="h-4 w-4 mr-2" /> <ExternalLink className="h-4 w-4 mr-2" />
Connect Stripe Account Connect Stripe Account
</Button> </Button>
@ -130,7 +140,10 @@ export function PayoutsWidget({
<div className="p-4 bg-black/30 rounded-lg border border-green-500/20 space-y-2"> <div className="p-4 bg-black/30 rounded-lg border border-green-500/20 space-y-2">
<p className="text-sm text-gray-400">Available for Payout</p> <p className="text-sm text-gray-400">Available for Payout</p>
<p className="text-3xl font-bold text-green-400"> <p className="text-3xl font-bold text-green-400">
${data.available_for_payout.toLocaleString('en-US', { minimumFractionDigits: 2 })} $
{data.available_for_payout.toLocaleString("en-US", {
minimumFractionDigits: 2,
})}
</p> </p>
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<Button <Button
@ -147,10 +160,16 @@ export function PayoutsWidget({
<div className="p-4 bg-black/30 rounded-lg border border-yellow-500/20 space-y-2"> <div className="p-4 bg-black/30 rounded-lg border border-yellow-500/20 space-y-2">
<p className="text-sm text-gray-400">Pending (30-day Clearance)</p> <p className="text-sm text-gray-400">Pending (30-day Clearance)</p>
<p className="text-3xl font-bold text-yellow-400"> <p className="text-3xl font-bold text-yellow-400">
${data.pending_30_day_clearance.toLocaleString('en-US', { minimumFractionDigits: 2 })} $
{data.pending_30_day_clearance.toLocaleString("en-US", {
minimumFractionDigits: 2,
})}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Next payout: {data.next_payout_date ? new Date(data.next_payout_date).toLocaleDateString() : "TBD"} Next payout:{" "}
{data.next_payout_date
? new Date(data.next_payout_date).toLocaleDateString()
: "TBD"}
</p> </p>
</div> </div>
@ -158,10 +177,16 @@ export function PayoutsWidget({
<div className="p-4 bg-black/30 rounded-lg border border-blue-500/20 space-y-2"> <div className="p-4 bg-black/30 rounded-lg border border-blue-500/20 space-y-2">
<p className="text-sm text-gray-400">Total Earned (All-Time)</p> <p className="text-sm text-gray-400">Total Earned (All-Time)</p>
<p className="text-3xl font-bold text-blue-400"> <p className="text-3xl font-bold text-blue-400">
${data.total_earned.toLocaleString('en-US', { minimumFractionDigits: 2 })} $
{data.total_earned.toLocaleString("en-US", {
minimumFractionDigits: 2,
})}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Last payout: {data.last_payout_date ? new Date(data.last_payout_date).toLocaleDateString() : "Never"} Last payout:{" "}
{data.last_payout_date
? new Date(data.last_payout_date).toLocaleDateString()
: "Never"}
</p> </p>
</div> </div>
</div> </div>
@ -186,10 +211,15 @@ export function PayoutsWidget({
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-semibold text-white"> <p className="font-semibold text-white">
${payout.amount.toLocaleString('en-US', { minimumFractionDigits: 2 })} $
{payout.amount.toLocaleString("en-US", {
minimumFractionDigits: 2,
})}
</p> </p>
{payout.description && ( {payout.description && (
<p className="text-xs text-gray-400 truncate">{payout.description}</p> <p className="text-xs text-gray-400 truncate">
{payout.description}
</p>
)} )}
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
{new Date(payout.date).toLocaleDateString()} {new Date(payout.date).toLocaleDateString()}
@ -201,8 +231,8 @@ export function PayoutsWidget({
payout.status === "completed" payout.status === "completed"
? "bg-green-600/50 text-green-100" ? "bg-green-600/50 text-green-100"
: payout.status === "pending" : payout.status === "pending"
? "bg-yellow-600/50 text-yellow-100" ? "bg-yellow-600/50 text-yellow-100"
: "bg-red-600/50 text-red-100" : "bg-red-600/50 text-red-100"
} }
> >
{payout.status} {payout.status}

View file

@ -1,7 +1,21 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Briefcase, AlertCircle, BarChart3, Clock, Users, DollarSign, ArrowRight } from "lucide-react"; import {
Briefcase,
AlertCircle,
BarChart3,
Clock,
Users,
DollarSign,
ArrowRight,
} from "lucide-react";
export interface PostedOpportunity { export interface PostedOpportunity {
id: string; id: string;
@ -96,24 +110,32 @@ export function PostedOpportunitiesWidget({
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 p-3 bg-black/20 rounded-lg mb-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-2 p-3 bg-black/20 rounded-lg mb-4">
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Total Posted</p> <p className="text-xs text-gray-400">Total Posted</p>
<p className="text-lg font-bold text-white">{opportunities.length}</p> <p className="text-lg font-bold text-white">
{opportunities.length}
</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Open</p> <p className="text-xs text-gray-400">Open</p>
<p className="text-lg font-bold text-green-400"> <p className="text-lg font-bold text-green-400">
{opportunities.filter(o => o.status === "open").length} {opportunities.filter((o) => o.status === "open").length}
</p> </p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">In Progress</p> <p className="text-xs text-gray-400">In Progress</p>
<p className="text-lg font-bold text-blue-400"> <p className="text-lg font-bold text-blue-400">
{opportunities.filter(o => o.status === "in_progress").length} {
opportunities.filter((o) => o.status === "in_progress")
.length
}
</p> </p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Total Applicants</p> <p className="text-xs text-gray-400">Total Applicants</p>
<p className="text-lg font-bold text-purple-400"> <p className="text-lg font-bold text-purple-400">
{opportunities.reduce((sum, o) => sum + (o.applications_count || 0), 0)} {opportunities.reduce(
(sum, o) => sum + (o.applications_count || 0),
0,
)}
</p> </p>
</div> </div>
</div> </div>
@ -132,7 +154,9 @@ export function PostedOpportunitiesWidget({
{opp.title} {opp.title}
</h4> </h4>
{opp.category && ( {opp.category && (
<p className="text-xs text-gray-400 mt-1">{opp.category}</p> <p className="text-xs text-gray-400 mt-1">
{opp.category}
</p>
)} )}
</div> </div>
<Badge className={statusBadge[opp.status]}> <Badge className={statusBadge[opp.status]}>
@ -183,13 +207,20 @@ export function PostedOpportunitiesWidget({
{opp.deadline && ( {opp.deadline && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
<span>Due {new Date(opp.deadline).toLocaleDateString()}</span> <span>
Due {new Date(opp.deadline).toLocaleDateString()}
</span>
</div> </div>
)} )}
{opp.applications_count !== undefined && ( {opp.applications_count !== undefined && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Users className="h-3 w-3" /> <Users className="h-3 w-3" />
<span>{opp.applications_count} {opp.applications_count === 1 ? "application" : "applications"}</span> <span>
{opp.applications_count}{" "}
{opp.applications_count === 1
? "application"
: "applications"}
</span>
</div> </div>
)} )}
</div> </div>

View file

@ -1,7 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { import {
@ -77,9 +83,16 @@ export function ProfileEditor({
arm_affiliations: (profile.arm_affiliations as string[]) || [], arm_affiliations: (profile.arm_affiliations as string[]) || [],
}); });
const [newSkill, setNewSkill] = useState({ name: "", level: "intermediate" as const }); const [newSkill, setNewSkill] = useState({
name: "",
level: "intermediate" as const,
});
const [newLanguage, setNewLanguage] = useState(""); const [newLanguage, setNewLanguage] = useState("");
const [newWorkExp, setNewWorkExp] = useState({ company: "", title: "", duration: "" }); const [newWorkExp, setNewWorkExp] = useState({
company: "",
title: "",
duration: "",
});
const [newPortfolio, setNewPortfolio] = useState({ title: "", url: "" }); const [newPortfolio, setNewPortfolio] = useState({ title: "", url: "" });
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@ -88,7 +101,9 @@ export function ProfileEditor({
const handleSubmit = async () => { const handleSubmit = async () => {
await onSave({ await onSave({
...formData, ...formData,
hourly_rate: formData.hourly_rate ? parseFloat(formData.hourly_rate) : undefined, hourly_rate: formData.hourly_rate
? parseFloat(formData.hourly_rate)
: undefined,
skills_detailed: formData.skills_detailed, skills_detailed: formData.skills_detailed,
languages: formData.languages, languages: formData.languages,
work_experience: formData.work_experience, work_experience: formData.work_experience,
@ -206,7 +221,11 @@ export function ProfileEditor({
onClick={copyProfileUrl} onClick={copyProfileUrl}
title={copied ? "Copied!" : "Copy"} title={copied ? "Copied!" : "Copy"}
> >
{copied ? <CheckCircle2 className="h-4 w-4" /> : <Copy className="h-4 w-4" />} {copied ? (
<CheckCircle2 className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@ -221,7 +240,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Bio</label> <label className="text-sm font-medium">Bio</label>
<textarea <textarea
value={formData.bio_detailed} value={formData.bio_detailed}
onChange={(e) => setFormData({ ...formData, bio_detailed: e.target.value })} onChange={(e) =>
setFormData({ ...formData, bio_detailed: e.target.value })
}
placeholder="Tell us about yourself..." placeholder="Tell us about yourself..."
className="w-full px-3 py-2 mt-1 border rounded-lg bg-background" className="w-full px-3 py-2 mt-1 border rounded-lg bg-background"
rows={4} rows={4}
@ -233,7 +254,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Location</label> <label className="text-sm font-medium">Location</label>
<Input <Input
value={formData.location} value={formData.location}
onChange={(e) => setFormData({ ...formData, location: e.target.value })} onChange={(e) =>
setFormData({ ...formData, location: e.target.value })
}
placeholder="City, Country" placeholder="City, Country"
/> />
</div> </div>
@ -241,7 +264,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Timezone</label> <label className="text-sm font-medium">Timezone</label>
<Input <Input
value={formData.timezone} value={formData.timezone}
onChange={(e) => setFormData({ ...formData, timezone: e.target.value })} onChange={(e) =>
setFormData({ ...formData, timezone: e.target.value })
}
placeholder="UTC-8 or America/Los_Angeles" placeholder="UTC-8 or America/Los_Angeles"
/> />
</div> </div>
@ -256,7 +281,9 @@ export function ProfileEditor({
<Input <Input
type="number" type="number"
value={formData.hourly_rate} value={formData.hourly_rate}
onChange={(e) => setFormData({ ...formData, hourly_rate: e.target.value })} onChange={(e) =>
setFormData({ ...formData, hourly_rate: e.target.value })
}
placeholder="50" placeholder="50"
/> />
</div> </div>
@ -268,7 +295,10 @@ export function ProfileEditor({
<select <select
value={formData.availability_status} value={formData.availability_status}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, availability_status: e.target.value }) setFormData({
...formData,
availability_status: e.target.value,
})
} }
className="w-full px-3 py-2 border rounded-lg bg-background" className="w-full px-3 py-2 border rounded-lg bg-background"
> >
@ -296,7 +326,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Twitter</label> <label className="text-sm font-medium">Twitter</label>
<Input <Input
value={formData.twitter_url} value={formData.twitter_url}
onChange={(e) => setFormData({ ...formData, twitter_url: e.target.value })} onChange={(e) =>
setFormData({ ...formData, twitter_url: e.target.value })
}
placeholder="https://twitter.com/username" placeholder="https://twitter.com/username"
/> />
</div> </div>
@ -304,7 +336,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">LinkedIn</label> <label className="text-sm font-medium">LinkedIn</label>
<Input <Input
value={formData.linkedin_url} value={formData.linkedin_url}
onChange={(e) => setFormData({ ...formData, linkedin_url: e.target.value })} onChange={(e) =>
setFormData({ ...formData, linkedin_url: e.target.value })
}
placeholder="https://linkedin.com/in/username" placeholder="https://linkedin.com/in/username"
/> />
</div> </div>
@ -312,7 +346,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">GitHub</label> <label className="text-sm font-medium">GitHub</label>
<Input <Input
value={formData.github_url} value={formData.github_url}
onChange={(e) => setFormData({ ...formData, github_url: e.target.value })} onChange={(e) =>
setFormData({ ...formData, github_url: e.target.value })
}
placeholder="https://github.com/username" placeholder="https://github.com/username"
/> />
</div> </div>
@ -320,7 +356,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Portfolio Website</label> <label className="text-sm font-medium">Portfolio Website</label>
<Input <Input
value={formData.portfolio_url} value={formData.portfolio_url}
onChange={(e) => setFormData({ ...formData, portfolio_url: e.target.value })} onChange={(e) =>
setFormData({ ...formData, portfolio_url: e.target.value })
}
placeholder="https://yourportfolio.com" placeholder="https://yourportfolio.com"
/> />
</div> </div>
@ -328,7 +366,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">YouTube</label> <label className="text-sm font-medium">YouTube</label>
<Input <Input
value={formData.youtube_url} value={formData.youtube_url}
onChange={(e) => setFormData({ ...formData, youtube_url: e.target.value })} onChange={(e) =>
setFormData({ ...formData, youtube_url: e.target.value })
}
placeholder="https://youtube.com/@username" placeholder="https://youtube.com/@username"
/> />
</div> </div>
@ -336,7 +376,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Twitch</label> <label className="text-sm font-medium">Twitch</label>
<Input <Input
value={formData.twitch_url} value={formData.twitch_url}
onChange={(e) => setFormData({ ...formData, twitch_url: e.target.value })} onChange={(e) =>
setFormData({ ...formData, twitch_url: e.target.value })
}
placeholder="https://twitch.tv/username" placeholder="https://twitch.tv/username"
/> />
</div> </div>
@ -359,10 +401,15 @@ export function ProfileEditor({
<h3 className="font-semibold mb-3">Technical Skills</h3> <h3 className="font-semibold mb-3">Technical Skills</h3>
<div className="space-y-2 mb-4"> <div className="space-y-2 mb-4">
{formData.skills_detailed.map((skill, idx) => ( {formData.skills_detailed.map((skill, idx) => (
<div key={idx} className="flex items-center justify-between p-2 bg-muted rounded"> <div
key={idx}
className="flex items-center justify-between p-2 bg-muted rounded"
>
<div> <div>
<p className="font-medium">{skill.name}</p> <p className="font-medium">{skill.name}</p>
<p className="text-xs text-muted-foreground capitalize">{skill.level}</p> <p className="text-xs text-muted-foreground capitalize">
{skill.level}
</p>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
@ -378,7 +425,9 @@ export function ProfileEditor({
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
value={newSkill.name} value={newSkill.name}
onChange={(e) => setNewSkill({ ...newSkill, name: e.target.value })} onChange={(e) =>
setNewSkill({ ...newSkill, name: e.target.value })
}
placeholder="Add skill" placeholder="Add skill"
/> />
<select <select
@ -386,7 +435,10 @@ export function ProfileEditor({
onChange={(e) => onChange={(e) =>
setNewSkill({ setNewSkill({
...newSkill, ...newSkill,
level: e.target.value as "beginner" | "intermediate" | "expert", level: e.target.value as
| "beginner"
| "intermediate"
| "expert",
}) })
} }
className="px-2 border rounded-lg bg-background" className="px-2 border rounded-lg bg-background"
@ -406,7 +458,10 @@ export function ProfileEditor({
<h3 className="font-semibold mb-3">Languages</h3> <h3 className="font-semibold mb-3">Languages</h3>
<div className="space-y-2 mb-4"> <div className="space-y-2 mb-4">
{formData.languages.map((lang, idx) => ( {formData.languages.map((lang, idx) => (
<div key={idx} className="flex items-center justify-between p-2 bg-muted rounded"> <div
key={idx}
className="flex items-center justify-between p-2 bg-muted rounded"
>
<p className="font-medium">{lang}</p> <p className="font-medium">{lang}</p>
<Button <Button
variant="ghost" variant="ghost"
@ -453,8 +508,12 @@ export function ProfileEditor({
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<p className="font-medium">{exp.title}</p> <p className="font-medium">{exp.title}</p>
<p className="text-sm text-muted-foreground">{exp.company}</p> <p className="text-sm text-muted-foreground">
<p className="text-xs text-muted-foreground">{exp.duration}</p> {exp.company}
</p>
<p className="text-xs text-muted-foreground">
{exp.duration}
</p>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
@ -466,7 +525,9 @@ export function ProfileEditor({
</Button> </Button>
</div> </div>
{exp.description && ( {exp.description && (
<p className="text-sm text-muted-foreground">{exp.description}</p> <p className="text-sm text-muted-foreground">
{exp.description}
</p>
)} )}
</div> </div>
))} ))}
@ -474,17 +535,23 @@ export function ProfileEditor({
<div className="space-y-2 p-3 border rounded-lg"> <div className="space-y-2 p-3 border rounded-lg">
<Input <Input
value={newWorkExp.title} value={newWorkExp.title}
onChange={(e) => setNewWorkExp({ ...newWorkExp, title: e.target.value })} onChange={(e) =>
setNewWorkExp({ ...newWorkExp, title: e.target.value })
}
placeholder="Job Title" placeholder="Job Title"
/> />
<Input <Input
value={newWorkExp.company} value={newWorkExp.company}
onChange={(e) => setNewWorkExp({ ...newWorkExp, company: e.target.value })} onChange={(e) =>
setNewWorkExp({ ...newWorkExp, company: e.target.value })
}
placeholder="Company" placeholder="Company"
/> />
<Input <Input
value={newWorkExp.duration} value={newWorkExp.duration}
onChange={(e) => setNewWorkExp({ ...newWorkExp, duration: e.target.value })} onChange={(e) =>
setNewWorkExp({ ...newWorkExp, duration: e.target.value })
}
placeholder="Duration (e.g., 2020-2023)" placeholder="Duration (e.g., 2020-2023)"
/> />
<Button size="sm" onClick={addWorkExp} className="w-full"> <Button size="sm" onClick={addWorkExp} className="w-full">
@ -522,7 +589,9 @@ export function ProfileEditor({
</Button> </Button>
</div> </div>
{item.description && ( {item.description && (
<p className="text-sm text-muted-foreground">{item.description}</p> <p className="text-sm text-muted-foreground">
{item.description}
</p>
)} )}
</div> </div>
))} ))}
@ -530,12 +599,16 @@ export function ProfileEditor({
<div className="space-y-2 p-3 border rounded-lg"> <div className="space-y-2 p-3 border rounded-lg">
<Input <Input
value={newPortfolio.title} value={newPortfolio.title}
onChange={(e) => setNewPortfolio({ ...newPortfolio, title: e.target.value })} onChange={(e) =>
setNewPortfolio({ ...newPortfolio, title: e.target.value })
}
placeholder="Project Title" placeholder="Project Title"
/> />
<Input <Input
value={newPortfolio.url} value={newPortfolio.url}
onChange={(e) => setNewPortfolio({ ...newPortfolio, url: e.target.value })} onChange={(e) =>
setNewPortfolio({ ...newPortfolio, url: e.target.value })
}
placeholder="Project URL" placeholder="Project URL"
/> />
<Button size="sm" onClick={addPortfolio} className="w-full"> <Button size="sm" onClick={addPortfolio} className="w-full">
@ -557,7 +630,8 @@ export function ProfileEditor({
Arm Affiliations Arm Affiliations
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Select the arms you're part of. They can also be auto-detected from your activities. Select the arms you're part of. They can also be auto-detected
from your activities.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View file

@ -1,6 +1,18 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { CheckCircle, Clock, AlertCircle, Calendar, DollarSign } from "lucide-react"; import {
CheckCircle,
Clock,
AlertCircle,
Calendar,
DollarSign,
} from "lucide-react";
export interface Milestone { export interface Milestone {
id: string; id: string;
@ -66,9 +78,13 @@ export function ProjectStatusWidget({
); );
} }
const completedMilestones = project.milestones?.filter(m => m.status === "completed").length || 0; const completedMilestones =
project.milestones?.filter((m) => m.status === "completed").length || 0;
const totalMilestones = project.milestones?.length || 0; const totalMilestones = project.milestones?.length || 0;
const completionPercentage = totalMilestones > 0 ? Math.round((completedMilestones / totalMilestones) * 100) : 0; const completionPercentage =
totalMilestones > 0
? Math.round((completedMilestones / totalMilestones) * 100)
: 0;
const getStatusIcon = (status: string) => { const getStatusIcon = (status: string) => {
switch (status) { switch (status) {
@ -95,7 +111,9 @@ export function ProjectStatusWidget({
const getDaysRemaining = (dueDate: string) => { const getDaysRemaining = (dueDate: string) => {
const today = new Date(); const today = new Date();
const due = new Date(dueDate); const due = new Date(dueDate);
const diff = Math.ceil((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); const diff = Math.ceil(
(due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24),
);
return diff; return diff;
}; };
@ -112,32 +130,40 @@ export function ProjectStatusWidget({
{project.description && ( {project.description && (
<p className="text-sm text-gray-400 mb-4">{project.description}</p> <p className="text-sm text-gray-400 mb-4">{project.description}</p>
)} )}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
<p className="text-gray-400 text-xs uppercase">Start</p> <p className="text-gray-400 text-xs uppercase">Start</p>
<div className="flex items-center gap-1 mt-1"> <div className="flex items-center gap-1 mt-1">
<Calendar className="h-4 w-4 text-gray-500" /> <Calendar className="h-4 w-4 text-gray-500" />
<p className="font-semibold text-white">{new Date(project.start_date).toLocaleDateString()}</p> <p className="font-semibold text-white">
{new Date(project.start_date).toLocaleDateString()}
</p>
</div> </div>
</div> </div>
<div> <div>
<p className="text-gray-400 text-xs uppercase">End</p> <p className="text-gray-400 text-xs uppercase">End</p>
<div className="flex items-center gap-1 mt-1"> <div className="flex items-center gap-1 mt-1">
<Calendar className="h-4 w-4 text-gray-500" /> <Calendar className="h-4 w-4 text-gray-500" />
<p className="font-semibold text-white">{new Date(project.end_date).toLocaleDateString()}</p> <p className="font-semibold text-white">
{new Date(project.end_date).toLocaleDateString()}
</p>
</div> </div>
</div> </div>
<div> <div>
<p className="text-gray-400 text-xs uppercase">Value</p> <p className="text-gray-400 text-xs uppercase">Value</p>
<div className="flex items-center gap-1 mt-1"> <div className="flex items-center gap-1 mt-1">
<DollarSign className="h-4 w-4 text-gray-500" /> <DollarSign className="h-4 w-4 text-gray-500" />
<p className="font-semibold text-white">${(project.total_value || 0).toLocaleString()}</p> <p className="font-semibold text-white">
${(project.total_value || 0).toLocaleString()}
</p>
</div> </div>
</div> </div>
<div> <div>
<p className="text-gray-400 text-xs uppercase">Status</p> <p className="text-gray-400 text-xs uppercase">Status</p>
<Badge className="bg-blue-600/50 text-blue-100 mt-1">{project.status}</Badge> <Badge className="bg-blue-600/50 text-blue-100 mt-1">
{project.status}
</Badge>
</div> </div>
</div> </div>
@ -145,7 +171,9 @@ export function ProjectStatusWidget({
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-400">Overall Progress</span> <span className="text-gray-400">Overall Progress</span>
<span className="font-semibold text-white">{completionPercentage}%</span> <span className="font-semibold text-white">
{completionPercentage}%
</span>
</div> </div>
<div className="w-full bg-black/50 rounded-full h-3"> <div className="w-full bg-black/50 rounded-full h-3">
<div <div
@ -153,7 +181,9 @@ export function ProjectStatusWidget({
style={{ width: `${completionPercentage}%` }} style={{ width: `${completionPercentage}%` }}
/> />
</div> </div>
<p className="text-xs text-gray-500">{completedMilestones} of {totalMilestones} milestones completed</p> <p className="text-xs text-gray-500">
{completedMilestones} of {totalMilestones} milestones completed
</p>
</div> </div>
</div> </div>
@ -164,7 +194,8 @@ export function ProjectStatusWidget({
<div className="space-y-3"> <div className="space-y-3">
{project.milestones.map((milestone, idx) => { {project.milestones.map((milestone, idx) => {
const daysRemaining = getDaysRemaining(milestone.due_date); const daysRemaining = getDaysRemaining(milestone.due_date);
const isOverdue = daysRemaining < 0 && milestone.status !== "completed"; const isOverdue =
daysRemaining < 0 && milestone.status !== "completed";
return ( return (
<div key={milestone.id} className="space-y-2"> <div key={milestone.id} className="space-y-2">
@ -174,17 +205,27 @@ export function ProjectStatusWidget({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div> <div>
<p className="font-semibold text-white">{milestone.title}</p> <p className="font-semibold text-white">
{milestone.title}
</p>
{milestone.description && ( {milestone.description && (
<p className="text-xs text-gray-400 mt-1">{milestone.description}</p> <p className="text-xs text-gray-400 mt-1">
{milestone.description}
</p>
)} )}
</div> </div>
<div className="text-right flex-shrink-0"> <div className="text-right flex-shrink-0">
{milestone.value && ( {milestone.value && (
<p className="text-sm font-semibold text-white">${milestone.value.toLocaleString()}</p> <p className="text-sm font-semibold text-white">
${milestone.value.toLocaleString()}
</p>
)} )}
<p className={`text-xs ${isOverdue ? "text-red-400" : "text-gray-400"}`}> <p
{isOverdue ? `${Math.abs(daysRemaining)} days overdue` : `${daysRemaining} days remaining`} className={`text-xs ${isOverdue ? "text-red-400" : "text-gray-400"}`}
>
{isOverdue
? `${Math.abs(daysRemaining)} days overdue`
: `${daysRemaining} days remaining`}
</p> </p>
</div> </div>
</div> </div>
@ -209,7 +250,9 @@ export function ProjectStatusWidget({
})} })}
</div> </div>
) : ( ) : (
<p className="text-center text-gray-400 py-4">No milestones defined</p> <p className="text-center text-gray-400 py-4">
No milestones defined
</p>
)} )}
</div> </div>
</CardContent> </CardContent>

View file

@ -1,7 +1,19 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { BookMarked, ExternalLink, CheckCircle, Clock, AlertCircle } from "lucide-react"; import {
BookMarked,
ExternalLink,
CheckCircle,
Clock,
AlertCircle,
} from "lucide-react";
export interface ResearchTrack { export interface ResearchTrack {
id: string; id: string;
@ -40,10 +52,26 @@ const colorMap = {
}; };
const statusMap = { const statusMap = {
active: { label: "Active", color: "bg-green-600/50 text-green-100", icon: CheckCircle }, active: {
paused: { label: "Paused", color: "bg-yellow-600/50 text-yellow-100", icon: Clock }, label: "Active",
completed: { label: "Completed", color: "bg-blue-600/50 text-blue-100", icon: CheckCircle }, color: "bg-green-600/50 text-green-100",
on_hold: { label: "On Hold", color: "bg-red-600/50 text-red-100", icon: AlertCircle }, icon: CheckCircle,
},
paused: {
label: "Paused",
color: "bg-yellow-600/50 text-yellow-100",
icon: Clock,
},
completed: {
label: "Completed",
color: "bg-blue-600/50 text-blue-100",
icon: CheckCircle,
},
on_hold: {
label: "On Hold",
color: "bg-red-600/50 text-red-100",
icon: AlertCircle,
},
}; };
export function ResearchWidget({ export function ResearchWidget({
@ -54,8 +82,8 @@ export function ResearchWidget({
accentColor = "yellow", accentColor = "yellow",
}: ResearchWidgetProps) { }: ResearchWidgetProps) {
const colors = colorMap[accentColor]; const colors = colorMap[accentColor];
const activeCount = tracks.filter(t => t.status === "active").length; const activeCount = tracks.filter((t) => t.status === "active").length;
const completedCount = tracks.filter(t => t.status === "completed").length; const completedCount = tracks.filter((t) => t.status === "completed").length;
return ( return (
<Card className={`${colors.bg} border ${colors.border}`}> <Card className={`${colors.bg} border ${colors.border}`}>
@ -82,11 +110,15 @@ export function ResearchWidget({
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Active</p> <p className="text-xs text-gray-400">Active</p>
<p className="text-lg font-bold text-green-400">{activeCount}</p> <p className="text-lg font-bold text-green-400">
{activeCount}
</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xs text-gray-400">Completed</p> <p className="text-xs text-gray-400">Completed</p>
<p className="text-lg font-bold text-blue-400">{completedCount}</p> <p className="text-lg font-bold text-blue-400">
{completedCount}
</p>
</div> </div>
</div> </div>
@ -105,9 +137,13 @@ export function ResearchWidget({
{/* Header */} {/* Header */}
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h4 className="font-semibold text-white">{track.title}</h4> <h4 className="font-semibold text-white">
{track.title}
</h4>
{track.category && ( {track.category && (
<p className="text-xs text-gray-400">{track.category}</p> <p className="text-xs text-gray-400">
{track.category}
</p>
)} )}
</div> </div>
<Badge className={statusInfo.color}> <Badge className={statusInfo.color}>
@ -118,7 +154,9 @@ export function ResearchWidget({
{/* Description */} {/* Description */}
{track.description && ( {track.description && (
<p className="text-sm text-gray-400">{track.description}</p> <p className="text-sm text-gray-400">
{track.description}
</p>
)} )}
{/* Meta */} {/* Meta */}

View file

@ -1,4 +1,10 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Clock, Zap, TrendingUp } from "lucide-react"; import { Clock, Zap, TrendingUp } from "lucide-react";
@ -66,11 +72,14 @@ export function SprintWidgetComponent({
const now = new Date(); const now = new Date();
const endDate = new Date(sprint.end_date); const endDate = new Date(sprint.end_date);
const daysRemaining = Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); const daysRemaining = Math.ceil(
(endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
);
const isActive = sprint.status === "active"; const isActive = sprint.status === "active";
const progress = sprint.tasks_total && sprint.tasks_total > 0 const progress =
? Math.round(((sprint.tasks_completed || 0) / sprint.tasks_total) * 100) sprint.tasks_total && sprint.tasks_total > 0
: 0; ? Math.round(((sprint.tasks_completed || 0) / sprint.tasks_total) * 100)
: 0;
const formatCountdown = (days: number) => { const formatCountdown = (days: number) => {
if (days < 0) return "Sprint Over"; if (days < 0) return "Sprint Over";
@ -87,11 +96,21 @@ export function SprintWidgetComponent({
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5" /> <Zap className="h-5 w-5" />
{sprint.project_name || "Sprint"} {sprint.project_name || "Sprint"}
{sprint.sprint_number && <span className="text-sm font-normal text-gray-400">#{sprint.sprint_number}</span>} {sprint.sprint_number && (
<span className="text-sm font-normal text-gray-400">
#{sprint.sprint_number}
</span>
)}
</CardTitle> </CardTitle>
<CardDescription>Sprint timeline and progress</CardDescription> <CardDescription>Sprint timeline and progress</CardDescription>
</div> </div>
<Badge className={isActive ? "bg-green-600/50 text-green-100" : "bg-blue-600/50 text-blue-100"}> <Badge
className={
isActive
? "bg-green-600/50 text-green-100"
: "bg-blue-600/50 text-blue-100"
}
>
{sprint.status} {sprint.status}
</Badge> </Badge>
</div> </div>
@ -106,12 +125,16 @@ export function SprintWidgetComponent({
<div className={`text-3xl font-bold ${colors.accent}`}> <div className={`text-3xl font-bold ${colors.accent}`}>
{daysRemaining > 0 ? daysRemaining : 0} days {daysRemaining > 0 ? daysRemaining : 0} days
</div> </div>
<p className="text-sm text-gray-400">{formatCountdown(daysRemaining)}</p> <p className="text-sm text-gray-400">
{formatCountdown(daysRemaining)}
</p>
</div> </div>
{/* Timeline */} {/* Timeline */}
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs font-semibold text-gray-300 uppercase">Timeline</p> <p className="text-xs font-semibold text-gray-300 uppercase">
Timeline
</p>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="p-2 bg-black/30 rounded border border-gray-500/10"> <div className="p-2 bg-black/30 rounded border border-gray-500/10">
<p className="text-xs text-gray-400">Start</p> <p className="text-xs text-gray-400">Start</p>
@ -132,7 +155,9 @@ export function SprintWidgetComponent({
{sprint.tasks_total && sprint.tasks_total > 0 && ( {sprint.tasks_total && sprint.tasks_total > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs font-semibold text-gray-300 uppercase">Task Progress</p> <p className="text-xs font-semibold text-gray-300 uppercase">
Task Progress
</p>
<p className="text-sm font-bold text-white">{progress}%</p> <p className="text-sm font-bold text-white">{progress}%</p>
</div> </div>
<div className="w-full bg-black/50 rounded-full h-3"> <div className="w-full bg-black/50 rounded-full h-3">
@ -142,7 +167,8 @@ export function SprintWidgetComponent({
/> />
</div> </div>
<p className="text-xs text-gray-400"> <p className="text-xs text-gray-400">
{sprint.tasks_completed || 0} of {sprint.tasks_total} tasks completed {sprint.tasks_completed || 0} of {sprint.tasks_total} tasks
completed
</p> </p>
</div> </div>
)} )}
@ -150,7 +176,9 @@ export function SprintWidgetComponent({
{/* Scope */} {/* Scope */}
{sprint.scope && ( {sprint.scope && (
<div className="p-3 bg-black/30 rounded-lg border border-gray-500/10 space-y-2"> <div className="p-3 bg-black/30 rounded-lg border border-gray-500/10 space-y-2">
<p className="text-xs font-semibold text-gray-300 uppercase">Scope</p> <p className="text-xs font-semibold text-gray-300 uppercase">
Scope
</p>
<p className="text-sm text-gray-300">{sprint.scope}</p> <p className="text-sm text-gray-300">{sprint.scope}</p>
</div> </div>
)} )}
@ -159,8 +187,12 @@ export function SprintWidgetComponent({
{sprint.team_size && ( {sprint.team_size && (
<div className="p-3 bg-black/30 rounded-lg border border-gray-500/10 flex items-center justify-between"> <div className="p-3 bg-black/30 rounded-lg border border-gray-500/10 flex items-center justify-between">
<div> <div>
<p className="text-xs font-semibold text-gray-300 uppercase">Team Size</p> <p className="text-xs font-semibold text-gray-300 uppercase">
<p className="text-sm text-gray-300 mt-1">{sprint.team_size} members</p> Team Size
</p>
<p className="text-sm text-gray-300 mt-1">
{sprint.team_size} members
</p>
</div> </div>
<TrendingUp className="h-5 w-5 text-gray-500" /> <TrendingUp className="h-5 w-5 text-gray-500" />
</div> </div>

View file

@ -1,4 +1,10 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Users, MessageCircle, ExternalLink } from "lucide-react"; import { Users, MessageCircle, ExternalLink } from "lucide-react";
@ -108,7 +114,9 @@ export function TeamWidget({
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h4 className="font-semibold text-white truncate">{member.name}</h4> <h4 className="font-semibold text-white truncate">
{member.name}
</h4>
<p className="text-xs text-gray-400">{member.role}</p> <p className="text-xs text-gray-400">{member.role}</p>
{member.title && ( {member.title && (
<p className="text-xs text-gray-500">{member.title}</p> <p className="text-xs text-gray-500">{member.title}</p>
@ -118,7 +126,9 @@ export function TeamWidget({
{/* Bio */} {/* Bio */}
{member.bio && ( {member.bio && (
<p className="text-xs text-gray-400 line-clamp-2">{member.bio}</p> <p className="text-xs text-gray-400 line-clamp-2">
{member.bio}
</p>
)} )}
{/* Links */} {/* Links */}

View file

@ -14,7 +14,9 @@ export const armAffiliationService = {
/** /**
* Check if user has enrolled in any Foundation courses * Check if user has enrolled in any Foundation courses
*/ */
async hasFoundationActivity(userId: string): Promise<{ detected: boolean; count: number }> { async hasFoundationActivity(
userId: string,
): Promise<{ detected: boolean; count: number }> {
try { try {
const { data: enrollments, error } = await supabase const { data: enrollments, error } = await supabase
.from("course_enrollments") .from("course_enrollments")
@ -23,7 +25,10 @@ export const armAffiliationService = {
.limit(1); .limit(1);
if (error) throw error; if (error) throw error;
return { detected: (enrollments?.length ?? 0) > 0, count: enrollments?.length ?? 0 }; return {
detected: (enrollments?.length ?? 0) > 0,
count: enrollments?.length ?? 0,
};
} catch (error) { } catch (error) {
console.error("Error checking Foundation activity:", error); console.error("Error checking Foundation activity:", error);
return { detected: false, count: 0 }; return { detected: false, count: 0 };
@ -33,7 +38,9 @@ export const armAffiliationService = {
/** /**
* Check if user has GameForge projects/teams * Check if user has GameForge projects/teams
*/ */
async hasGameForgeActivity(userId: string): Promise<{ detected: boolean; count: number }> { async hasGameForgeActivity(
userId: string,
): Promise<{ detected: boolean; count: number }> {
try { try {
const { data: projects, error: projectError } = await supabase const { data: projects, error: projectError } = await supabase
.from("gameforge_projects") .from("gameforge_projects")
@ -62,7 +69,9 @@ export const armAffiliationService = {
/** /**
* Check if user has Labs research activities * Check if user has Labs research activities
*/ */
async hasLabsActivity(userId: string): Promise<{ detected: boolean; count: number }> { async hasLabsActivity(
userId: string,
): Promise<{ detected: boolean; count: number }> {
try { try {
const { data: research, error } = await supabase const { data: research, error } = await supabase
.from("labs_research_tracks") .from("labs_research_tracks")
@ -71,7 +80,10 @@ export const armAffiliationService = {
.limit(1); .limit(1);
if (error) throw error; if (error) throw error;
return { detected: (research?.length ?? 0) > 0, count: research?.length ?? 0 }; return {
detected: (research?.length ?? 0) > 0,
count: research?.length ?? 0,
};
} catch (error) { } catch (error) {
console.error("Error checking Labs activity:", error); console.error("Error checking Labs activity:", error);
return { detected: false, count: 0 }; return { detected: false, count: 0 };
@ -81,7 +93,9 @@ export const armAffiliationService = {
/** /**
* Check if user has Corp-related activities * Check if user has Corp-related activities
*/ */
async hasCorpActivity(userId: string): Promise<{ detected: boolean; count: number }> { async hasCorpActivity(
userId: string,
): Promise<{ detected: boolean; count: number }> {
try { try {
// Corp activities could include partnerships, investments, or business accounts // Corp activities could include partnerships, investments, or business accounts
const { data: accounts, error } = await supabase const { data: accounts, error } = await supabase
@ -91,7 +105,10 @@ export const armAffiliationService = {
.limit(1); .limit(1);
if (error) throw error; if (error) throw error;
return { detected: (accounts?.length ?? 0) > 0, count: accounts?.length ?? 0 }; return {
detected: (accounts?.length ?? 0) > 0,
count: accounts?.length ?? 0,
};
} catch (error) { } catch (error) {
// Corp table may not exist yet, return false // Corp table may not exist yet, return false
return { detected: false, count: 0 }; return { detected: false, count: 0 };
@ -101,7 +118,9 @@ export const armAffiliationService = {
/** /**
* Check if user has DevLink/Roblox development activity * Check if user has DevLink/Roblox development activity
*/ */
async hasDevLinkActivity(userId: string): Promise<{ detected: boolean; count: number }> { async hasDevLinkActivity(
userId: string,
): Promise<{ detected: boolean; count: number }> {
try { try {
const { data: devLinkProfiles, error } = await supabase const { data: devLinkProfiles, error } = await supabase
.from("devlink_profiles") .from("devlink_profiles")
@ -110,7 +129,10 @@ export const armAffiliationService = {
.limit(1); .limit(1);
if (error && error.code !== "42P01") throw error; // Ignore table not found errors if (error && error.code !== "42P01") throw error; // Ignore table not found errors
return { detected: (devLinkProfiles?.length ?? 0) > 0, count: devLinkProfiles?.length ?? 0 }; return {
detected: (devLinkProfiles?.length ?? 0) > 0,
count: devLinkProfiles?.length ?? 0,
};
} catch (error) { } catch (error) {
// Table may not exist yet // Table may not exist yet
return { detected: false, count: 0 }; return { detected: false, count: 0 };
@ -133,31 +155,41 @@ export const armAffiliationService = {
{ {
arm: "foundation", arm: "foundation",
detected: foundation.detected, detected: foundation.detected,
reason: foundation.detected ? `${foundation.count} course(s) enrolled` : "No courses enrolled", reason: foundation.detected
? `${foundation.count} course(s) enrolled`
: "No courses enrolled",
activityCount: foundation.count, activityCount: foundation.count,
}, },
{ {
arm: "gameforge", arm: "gameforge",
detected: gameforge.detected, detected: gameforge.detected,
reason: gameforge.detected ? `${gameforge.count} project(s) and team(s)` : "No projects or teams", reason: gameforge.detected
? `${gameforge.count} project(s) and team(s)`
: "No projects or teams",
activityCount: gameforge.count, activityCount: gameforge.count,
}, },
{ {
arm: "labs", arm: "labs",
detected: labs.detected, detected: labs.detected,
reason: labs.detected ? `${labs.count} research track(s)` : "No research tracks", reason: labs.detected
? `${labs.count} research track(s)`
: "No research tracks",
activityCount: labs.count, activityCount: labs.count,
}, },
{ {
arm: "corp", arm: "corp",
detected: corp.detected, detected: corp.detected,
reason: corp.detected ? `${corp.count} corp account(s)` : "No corp accounts", reason: corp.detected
? `${corp.count} corp account(s)`
: "No corp accounts",
activityCount: corp.count, activityCount: corp.count,
}, },
{ {
arm: "devlink", arm: "devlink",
detected: devlink.detected, detected: devlink.detected,
reason: devlink.detected ? "DevLink profile created" : "No DevLink profile", reason: devlink.detected
? "DevLink profile created"
: "No DevLink profile",
activityCount: devlink.count, activityCount: devlink.count,
}, },
]; ];
@ -172,22 +204,25 @@ export const armAffiliationService = {
for (const affiliation of detectedArms) { for (const affiliation of detectedArms) {
if (affiliation.detected) { if (affiliation.detected) {
await fetch(`${import.meta.env.VITE_API_BASE}/api/user/arm-affiliations`, { await fetch(
method: "POST", `${import.meta.env.VITE_API_BASE}/api/user/arm-affiliations`,
headers: { {
Authorization: `Bearer ${token}`, method: "POST",
"Content-Type": "application/json", headers: {
}, Authorization: `Bearer ${token}`,
body: JSON.stringify({ "Content-Type": "application/json",
arm: affiliation.arm,
affiliation_type: "courses", // Generic type for auto-detected
affiliation_data: {
detected: true,
reason: affiliation.reason,
}, },
confirmed: false, body: JSON.stringify({
}), arm: affiliation.arm,
}); affiliation_type: "courses", // Generic type for auto-detected
affiliation_data: {
detected: true,
reason: affiliation.reason,
},
confirmed: false,
}),
},
);
} }
} }
} catch (error) { } catch (error) {

View file

@ -115,9 +115,20 @@ const ARMS = [
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const { user, profile, loading: authLoading, signOut, profileComplete, linkedProviders, linkProvider, unlinkProvider } = useAuth(); const {
user,
profile,
loading: authLoading,
signOut,
profileComplete,
linkedProviders,
linkProvider,
unlinkProvider,
} = useAuth();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState(() => searchParams.get("tab") ?? "realms"); const [activeTab, setActiveTab] = useState(
() => searchParams.get("tab") ?? "realms",
);
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState("");
const [bio, setBio] = useState(""); const [bio, setBio] = useState("");
const [website, setWebsite] = useState(""); const [website, setWebsite] = useState("");
@ -218,7 +229,10 @@ export default function Dashboard() {
Dashboard Dashboard
</h1> </h1>
<p className="text-gray-400 text-lg"> <p className="text-gray-400 text-lg">
Welcome back, <span className="text-purple-300 font-semibold">{profile?.full_name || user.email?.split("@")[0]}</span> Welcome back,{" "}
<span className="text-purple-300 font-semibold">
{profile?.full_name || user.email?.split("@")[0]}
</span>
</p> </p>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@ -229,7 +243,10 @@ export default function Dashboard() {
</Badge> </Badge>
)} )}
{profile?.level && ( {profile?.level && (
<Badge variant="outline" className="border-purple-500/30 text-purple-300 bg-purple-500/10 text-sm py-1 px-3"> <Badge
variant="outline"
className="border-purple-500/30 text-purple-300 bg-purple-500/10 text-sm py-1 px-3"
>
<Star className="h-3 w-3 mr-1" /> <Star className="h-3 w-3 mr-1" />
Level {profile.level} Level {profile.level}
</Badge> </Badge>
@ -243,8 +260,13 @@ export default function Dashboard() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="space-y-1"> <div className="space-y-1">
<p className="font-semibold text-white">Complete Your Profile</p> <p className="font-semibold text-white">
<p className="text-sm text-orange-200">Set up your profile to unlock all AeThex features and join the community</p> Complete Your Profile
</p>
<p className="text-sm text-orange-200">
Set up your profile to unlock all AeThex features and
join the community
</p>
</div> </div>
<Button <Button
size="sm" size="sm"
@ -260,7 +282,11 @@ export default function Dashboard() {
</div> </div>
{/* Tabs Section */} {/* Tabs Section */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs
value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList className="grid w-full grid-cols-4 lg:grid-cols-4 bg-purple-950/30 border border-purple-500/20 p-1"> <TabsList className="grid w-full grid-cols-4 lg:grid-cols-4 bg-purple-950/30 border border-purple-500/20 p-1">
<TabsTrigger value="realms" className="text-sm md:text-base"> <TabsTrigger value="realms" className="text-sm md:text-base">
<span className="hidden sm:inline">Realms</span> <span className="hidden sm:inline">Realms</span>
@ -291,17 +317,26 @@ export default function Dashboard() {
}} }}
className="group relative overflow-hidden" className="group relative overflow-hidden"
> >
<Card className={`bg-gradient-to-br ${arm.bgGradient} border transition-all duration-300 h-full hover:shadow-lg hover:shadow-purple-500/20 ${arm.borderColor} cursor-pointer`}> <Card
className={`bg-gradient-to-br ${arm.bgGradient} border transition-all duration-300 h-full hover:shadow-lg hover:shadow-purple-500/20 ${arm.borderColor} cursor-pointer`}
>
<CardContent className="p-6 space-y-4"> <CardContent className="p-6 space-y-4">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-black/30 group-hover:bg-black/50 transition-colors"> <div className="p-2 rounded-lg bg-black/30 group-hover:bg-black/50 transition-colors">
<IconComponent className="h-5 w-5" style={{ color: arm.color }} /> <IconComponent
className="h-5 w-5"
style={{ color: arm.color }}
/>
</div> </div>
<h3 className="text-xl font-bold text-white">{arm.label}</h3> <h3 className="text-xl font-bold text-white">
{arm.label}
</h3>
</div> </div>
<p className="text-sm text-gray-400">{arm.description}</p> <p className="text-sm text-gray-400">
{arm.description}
</p>
</div> </div>
</div> </div>
<div className="pt-2 flex items-center gap-2 text-purple-300 group-hover:gap-3 transition-all"> <div className="pt-2 flex items-center gap-2 text-purple-300 group-hover:gap-3 transition-all">
@ -325,13 +360,18 @@ export default function Dashboard() {
<div className="relative mx-auto w-24 h-24"> <div className="relative mx-auto w-24 h-24">
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-purple-500/30 to-blue-500/30 blur-lg" /> <div className="absolute inset-0 rounded-full bg-gradient-to-r from-purple-500/30 to-blue-500/30 blur-lg" />
<img <img
src={profile?.avatar_url || "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=400&fit=crop&crop=face"} src={
profile?.avatar_url ||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=400&fit=crop&crop=face"
}
alt="Profile" alt="Profile"
className="w-24 h-24 rounded-full ring-4 ring-purple-500/40 relative" className="w-24 h-24 rounded-full ring-4 ring-purple-500/40 relative"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-lg font-bold text-white">{profile?.full_name || "User"}</h3> <h3 className="text-lg font-bold text-white">
{profile?.full_name || "User"}
</h3>
<p className="text-sm text-gray-400">{user.email}</p> <p className="text-sm text-gray-400">{user.email}</p>
<Badge className="bg-purple-600/50 text-purple-100 mx-auto"> <Badge className="bg-purple-600/50 text-purple-100 mx-auto">
<Star className="h-3 w-3 mr-1" /> <Star className="h-3 w-3 mr-1" />
@ -345,7 +385,9 @@ export default function Dashboard() {
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20 lg:col-span-2"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20 lg:col-span-2">
<CardHeader> <CardHeader>
<CardTitle>Edit Profile</CardTitle> <CardTitle>Edit Profile</CardTitle>
<CardDescription>Update your public profile information</CardDescription> <CardDescription>
Update your public profile information
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
@ -429,14 +471,23 @@ export default function Dashboard() {
</TabsContent> </TabsContent>
{/* Connections Tab */} {/* Connections Tab */}
<TabsContent value="connections" className="space-y-6 animate-fade-in"> <TabsContent
value="connections"
className="space-y-6 animate-fade-in"
>
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader> <CardHeader>
<CardTitle>Connected Accounts</CardTitle> <CardTitle>Connected Accounts</CardTitle>
<CardDescription>Link external accounts to your AeThex profile</CardDescription> <CardDescription>
Link external accounts to your AeThex profile
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<OAuthConnections linkedProviders={linkedProviders} linkProvider={linkProvider} unlinkProvider={unlinkProvider} /> <OAuthConnections
linkedProviders={linkedProviders}
linkProvider={linkProvider}
unlinkProvider={unlinkProvider}
/>
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
@ -448,7 +499,9 @@ export default function Dashboard() {
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader> <CardHeader>
<CardTitle>Primary Realm</CardTitle> <CardTitle>Primary Realm</CardTitle>
<CardDescription>Choose your primary area of focus</CardDescription> <CardDescription>
Choose your primary area of focus
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<RealmSwitcher /> <RealmSwitcher />

View file

@ -5,11 +5,26 @@ import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext"; import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext"; import { useArmTheme } from "@/contexts/ArmThemeContext";
import { supabase } from "@/lib/supabase"; import { supabase } from "@/lib/supabase";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import LoadingScreen from "@/components/LoadingScreen"; import LoadingScreen from "@/components/LoadingScreen";
import { Code, Users, Briefcase, ExternalLink, ArrowRight, AlertCircle, Edit, Save } from "lucide-react"; import {
Code,
Users,
Briefcase,
ExternalLink,
ArrowRight,
AlertCircle,
Edit,
Save,
} from "lucide-react";
import { TeamWidget } from "@/components/TeamWidget"; import { TeamWidget } from "@/components/TeamWidget";
const API_BASE = import.meta.env.VITE_API_BASE || ""; const API_BASE = import.meta.env.VITE_API_BASE || "";
@ -34,7 +49,9 @@ export default function DevLinkDashboard() {
const loadDashboardData = async () => { const loadDashboardData = async () => {
try { try {
setLoading(true); setLoading(true);
const { data: { session } } = await supabase.auth.getSession(); const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token; const token = session?.access_token;
if (!token) throw new Error("No auth token"); if (!token) throw new Error("No auth token");
@ -42,7 +59,10 @@ export default function DevLinkDashboard() {
const profileRes = await fetch(`${API_BASE}/api/devlink/profile`, { const profileRes = await fetch(`${API_BASE}/api/devlink/profile`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (profileRes.ok && profileRes.headers.get("content-type")?.includes("application/json")) { if (
profileRes.ok &&
profileRes.headers.get("content-type")?.includes("application/json")
) {
const data = await profileRes.json(); const data = await profileRes.json();
setProfile(data); setProfile(data);
} }
@ -54,7 +74,10 @@ export default function DevLinkDashboard() {
const oppRes = await fetch(`${API_BASE}/api/devlink/opportunities`, { const oppRes = await fetch(`${API_BASE}/api/devlink/opportunities`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (oppRes.ok && oppRes.headers.get("content-type")?.includes("application/json")) { if (
oppRes.ok &&
oppRes.headers.get("content-type")?.includes("application/json")
) {
const data = await oppRes.json(); const data = await oppRes.json();
setOpportunities(Array.isArray(data) ? data : []); setOpportunities(Array.isArray(data) ? data : []);
} }
@ -66,7 +89,10 @@ export default function DevLinkDashboard() {
const teamsRes = await fetch(`${API_BASE}/api/devlink/teams`, { const teamsRes = await fetch(`${API_BASE}/api/devlink/teams`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (teamsRes.ok && teamsRes.headers.get("content-type")?.includes("application/json")) { if (
teamsRes.ok &&
teamsRes.headers.get("content-type")?.includes("application/json")
) {
const data = await teamsRes.json(); const data = await teamsRes.json();
setTeams(Array.isArray(data) ? data : []); setTeams(Array.isArray(data) ? data : []);
} }
@ -107,19 +133,33 @@ export default function DevLinkDashboard() {
return ( return (
<Layout> <Layout>
<div className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`} style={{ backgroundImage: theme.wallpaperPattern }}> <div
className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`}
style={{ backgroundImage: theme.wallpaperPattern }}
>
<div className="container mx-auto px-4 max-w-7xl space-y-8"> <div className="container mx-auto px-4 max-w-7xl space-y-8">
{/* Header */} {/* Header */}
<div className="space-y-4 animate-slide-down"> <div className="space-y-4 animate-slide-down">
<h1 className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}> <h1
className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}
>
DEV-LINK DEV-LINK
</h1> </h1>
<p className="text-gray-400 text-lg">Roblox Developer Network | Vibrant Cyan</p> <p className="text-gray-400 text-lg">
Roblox Developer Network | Vibrant Cyan
</p>
</div> </div>
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs
<TabsList className="grid w-full grid-cols-4 bg-cyan-950/30 border border-cyan-500/20 p-1" style={{ fontFamily: theme.fontFamily }}> value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList
className="grid w-full grid-cols-4 bg-cyan-950/30 border border-cyan-500/20 p-1"
style={{ fontFamily: theme.fontFamily }}
>
<TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="profile">Profile Editor</TabsTrigger> <TabsTrigger value="profile">Profile Editor</TabsTrigger>
<TabsTrigger value="jobs">Roblox Jobs</TabsTrigger> <TabsTrigger value="jobs">Roblox Jobs</TabsTrigger>
@ -132,19 +172,25 @@ export default function DevLinkDashboard() {
<Card className="bg-gradient-to-br from-cyan-950/40 to-cyan-900/20 border-cyan-500/20"> <Card className="bg-gradient-to-br from-cyan-950/40 to-cyan-900/20 border-cyan-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Profile Views</p> <p className="text-sm text-gray-400">Profile Views</p>
<p className="text-3xl font-bold text-white">{profile?.profile_views || 0}</p> <p className="text-3xl font-bold text-white">
{profile?.profile_views || 0}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20"> <Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Job Matches</p> <p className="text-sm text-gray-400">Job Matches</p>
<p className="text-3xl font-bold text-white">{opportunities.length}</p> <p className="text-3xl font-bold text-white">
{opportunities.length}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Team Requests</p> <p className="text-sm text-gray-400">Team Requests</p>
<p className="text-3xl font-bold text-white">{teams.length}</p> <p className="text-3xl font-bold text-white">
{teams.length}
</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -153,7 +199,12 @@ export default function DevLinkDashboard() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-gradient-to-br from-cyan-600/20 to-blue-600/20 border-cyan-500/40"> <Card className="bg-gradient-to-br from-cyan-600/20 to-blue-600/20 border-cyan-500/40">
<CardContent className="p-6 text-center space-y-4"> <CardContent className="p-6 text-center space-y-4">
<h3 className="text-lg font-bold text-white" style={{ fontFamily: theme.fontFamily }}>Browse Roblox Jobs</h3> <h3
className="text-lg font-bold text-white"
style={{ fontFamily: theme.fontFamily }}
>
Browse Roblox Jobs
</h3>
<Button <Button
onClick={() => navigate("/dev-link/jobs")} onClick={() => navigate("/dev-link/jobs")}
variant="outline" variant="outline"
@ -167,7 +218,12 @@ export default function DevLinkDashboard() {
</Card> </Card>
<Card className="bg-gradient-to-br from-blue-600/20 to-purple-600/20 border-blue-500/40"> <Card className="bg-gradient-to-br from-blue-600/20 to-purple-600/20 border-blue-500/40">
<CardContent className="p-6 text-center space-y-4"> <CardContent className="p-6 text-center space-y-4">
<h3 className="text-lg font-bold text-white" style={{ fontFamily: theme.fontFamily }}>Find a Teammate</h3> <h3
className="text-lg font-bold text-white"
style={{ fontFamily: theme.fontFamily }}
>
Find a Teammate
</h3>
<Button <Button
onClick={() => navigate("/dev-link/teams")} onClick={() => navigate("/dev-link/teams")}
variant="outline" variant="outline"
@ -188,17 +244,30 @@ export default function DevLinkDashboard() {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between">
<span>My dev-link Profile Editor</span> <span>My dev-link Profile Editor</span>
<Button size="sm" variant="outline" className="border-cyan-500/30" onClick={() => setIsEditing(!isEditing)}> <Button
{isEditing ? <Save className="h-4 w-4 mr-2" /> : <Edit className="h-4 w-4 mr-2" />} size="sm"
variant="outline"
className="border-cyan-500/30"
onClick={() => setIsEditing(!isEditing)}
>
{isEditing ? (
<Save className="h-4 w-4 mr-2" />
) : (
<Edit className="h-4 w-4 mr-2" />
)}
{isEditing ? "Save" : "Edit"} {isEditing ? "Save" : "Edit"}
</Button> </Button>
</CardTitle> </CardTitle>
<CardDescription>Customize your Roblox portfolio</CardDescription> <CardDescription>
Customize your Roblox portfolio
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* Roblox Creations */} {/* Roblox Creations */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="font-semibold text-white">My Roblox Creations</h3> <h3 className="font-semibold text-white">
My Roblox Creations
</h3>
{isEditing ? ( {isEditing ? (
<textarea <textarea
className="w-full px-4 py-2 bg-black/30 border border-cyan-500/20 rounded-lg text-white placeholder-gray-500" className="w-full px-4 py-2 bg-black/30 border border-cyan-500/20 rounded-lg text-white placeholder-gray-500"
@ -207,13 +276,17 @@ export default function DevLinkDashboard() {
rows={4} rows={4}
/> />
) : ( ) : (
<p className="text-gray-400">{profile?.creations || "No creations listed yet"}</p> <p className="text-gray-400">
{profile?.creations || "No creations listed yet"}
</p>
)} )}
</div> </div>
{/* Experiences */} {/* Experiences */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="font-semibold text-white">My Experiences (Asset IDs)</h3> <h3 className="font-semibold text-white">
My Experiences (Asset IDs)
</h3>
{isEditing ? ( {isEditing ? (
<textarea <textarea
className="w-full px-4 py-2 bg-black/30 border border-cyan-500/20 rounded-lg text-white placeholder-gray-500" className="w-full px-4 py-2 bg-black/30 border border-cyan-500/20 rounded-lg text-white placeholder-gray-500"
@ -222,13 +295,17 @@ export default function DevLinkDashboard() {
rows={4} rows={4}
/> />
) : ( ) : (
<p className="text-gray-400">{profile?.experiences || "No experiences listed yet"}</p> <p className="text-gray-400">
{profile?.experiences || "No experiences listed yet"}
</p>
)} )}
</div> </div>
{/* Certifications */} {/* Certifications */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="font-semibold text-white">EdTech Certifications</h3> <h3 className="font-semibold text-white">
EdTech Certifications
</h3>
{isEditing ? ( {isEditing ? (
<textarea <textarea
className="w-full px-4 py-2 bg-black/30 border border-cyan-500/20 rounded-lg text-white placeholder-gray-500" className="w-full px-4 py-2 bg-black/30 border border-cyan-500/20 rounded-lg text-white placeholder-gray-500"
@ -237,7 +314,10 @@ export default function DevLinkDashboard() {
rows={3} rows={3}
/> />
) : ( ) : (
<p className="text-gray-400">{profile?.certifications || "No certifications listed yet"}</p> <p className="text-gray-400">
{profile?.certifications ||
"No certifications listed yet"}
</p>
)} )}
</div> </div>
</CardContent> </CardContent>
@ -249,25 +329,44 @@ export default function DevLinkDashboard() {
<Card className="bg-gradient-to-br from-cyan-950/40 to-cyan-900/20 border-cyan-500/20"> <Card className="bg-gradient-to-br from-cyan-950/40 to-cyan-900/20 border-cyan-500/20">
<CardHeader> <CardHeader>
<CardTitle>Roblox Job Feed</CardTitle> <CardTitle>Roblox Job Feed</CardTitle>
<CardDescription>Pre-filtered DEV-LINK opportunities</CardDescription> <CardDescription>
Pre-filtered DEV-LINK opportunities
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{opportunities.length === 0 ? ( {opportunities.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<Briefcase className="h-12 w-12 mx-auto text-gray-500 opacity-50 mb-4" /> <Briefcase className="h-12 w-12 mx-auto text-gray-500 opacity-50 mb-4" />
<p className="text-gray-400">No matching jobs at this time</p> <p className="text-gray-400">
No matching jobs at this time
</p>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{opportunities.map((job: any) => ( {opportunities.map((job: any) => (
<div key={job.id} className="p-4 bg-black/30 rounded-lg border border-cyan-500/10 hover:border-cyan-500/30 transition"> <div
key={job.id}
className="p-4 bg-black/30 rounded-lg border border-cyan-500/10 hover:border-cyan-500/30 transition"
>
<div className="flex items-start justify-between gap-4 mb-2"> <div className="flex items-start justify-between gap-4 mb-2">
<h4 className="font-semibold text-white">{job.title}</h4> <h4 className="font-semibold text-white">
<Badge className="bg-cyan-600/50 text-cyan-100">Roblox</Badge> {job.title}
</h4>
<Badge className="bg-cyan-600/50 text-cyan-100">
Roblox
</Badge>
</div> </div>
<p className="text-sm text-gray-400">{job.description?.substring(0, 100)}...</p> <p className="text-sm text-gray-400">
<p className="text-sm font-semibold text-white mt-2">${job.budget?.toLocaleString()}</p> {job.description?.substring(0, 100)}...
<Button size="sm" variant="outline" className="mt-3 border-cyan-500/30 text-cyan-300 hover:bg-cyan-500/10"> </p>
<p className="text-sm font-semibold text-white mt-2">
${job.budget?.toLocaleString()}
</p>
<Button
size="sm"
variant="outline"
className="mt-3 border-cyan-500/30 text-cyan-300 hover:bg-cyan-500/10"
>
View Details <ArrowRight className="h-3 w-3 ml-2" /> View Details <ArrowRight className="h-3 w-3 ml-2" />
</Button> </Button>
</div> </div>
@ -281,14 +380,16 @@ export default function DevLinkDashboard() {
{/* Teams Tab */} {/* Teams Tab */}
<TabsContent value="teams" className="space-y-4 animate-fade-in"> <TabsContent value="teams" className="space-y-4 animate-fade-in">
<TeamWidget <TeamWidget
members={teams.flatMap((t: any) => (t.members || []).map((m: any) => ({ members={teams.flatMap((t: any) =>
id: m.id, (t.members || []).map((m: any) => ({
name: m.full_name, id: m.id,
role: m.role || "Member", name: m.full_name,
type: m.role === "lead" ? "lead" : "member", role: m.role || "Member",
avatar: m.avatar_url, type: m.role === "lead" ? "lead" : "member",
team_name: t.name, avatar: m.avatar_url,
})))} team_name: t.name,
})),
)}
title="My dev-link Teams" title="My dev-link Teams"
description="Find and manage Roblox development teams" description="Find and manage Roblox development teams"
accentColor="cyan" accentColor="cyan"

View file

@ -54,7 +54,9 @@ export default function FoundationDashboard() {
const loadDashboardData = async () => { const loadDashboardData = async () => {
try { try {
setLoading(true); setLoading(true);
const { data: { session } } = await supabase.auth.getSession(); const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token; const token = session?.access_token;
if (!token) throw new Error("No auth token"); if (!token) throw new Error("No auth token");
@ -62,7 +64,10 @@ export default function FoundationDashboard() {
const coursesRes = await fetch(`${API_BASE}/api/foundation/courses`, { const coursesRes = await fetch(`${API_BASE}/api/foundation/courses`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (coursesRes.ok && coursesRes.headers.get("content-type")?.includes("application/json")) { if (
coursesRes.ok &&
coursesRes.headers.get("content-type")?.includes("application/json")
) {
const data = await coursesRes.json(); const data = await coursesRes.json();
setCourses(Array.isArray(data) ? data : []); setCourses(Array.isArray(data) ? data : []);
} }
@ -71,10 +76,16 @@ export default function FoundationDashboard() {
} }
try { try {
const mentorRes = await fetch(`${API_BASE}/api/foundation/mentorships`, { const mentorRes = await fetch(
headers: { Authorization: `Bearer ${token}` }, `${API_BASE}/api/foundation/mentorships`,
}); {
if (mentorRes.ok && mentorRes.headers.get("content-type")?.includes("application/json")) { headers: { Authorization: `Bearer ${token}` },
},
);
if (
mentorRes.ok &&
mentorRes.headers.get("content-type")?.includes("application/json")
) {
const data = await mentorRes.json(); const data = await mentorRes.json();
setMentorships(data.as_mentee || []); setMentorships(data.as_mentee || []);
} }
@ -100,7 +111,9 @@ export default function FoundationDashboard() {
<h1 className="text-4xl font-bold bg-gradient-to-r from-red-300 to-orange-300 bg-clip-text text-transparent"> <h1 className="text-4xl font-bold bg-gradient-to-r from-red-300 to-orange-300 bg-clip-text text-transparent">
Join FOUNDATION Join FOUNDATION
</h1> </h1>
<p className="text-gray-400">Learn from industry experts and mentors</p> <p className="text-gray-400">
Learn from industry experts and mentors
</p>
<Button <Button
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
className="w-full bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 text-lg py-6" className="w-full bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 text-lg py-6"
@ -114,18 +127,25 @@ export default function FoundationDashboard() {
} }
const enrolledCourses = courses.filter((c: any) => c.userEnrollment); const enrolledCourses = courses.filter((c: any) => c.userEnrollment);
const completedCourses = enrolledCourses.filter((c: any) => c.userEnrollment?.status === "completed"); const completedCourses = enrolledCourses.filter(
(c: any) => c.userEnrollment?.status === "completed",
);
const activeMentor = mentorships.find((m: any) => m.status === "accepted"); const activeMentor = mentorships.find((m: any) => m.status === "accepted");
return ( return (
<Layout> <Layout>
<div className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`} style={{ backgroundImage: theme.wallpaperPattern }}> <div
className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`}
style={{ backgroundImage: theme.wallpaperPattern }}
>
<div className="container mx-auto px-4 max-w-7xl space-y-8"> <div className="container mx-auto px-4 max-w-7xl space-y-8">
{/* Header */} {/* Header */}
<div className="space-y-4 animate-slide-down"> <div className="space-y-4 animate-slide-down">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="space-y-2"> <div className="space-y-2">
<h1 className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}> <h1
className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}
>
FOUNDATION University FOUNDATION University
</h1> </h1>
<p className="text-gray-400 text-lg"> <p className="text-gray-400 text-lg">
@ -140,8 +160,12 @@ export default function FoundationDashboard() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Courses Enrolled</p> <p className="text-xs text-gray-400 uppercase tracking-wider">
<p className="text-2xl font-bold text-white mt-1">{enrolledCourses.length}</p> Courses Enrolled
</p>
<p className="text-2xl font-bold text-white mt-1">
{enrolledCourses.length}
</p>
</div> </div>
<BookOpen className="h-6 w-6 text-red-500 opacity-50" /> <BookOpen className="h-6 w-6 text-red-500 opacity-50" />
</div> </div>
@ -152,8 +176,12 @@ export default function FoundationDashboard() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Completed</p> <p className="text-xs text-gray-400 uppercase tracking-wider">
<p className="text-2xl font-bold text-white mt-1">{completedCourses.length}</p> Completed
</p>
<p className="text-2xl font-bold text-white mt-1">
{completedCourses.length}
</p>
</div> </div>
<CheckCircle className="h-6 w-6 text-red-500 opacity-50" /> <CheckCircle className="h-6 w-6 text-red-500 opacity-50" />
</div> </div>
@ -164,27 +192,38 @@ export default function FoundationDashboard() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Achievements</p> <p className="text-xs text-gray-400 uppercase tracking-wider">
<p className="text-2xl font-bold text-white mt-1">{achievements.length}</p> Achievements
</p>
<p className="text-2xl font-bold text-white mt-1">
{achievements.length}
</p>
</div> </div>
<Award className="h-6 w-6 text-red-500 opacity-50" /> <Award className="h-6 w-6 text-red-500 opacity-50" />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className={`bg-gradient-to-br ${activeMentor ? 'from-green-950/40 to-green-900/20 border-green-500/20' : 'from-gray-950/40 to-gray-900/20 border-gray-500/20'}`}> <Card
className={`bg-gradient-to-br ${activeMentor ? "from-green-950/40 to-green-900/20 border-green-500/20" : "from-gray-950/40 to-gray-900/20 border-gray-500/20"}`}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Mentor</p> <p className="text-xs text-gray-400 uppercase tracking-wider">
Mentor
</p>
<p className="text-2xl font-bold text-white mt-1"> <p className="text-2xl font-bold text-white mt-1">
{activeMentor ? '✓' : '—'} {activeMentor ? "✓" : "—"}
</p> </p>
</div> </div>
<Users className="h-6 w-6" style={{ <Users
color: activeMentor ? '#22c55e' : '#666', className="h-6 w-6"
opacity: 0.5 style={{
}} /> color: activeMentor ? "#22c55e" : "#666",
opacity: 0.5,
}}
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -192,8 +231,15 @@ export default function FoundationDashboard() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs
<TabsList className="grid w-full grid-cols-4 bg-red-950/30 border border-red-500/20 p-1" style={{ fontFamily: theme.fontFamily }}> value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList
className="grid w-full grid-cols-4 bg-red-950/30 border border-red-500/20 p-1"
style={{ fontFamily: theme.fontFamily }}
>
<TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="courses">Courses</TabsTrigger> <TabsTrigger value="courses">Courses</TabsTrigger>
<TabsTrigger value="mentorship">Mentorship</TabsTrigger> <TabsTrigger value="mentorship">Mentorship</TabsTrigger>
@ -213,7 +259,9 @@ export default function FoundationDashboard() {
expertise: m.mentor?.role_title, expertise: m.mentor?.role_title,
}, },
status: m.status, status: m.status,
connectedSince: m.accepted_at ? new Date(m.accepted_at).toLocaleDateString() : null, connectedSince: m.accepted_at
? new Date(m.accepted_at).toLocaleDateString()
: null,
lastSession: m.last_session_date, lastSession: m.last_session_date,
nextSession: m.next_session_date, nextSession: m.next_session_date,
}))} }))}
@ -269,7 +317,10 @@ export default function FoundationDashboard() {
</TabsContent> </TabsContent>
{/* Mentorship Tab */} {/* Mentorship Tab */}
<TabsContent value="mentorship" className="space-y-4 animate-fade-in"> <TabsContent
value="mentorship"
className="space-y-4 animate-fade-in"
>
<MentorshipWidget <MentorshipWidget
mentorships={mentorships.map((m: any) => ({ mentorships={mentorships.map((m: any) => ({
id: m.id, id: m.id,
@ -280,7 +331,9 @@ export default function FoundationDashboard() {
expertise: m.mentor?.role_title, expertise: m.mentor?.role_title,
}, },
status: m.status, status: m.status,
connectedSince: m.accepted_at ? new Date(m.accepted_at).toLocaleDateString() : null, connectedSince: m.accepted_at
? new Date(m.accepted_at).toLocaleDateString()
: null,
lastSession: m.last_session_date, lastSession: m.last_session_date,
nextSession: m.next_session_date, nextSession: m.next_session_date,
}))} }))}
@ -292,7 +345,10 @@ export default function FoundationDashboard() {
</TabsContent> </TabsContent>
{/* Achievements Tab */} {/* Achievements Tab */}
<TabsContent value="achievements" className="space-y-4 animate-fade-in"> <TabsContent
value="achievements"
className="space-y-4 animate-fade-in"
>
<AchievementsWidget <AchievementsWidget
achievements={achievements.map((a: any) => ({ achievements={achievements.map((a: any) => ({
id: a.id, id: a.id,

View file

@ -5,11 +5,26 @@ import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext"; import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext"; import { useArmTheme } from "@/contexts/ArmThemeContext";
import { supabase } from "@/lib/supabase"; import { supabase } from "@/lib/supabase";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import LoadingScreen from "@/components/LoadingScreen"; import LoadingScreen from "@/components/LoadingScreen";
import { Gamepad2, Users, Clock, CheckCircle, AlertCircle, Rocket, Send, Home } from "lucide-react"; import {
Gamepad2,
Users,
Clock,
CheckCircle,
AlertCircle,
Rocket,
Send,
Home,
} from "lucide-react";
import { SprintWidgetComponent } from "@/components/SprintWidget"; import { SprintWidgetComponent } from "@/components/SprintWidget";
import { TeamWidget } from "@/components/TeamWidget"; import { TeamWidget } from "@/components/TeamWidget";
@ -36,7 +51,9 @@ export default function GameForgeDashboard() {
const loadDashboardData = async () => { const loadDashboardData = async () => {
try { try {
setLoading(true); setLoading(true);
const { data: { session } } = await supabase.auth.getSession(); const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token; const token = session?.access_token;
if (!token) throw new Error("No auth token"); if (!token) throw new Error("No auth token");
@ -44,7 +61,10 @@ export default function GameForgeDashboard() {
const sprintRes = await fetch(`${API_BASE}/api/gameforge/sprint`, { const sprintRes = await fetch(`${API_BASE}/api/gameforge/sprint`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (sprintRes.ok && sprintRes.headers.get("content-type")?.includes("application/json")) { if (
sprintRes.ok &&
sprintRes.headers.get("content-type")?.includes("application/json")
) {
const data = await sprintRes.json(); const data = await sprintRes.json();
setSprint(data); setSprint(data);
} }
@ -56,7 +76,10 @@ export default function GameForgeDashboard() {
const teamRes = await fetch(`${API_BASE}/api/gameforge/team`, { const teamRes = await fetch(`${API_BASE}/api/gameforge/team`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (teamRes.ok && teamRes.headers.get("content-type")?.includes("application/json")) { if (
teamRes.ok &&
teamRes.headers.get("content-type")?.includes("application/json")
) {
const data = await teamRes.json(); const data = await teamRes.json();
setTeam(Array.isArray(data) ? data : []); setTeam(Array.isArray(data) ? data : []);
} }
@ -68,7 +91,10 @@ export default function GameForgeDashboard() {
const tasksRes = await fetch(`${API_BASE}/api/gameforge/tasks`, { const tasksRes = await fetch(`${API_BASE}/api/gameforge/tasks`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (tasksRes.ok && tasksRes.headers.get("content-type")?.includes("application/json")) { if (
tasksRes.ok &&
tasksRes.headers.get("content-type")?.includes("application/json")
) {
const data = await tasksRes.json(); const data = await tasksRes.json();
setTasks(Array.isArray(data) ? data : []); setTasks(Array.isArray(data) ? data : []);
} }
@ -95,9 +121,9 @@ export default function GameForgeDashboard() {
}, [sprint]); }, [sprint]);
const tasksByStatus = { const tasksByStatus = {
todo: tasks.filter(t => t.status === "todo"), todo: tasks.filter((t) => t.status === "todo"),
inprogress: tasks.filter(t => t.status === "in_progress"), inprogress: tasks.filter((t) => t.status === "in_progress"),
done: tasks.filter(t => t.status === "done"), done: tasks.filter((t) => t.status === "done"),
}; };
if (authLoading || loading) { if (authLoading || loading) {
@ -127,13 +153,18 @@ export default function GameForgeDashboard() {
return ( return (
<Layout> <Layout>
<div className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`} style={{ backgroundImage: theme.wallpaperPattern }}> <div
className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`}
style={{ backgroundImage: theme.wallpaperPattern }}
>
<div className="container mx-auto px-4 max-w-7xl space-y-8"> <div className="container mx-auto px-4 max-w-7xl space-y-8">
{sprint ? ( {sprint ? (
<> <>
{/* Active Sprint Header */} {/* Active Sprint Header */}
<div className="space-y-4 animate-slide-down"> <div className="space-y-4 animate-slide-down">
<h1 className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}> <h1
className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}
>
Mission Control Mission Control
</h1> </h1>
<p className="text-gray-400 text-lg">Project: {sprint.title}</p> <p className="text-gray-400 text-lg">Project: {sprint.title}</p>
@ -143,22 +174,32 @@ export default function GameForgeDashboard() {
<Card className="bg-gradient-to-br from-green-950/40 to-emerald-950/40 border-green-500/30"> <Card className="bg-gradient-to-br from-green-950/40 to-emerald-950/40 border-green-500/30">
<CardContent className="p-6"> <CardContent className="p-6">
<div className="text-center space-y-4"> <div className="text-center space-y-4">
<p className="text-gray-400 text-sm">Time Remaining in Sprint</p> <p className="text-gray-400 text-sm">
Time Remaining in Sprint
</p>
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
<div className="text-center"> <div className="text-center">
<p className="text-3xl font-bold text-green-400">{timeRemaining.days}</p> <p className="text-3xl font-bold text-green-400">
{timeRemaining.days}
</p>
<p className="text-xs text-gray-400">Days</p> <p className="text-xs text-gray-400">Days</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-3xl font-bold text-green-400">{String(timeRemaining.hours).padStart(2, '0')}</p> <p className="text-3xl font-bold text-green-400">
{String(timeRemaining.hours).padStart(2, "0")}
</p>
<p className="text-xs text-gray-400">Hours</p> <p className="text-xs text-gray-400">Hours</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-3xl font-bold text-green-400">{String(timeRemaining.minutes).padStart(2, '0')}</p> <p className="text-3xl font-bold text-green-400">
{String(timeRemaining.minutes).padStart(2, "0")}
</p>
<p className="text-xs text-gray-400">Minutes</p> <p className="text-xs text-gray-400">Minutes</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-3xl font-bold text-green-400">{String(timeRemaining.seconds).padStart(2, '0')}</p> <p className="text-3xl font-bold text-green-400">
{String(timeRemaining.seconds).padStart(2, "0")}
</p>
<p className="text-xs text-gray-400">Seconds</p> <p className="text-xs text-gray-400">Seconds</p>
</div> </div>
</div> </div>
@ -169,8 +210,15 @@ export default function GameForgeDashboard() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs
<TabsList className="grid w-full grid-cols-4 bg-green-950/30 border border-green-500/20 p-1" style={{ fontFamily: theme.fontFamily }}> value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList
className="grid w-full grid-cols-4 bg-green-950/30 border border-green-500/20 p-1"
style={{ fontFamily: theme.fontFamily }}
>
<TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="scope">Scope</TabsTrigger> <TabsTrigger value="scope">Scope</TabsTrigger>
<TabsTrigger value="team">Team</TabsTrigger> <TabsTrigger value="team">Team</TabsTrigger>
@ -178,24 +226,33 @@ export default function GameForgeDashboard() {
</TabsList> </TabsList>
{/* Overview Tab */} {/* Overview Tab */}
<TabsContent value="overview" className="space-y-6 animate-fade-in"> <TabsContent
value="overview"
className="space-y-6 animate-fade-in"
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20"> <Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Sprint Phase</p> <p className="text-sm text-gray-400">Sprint Phase</p>
<p className="text-3xl font-bold text-white">{sprint.phase}</p> <p className="text-3xl font-bold text-white">
{sprint.phase}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-emerald-950/40 to-emerald-900/20 border-emerald-500/20"> <Card className="bg-gradient-to-br from-emerald-950/40 to-emerald-900/20 border-emerald-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Team Size</p> <p className="text-sm text-gray-400">Team Size</p>
<p className="text-3xl font-bold text-white">{team.length}</p> <p className="text-3xl font-bold text-white">
{team.length}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-teal-950/40 to-teal-900/20 border-teal-500/20"> <Card className="bg-gradient-to-br from-teal-950/40 to-teal-900/20 border-teal-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Tasks Completed</p> <p className="text-sm text-gray-400">Tasks Completed</p>
<p className="text-3xl font-bold text-white">{tasksByStatus.done.length}/{tasks.length}</p> <p className="text-3xl font-bold text-white">
{tasksByStatus.done.length}/{tasks.length}
</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -203,8 +260,15 @@ export default function GameForgeDashboard() {
{/* Submit Build CTA */} {/* Submit Build CTA */}
<Card className="bg-gradient-to-br from-green-600/20 to-emerald-600/20 border-green-500/40"> <Card className="bg-gradient-to-br from-green-600/20 to-emerald-600/20 border-green-500/40">
<CardContent className="p-8 text-center space-y-4"> <CardContent className="p-8 text-center space-y-4">
<h3 className="text-2xl font-bold text-white" style={{ fontFamily: theme.fontFamily }}>Ready to Ship?</h3> <h3
<p className="text-gray-300">Submit your final build for evaluation</p> className="text-2xl font-bold text-white"
style={{ fontFamily: theme.fontFamily }}
>
Ready to Ship?
</h3>
<p className="text-gray-300">
Submit your final build for evaluation
</p>
<Button <Button
onClick={() => navigate("/gameforge/submit-build")} onClick={() => navigate("/gameforge/submit-build")}
className="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-lg px-8" className="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-lg px-8"
@ -218,15 +282,22 @@ export default function GameForgeDashboard() {
</TabsContent> </TabsContent>
{/* Scope Tab */} {/* Scope Tab */}
<TabsContent value="scope" className="space-y-4 animate-fade-in"> <TabsContent
value="scope"
className="space-y-4 animate-fade-in"
>
<Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20"> <Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20">
<CardHeader> <CardHeader>
<CardTitle>The Scope Anchor (KND-001)</CardTitle> <CardTitle>The Scope Anchor (KND-001)</CardTitle>
<CardDescription>Your north star - prevent feature creep</CardDescription> <CardDescription>
Your north star - prevent feature creep
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="p-6 bg-black/30 rounded-lg border border-green-500/20"> <div className="p-6 bg-black/30 rounded-lg border border-green-500/20">
<p className="text-white leading-relaxed">{sprint.gdd || "Game Design Document not available"}</p> <p className="text-white leading-relaxed">
{sprint.gdd || "Game Design Document not available"}
</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -255,21 +326,35 @@ export default function GameForgeDashboard() {
</TabsContent> </TabsContent>
{/* Tasks Tab - Kanban */} {/* Tasks Tab - Kanban */}
<TabsContent value="tasks" className="space-y-4 animate-fade-in"> <TabsContent
value="tasks"
className="space-y-4 animate-fade-in"
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* To Do */} {/* To Do */}
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20"> <Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20">
<CardHeader> <CardHeader>
<CardTitle className="text-lg">To Do ({tasksByStatus.todo.length})</CardTitle> <CardTitle className="text-lg">
To Do ({tasksByStatus.todo.length})
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{tasksByStatus.todo.length === 0 ? ( {tasksByStatus.todo.length === 0 ? (
<p className="text-center text-gray-400 text-sm py-4">No tasks</p> <p className="text-center text-gray-400 text-sm py-4">
No tasks
</p>
) : ( ) : (
tasksByStatus.todo.map((task: any) => ( tasksByStatus.todo.map((task: any) => (
<div key={task.id} className="p-3 bg-black/30 rounded-lg border border-red-500/20 hover:border-red-500/40 transition"> <div
<p className="font-semibold text-white text-sm">{task.title}</p> key={task.id}
<p className="text-xs text-gray-400 mt-1">{task.assigned_to?.full_name}</p> className="p-3 bg-black/30 rounded-lg border border-red-500/20 hover:border-red-500/40 transition"
>
<p className="font-semibold text-white text-sm">
{task.title}
</p>
<p className="text-xs text-gray-400 mt-1">
{task.assigned_to?.full_name}
</p>
</div> </div>
)) ))
)} )}
@ -279,16 +364,27 @@ export default function GameForgeDashboard() {
{/* In Progress */} {/* In Progress */}
<Card className="bg-gradient-to-br from-yellow-950/40 to-yellow-900/20 border-yellow-500/20"> <Card className="bg-gradient-to-br from-yellow-950/40 to-yellow-900/20 border-yellow-500/20">
<CardHeader> <CardHeader>
<CardTitle className="text-lg">In Progress ({tasksByStatus.inprogress.length})</CardTitle> <CardTitle className="text-lg">
In Progress ({tasksByStatus.inprogress.length})
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{tasksByStatus.inprogress.length === 0 ? ( {tasksByStatus.inprogress.length === 0 ? (
<p className="text-center text-gray-400 text-sm py-4">No tasks</p> <p className="text-center text-gray-400 text-sm py-4">
No tasks
</p>
) : ( ) : (
tasksByStatus.inprogress.map((task: any) => ( tasksByStatus.inprogress.map((task: any) => (
<div key={task.id} className="p-3 bg-black/30 rounded-lg border border-yellow-500/20 hover:border-yellow-500/40 transition"> <div
<p className="font-semibold text-white text-sm">{task.title}</p> key={task.id}
<p className="text-xs text-gray-400 mt-1">{task.assigned_to?.full_name}</p> className="p-3 bg-black/30 rounded-lg border border-yellow-500/20 hover:border-yellow-500/40 transition"
>
<p className="font-semibold text-white text-sm">
{task.title}
</p>
<p className="text-xs text-gray-400 mt-1">
{task.assigned_to?.full_name}
</p>
</div> </div>
)) ))
)} )}
@ -298,19 +394,30 @@ export default function GameForgeDashboard() {
{/* Done */} {/* Done */}
<Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20"> <Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20">
<CardHeader> <CardHeader>
<CardTitle className="text-lg">Done ({tasksByStatus.done.length})</CardTitle> <CardTitle className="text-lg">
Done ({tasksByStatus.done.length})
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{tasksByStatus.done.length === 0 ? ( {tasksByStatus.done.length === 0 ? (
<p className="text-center text-gray-400 text-sm py-4">No tasks</p> <p className="text-center text-gray-400 text-sm py-4">
No tasks
</p>
) : ( ) : (
tasksByStatus.done.map((task: any) => ( tasksByStatus.done.map((task: any) => (
<div key={task.id} className="p-3 bg-black/30 rounded-lg border border-green-500/20 hover:border-green-500/40 transition"> <div
key={task.id}
className="p-3 bg-black/30 rounded-lg border border-green-500/20 hover:border-green-500/40 transition"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-500" /> <CheckCircle className="h-4 w-4 text-green-500" />
<div className="flex-1"> <div className="flex-1">
<p className="font-semibold text-white text-sm line-through">{task.title}</p> <p className="font-semibold text-white text-sm line-through">
<p className="text-xs text-gray-400 mt-1">{task.assigned_to?.full_name}</p> {task.title}
</p>
<p className="text-xs text-gray-400 mt-1">
{task.assigned_to?.full_name}
</p>
</div> </div>
</div> </div>
</div> </div>
@ -326,8 +433,12 @@ export default function GameForgeDashboard() {
<Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20"> <Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20">
<CardContent className="p-12 text-center space-y-4"> <CardContent className="p-12 text-center space-y-4">
<Gamepad2 className="h-12 w-12 mx-auto text-green-500 opacity-50" /> <Gamepad2 className="h-12 w-12 mx-auto text-green-500 opacity-50" />
<p className="text-gray-400">No active sprint. Join one to get started!</p> <p className="text-gray-400">
<Button onClick={() => navigate("/gameforge")}>Browse GAMEFORGE</Button> No active sprint. Join one to get started!
</p>
<Button onClick={() => navigate("/gameforge")}>
Browse GAMEFORGE
</Button>
</CardContent> </CardContent>
</Card> </Card>
)} )}

View file

@ -5,11 +5,26 @@ import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext"; import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext"; import { useArmTheme } from "@/contexts/ArmThemeContext";
import { supabase } from "@/lib/supabase"; import { supabase } from "@/lib/supabase";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import LoadingScreen from "@/components/LoadingScreen"; import LoadingScreen from "@/components/LoadingScreen";
import { Lightbulb, FileText, Zap, Lock, ExternalLink, ArrowRight, AlertCircle, Send } from "lucide-react"; import {
Lightbulb,
FileText,
Zap,
Lock,
ExternalLink,
ArrowRight,
AlertCircle,
Send,
} from "lucide-react";
import { ResearchWidget } from "@/components/ResearchWidget"; import { ResearchWidget } from "@/components/ResearchWidget";
const API_BASE = import.meta.env.VITE_API_BASE || ""; const API_BASE = import.meta.env.VITE_API_BASE || "";
@ -35,7 +50,9 @@ export default function LabsDashboard() {
const loadDashboardData = async () => { const loadDashboardData = async () => {
try { try {
setLoading(true); setLoading(true);
const { data: { session } } = await supabase.auth.getSession(); const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token; const token = session?.access_token;
if (!token) throw new Error("No auth token"); if (!token) throw new Error("No auth token");
@ -43,7 +60,10 @@ export default function LabsDashboard() {
const tracksRes = await fetch(`${API_BASE}/api/labs/research-tracks`, { const tracksRes = await fetch(`${API_BASE}/api/labs/research-tracks`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (tracksRes.ok && tracksRes.headers.get("content-type")?.includes("application/json")) { if (
tracksRes.ok &&
tracksRes.headers.get("content-type")?.includes("application/json")
) {
const data = await tracksRes.json(); const data = await tracksRes.json();
setResearchTracks(Array.isArray(data) ? data : []); setResearchTracks(Array.isArray(data) ? data : []);
} }
@ -55,7 +75,10 @@ export default function LabsDashboard() {
const bountiesRes = await fetch(`${API_BASE}/api/labs/bounties`, { const bountiesRes = await fetch(`${API_BASE}/api/labs/bounties`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (bountiesRes.ok && bountiesRes.headers.get("content-type")?.includes("application/json")) { if (
bountiesRes.ok &&
bountiesRes.headers.get("content-type")?.includes("application/json")
) {
const data = await bountiesRes.json(); const data = await bountiesRes.json();
setBounties(Array.isArray(data) ? data : []); setBounties(Array.isArray(data) ? data : []);
} }
@ -67,7 +90,10 @@ export default function LabsDashboard() {
const pubRes = await fetch(`${API_BASE}/api/labs/publications`, { const pubRes = await fetch(`${API_BASE}/api/labs/publications`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (pubRes.ok && pubRes.headers.get("content-type")?.includes("application/json")) { if (
pubRes.ok &&
pubRes.headers.get("content-type")?.includes("application/json")
) {
const data = await pubRes.json(); const data = await pubRes.json();
setPublications(Array.isArray(data) ? data : []); setPublications(Array.isArray(data) ? data : []);
} }
@ -79,7 +105,10 @@ export default function LabsDashboard() {
const ipRes = await fetch(`${API_BASE}/api/labs/ip-portfolio`, { const ipRes = await fetch(`${API_BASE}/api/labs/ip-portfolio`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (ipRes.ok && ipRes.headers.get("content-type")?.includes("application/json")) { if (
ipRes.ok &&
ipRes.headers.get("content-type")?.includes("application/json")
) {
const data = await ipRes.json(); const data = await ipRes.json();
setIpPortfolio(data); setIpPortfolio(data);
setIsAdmin(data?.is_admin || false); setIsAdmin(data?.is_admin || false);
@ -121,19 +150,33 @@ export default function LabsDashboard() {
return ( return (
<Layout> <Layout>
<div className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`} style={{ backgroundImage: theme.wallpaperPattern }}> <div
className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`}
style={{ backgroundImage: theme.wallpaperPattern }}
>
<div className="container mx-auto px-4 max-w-7xl space-y-8"> <div className="container mx-auto px-4 max-w-7xl space-y-8">
{/* Header */} {/* Header */}
<div className="space-y-4 animate-slide-down"> <div className="space-y-4 animate-slide-down">
<h1 className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}> <h1
className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}
>
Research LABS Research LABS
</h1> </h1>
<p className="text-gray-400 text-lg">R&D Workshop | Blueprint Technical</p> <p className="text-gray-400 text-lg">
R&D Workshop | Blueprint Technical
</p>
</div> </div>
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs
<TabsList className="grid w-full grid-cols-4 bg-amber-950/30 border border-amber-500/20 p-1" style={{ fontFamily: theme.fontFamily }}> value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList
className="grid w-full grid-cols-4 bg-amber-950/30 border border-amber-500/20 p-1"
style={{ fontFamily: theme.fontFamily }}
>
<TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="tracks">Tracks</TabsTrigger> <TabsTrigger value="tracks">Tracks</TabsTrigger>
<TabsTrigger value="bounties">Bounties</TabsTrigger> <TabsTrigger value="bounties">Bounties</TabsTrigger>
@ -146,19 +189,25 @@ export default function LabsDashboard() {
<Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20"> <Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Active Tracks</p> <p className="text-sm text-gray-400">Active Tracks</p>
<p className="text-3xl font-bold text-white">{researchTracks.length}</p> <p className="text-3xl font-bold text-white">
{researchTracks.length}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-yellow-950/40 to-yellow-900/20 border-yellow-500/20"> <Card className="bg-gradient-to-br from-yellow-950/40 to-yellow-900/20 border-yellow-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Available Bounties</p> <p className="text-sm text-gray-400">Available Bounties</p>
<p className="text-3xl font-bold text-white">{bounties.length}</p> <p className="text-3xl font-bold text-white">
{bounties.length}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-orange-950/40 to-orange-900/20 border-orange-500/20"> <Card className="bg-gradient-to-br from-orange-950/40 to-orange-900/20 border-orange-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Publications</p> <p className="text-sm text-gray-400">Publications</p>
<p className="text-3xl font-bold text-white">{publications.length}</p> <p className="text-3xl font-bold text-white">
{publications.length}
</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -171,12 +220,24 @@ export default function LabsDashboard() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{publications.slice(0, 3).map((pub: any) => ( {publications.slice(0, 3).map((pub: any) => (
<a key={pub.id} href={pub.url} target="_blank" rel="noopener noreferrer" className="p-4 bg-black/30 rounded-lg border border-amber-500/10 hover:border-amber-500/30 transition block"> <a
key={pub.id}
href={pub.url}
target="_blank"
rel="noopener noreferrer"
className="p-4 bg-black/30 rounded-lg border border-amber-500/10 hover:border-amber-500/30 transition block"
>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<FileText className="h-5 w-5 text-amber-500 flex-shrink-0 mt-1" /> <FileText className="h-5 w-5 text-amber-500 flex-shrink-0 mt-1" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-semibold text-white truncate">{pub.title}</p> <p className="font-semibold text-white truncate">
<p className="text-xs text-gray-400 mt-1">{new Date(pub.published_date).toLocaleDateString()}</p> {pub.title}
</p>
<p className="text-xs text-gray-400 mt-1">
{new Date(
pub.published_date,
).toLocaleDateString()}
</p>
</div> </div>
<ExternalLink className="h-4 w-4 text-gray-500 flex-shrink-0" /> <ExternalLink className="h-4 w-4 text-gray-500 flex-shrink-0" />
</div> </div>
@ -189,8 +250,15 @@ export default function LabsDashboard() {
{/* Submit Research Proposal CTA */} {/* Submit Research Proposal CTA */}
<Card className="bg-gradient-to-br from-amber-600/20 to-yellow-600/20 border-amber-500/40"> <Card className="bg-gradient-to-br from-amber-600/20 to-yellow-600/20 border-amber-500/40">
<CardContent className="p-8 text-center space-y-4"> <CardContent className="p-8 text-center space-y-4">
<h3 className="text-2xl font-bold text-white" style={{ fontFamily: theme.fontFamily }}>Have a Research Idea?</h3> <h3
<p className="text-gray-300">Submit your research proposal for the LABS pipeline</p> className="text-2xl font-bold text-white"
style={{ fontFamily: theme.fontFamily }}
>
Have a Research Idea?
</h3>
<p className="text-gray-300">
Submit your research proposal for the LABS pipeline
</p>
<Button <Button
onClick={() => navigate("/labs/submit-proposal")} onClick={() => navigate("/labs/submit-proposal")}
className="bg-gradient-to-r from-amber-600 to-yellow-600 hover:from-amber-700 hover:to-yellow-700" className="bg-gradient-to-r from-amber-600 to-yellow-600 hover:from-amber-700 hover:to-yellow-700"
@ -227,7 +295,9 @@ export default function LabsDashboard() {
<Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20"> <Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20">
<CardHeader> <CardHeader>
<CardTitle>Research Bounties</CardTitle> <CardTitle>Research Bounties</CardTitle>
<CardDescription>High-difficulty opportunities from NEXUS</CardDescription> <CardDescription>
High-difficulty opportunities from NEXUS
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{bounties.length === 0 ? ( {bounties.length === 0 ? (
@ -238,13 +308,26 @@ export default function LabsDashboard() {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{bounties.map((bounty: any) => ( {bounties.map((bounty: any) => (
<div key={bounty.id} className="p-4 bg-black/30 rounded-lg border border-amber-500/10 hover:border-amber-500/30 transition"> <div
key={bounty.id}
className="p-4 bg-black/30 rounded-lg border border-amber-500/10 hover:border-amber-500/30 transition"
>
<div className="flex items-start justify-between gap-4 mb-2"> <div className="flex items-start justify-between gap-4 mb-2">
<h4 className="font-semibold text-white">{bounty.title}</h4> <h4 className="font-semibold text-white">
<p className="text-lg font-bold text-amber-400">${bounty.reward?.toLocaleString()}</p> {bounty.title}
</h4>
<p className="text-lg font-bold text-amber-400">
${bounty.reward?.toLocaleString()}
</p>
</div> </div>
<p className="text-sm text-gray-400">{bounty.description}</p> <p className="text-sm text-gray-400">
<Button size="sm" variant="outline" className="mt-3 border-amber-500/30 text-amber-300 hover:bg-amber-500/10"> {bounty.description}
</p>
<Button
size="sm"
variant="outline"
className="mt-3 border-amber-500/30 text-amber-300 hover:bg-amber-500/10"
>
View Details <ArrowRight className="h-3 w-3 ml-2" /> View Details <ArrowRight className="h-3 w-3 ml-2" />
</Button> </Button>
</div> </div>
@ -260,7 +343,9 @@ export default function LabsDashboard() {
<Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20"> <Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20">
<CardHeader> <CardHeader>
<CardTitle>Publication Pipeline</CardTitle> <CardTitle>Publication Pipeline</CardTitle>
<CardDescription>Upcoming whitepapers and technical blog posts</CardDescription> <CardDescription>
Upcoming whitepapers and technical blog posts
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{publications.length === 0 ? ( {publications.length === 0 ? (
@ -271,15 +356,33 @@ export default function LabsDashboard() {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{publications.map((pub: any) => ( {publications.map((pub: any) => (
<a key={pub.id} href={pub.url} target="_blank" rel="noopener noreferrer" className="p-4 bg-black/30 rounded-lg border border-amber-500/10 hover:border-amber-500/30 transition block"> <a
key={pub.id}
href={pub.url}
target="_blank"
rel="noopener noreferrer"
className="p-4 bg-black/30 rounded-lg border border-amber-500/10 hover:border-amber-500/30 transition block"
>
<div className="flex items-start justify-between gap-4 mb-2"> <div className="flex items-start justify-between gap-4 mb-2">
<h4 className="font-semibold text-white">{pub.title}</h4> <h4 className="font-semibold text-white">
<Badge className={pub.status === "published" ? "bg-green-600/50 text-green-100" : "bg-blue-600/50 text-blue-100"}> {pub.title}
</h4>
<Badge
className={
pub.status === "published"
? "bg-green-600/50 text-green-100"
: "bg-blue-600/50 text-blue-100"
}
>
{pub.status} {pub.status}
</Badge> </Badge>
</div> </div>
<p className="text-sm text-gray-400">{pub.description}</p> <p className="text-sm text-gray-400">
<p className="text-xs text-gray-500 mt-2">{new Date(pub.published_date).toLocaleDateString()}</p> {pub.description}
</p>
<p className="text-xs text-gray-500 mt-2">
{new Date(pub.published_date).toLocaleDateString()}
</p>
</a> </a>
))} ))}
</div> </div>
@ -302,23 +405,33 @@ export default function LabsDashboard() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-black/30 rounded-lg border border-red-500/20"> <div className="p-4 bg-black/30 rounded-lg border border-red-500/20">
<p className="text-sm text-gray-400">Patents Filed</p> <p className="text-sm text-gray-400">Patents Filed</p>
<p className="text-3xl font-bold text-white">{ipPortfolio.patents_count || 0}</p> <p className="text-3xl font-bold text-white">
{ipPortfolio.patents_count || 0}
</p>
</div> </div>
<div className="p-4 bg-black/30 rounded-lg border border-red-500/20"> <div className="p-4 bg-black/30 rounded-lg border border-red-500/20">
<p className="text-sm text-gray-400">Trademarks</p> <p className="text-sm text-gray-400">Trademarks</p>
<p className="text-3xl font-bold text-white">{ipPortfolio.trademarks_count || 0}</p> <p className="text-3xl font-bold text-white">
{ipPortfolio.trademarks_count || 0}
</p>
</div> </div>
<div className="p-4 bg-black/30 rounded-lg border border-red-500/20"> <div className="p-4 bg-black/30 rounded-lg border border-red-500/20">
<p className="text-sm text-gray-400">Trade Secrets</p> <p className="text-sm text-gray-400">Trade Secrets</p>
<p className="text-3xl font-bold text-white">{ipPortfolio.trade_secrets_count || 0}</p> <p className="text-3xl font-bold text-white">
{ipPortfolio.trade_secrets_count || 0}
</p>
</div> </div>
<div className="p-4 bg-black/30 rounded-lg border border-red-500/20"> <div className="p-4 bg-black/30 rounded-lg border border-red-500/20">
<p className="text-sm text-gray-400">Copyrights</p> <p className="text-sm text-gray-400">Copyrights</p>
<p className="text-3xl font-bold text-white">{ipPortfolio.copyrights_count || 0}</p> <p className="text-3xl font-bold text-white">
{ipPortfolio.copyrights_count || 0}
</p>
</div> </div>
</div> </div>
) : ( ) : (
<p className="text-gray-400 text-center py-8">IP portfolio data not available</p> <p className="text-gray-400 text-center py-8">
IP portfolio data not available
</p>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View file

@ -63,7 +63,9 @@ export default function NexusDashboard() {
const loadDashboardData = async () => { const loadDashboardData = async () => {
try { try {
setLoading(true); setLoading(true);
const { data: { session } } = await supabase.auth.getSession(); const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token; const token = session?.access_token;
if (!token) throw new Error("No auth token"); if (!token) throw new Error("No auth token");
@ -76,54 +78,72 @@ export default function NexusDashboard() {
} }
// Load applications // Load applications
const appRes = await fetch(`${API_BASE}/api/nexus/creator/applications?limit=10`, { const appRes = await fetch(
headers: { Authorization: `Bearer ${token}` }, `${API_BASE}/api/nexus/creator/applications?limit=10`,
}); {
headers: { Authorization: `Bearer ${token}` },
},
);
if (appRes.ok) { if (appRes.ok) {
const data = await appRes.json(); const data = await appRes.json();
setApplications(data.applications || []); setApplications(data.applications || []);
} }
// Load contracts // Load contracts
const contractRes = await fetch(`${API_BASE}/api/nexus/creator/contracts?limit=10`, { const contractRes = await fetch(
headers: { Authorization: `Bearer ${token}` }, `${API_BASE}/api/nexus/creator/contracts?limit=10`,
}); {
headers: { Authorization: `Bearer ${token}` },
},
);
if (contractRes.ok) { if (contractRes.ok) {
const data = await contractRes.json(); const data = await contractRes.json();
setContracts(data.contracts || []); setContracts(data.contracts || []);
} }
// Load payout info // Load payout info
const payoutRes = await fetch(`${API_BASE}/api/nexus/creator/payouts?limit=10`, { const payoutRes = await fetch(
headers: { Authorization: `Bearer ${token}` }, `${API_BASE}/api/nexus/creator/payouts?limit=10`,
}); {
headers: { Authorization: `Bearer ${token}` },
},
);
if (payoutRes.ok) { if (payoutRes.ok) {
const data = await payoutRes.json(); const data = await payoutRes.json();
setPayoutInfo(data.summary); setPayoutInfo(data.summary);
} }
// Load client data (posted opportunities) // Load client data (posted opportunities)
const oppRes = await fetch(`${API_BASE}/api/nexus/client/opportunities?limit=10`, { const oppRes = await fetch(
headers: { Authorization: `Bearer ${token}` }, `${API_BASE}/api/nexus/client/opportunities?limit=10`,
}); {
headers: { Authorization: `Bearer ${token}` },
},
);
if (oppRes.ok) { if (oppRes.ok) {
const data = await oppRes.json(); const data = await oppRes.json();
setPostedOpportunities(data.opportunities || []); setPostedOpportunities(data.opportunities || []);
} }
// Load applicants // Load applicants
const appliRes = await fetch(`${API_BASE}/api/nexus/client/applicants?limit=50`, { const appliRes = await fetch(
headers: { Authorization: `Bearer ${token}` }, `${API_BASE}/api/nexus/client/applicants?limit=50`,
}); {
headers: { Authorization: `Bearer ${token}` },
},
);
if (appliRes.ok) { if (appliRes.ok) {
const data = await appliRes.json(); const data = await appliRes.json();
setApplicants(data.applicants || []); setApplicants(data.applicants || []);
} }
// Load payment history // Load payment history
const payHistRes = await fetch(`${API_BASE}/api/nexus/client/payment-history?limit=10`, { const payHistRes = await fetch(
headers: { Authorization: `Bearer ${token}` }, `${API_BASE}/api/nexus/client/payment-history?limit=10`,
}); {
headers: { Authorization: `Bearer ${token}` },
},
);
if (payHistRes.ok) { if (payHistRes.ok) {
const data = await payHistRes.json(); const data = await payHistRes.json();
setPaymentHistory(data.payments || []); setPaymentHistory(data.payments || []);
@ -150,7 +170,9 @@ export default function NexusDashboard() {
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-300 to-blue-300 bg-clip-text text-transparent"> <h1 className="text-4xl font-bold bg-gradient-to-r from-purple-300 to-blue-300 bg-clip-text text-transparent">
Sign In to NEXUS Sign In to NEXUS
</h1> </h1>
<p className="text-gray-400">Access the marketplace and start earning</p> <p className="text-gray-400">
Access the marketplace and start earning
</p>
<Button <Button
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-lg py-6" className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-lg py-6"
@ -163,10 +185,16 @@ export default function NexusDashboard() {
); );
} }
const isProfileComplete = creatorProfile?.verified || (creatorProfile?.headline && creatorProfile?.skills?.length > 0); const isProfileComplete =
const pendingApplications = applications.filter((a) => a.status === "submitted").length; creatorProfile?.verified ||
(creatorProfile?.headline && creatorProfile?.skills?.length > 0);
const pendingApplications = applications.filter(
(a) => a.status === "submitted",
).length;
const activeContracts = contracts.filter((c) => c.status === "active").length; const activeContracts = contracts.filter((c) => c.status === "active").length;
const openOpportunities = postedOpportunities.filter((o) => o.status === "open").length; const openOpportunities = postedOpportunities.filter(
(o) => o.status === "open",
).length;
const applicantStats = { const applicantStats = {
applied: applicants.filter((a) => a.status === "applied").length, applied: applicants.filter((a) => a.status === "applied").length,
interviewing: applicants.filter((a) => a.status === "interviewing").length, interviewing: applicants.filter((a) => a.status === "interviewing").length,
@ -200,9 +228,17 @@ export default function NexusDashboard() {
setViewMode("creator"); setViewMode("creator");
setActiveTab("overview"); setActiveTab("overview");
}} }}
className={viewMode === "creator" ? "bg-purple-600 hover:bg-purple-700" : ""} className={
viewMode === "creator"
? "bg-purple-600 hover:bg-purple-700"
: ""
}
> >
{viewMode === "creator" ? <ToggleRight className="h-4 w-4 mr-1" /> : <ToggleLeft className="h-4 w-4 mr-1" />} {viewMode === "creator" ? (
<ToggleRight className="h-4 w-4 mr-1" />
) : (
<ToggleLeft className="h-4 w-4 mr-1" />
)}
Creator Creator
</Button> </Button>
<Button <Button
@ -212,9 +248,15 @@ export default function NexusDashboard() {
setViewMode("client"); setViewMode("client");
setActiveTab("overview"); setActiveTab("overview");
}} }}
className={viewMode === "client" ? "bg-blue-600 hover:bg-blue-700" : ""} className={
viewMode === "client" ? "bg-blue-600 hover:bg-blue-700" : ""
}
> >
{viewMode === "client" ? <ToggleRight className="h-4 w-4 mr-1" /> : <ToggleLeft className="h-4 w-4 mr-1" />} {viewMode === "client" ? (
<ToggleRight className="h-4 w-4 mr-1" />
) : (
<ToggleLeft className="h-4 w-4 mr-1" />
)}
Client Client
</Button> </Button>
</div> </div>
@ -231,7 +273,8 @@ export default function NexusDashboard() {
Complete Your NEXUS Profile Complete Your NEXUS Profile
</p> </p>
<p className="text-sm text-orange-200"> <p className="text-sm text-orange-200">
Add a headline, skills, and hourly rate to attract clients and start bidding on opportunities Add a headline, skills, and hourly rate to attract
clients and start bidding on opportunities
</p> </p>
</div> </div>
<Button <Button
@ -251,7 +294,11 @@ export default function NexusDashboard() {
{viewMode === "creator" && ( {viewMode === "creator" && (
<> <>
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs
value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList className="grid w-full grid-cols-4 bg-purple-950/30 border border-purple-500/20 p-1"> <TabsList className="grid w-full grid-cols-4 bg-purple-950/30 border border-purple-500/20 p-1">
<TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="applications">Applications</TabsTrigger> <TabsTrigger value="applications">Applications</TabsTrigger>
@ -260,17 +307,26 @@ export default function NexusDashboard() {
</TabsList> </TabsList>
{/* Overview Tab */} {/* Overview Tab */}
<TabsContent value="overview" className="space-y-6 animate-fade-in"> <TabsContent
value="overview"
className="space-y-6 animate-fade-in"
>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Stat: Total Earnings */} {/* Stat: Total Earnings */}
<Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20"> <Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-gray-400">Total Earnings</p> <p className="text-sm text-gray-400">
Total Earnings
</p>
<DollarSign className="h-5 w-5 text-green-500" /> <DollarSign className="h-5 w-5 text-green-500" />
</div> </div>
<p className="text-3xl font-bold text-white"> <p className="text-3xl font-bold text-white">
${(payoutInfo?.total_earnings || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })} $
{(payoutInfo?.total_earnings || 0).toLocaleString(
"en-US",
{ minimumFractionDigits: 2 },
)}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -279,11 +335,17 @@ export default function NexusDashboard() {
<Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20"> <Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-gray-400">Pending Payouts</p> <p className="text-sm text-gray-400">
Pending Payouts
</p>
<Clock className="h-5 w-5 text-blue-500" /> <Clock className="h-5 w-5 text-blue-500" />
</div> </div>
<p className="text-3xl font-bold text-white"> <p className="text-3xl font-bold text-white">
${(payoutInfo?.pending_payouts || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })} $
{(payoutInfo?.pending_payouts || 0).toLocaleString(
"en-US",
{ minimumFractionDigits: 2 },
)}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -292,10 +354,14 @@ export default function NexusDashboard() {
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-gray-400">Pending Applications</p> <p className="text-sm text-gray-400">
Pending Applications
</p>
<Briefcase className="h-5 w-5 text-purple-500" /> <Briefcase className="h-5 w-5 text-purple-500" />
</div> </div>
<p className="text-3xl font-bold text-white">{pendingApplications}</p> <p className="text-3xl font-bold text-white">
{pendingApplications}
</p>
</CardContent> </CardContent>
</Card> </Card>
@ -303,10 +369,14 @@ export default function NexusDashboard() {
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20"> <Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-gray-400">Active Contracts</p> <p className="text-sm text-gray-400">
Active Contracts
</p>
<CheckCircle className="h-5 w-5 text-red-500" /> <CheckCircle className="h-5 w-5 text-red-500" />
</div> </div>
<p className="text-3xl font-bold text-white">{activeContracts}</p> <p className="text-3xl font-bold text-white">
{activeContracts}
</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -321,7 +391,10 @@ export default function NexusDashboard() {
{applications.length === 0 ? ( {applications.length === 0 ? (
<div className="text-center py-8 space-y-4"> <div className="text-center py-8 space-y-4">
<AlertCircle className="h-12 w-12 mx-auto text-gray-500 opacity-50" /> <AlertCircle className="h-12 w-12 mx-auto text-gray-500 opacity-50" />
<p className="text-gray-400">No applications yet. Browse opportunities to get started!</p> <p className="text-gray-400">
No applications yet. Browse opportunities to get
started!
</p>
<Button <Button
onClick={() => navigate("/nexus")} onClick={() => navigate("/nexus")}
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700" className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
@ -333,16 +406,27 @@ export default function NexusDashboard() {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{applications.slice(0, 5).map((app: any) => ( {applications.slice(0, 5).map((app: any) => (
<div key={app.id} className="flex items-center justify-between p-3 bg-black/30 rounded-lg border border-purple-500/10 hover:border-purple-500/30 transition"> <div
key={app.id}
className="flex items-center justify-between p-3 bg-black/30 rounded-lg border border-purple-500/10 hover:border-purple-500/30 transition"
>
<div className="space-y-1 flex-1"> <div className="space-y-1 flex-1">
<p className="font-semibold text-white">{app.opportunity?.title}</p> <p className="font-semibold text-white">
<p className="text-sm text-gray-400">{app.opportunity?.category}</p> {app.opportunity?.title}
</p>
<p className="text-sm text-gray-400">
{app.opportunity?.category}
</p>
</div> </div>
<Badge variant={ <Badge
app.status === "accepted" ? "default" : variant={
app.status === "rejected" ? "destructive" : app.status === "accepted"
"secondary" ? "default"
}> : app.status === "rejected"
? "destructive"
: "secondary"
}
>
{app.status} {app.status}
</Badge> </Badge>
</div> </div>
@ -357,8 +441,13 @@ export default function NexusDashboard() {
<CardContent className="p-8"> <CardContent className="p-8">
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<h3 className="text-2xl font-bold text-white">Maximize Your Earnings</h3> <h3 className="text-2xl font-bold text-white">
<p className="text-gray-300">Complete your profile and start bidding on opportunities</p> Maximize Your Earnings
</h3>
<p className="text-gray-300">
Complete your profile and start bidding on
opportunities
</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Button <Button
@ -382,7 +471,10 @@ export default function NexusDashboard() {
</TabsContent> </TabsContent>
{/* Applications Tab */} {/* Applications Tab */}
<TabsContent value="applications" className="space-y-4 animate-fade-in"> <TabsContent
value="applications"
className="space-y-4 animate-fade-in"
>
<ApplicationsWidget <ApplicationsWidget
applications={applications.map((a: any) => ({ applications={applications.map((a: any) => ({
id: a.id, id: a.id,
@ -408,7 +500,10 @@ export default function NexusDashboard() {
</TabsContent> </TabsContent>
{/* Contracts Tab */} {/* Contracts Tab */}
<TabsContent value="contracts" className="space-y-4 animate-fade-in"> <TabsContent
value="contracts"
className="space-y-4 animate-fade-in"
>
<ContractsWidget <ContractsWidget
contracts={contracts.map((c: any) => ({ contracts={contracts.map((c: any) => ({
id: c.id, id: c.id,
@ -416,7 +511,11 @@ export default function NexusDashboard() {
client_name: c.client?.full_name || "Client", client_name: c.client?.full_name || "Client",
status: c.status || "active", status: c.status || "active",
total_amount: c.total_amount || 0, total_amount: c.total_amount || 0,
paid_amount: c.payments?.reduce((sum: number, p: any) => sum + (p.amount || 0), 0) || 0, paid_amount:
c.payments?.reduce(
(sum: number, p: any) => sum + (p.amount || 0),
0,
) || 0,
start_date: c.start_date, start_date: c.start_date,
end_date: c.end_date, end_date: c.end_date,
description: c.description, description: c.description,
@ -430,16 +529,23 @@ export default function NexusDashboard() {
</TabsContent> </TabsContent>
{/* Profile Tab */} {/* Profile Tab */}
<TabsContent value="profile" className="space-y-6 animate-fade-in"> <TabsContent
value="profile"
className="space-y-6 animate-fade-in"
>
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader> <CardHeader>
<CardTitle>Your NEXUS Profile</CardTitle> <CardTitle>Your NEXUS Profile</CardTitle>
<CardDescription>Your marketplace identity</CardDescription> <CardDescription>
Your marketplace identity
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold text-gray-300">Headline</label> <label className="text-sm font-semibold text-gray-300">
Headline
</label>
<input <input
type="text" type="text"
value={creatorProfile?.headline || ""} value={creatorProfile?.headline || ""}
@ -450,9 +556,13 @@ export default function NexusDashboard() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold text-gray-300">Experience Level</label> <label className="text-sm font-semibold text-gray-300">
Experience Level
</label>
<select <select
value={creatorProfile?.experience_level || "intermediate"} value={
creatorProfile?.experience_level || "intermediate"
}
className="w-full px-4 py-2 bg-black/30 border border-purple-500/20 rounded-lg text-white disabled:opacity-50" className="w-full px-4 py-2 bg-black/30 border border-purple-500/20 rounded-lg text-white disabled:opacity-50"
disabled disabled
> >
@ -464,7 +574,9 @@ export default function NexusDashboard() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold text-gray-300">Hourly Rate</label> <label className="text-sm font-semibold text-gray-300">
Hourly Rate
</label>
<input <input
type="number" type="number"
value={creatorProfile?.hourly_rate || ""} value={creatorProfile?.hourly_rate || ""}
@ -475,9 +587,13 @@ export default function NexusDashboard() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold text-gray-300">Availability</label> <label className="text-sm font-semibold text-gray-300">
Availability
</label>
<select <select
value={creatorProfile?.availability_status || "available"} value={
creatorProfile?.availability_status || "available"
}
className="w-full px-4 py-2 bg-black/30 border border-purple-500/20 rounded-lg text-white disabled:opacity-50" className="w-full px-4 py-2 bg-black/30 border border-purple-500/20 rounded-lg text-white disabled:opacity-50"
disabled disabled
> >
@ -492,7 +608,9 @@ export default function NexusDashboard() {
{creatorProfile?.verified && ( {creatorProfile?.verified && (
<div className="p-3 bg-green-600/20 border border-green-500/30 rounded-lg flex items-center gap-2"> <div className="p-3 bg-green-600/20 border border-green-500/30 rounded-lg flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-500" /> <CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-sm text-green-200">Profile Verified </span> <span className="text-sm text-green-200">
Profile Verified
</span>
</div> </div>
)} )}
@ -506,17 +624,18 @@ export default function NexusDashboard() {
<Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20"> <Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20">
<CardHeader> <CardHeader>
<CardTitle>Payout Information</CardTitle> <CardTitle>Payout Information</CardTitle>
<CardDescription>Manage how you receive payments</CardDescription> <CardDescription>
Manage how you receive payments
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<Button <Button className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
>
Connect Stripe Account Connect Stripe Account
<ExternalLink className="h-4 w-4 ml-2" /> <ExternalLink className="h-4 w-4 ml-2" />
</Button> </Button>
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">
Connect your Stripe account to receive payouts for completed contracts Connect your Stripe account to receive payouts for
completed contracts
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -528,7 +647,11 @@ export default function NexusDashboard() {
{/* Client View */} {/* Client View */}
{viewMode === "client" && ( {viewMode === "client" && (
<> <>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs
value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList className="grid w-full grid-cols-4 bg-blue-950/30 border border-blue-500/20 p-1"> <TabsList className="grid w-full grid-cols-4 bg-blue-950/30 border border-blue-500/20 p-1">
<TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="opportunities">Opportunities</TabsTrigger> <TabsTrigger value="opportunities">Opportunities</TabsTrigger>
@ -537,16 +660,23 @@ export default function NexusDashboard() {
</TabsList> </TabsList>
{/* Overview Tab */} {/* Overview Tab */}
<TabsContent value="overview" className="space-y-6 animate-fade-in"> <TabsContent
value="overview"
className="space-y-6 animate-fade-in"
>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Stat: Open Opportunities */} {/* Stat: Open Opportunities */}
<Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20"> <Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-gray-400">Open Opportunities</p> <p className="text-sm text-gray-400">
Open Opportunities
</p>
<Briefcase className="h-5 w-5 text-blue-500" /> <Briefcase className="h-5 w-5 text-blue-500" />
</div> </div>
<p className="text-3xl font-bold text-white">{openOpportunities}</p> <p className="text-3xl font-bold text-white">
{openOpportunities}
</p>
</CardContent> </CardContent>
</Card> </Card>
@ -554,10 +684,14 @@ export default function NexusDashboard() {
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-gray-400">Total Applicants</p> <p className="text-sm text-gray-400">
Total Applicants
</p>
<Users className="h-5 w-5 text-purple-500" /> <Users className="h-5 w-5 text-purple-500" />
</div> </div>
<p className="text-3xl font-bold text-white">{applicants.length}</p> <p className="text-3xl font-bold text-white">
{applicants.length}
</p>
</CardContent> </CardContent>
</Card> </Card>
@ -565,10 +699,17 @@ export default function NexusDashboard() {
<Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20"> <Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-gray-400">Active Contracts</p> <p className="text-sm text-gray-400">
Active Contracts
</p>
<FileText className="h-5 w-5 text-green-500" /> <FileText className="h-5 w-5 text-green-500" />
</div> </div>
<p className="text-3xl font-bold text-white">{contracts.filter(c => c.status === "active").length}</p> <p className="text-3xl font-bold text-white">
{
contracts.filter((c) => c.status === "active")
.length
}
</p>
</CardContent> </CardContent>
</Card> </Card>
@ -580,7 +721,12 @@ export default function NexusDashboard() {
<DollarSign className="h-5 w-5 text-orange-500" /> <DollarSign className="h-5 w-5 text-orange-500" />
</div> </div>
<p className="text-3xl font-bold text-white"> <p className="text-3xl font-bold text-white">
${(paymentHistory.reduce((acc, p) => acc + (p.amount || 0), 0)).toLocaleString('en-US', { minimumFractionDigits: 2 })} $
{paymentHistory
.reduce((acc, p) => acc + (p.amount || 0), 0)
.toLocaleString("en-US", {
minimumFractionDigits: 2,
})}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -591,19 +737,25 @@ export default function NexusDashboard() {
<Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20"> <Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20">
<CardContent className="p-4 text-center space-y-2"> <CardContent className="p-4 text-center space-y-2">
<p className="text-sm text-gray-400">Reviewing</p> <p className="text-sm text-gray-400">Reviewing</p>
<p className="text-2xl font-bold text-blue-400">{applicantStats.applied}</p> <p className="text-2xl font-bold text-blue-400">
{applicantStats.applied}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardContent className="p-4 text-center space-y-2"> <CardContent className="p-4 text-center space-y-2">
<p className="text-sm text-gray-400">Interviewing</p> <p className="text-sm text-gray-400">Interviewing</p>
<p className="text-2xl font-bold text-purple-400">{applicantStats.interviewing}</p> <p className="text-2xl font-bold text-purple-400">
{applicantStats.interviewing}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20"> <Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20">
<CardContent className="p-4 text-center space-y-2"> <CardContent className="p-4 text-center space-y-2">
<p className="text-sm text-gray-400">Hired</p> <p className="text-sm text-gray-400">Hired</p>
<p className="text-2xl font-bold text-green-400">{applicantStats.hired}</p> <p className="text-2xl font-bold text-green-400">
{applicantStats.hired}
</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -611,9 +763,12 @@ export default function NexusDashboard() {
{/* CTA Section */} {/* CTA Section */}
<Card className="bg-gradient-to-br from-blue-600/20 to-purple-600/20 border-blue-500/40"> <Card className="bg-gradient-to-br from-blue-600/20 to-purple-600/20 border-blue-500/40">
<CardContent className="p-8 text-center space-y-4"> <CardContent className="p-8 text-center space-y-4">
<h3 className="text-2xl font-bold text-white">Hire Top Talent</h3> <h3 className="text-2xl font-bold text-white">
Hire Top Talent
</h3>
<p className="text-gray-300 max-w-md mx-auto"> <p className="text-gray-300 max-w-md mx-auto">
Post opportunities and find the perfect creators for your projects Post opportunities and find the perfect creators for
your projects
</p> </p>
<Button <Button
onClick={() => navigate("/opportunities/post")} onClick={() => navigate("/opportunities/post")}
@ -627,7 +782,10 @@ export default function NexusDashboard() {
</TabsContent> </TabsContent>
{/* Opportunities Tab */} {/* Opportunities Tab */}
<TabsContent value="opportunities" className="space-y-4 animate-fade-in"> <TabsContent
value="opportunities"
className="space-y-4 animate-fade-in"
>
<PostedOpportunitiesWidget <PostedOpportunitiesWidget
opportunities={postedOpportunities.map((o: any) => ({ opportunities={postedOpportunities.map((o: any) => ({
id: o.id, id: o.id,
@ -659,8 +817,13 @@ export default function NexusDashboard() {
{postedOpportunities.length === 0 && ( {postedOpportunities.length === 0 && (
<Card className="bg-gradient-to-br from-blue-600/20 to-cyan-600/20 border-blue-500/40"> <Card className="bg-gradient-to-br from-blue-600/20 to-cyan-600/20 border-blue-500/40">
<CardContent className="p-8 text-center space-y-4"> <CardContent className="p-8 text-center space-y-4">
<h3 className="text-2xl font-bold text-white">Start Hiring Talent</h3> <h3 className="text-2xl font-bold text-white">
<p className="text-gray-300">Post opportunities and find the perfect creators for your projects</p> Start Hiring Talent
</h3>
<p className="text-gray-300">
Post opportunities and find the perfect creators for
your projects
</p>
<Button <Button
onClick={() => navigate("/opportunities/post")} onClick={() => navigate("/opportunities/post")}
className="bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700" className="bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700"
@ -674,7 +837,10 @@ export default function NexusDashboard() {
</TabsContent> </TabsContent>
{/* Applicants Tab - Kanban Style */} {/* Applicants Tab - Kanban Style */}
<TabsContent value="applicants" className="space-y-4 animate-fade-in"> <TabsContent
value="applicants"
className="space-y-4 animate-fade-in"
>
<ApplicantTrackerWidget <ApplicantTrackerWidget
applicants={applicants.map((a: any) => ({ applicants={applicants.map((a: any) => ({
id: a.id, id: a.id,
@ -704,7 +870,10 @@ export default function NexusDashboard() {
</TabsContent> </TabsContent>
{/* Contracts Tab */} {/* Contracts Tab */}
<TabsContent value="contracts" className="space-y-4 animate-fade-in"> <TabsContent
value="contracts"
className="space-y-4 animate-fade-in"
>
<ContractsWidget <ContractsWidget
contracts={contracts.map((c: any) => ({ contracts={contracts.map((c: any) => ({
id: c.id, id: c.id,
@ -712,7 +881,11 @@ export default function NexusDashboard() {
creator_name: c.creator?.full_name || "Creator", creator_name: c.creator?.full_name || "Creator",
status: c.status || "active", status: c.status || "active",
total_amount: c.total_amount || 0, total_amount: c.total_amount || 0,
paid_amount: c.payments?.reduce((sum: number, p: any) => sum + (p.amount || 0), 0) || 0, paid_amount:
c.payments?.reduce(
(sum: number, p: any) => sum + (p.amount || 0),
0,
) || 0,
start_date: c.start_date, start_date: c.start_date,
end_date: c.end_date, end_date: c.end_date,
description: c.description, description: c.description,
@ -729,17 +902,30 @@ export default function NexusDashboard() {
<Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20"> <Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20">
<CardHeader> <CardHeader>
<CardTitle>Payment History</CardTitle> <CardTitle>Payment History</CardTitle>
<CardDescription>Recent payments made to creators</CardDescription> <CardDescription>
Recent payments made to creators
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-3"> <div className="space-y-3">
{paymentHistory.map((payment: any) => ( {paymentHistory.map((payment: any) => (
<div key={payment.id} className="flex items-center justify-between p-3 bg-black/30 rounded-lg border border-green-500/10"> <div
key={payment.id}
className="flex items-center justify-between p-3 bg-black/30 rounded-lg border border-green-500/10"
>
<div className="space-y-1"> <div className="space-y-1">
<p className="font-semibold text-white text-sm">{payment.description}</p> <p className="font-semibold text-white text-sm">
<p className="text-xs text-gray-400">{new Date(payment.created_at).toLocaleDateString()}</p> {payment.description}
</p>
<p className="text-xs text-gray-400">
{new Date(
payment.created_at,
).toLocaleDateString()}
</p>
</div> </div>
<p className="font-semibold text-green-400">${payment.amount?.toLocaleString()}</p> <p className="font-semibold text-green-400">
${payment.amount?.toLocaleString()}
</p>
</div> </div>
))} ))}
</div> </div>

View file

@ -5,11 +5,29 @@ import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext"; import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext"; import { useArmTheme } from "@/contexts/ArmThemeContext";
import { supabase } from "@/lib/supabase"; import { supabase } from "@/lib/supabase";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import LoadingScreen from "@/components/LoadingScreen"; import LoadingScreen from "@/components/LoadingScreen";
import { Shield, Target, DollarSign, FileText, Users, Link as LinkIcon, Calendar, Book, AlertCircle, Search, ExternalLink } from "lucide-react"; import {
Shield,
Target,
DollarSign,
FileText,
Users,
Link as LinkIcon,
Calendar,
Book,
AlertCircle,
Search,
ExternalLink,
} from "lucide-react";
import { DirectoryWidget } from "@/components/DirectoryWidget"; import { DirectoryWidget } from "@/components/DirectoryWidget";
const API_BASE = import.meta.env.VITE_API_BASE || ""; const API_BASE = import.meta.env.VITE_API_BASE || "";
@ -35,7 +53,9 @@ export default function StaffDashboard() {
const loadDashboardData = async () => { const loadDashboardData = async () => {
try { try {
setLoading(true); setLoading(true);
const { data: { session } } = await supabase.auth.getSession(); const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token; const token = session?.access_token;
if (!token) throw new Error("No auth token"); if (!token) throw new Error("No auth token");
@ -43,7 +63,10 @@ export default function StaffDashboard() {
const memberRes = await fetch(`${API_BASE}/api/staff/me`, { const memberRes = await fetch(`${API_BASE}/api/staff/me`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (memberRes.ok && memberRes.headers.get("content-type")?.includes("application/json")) { if (
memberRes.ok &&
memberRes.headers.get("content-type")?.includes("application/json")
) {
const data = await memberRes.json(); const data = await memberRes.json();
setStaffMember(data); setStaffMember(data);
} }
@ -55,7 +78,10 @@ export default function StaffDashboard() {
const okrRes = await fetch(`${API_BASE}/api/staff/okrs`, { const okrRes = await fetch(`${API_BASE}/api/staff/okrs`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (okrRes.ok && okrRes.headers.get("content-type")?.includes("application/json")) { if (
okrRes.ok &&
okrRes.headers.get("content-type")?.includes("application/json")
) {
const data = await okrRes.json(); const data = await okrRes.json();
setOkrs(Array.isArray(data) ? data : []); setOkrs(Array.isArray(data) ? data : []);
} }
@ -67,7 +93,10 @@ export default function StaffDashboard() {
const invRes = await fetch(`${API_BASE}/api/staff/invoices`, { const invRes = await fetch(`${API_BASE}/api/staff/invoices`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (invRes.ok && invRes.headers.get("content-type")?.includes("application/json")) { if (
invRes.ok &&
invRes.headers.get("content-type")?.includes("application/json")
) {
const data = await invRes.json(); const data = await invRes.json();
setInvoices(Array.isArray(data) ? data : []); setInvoices(Array.isArray(data) ? data : []);
} }
@ -79,7 +108,10 @@ export default function StaffDashboard() {
const dirRes = await fetch(`${API_BASE}/api/staff/directory`, { const dirRes = await fetch(`${API_BASE}/api/staff/directory`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (dirRes.ok && dirRes.headers.get("content-type")?.includes("application/json")) { if (
dirRes.ok &&
dirRes.headers.get("content-type")?.includes("application/json")
) {
const data = await dirRes.json(); const data = await dirRes.json();
setDirectory(Array.isArray(data) ? data : []); setDirectory(Array.isArray(data) ? data : []);
} }
@ -95,9 +127,10 @@ export default function StaffDashboard() {
const isEmployee = staffMember?.employment_type === "employee"; const isEmployee = staffMember?.employment_type === "employee";
const isContractor = staffMember?.employment_type === "contractor"; const isContractor = staffMember?.employment_type === "contractor";
const filteredDirectory = directory.filter(member => const filteredDirectory = directory.filter(
member.full_name?.toLowerCase().includes(searchQuery.toLowerCase()) || (member) =>
member.role?.toLowerCase().includes(searchQuery.toLowerCase()) member.full_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
member.role?.toLowerCase().includes(searchQuery.toLowerCase()),
); );
if (authLoading || loading) { if (authLoading || loading) {
@ -127,23 +160,41 @@ export default function StaffDashboard() {
return ( return (
<Layout> <Layout>
<div className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`} style={{ backgroundImage: theme.wallpaperPattern }}> <div
className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`}
style={{ backgroundImage: theme.wallpaperPattern }}
>
<div className="container mx-auto px-4 max-w-7xl space-y-8"> <div className="container mx-auto px-4 max-w-7xl space-y-8">
{/* Header */} {/* Header */}
<div className="space-y-4 animate-slide-down"> <div className="space-y-4 animate-slide-down">
<h1 className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}> <h1
className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent`}
>
STAFF Portal STAFF Portal
</h1> </h1>
<p className="text-gray-400 text-lg">Employee & Contractor Management | Professional Utility Purple</p> <p className="text-gray-400 text-lg">
Employee & Contractor Management | Professional Utility Purple
</p>
</div> </div>
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs
<TabsList className="grid w-full grid-cols-5 bg-purple-950/30 border border-purple-500/20 p-1" style={{ fontFamily: theme.fontFamily }}> value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList
className="grid w-full grid-cols-5 bg-purple-950/30 border border-purple-500/20 p-1"
style={{ fontFamily: theme.fontFamily }}
>
<TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="overview">Overview</TabsTrigger>
{isEmployee && <TabsTrigger value="okrs">OKRs</TabsTrigger>} {isEmployee && <TabsTrigger value="okrs">OKRs</TabsTrigger>}
{isEmployee && <TabsTrigger value="benefits">Pay & Benefits</TabsTrigger>} {isEmployee && (
{isContractor && <TabsTrigger value="invoices">Invoices</TabsTrigger>} <TabsTrigger value="benefits">Pay & Benefits</TabsTrigger>
)}
{isContractor && (
<TabsTrigger value="invoices">Invoices</TabsTrigger>
)}
<TabsTrigger value="directory">Directory</TabsTrigger> <TabsTrigger value="directory">Directory</TabsTrigger>
</TabsList> </TabsList>
@ -153,19 +204,27 @@ export default function StaffDashboard() {
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Employment Type</p> <p className="text-sm text-gray-400">Employment Type</p>
<p className="text-2xl font-bold text-white capitalize">{staffMember?.employment_type || "—"}</p> <p className="text-2xl font-bold text-white capitalize">
{staffMember?.employment_type || "—"}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-pink-950/40 to-pink-900/20 border-pink-500/20"> <Card className="bg-gradient-to-br from-pink-950/40 to-pink-900/20 border-pink-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Department</p> <p className="text-sm text-gray-400">Department</p>
<p className="text-2xl font-bold text-white">{staffMember?.department || "—"}</p> <p className="text-2xl font-bold text-white">
{staffMember?.department || "—"}
</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20"> <Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20">
<CardContent className="p-6 space-y-2"> <CardContent className="p-6 space-y-2">
<p className="text-sm text-gray-400">Start Date</p> <p className="text-sm text-gray-400">Start Date</p>
<p className="text-2xl font-bold text-white">{staffMember?.start_date ? new Date(staffMember.start_date).toLocaleDateString() : "—"}</p> <p className="text-2xl font-bold text-white">
{staffMember?.start_date
? new Date(staffMember.start_date).toLocaleDateString()
: "—"}
</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -173,7 +232,9 @@ export default function StaffDashboard() {
{/* Quick Links */} {/* Quick Links */}
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader> <CardHeader>
<CardTitle style={{ fontFamily: theme.fontFamily }}>Quick Actions</CardTitle> <CardTitle style={{ fontFamily: theme.fontFamily }}>
Quick Actions
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-3"> <CardContent className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Button <Button
@ -213,31 +274,53 @@ export default function StaffDashboard() {
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader> <CardHeader>
<CardTitle>My OKRs</CardTitle> <CardTitle>My OKRs</CardTitle>
<CardDescription>Quarterly Objectives & Key Results</CardDescription> <CardDescription>
Quarterly Objectives & Key Results
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{okrs.length === 0 ? ( {okrs.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<Target className="h-12 w-12 mx-auto text-gray-500 opacity-50 mb-4" /> <Target className="h-12 w-12 mx-auto text-gray-500 opacity-50 mb-4" />
<p className="text-gray-400">No OKRs set for this quarter</p> <p className="text-gray-400">
No OKRs set for this quarter
</p>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{okrs.map((okr: any) => ( {okrs.map((okr: any) => (
<div key={okr.id} className="p-4 bg-black/30 rounded-lg border border-purple-500/10 space-y-3"> <div
key={okr.id}
className="p-4 bg-black/30 rounded-lg border border-purple-500/10 space-y-3"
>
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1"> <div className="flex-1">
<h4 className="font-semibold text-white">{okr.objective}</h4> <h4 className="font-semibold text-white">
<p className="text-sm text-gray-400 mt-1">{okr.description}</p> {okr.objective}
</h4>
<p className="text-sm text-gray-400 mt-1">
{okr.description}
</p>
</div> </div>
<Badge className={okr.status === "achieved" ? "bg-green-600/50 text-green-100" : "bg-blue-600/50 text-blue-100"}> <Badge
className={
okr.status === "achieved"
? "bg-green-600/50 text-green-100"
: "bg-blue-600/50 text-blue-100"
}
>
{okr.status} {okr.status}
</Badge> </Badge>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{okr.key_results?.map((kr: any) => ( {okr.key_results?.map((kr: any) => (
<div key={kr.id} className="flex items-start gap-3 text-sm"> <div
<span className="text-purple-400 mt-1"></span> key={kr.id}
className="flex items-start gap-3 text-sm"
>
<span className="text-purple-400 mt-1">
</span>
<div className="flex-1"> <div className="flex-1">
<p className="text-white">{kr.title}</p> <p className="text-white">{kr.title}</p>
<div className="flex justify-between text-xs text-gray-400 mt-1"> <div className="flex justify-between text-xs text-gray-400 mt-1">
@ -265,23 +348,31 @@ export default function StaffDashboard() {
{/* Pay & Benefits Tab - Employee Only */} {/* Pay & Benefits Tab - Employee Only */}
{isEmployee && ( {isEmployee && (
<TabsContent value="benefits" className="space-y-4 animate-fade-in"> <TabsContent
value="benefits"
className="space-y-4 animate-fade-in"
>
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader> <CardHeader>
<CardTitle>Pay & Benefits</CardTitle> <CardTitle>Pay & Benefits</CardTitle>
<CardDescription>Payroll and compensation information</CardDescription> <CardDescription>
Payroll and compensation information
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="p-4 bg-black/30 rounded-lg border border-purple-500/20 space-y-2"> <div className="p-4 bg-black/30 rounded-lg border border-purple-500/20 space-y-2">
<p className="text-sm text-gray-400">Base Salary</p> <p className="text-sm text-gray-400">Base Salary</p>
<p className="text-3xl font-bold text-white">${staffMember?.salary?.toLocaleString() || "—"}</p> <p className="text-3xl font-bold text-white">
${staffMember?.salary?.toLocaleString() || "—"}
</p>
</div> </div>
<Button className="w-full bg-purple-600 hover:bg-purple-700"> <Button className="w-full bg-purple-600 hover:bg-purple-700">
<ExternalLink className="h-4 w-4 mr-2" /> <ExternalLink className="h-4 w-4 mr-2" />
Open Rippling (Payroll System) Open Rippling (Payroll System)
</Button> </Button>
<p className="text-xs text-gray-400"> <p className="text-xs text-gray-400">
View your paystubs, tax documents, and benefits in the Rippling employee portal. View your paystubs, tax documents, and benefits in the
Rippling employee portal.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -290,11 +381,16 @@ export default function StaffDashboard() {
{/* Invoices Tab - Contractor Only */} {/* Invoices Tab - Contractor Only */}
{isContractor && ( {isContractor && (
<TabsContent value="invoices" className="space-y-4 animate-fade-in"> <TabsContent
value="invoices"
className="space-y-4 animate-fade-in"
>
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader> <CardHeader>
<CardTitle>My Invoices</CardTitle> <CardTitle>My Invoices</CardTitle>
<CardDescription>SOP-301: Contractor Invoice Portal</CardDescription> <CardDescription>
SOP-301: Contractor Invoice Portal
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{invoices.length === 0 ? ( {invoices.length === 0 ? (
@ -305,21 +401,32 @@ export default function StaffDashboard() {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{invoices.map((invoice: any) => ( {invoices.map((invoice: any) => (
<div key={invoice.id} className="p-4 bg-black/30 rounded-lg border border-purple-500/10 space-y-3"> <div
key={invoice.id}
className="p-4 bg-black/30 rounded-lg border border-purple-500/10 space-y-3"
>
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1"> <div className="flex-1">
<p className="font-semibold text-white">{invoice.invoice_number}</p> <p className="font-semibold text-white">
<p className="text-xs text-gray-400 mt-1">{new Date(invoice.date).toLocaleDateString()}</p> {invoice.invoice_number}
</p>
<p className="text-xs text-gray-400 mt-1">
{new Date(invoice.date).toLocaleDateString()}
</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="font-semibold text-white">${invoice.amount?.toLocaleString()}</p> <p className="font-semibold text-white">
<Badge className={ ${invoice.amount?.toLocaleString()}
invoice.status === "paid" </p>
? "bg-green-600/50 text-green-100" <Badge
: invoice.status === "pending" className={
? "bg-yellow-600/50 text-yellow-100" invoice.status === "paid"
: "bg-blue-600/50 text-blue-100" ? "bg-green-600/50 text-green-100"
}> : invoice.status === "pending"
? "bg-yellow-600/50 text-yellow-100"
: "bg-blue-600/50 text-blue-100"
}
>
{invoice.status} {invoice.status}
</Badge> </Badge>
</div> </div>
@ -334,7 +441,10 @@ export default function StaffDashboard() {
)} )}
{/* Directory Tab */} {/* Directory Tab */}
<TabsContent value="directory" className="space-y-4 animate-fade-in"> <TabsContent
value="directory"
className="space-y-4 animate-fade-in"
>
<DirectoryWidget <DirectoryWidget
members={directory.map((m: any) => ({ members={directory.map((m: any) => ({
id: m.id, id: m.id,
@ -345,7 +455,10 @@ export default function StaffDashboard() {
phone: m.phone, phone: m.phone,
location: m.location, location: m.location,
avatar_url: m.avatar_url, avatar_url: m.avatar_url,
employment_type: m.employment_type === "employee" ? "employee" : "contractor", employment_type:
m.employment_type === "employee"
? "employee"
: "contractor",
}))} }))}
title="Internal Directory" title="Internal Directory"
description="Find employees and contractors" description="Find employees and contractors"

View file

@ -54,14 +54,19 @@ export default function ClientHub() {
const loadDashboardData = async () => { const loadDashboardData = async () => {
try { try {
setLoading(true); setLoading(true);
const { data: { session } } = await supabase.auth.getSession(); const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token; const token = session?.access_token;
if (!token) throw new Error("No auth token"); if (!token) throw new Error("No auth token");
// Load contracts for milestone tracking // Load contracts for milestone tracking
const contractRes = await fetch(`${API_BASE}/api/corp/contracts?limit=10`, { const contractRes = await fetch(
headers: { Authorization: `Bearer ${token}` }, `${API_BASE}/api/corp/contracts?limit=10`,
}); {
headers: { Authorization: `Bearer ${token}` },
},
);
if (contractRes.ok) { if (contractRes.ok) {
const data = await contractRes.json(); const data = await contractRes.json();
setContracts(Array.isArray(data) ? data : data.contracts || []); setContracts(Array.isArray(data) ? data : data.contracts || []);
@ -107,7 +112,9 @@ export default function ClientHub() {
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-300 to-cyan-300 bg-clip-text text-transparent"> <h1 className="text-4xl font-bold bg-gradient-to-r from-blue-300 to-cyan-300 bg-clip-text text-transparent">
CORP Client Portal CORP Client Portal
</h1> </h1>
<p className="text-gray-400">Enterprise solutions for your business</p> <p className="text-gray-400">
Enterprise solutions for your business
</p>
<Button <Button
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
className="w-full bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-lg py-6" className="w-full bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-lg py-6"
@ -120,13 +127,22 @@ export default function ClientHub() {
); );
} }
const activeContract = contracts.find(c => c.status === "active"); const activeContract = contracts.find((c) => c.status === "active");
const completedMilestones = activeContract?.milestones?.filter((m: any) => m.status === "completed").length || 0; const completedMilestones =
activeContract?.milestones?.filter((m: any) => m.status === "completed")
.length || 0;
const totalMilestones = activeContract?.milestones?.length || 0; const totalMilestones = activeContract?.milestones?.length || 0;
const outstandingInvoices = invoices.filter((i: any) => i.status === "pending" || i.status === "overdue").length; const outstandingInvoices = invoices.filter(
const totalInvoiceValue = invoices.reduce((acc, inv) => acc + (inv.amount || 0), 0); (i: any) => i.status === "pending" || i.status === "overdue",
const accountManager = teamMembers.find(t => t.role === "account_manager"); ).length;
const solutionsArchitect = teamMembers.find(t => t.role === "solutions_architect"); const totalInvoiceValue = invoices.reduce(
(acc, inv) => acc + (inv.amount || 0),
0,
);
const accountManager = teamMembers.find((t) => t.role === "account_manager");
const solutionsArchitect = teamMembers.find(
(t) => t.role === "solutions_architect",
);
return ( return (
<Layout> <Layout>
@ -151,8 +167,12 @@ export default function ClientHub() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Active Projects</p> <p className="text-xs text-gray-400 uppercase tracking-wider">
<p className="text-2xl font-bold text-white mt-1">{contracts.filter(c => c.status === "active").length}</p> Active Projects
</p>
<p className="text-2xl font-bold text-white mt-1">
{contracts.filter((c) => c.status === "active").length}
</p>
</div> </div>
<Briefcase className="h-6 w-6 text-blue-500 opacity-50" /> <Briefcase className="h-6 w-6 text-blue-500 opacity-50" />
</div> </div>
@ -163,25 +183,38 @@ export default function ClientHub() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Total Invoices</p> <p className="text-xs text-gray-400 uppercase tracking-wider">
<p className="text-2xl font-bold text-white mt-1">${(totalInvoiceValue / 1000).toFixed(0)}k</p> Total Invoices
</p>
<p className="text-2xl font-bold text-white mt-1">
${(totalInvoiceValue / 1000).toFixed(0)}k
</p>
</div> </div>
<DollarSign className="h-6 w-6 text-cyan-500 opacity-50" /> <DollarSign className="h-6 w-6 text-cyan-500 opacity-50" />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className={`bg-gradient-to-br ${outstandingInvoices > 0 ? 'from-orange-950/40 to-orange-900/20 border-orange-500/20' : 'from-green-950/40 to-green-900/20 border-green-500/20'}`}> <Card
className={`bg-gradient-to-br ${outstandingInvoices > 0 ? "from-orange-950/40 to-orange-900/20 border-orange-500/20" : "from-green-950/40 to-green-900/20 border-green-500/20"}`}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Outstanding</p> <p className="text-xs text-gray-400 uppercase tracking-wider">
<p className="text-2xl font-bold text-white mt-1">{outstandingInvoices}</p> Outstanding
</p>
<p className="text-2xl font-bold text-white mt-1">
{outstandingInvoices}
</p>
</div> </div>
<FileText className="h-6 w-6" style={{ <FileText
color: outstandingInvoices > 0 ? "#f97316" : "#22c55e", className="h-6 w-6"
opacity: 0.5 style={{
}} /> color: outstandingInvoices > 0 ? "#f97316" : "#22c55e",
opacity: 0.5,
}}
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -190,8 +223,12 @@ export default function ClientHub() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Team Members</p> <p className="text-xs text-gray-400 uppercase tracking-wider">
<p className="text-2xl font-bold text-white mt-1">{teamMembers.length}</p> Team Members
</p>
<p className="text-2xl font-bold text-white mt-1">
{teamMembers.length}
</p>
</div> </div>
<Users className="h-6 w-6 text-purple-500 opacity-50" /> <Users className="h-6 w-6 text-purple-500 opacity-50" />
</div> </div>
@ -201,7 +238,11 @@ export default function ClientHub() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs
value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList className="grid w-full grid-cols-4 bg-blue-950/30 border border-blue-500/20 p-1"> <TabsList className="grid w-full grid-cols-4 bg-blue-950/30 border border-blue-500/20 p-1">
<TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="project">Project Status</TabsTrigger> <TabsTrigger value="project">Project Status</TabsTrigger>
@ -215,7 +256,9 @@ export default function ClientHub() {
<Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20"> <Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20">
<CardHeader> <CardHeader>
<CardTitle>My QuantumLeap Dashboard</CardTitle> <CardTitle>My QuantumLeap Dashboard</CardTitle>
<CardDescription>Live AI analytics and insights for your project</CardDescription> <CardDescription>
Live AI analytics and insights for your project
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="aspect-video bg-black/50 rounded-lg border border-blue-500/20 flex items-center justify-center"> <div className="aspect-video bg-black/50 rounded-lg border border-blue-500/20 flex items-center justify-center">
@ -227,7 +270,8 @@ export default function ClientHub() {
/> />
</div> </div>
<p className="text-sm text-gray-400 mt-4"> <p className="text-sm text-gray-400 mt-4">
Embedded QuantumLeap analytics dashboard. Real-time data about your project performance. Embedded QuantumLeap analytics dashboard. Real-time data
about your project performance.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -242,16 +286,31 @@ export default function ClientHub() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs text-gray-400 uppercase">Total Value</p> <p className="text-xs text-gray-400 uppercase">
<p className="text-2xl font-bold text-white">${(activeContract.total_value / 1000).toFixed(0)}k</p> Total Value
</p>
<p className="text-2xl font-bold text-white">
${(activeContract.total_value / 1000).toFixed(0)}k
</p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs text-gray-400 uppercase">Status</p> <p className="text-xs text-gray-400 uppercase">
<Badge className="bg-blue-600/50 text-blue-100">{activeContract.status}</Badge> Status
</p>
<Badge className="bg-blue-600/50 text-blue-100">
{activeContract.status}
</Badge>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs text-gray-400 uppercase">Completion</p> <p className="text-xs text-gray-400 uppercase">
<p className="text-2xl font-bold text-cyan-400">{Math.round((completedMilestones / totalMilestones) * 100)}%</p> Completion
</p>
<p className="text-2xl font-bold text-cyan-400">
{Math.round(
(completedMilestones / totalMilestones) * 100,
)}
%
</p>
</div> </div>
</div> </div>
@ -259,12 +318,16 @@ export default function ClientHub() {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-xs text-gray-400"> <div className="flex justify-between text-xs text-gray-400">
<span>Milestone Progress</span> <span>Milestone Progress</span>
<span>{completedMilestones} of {totalMilestones}</span> <span>
{completedMilestones} of {totalMilestones}
</span>
</div> </div>
<div className="w-full bg-black/50 rounded-full h-3"> <div className="w-full bg-black/50 rounded-full h-3">
<div <div
className="bg-gradient-to-r from-cyan-500 to-blue-500 h-3 rounded-full" className="bg-gradient-to-r from-cyan-500 to-blue-500 h-3 rounded-full"
style={{ width: `${(completedMilestones / totalMilestones) * 100}%` }} style={{
width: `${(completedMilestones / totalMilestones) * 100}%`,
}}
/> />
</div> </div>
</div> </div>
@ -285,7 +348,9 @@ export default function ClientHub() {
<Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20"> <Card className="bg-gradient-to-br from-blue-950/40 to-blue-900/20 border-blue-500/20">
<CardContent className="p-8 text-center space-y-4"> <CardContent className="p-8 text-center space-y-4">
<Briefcase className="h-12 w-12 mx-auto text-blue-500 opacity-50" /> <Briefcase className="h-12 w-12 mx-auto text-blue-500 opacity-50" />
<p className="text-gray-400">No active projects at this time</p> <p className="text-gray-400">
No active projects at this time
</p>
</CardContent> </CardContent>
</Card> </Card>
)} )}
@ -294,7 +359,9 @@ export default function ClientHub() {
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader> <CardHeader>
<CardTitle>Recent Invoices</CardTitle> <CardTitle>Recent Invoices</CardTitle>
<CardDescription>Your latest billing activity</CardDescription> <CardDescription>
Your latest billing activity
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{invoices.length === 0 ? ( {invoices.length === 0 ? (
@ -305,18 +372,34 @@ export default function ClientHub() {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{invoices.slice(0, 5).map((invoice: any) => ( {invoices.slice(0, 5).map((invoice: any) => (
<div key={invoice.id} className="flex items-center justify-between p-3 bg-black/30 rounded-lg border border-purple-500/10"> <div
key={invoice.id}
className="flex items-center justify-between p-3 bg-black/30 rounded-lg border border-purple-500/10"
>
<div className="flex-1"> <div className="flex-1">
<p className="font-semibold text-white text-sm">{invoice.invoice_number}</p> <p className="font-semibold text-white text-sm">
<p className="text-xs text-gray-400">{new Date(invoice.created_at).toLocaleDateString()}</p> {invoice.invoice_number}
</p>
<p className="text-xs text-gray-400">
{new Date(
invoice.created_at,
).toLocaleDateString()}
</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="font-semibold text-white">${invoice.amount?.toLocaleString()}</p> <p className="font-semibold text-white">
<Badge variant="outline" className={ ${invoice.amount?.toLocaleString()}
invoice.status === "paid" ? "bg-green-500/20 border-green-500/30 text-green-300" : </p>
invoice.status === "overdue" ? "bg-red-500/20 border-red-500/30 text-red-300" : <Badge
"" variant="outline"
}> className={
invoice.status === "paid"
? "bg-green-500/20 border-green-500/30 text-green-300"
: invoice.status === "overdue"
? "bg-red-500/20 border-red-500/30 text-red-300"
: ""
}
>
{invoice.status} {invoice.status}
</Badge> </Badge>
</div> </div>
@ -331,16 +414,20 @@ export default function ClientHub() {
{/* Project Status Tab - Gantt Style */} {/* Project Status Tab - Gantt Style */}
<TabsContent value="project" className="space-y-4 animate-fade-in"> <TabsContent value="project" className="space-y-4 animate-fade-in">
<ProjectStatusWidget <ProjectStatusWidget
project={activeContract ? { project={
id: activeContract.id, activeContract
title: activeContract.title, ? {
description: activeContract.description, id: activeContract.id,
status: activeContract.status || "active", title: activeContract.title,
start_date: activeContract.start_date, description: activeContract.description,
end_date: activeContract.end_date, status: activeContract.status || "active",
total_value: activeContract.total_value, start_date: activeContract.start_date,
milestones: activeContract.milestones || [], end_date: activeContract.end_date,
} : null} total_value: activeContract.total_value,
milestones: activeContract.milestones || [],
}
: null
}
accentColor="cyan" accentColor="cyan"
/> />
</TabsContent> </TabsContent>
@ -350,25 +437,45 @@ export default function ClientHub() {
<Card className="bg-gradient-to-br from-cyan-950/40 to-cyan-900/20 border-cyan-500/20"> <Card className="bg-gradient-to-br from-cyan-950/40 to-cyan-900/20 border-cyan-500/20">
<CardHeader> <CardHeader>
<CardTitle>Invoices & Billing</CardTitle> <CardTitle>Invoices & Billing</CardTitle>
<CardDescription>Secure portal to manage all invoices and payments</CardDescription> <CardDescription>
Secure portal to manage all invoices and payments
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* Billing Summary */} {/* Billing Summary */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 bg-black/30 rounded-lg border border-cyan-500/20 space-y-1"> <div className="p-4 bg-black/30 rounded-lg border border-cyan-500/20 space-y-1">
<p className="text-xs text-gray-400 uppercase">Total Invoices</p> <p className="text-xs text-gray-400 uppercase">
<p className="text-3xl font-bold text-white">${(totalInvoiceValue / 1000).toFixed(0)}k</p> Total Invoices
</p>
<p className="text-3xl font-bold text-white">
${(totalInvoiceValue / 1000).toFixed(0)}k
</p>
</div> </div>
<div className="p-4 bg-black/30 rounded-lg border border-green-500/20 space-y-1"> <div className="p-4 bg-black/30 rounded-lg border border-green-500/20 space-y-1">
<p className="text-xs text-gray-400 uppercase">Paid</p> <p className="text-xs text-gray-400 uppercase">Paid</p>
<p className="text-3xl font-bold text-green-400"> <p className="text-3xl font-bold text-green-400">
${(invoices.filter(i => i.status === "paid").reduce((acc, i) => acc + (i.amount || 0), 0) / 1000).toFixed(0)}k $
{(
invoices
.filter((i) => i.status === "paid")
.reduce((acc, i) => acc + (i.amount || 0), 0) / 1000
).toFixed(0)}
k
</p> </p>
</div> </div>
<div className="p-4 bg-black/30 rounded-lg border border-orange-500/20 space-y-1"> <div className="p-4 bg-black/30 rounded-lg border border-orange-500/20 space-y-1">
<p className="text-xs text-gray-400 uppercase">Outstanding</p> <p className="text-xs text-gray-400 uppercase">
Outstanding
</p>
<p className="text-3xl font-bold text-orange-400"> <p className="text-3xl font-bold text-orange-400">
${(invoices.filter(i => i.status !== "paid").reduce((acc, i) => acc + (i.amount || 0), 0) / 1000).toFixed(0)}k $
{(
invoices
.filter((i) => i.status !== "paid")
.reduce((acc, i) => acc + (i.amount || 0), 0) / 1000
).toFixed(0)}
k
</p> </p>
</div> </div>
</div> </div>
@ -382,21 +489,32 @@ export default function ClientHub() {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{invoices.map((invoice: any) => ( {invoices.map((invoice: any) => (
<div key={invoice.id} className="p-4 bg-black/30 rounded-lg border border-cyan-500/10 hover:border-cyan-500/30 transition space-y-3"> <div
key={invoice.id}
className="p-4 bg-black/30 rounded-lg border border-cyan-500/10 hover:border-cyan-500/30 transition space-y-3"
>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<h4 className="font-semibold text-white">{invoice.invoice_number}</h4> <h4 className="font-semibold text-white">
<p className="text-sm text-gray-400">{invoice.description}</p> {invoice.invoice_number}
</h4>
<p className="text-sm text-gray-400">
{invoice.description}
</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-lg font-semibold text-white">${invoice.amount?.toLocaleString()}</p> <p className="text-lg font-semibold text-white">
<Badge className={ ${invoice.amount?.toLocaleString()}
invoice.status === "paid" </p>
? "bg-green-500/20 border-green-500/30 text-green-300" <Badge
: invoice.status === "overdue" className={
? "bg-red-500/20 border-red-500/30 text-red-300" invoice.status === "paid"
: "bg-yellow-500/20 border-yellow-500/30 text-yellow-300" ? "bg-green-500/20 border-green-500/30 text-green-300"
}> : invoice.status === "overdue"
? "bg-red-500/20 border-red-500/30 text-red-300"
: "bg-yellow-500/20 border-yellow-500/30 text-yellow-300"
}
>
{invoice.status} {invoice.status}
</Badge> </Badge>
</div> </div>
@ -404,15 +522,27 @@ export default function ClientHub() {
<div className="grid grid-cols-2 gap-4 text-sm text-gray-400"> <div className="grid grid-cols-2 gap-4 text-sm text-gray-400">
<div> <div>
<span>Issued:</span> <span>Issued:</span>
<p className="text-white font-semibold">{new Date(invoice.issued_date).toLocaleDateString()}</p> <p className="text-white font-semibold">
{new Date(
invoice.issued_date,
).toLocaleDateString()}
</p>
</div> </div>
<div> <div>
<span>Due:</span> <span>Due:</span>
<p className="text-white font-semibold">{new Date(invoice.due_date).toLocaleDateString()}</p> <p className="text-white font-semibold">
{new Date(
invoice.due_date,
).toLocaleDateString()}
</p>
</div> </div>
</div> </div>
{invoice.status !== "paid" && ( {invoice.status !== "paid" && (
<Button size="sm" variant="outline" className="w-full border-cyan-500/30 text-cyan-300 hover:bg-cyan-500/10"> <Button
size="sm"
variant="outline"
className="w-full border-cyan-500/30 text-cyan-300 hover:bg-cyan-500/10"
>
Pay Now Pay Now
<ArrowRight className="h-4 w-4 ml-2" /> <ArrowRight className="h-4 w-4 ml-2" />
</Button> </Button>
@ -430,7 +560,9 @@ export default function ClientHub() {
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20"> <Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader> <CardHeader>
<CardTitle>Your Dedicated AeThex Team</CardTitle> <CardTitle>Your Dedicated AeThex Team</CardTitle>
<CardDescription>White-glove service with personalized support</CardDescription> <CardDescription>
White-glove service with personalized support
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
@ -439,25 +571,47 @@ export default function ClientHub() {
<div className="p-6 bg-black/30 rounded-lg border border-blue-500/20 space-y-4"> <div className="p-6 bg-black/30 rounded-lg border border-blue-500/20 space-y-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<img <img
src={accountManager.avatar_url || "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop"} src={
accountManager.avatar_url ||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop"
}
alt={accountManager.full_name} alt={accountManager.full_name}
className="w-16 h-16 rounded-full border-2 border-blue-500/40 object-cover" className="w-16 h-16 rounded-full border-2 border-blue-500/40 object-cover"
/> />
<div className="flex-1"> <div className="flex-1">
<Badge className="bg-blue-600/50 text-blue-100 mb-2">Account Manager</Badge> <Badge className="bg-blue-600/50 text-blue-100 mb-2">
<h3 className="text-lg font-semibold text-white">{accountManager.full_name}</h3> Account Manager
<p className="text-sm text-gray-400 mb-2">{accountManager.title}</p> </Badge>
<p className="text-sm text-gray-300 mb-4">{accountManager.bio}</p> <h3 className="text-lg font-semibold text-white">
{accountManager.full_name}
</h3>
<p className="text-sm text-gray-400 mb-2">
{accountManager.title}
</p>
<p className="text-sm text-gray-300 mb-4">
{accountManager.bio}
</p>
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" variant="outline" className="border-blue-500/30 text-blue-300 hover:bg-blue-500/10"> <Button
size="sm"
variant="outline"
className="border-blue-500/30 text-blue-300 hover:bg-blue-500/10"
>
<MessageSquare className="h-4 w-4 mr-2" /> <MessageSquare className="h-4 w-4 mr-2" />
Message Message
</Button> </Button>
<Button size="sm" variant="outline" className="border-blue-500/30 text-blue-300 hover:bg-blue-500/10"> <Button
size="sm"
variant="outline"
className="border-blue-500/30 text-blue-300 hover:bg-blue-500/10"
>
<Phone className="h-4 w-4 mr-2" /> <Phone className="h-4 w-4 mr-2" />
Call Call
</Button> </Button>
<Button size="sm" className="bg-blue-600 hover:bg-blue-700"> <Button
size="sm"
className="bg-blue-600 hover:bg-blue-700"
>
<Calendar className="h-4 w-4 mr-2" /> <Calendar className="h-4 w-4 mr-2" />
Book Meeting Book Meeting
</Button> </Button>
@ -467,11 +621,13 @@ export default function ClientHub() {
{accountManager.contact_info && ( {accountManager.contact_info && (
<div className="pt-4 border-t border-blue-500/20 space-y-2 text-sm"> <div className="pt-4 border-t border-blue-500/20 space-y-2 text-sm">
<p className="text-gray-400"> <p className="text-gray-400">
<span className="font-semibold">Email:</span> {accountManager.contact_info.email} <span className="font-semibold">Email:</span>{" "}
{accountManager.contact_info.email}
</p> </p>
{accountManager.contact_info.phone && ( {accountManager.contact_info.phone && (
<p className="text-gray-400"> <p className="text-gray-400">
<span className="font-semibold">Phone:</span> {accountManager.contact_info.phone} <span className="font-semibold">Phone:</span>{" "}
{accountManager.contact_info.phone}
</p> </p>
)} )}
</div> </div>
@ -489,25 +645,47 @@ export default function ClientHub() {
<div className="p-6 bg-black/30 rounded-lg border border-purple-500/20 space-y-4"> <div className="p-6 bg-black/30 rounded-lg border border-purple-500/20 space-y-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<img <img
src={solutionsArchitect.avatar_url || "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop"} src={
solutionsArchitect.avatar_url ||
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop"
}
alt={solutionsArchitect.full_name} alt={solutionsArchitect.full_name}
className="w-16 h-16 rounded-full border-2 border-purple-500/40 object-cover" className="w-16 h-16 rounded-full border-2 border-purple-500/40 object-cover"
/> />
<div className="flex-1"> <div className="flex-1">
<Badge className="bg-purple-600/50 text-purple-100 mb-2">Solutions Architect</Badge> <Badge className="bg-purple-600/50 text-purple-100 mb-2">
<h3 className="text-lg font-semibold text-white">{solutionsArchitect.full_name}</h3> Solutions Architect
<p className="text-sm text-gray-400 mb-2">{solutionsArchitect.title}</p> </Badge>
<p className="text-sm text-gray-300 mb-4">{solutionsArchitect.bio}</p> <h3 className="text-lg font-semibold text-white">
{solutionsArchitect.full_name}
</h3>
<p className="text-sm text-gray-400 mb-2">
{solutionsArchitect.title}
</p>
<p className="text-sm text-gray-300 mb-4">
{solutionsArchitect.bio}
</p>
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" variant="outline" className="border-purple-500/30 text-purple-300 hover:bg-purple-500/10"> <Button
size="sm"
variant="outline"
className="border-purple-500/30 text-purple-300 hover:bg-purple-500/10"
>
<MessageSquare className="h-4 w-4 mr-2" /> <MessageSquare className="h-4 w-4 mr-2" />
Message Message
</Button> </Button>
<Button size="sm" variant="outline" className="border-purple-500/30 text-purple-300 hover:bg-purple-500/10"> <Button
size="sm"
variant="outline"
className="border-purple-500/30 text-purple-300 hover:bg-purple-500/10"
>
<Phone className="h-4 w-4 mr-2" /> <Phone className="h-4 w-4 mr-2" />
Call Call
</Button> </Button>
<Button size="sm" className="bg-purple-600 hover:bg-purple-700"> <Button
size="sm"
className="bg-purple-600 hover:bg-purple-700"
>
<Calendar className="h-4 w-4 mr-2" /> <Calendar className="h-4 w-4 mr-2" />
Book Meeting Book Meeting
</Button> </Button>
@ -517,11 +695,13 @@ export default function ClientHub() {
{solutionsArchitect.contact_info && ( {solutionsArchitect.contact_info && (
<div className="pt-4 border-t border-purple-500/20 space-y-2 text-sm"> <div className="pt-4 border-t border-purple-500/20 space-y-2 text-sm">
<p className="text-gray-400"> <p className="text-gray-400">
<span className="font-semibold">Email:</span> {solutionsArchitect.contact_info.email} <span className="font-semibold">Email:</span>{" "}
{solutionsArchitect.contact_info.email}
</p> </p>
{solutionsArchitect.contact_info.phone && ( {solutionsArchitect.contact_info.phone && (
<p className="text-gray-400"> <p className="text-gray-400">
<span className="font-semibold">Phone:</span> {solutionsArchitect.contact_info.phone} <span className="font-semibold">Phone:</span>{" "}
{solutionsArchitect.contact_info.phone}
</p> </p>
)} )}
</div> </div>
@ -547,7 +727,10 @@ export default function ClientHub() {
<MessageSquare className="h-4 w-4 mr-2" /> <MessageSquare className="h-4 w-4 mr-2" />
Open Support Ticket Open Support Ticket
</Button> </Button>
<Button variant="outline" className="w-full border-cyan-500/30 text-cyan-300 hover:bg-cyan-500/10"> <Button
variant="outline"
className="w-full border-cyan-500/30 text-cyan-300 hover:bg-cyan-500/10"
>
<Phone className="h-4 w-4 mr-2" /> <Phone className="h-4 w-4 mr-2" />
Schedule Support Call Schedule Support Call
</Button> </Button>