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
.from("devlink_opportunities")
.select(`
.select(
`
id,
title,
description,
@ -27,7 +28,8 @@ export default async (req: Request) => {
status,
skills_required,
created_at
`)
`,
)
.eq("status", "open")
.eq("type", "roblox")
.order("created_at", { ascending: false });

View file

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

View file

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

View file

@ -10,7 +10,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
if (authHeader) {
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) {
userId = user.id;
}
@ -21,10 +24,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
// List all published courses
const { data: courses, error: coursesError } = await admin
.from("foundation_courses")
.select(`
.select(
`
*,
instructor:user_profiles(id, full_name, avatar_url)
`)
`,
)
.eq("is_published", true)
.order("order_index", { ascending: true });
@ -42,7 +47,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
if (userEnrollments) {
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",
enrolled_at: new Date().toISOString(),
},
{ onConflict: "user_id,course_id" }
{ onConflict: "user_id,course_id" },
)
.select()
.single();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
}
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) {
return res.status(401).json({ error: "Invalid token" });
@ -49,7 +52,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
// Get applicants
let query = admin
.from("nexus_applications")
.select(`
.select(
`
*,
creator:user_profiles(
id,
@ -64,7 +68,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
rating,
review_count
)
`)
`,
)
.eq("opportunity_id", opportunityId)
.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(
offset,
offset + limit - 1
offset + limit - 1,
);
if (appError) {

View file

@ -15,7 +15,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
}
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) {
return res.status(401).json({ error: "Invalid token" });
@ -28,7 +31,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
let query = admin
.from("nexus_contracts")
.select(`
.select(
`
*,
creator:user_profiles(
id,
@ -38,7 +42,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
),
milestones:nexus_milestones(*),
payments:nexus_payments(*)
`)
`,
)
.eq("client_id", user.id)
.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(
offset,
offset + limit - 1
offset + limit - 1,
);
if (contractsError) {
@ -63,11 +68,11 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
const totalSpent = (allContracts || []).reduce(
(sum: number, c: any) => sum + (c.total_amount || 0),
0
0,
);
const activeContracts = (allContracts || []).filter(
(c: any) => c.status === "active"
(c: any) => c.status === "active",
).length;
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 { data: { user }, error: authError } = await admin.auth.getUser(token);
const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) {
return res.status(401).json({ error: "Invalid token" });
@ -26,10 +29,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
let query = admin
.from("nexus_opportunities")
.select(`
.select(
`
*,
applications:nexus_applications(id, status, creator_id)
`)
`,
)
.eq("posted_by", user.id)
.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(
offset,
offset + limit - 1
offset + limit - 1,
);
if (oppError) {
@ -72,7 +77,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
if (!title || !description || !category || !budget_type) {
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,
description,
category,
required_skills: Array.isArray(required_skills) ? required_skills : [],
required_skills: Array.isArray(required_skills)
? required_skills
: [],
budget_type,
budget_min: budget_min || 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 { data: { user }, error: authError } = await admin.auth.getUser(token);
const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) {
return res.status(401).json({ error: "Invalid token" });
@ -28,7 +31,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
let query = admin
.from("nexus_applications")
.select(`
.select(
`
*,
opportunity:nexus_opportunities(
id,
@ -43,7 +47,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
posted_by,
created_at
)
`)
`,
)
.eq("creator_id", user.id)
.order("created_at", { ascending: false });
@ -51,9 +56,13 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
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)
.then(result => ({ ...result, count: result.data?.length || 0 }));
.then((result) => ({ ...result, count: result.data?.length || 0 }));
if (applicationsError) {
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 { data: { user }, error: authError } = await admin.auth.getUser(token);
const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) {
return res.status(401).json({ error: "Invalid token" });
@ -28,12 +31,14 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
let query = admin
.from("nexus_contracts")
.select(`
.select(
`
*,
client:user_profiles(id, full_name, avatar_url),
milestones:nexus_milestones(*),
payments:nexus_payments(*)
`)
`,
)
.eq("creator_id", user.id)
.order("created_at", { ascending: false });
@ -41,9 +46,13 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
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)
.then(result => ({ ...result, count: result.data?.length || 0 }));
.then((result) => ({ ...result, count: result.data?.length || 0 }));
if (contractsError) {
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 { data: { user }, error: authError } = await admin.auth.getUser(token);
const {
data: { user },
error: authError,
} = await admin.auth.getUser(token);
if (authError || !user) {
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
let query = admin
.from("nexus_payments")
.select(`
.select(
`
*,
contract:nexus_contracts(
id,
@ -40,7 +44,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
created_at
),
milestone:nexus_milestones(id, description, amount, status)
`)
`,
)
.eq("contract.creator_id", user.id)
.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(
offset,
offset + limit - 1
offset + limit - 1,
);
if (paymentsError) {
@ -63,11 +68,13 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
.select("total_amount, creator_payout_amount, status")
.eq("creator_id", user.id);
const totalEarnings = (contracts || [])
.reduce((sum: number, c: any) => sum + (c.creator_payout_amount || 0), 0);
const totalEarnings = (contracts || []).reduce(
(sum: number, c: any) => sum + (c.creator_payout_amount || 0),
0,
);
const completedContracts = (contracts || []).filter(
(c: any) => c.status === "completed"
(c: any) => c.status === "completed",
).length;
const pendingPayouts = (payments || [])

View file

@ -11,7 +11,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
}
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) {
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_hours_per_week: availability_hours_per_week || null,
},
{ onConflict: "user_id" }
{ onConflict: "user_id" },
)
.select()
.single();

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,13 @@
import { supabase } from "../_supabase";
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) => {
try {
@ -28,7 +34,9 @@ export default async (req: Request) => {
.order("created_at", { ascending: false });
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), {
@ -42,7 +50,10 @@ export default async (req: Request) => {
const body = await req.json();
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 });
}
@ -57,13 +68,15 @@ export default async (req: Request) => {
affiliation_data: affiliation_data || {},
confirmed: confirmed === true,
},
{ onConflict: "user_id,arm,affiliation_type" }
{ onConflict: "user_id,arm,affiliation_type" },
)
.select()
.single();
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), {
@ -89,7 +102,9 @@ export default async (req: Request) => {
.eq("affiliation_type", affiliation_type || null);
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 }), {
@ -101,6 +116,8 @@ export default async (req: Request) => {
return new Response("Method not allowed", { status: 405 });
} catch (error: any) {
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 = {};
// 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 ("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 ("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 ("twitch_url" in body) updates.twitch_url = body.twitch_url || null;
// Professional info
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)
updates.availability_status =
["available", "limited", "unavailable"].includes(body.availability_status) ?
body.availability_status : "available";
updates.availability_status = [
"available",
"limited",
"unavailable",
].includes(body.availability_status)
? body.availability_status
: "available";
if ("timezone" in body) updates.timezone = body.timezone || 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 : [];
}
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) {
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) {
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) {
const validArms = ["foundation", "gameforge", "labs", "corp", "devlink"];
updates.arm_affiliations = Array.isArray(body.arm_affiliations) ?
body.arm_affiliations.filter((a: string) => validArms.includes(a)) : [];
updates.arm_affiliations = Array.isArray(body.arm_affiliations)
? body.arm_affiliations.filter((a: string) => validArms.includes(a))
: [];
}
// Nexus specific
if ("nexus_profile_complete" in body) updates.nexus_profile_complete = body.nexus_profile_complete === true;
if ("nexus_headline" in body) updates.nexus_headline = body.nexus_headline || null;
if ("nexus_profile_complete" in body)
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) {
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
@ -79,7 +99,9 @@ export default async (req: Request) => {
if (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), {
@ -88,6 +110,8 @@ export default async (req: Request) => {
});
} catch (error: any) {
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="/onboarding" element={<Onboarding />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/dashboard/nexus" element={<NexusDashboard />} />
<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
path="/dashboard/nexus"
element={<NexusDashboard />}
/>
<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
path="/hub/client"
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 { Button } from "@/components/ui/button";
import { Trophy, Lock, Star, ArrowRight } from "lucide-react";
@ -25,11 +31,23 @@ interface AchievementsWidgetProps {
}
const rarityMap = {
common: { 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" },
common: {
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" },
epic: { 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" },
epic: {
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 = {
@ -92,7 +110,9 @@ export function AchievementsWidget({
<div className="text-center py-12 space-y-4">
<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-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 className="space-y-4">
@ -144,9 +164,7 @@ export function AchievementsWidget({
{/* Locked Achievements Placeholder */}
{hasMore && (
<div
className="p-3 rounded-lg border border-gray-600/30 bg-black/30 text-center space-y-2 opacity-50"
>
<div 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">
<Lock className="w-6 h-6 text-gray-500" />
</div>

View file

@ -1,8 +1,20 @@
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 { 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 {
id: string;
@ -27,7 +39,10 @@ interface ApplicantTrackerWidgetProps {
description?: string;
onViewProfile?: (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";
}
@ -73,12 +88,16 @@ export function ApplicantTrackerWidget({
const [draggedApplicant, setDraggedApplicant] = useState<string | null>(null);
const statusCounts = {
applied: applicants.filter(a => a.status === "applied").length,
interviewing: applicants.filter(a => a.status === "interviewing").length,
hired: applicants.filter(a => a.status === "hired").length,
applied: applicants.filter((a) => a.status === "applied").length,
interviewing: applicants.filter((a) => a.status === "interviewing").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) => {
setDraggedApplicant(applicantId);
@ -115,7 +134,9 @@ export function ApplicantTrackerWidget({
{allStatuses.map((status) => {
const statusInfo = statusColors[status];
const StatusIcon = statusInfo.icon;
const statusApplicants = applicants.filter(a => a.status === status);
const statusApplicants = applicants.filter(
(a) => a.status === status,
);
return (
<div
@ -128,9 +149,13 @@ export function ApplicantTrackerWidget({
<div className="mb-4 pb-3 border-b border-gray-500/20">
<div className="flex items-center gap-2 mb-2">
<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>
<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>
{/* 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 { Briefcase, AlertCircle, CheckCircle, Clock, ArrowRight } from "lucide-react";
import {
Briefcase,
AlertCircle,
CheckCircle,
Clock,
ArrowRight,
} from "lucide-react";
import { Button } from "@/components/ui/button";
export interface Application {
@ -70,11 +82,31 @@ const colorMap = {
};
const statusMap = {
submitted: { color: "bg-blue-600/50 text-blue-100", icon: Clock, label: "Submitted" },
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" },
submitted: {
color: "bg-blue-600/50 text-blue-100",
icon: Clock,
label: "Submitted",
},
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({
@ -89,11 +121,11 @@ export function ApplicationsWidget({
}: ApplicationsWidgetProps) {
const colors = colorMap[accentColor];
const statusCounts = {
submitted: applications.filter(a => a.status === "submitted").length,
pending: applications.filter(a => a.status === "pending").length,
accepted: applications.filter(a => a.status === "accepted").length,
rejected: applications.filter(a => a.status === "rejected").length,
interview: applications.filter(a => a.status === "interview").length,
submitted: applications.filter((a) => a.status === "submitted").length,
pending: applications.filter((a) => a.status === "pending").length,
accepted: applications.filter((a) => a.status === "accepted").length,
rejected: applications.filter((a) => a.status === "rejected").length,
interview: applications.filter((a) => a.status === "interview").length,
};
return (
@ -123,36 +155,47 @@ export function ApplicationsWidget({
) : (
<div className="space-y-4">
{/* 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">
{statusCounts.submitted > 0 && (
<div className="text-center">
<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>
)}
{statusCounts.interview > 0 && (
<div className="text-center">
<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>
)}
{statusCounts.accepted > 0 && (
<div className="text-center">
<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>
)}
{statusCounts.pending > 0 && (
<div className="text-center">
<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>
)}
{statusCounts.rejected > 0 && (
<div className="text-center">
<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>
@ -176,10 +219,14 @@ export function ApplicationsWidget({
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-white truncate">
{app.opportunity?.title || app.title || "Untitled Opportunity"}
{app.opportunity?.title ||
app.title ||
"Untitled Opportunity"}
</h4>
{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>
<Badge className={statusInfo.color}>
@ -216,11 +263,7 @@ export function ApplicationsWidget({
</div>
{showCTA && onCTA && applications.length > 0 && (
<Button
onClick={onCTA}
variant="outline"
className="w-full mt-4"
>
<Button onClick={onCTA} variant="outline" className="w-full mt-4">
<ArrowRight className="h-4 w-4 mr-2" />
{ctaText}
</Button>

View file

@ -46,15 +46,15 @@ export function CTAButtonGroup({
btn.size === "sm"
? "text-sm px-3 py-1"
: btn.size === "lg"
? "text-lg px-6 py-3"
: "px-4 py-2";
? "text-lg px-6 py-3"
: "px-4 py-2";
const variantClass =
btn.variant === "outline"
? "border border-opacity-30 text-opacity-75 hover:bg-opacity-10"
: btn.variant === "secondary"
? "bg-opacity-50 hover:bg-opacity-75"
: "";
? "bg-opacity-50 hover:bg-opacity-75"
: "";
const widthClass = btn.fullWidth ? "w-full" : "";
@ -66,11 +66,18 @@ export function CTAButtonGroup({
key: idx,
className: `${sizeClass} ${variantClass} ${widthClass}`,
onClick: btn.onClick,
...(btn.href && { href: btn.href, target: "_blank", rel: "noopener noreferrer" }),
...(btn.href && {
href: btn.href,
target: "_blank",
rel: "noopener noreferrer",
}),
};
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" />}
{btn.label}
</Button>
@ -100,7 +107,9 @@ export function CTASection({
layout = "vertical",
}: CTASectionProps) {
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>
{subtitle && <p className="text-gray-300">{subtitle}</p>}
{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 { FileText, AlertCircle, CheckCircle, Clock, DollarSign } from "lucide-react";
import {
FileText,
AlertCircle,
CheckCircle,
Clock,
DollarSign,
} from "lucide-react";
export interface Contract {
id: string;
@ -79,7 +91,7 @@ export function ContractsWidget({
accentColor = "purple",
}: ContractsWidgetProps) {
const colors = colorMap[accentColor];
const activeContracts = contracts.filter(c => c.status === "active");
const activeContracts = contracts.filter((c) => c.status === "active");
return (
<Card className={`${colors.bg} border ${colors.border}`}>
@ -100,9 +112,12 @@ export function ContractsWidget({
<div className="space-y-4">
{contracts.map((contract) => {
const StatusIcon = statusMap[contract.status].icon;
const progress = contract.paid_amount && contract.total_amount
? Math.round((contract.paid_amount / contract.total_amount) * 100)
: 0;
const progress =
contract.paid_amount && contract.total_amount
? Math.round(
(contract.paid_amount / contract.total_amount) * 100,
)
: 0;
return (
<div
@ -128,7 +143,9 @@ export function ContractsWidget({
</div>
{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">
@ -150,7 +167,9 @@ export function ContractsWidget({
{contract.milestones && contract.milestones.length > 0 && (
<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">
{contract.milestones.map((m) => (
<div
@ -165,8 +184,8 @@ export function ContractsWidget({
m.status === "paid"
? "#22c55e"
: m.status === "approved"
? "#3b82f6"
: "#666",
? "#3b82f6"
: "#666",
}}
/>
<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 { BookOpen, CheckCircle, Clock, Lock, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
@ -45,9 +51,21 @@ const colorMap = {
};
const statusMap = {
not_started: { label: "Not Started", 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 },
not_started: {
label: "Not Started",
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({
@ -58,8 +76,10 @@ export function CoursesWidget({
accentColor = "red",
}: CoursesWidgetProps) {
const colors = colorMap[accentColor];
const completedCount = courses.filter(c => c.status === "completed").length;
const inProgressCount = courses.filter(c => c.status === "in_progress").length;
const completedCount = courses.filter((c) => c.status === "completed").length;
const inProgressCount = courses.filter(
(c) => c.status === "in_progress",
).length;
return (
<Card className={`${colors.bg} border ${colors.border}`}>
@ -75,7 +95,9 @@ export function CoursesWidget({
<div className="text-center py-12">
<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-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 className="space-y-4">
@ -87,11 +109,15 @@ export function CoursesWidget({
</div>
<div className="text-center">
<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 className="text-center">
<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>
@ -111,25 +137,25 @@ export function CoursesWidget({
{/* Course Header */}
<div className="space-y-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}>
<StatusIcon className="h-3 w-3 mr-1" />
{statusInfo.label}
</Badge>
</div>
{course.description && (
<p className="text-xs text-gray-400">{course.description}</p>
<p className="text-xs text-gray-400">
{course.description}
</p>
)}
</div>
{/* Course Meta */}
<div className="flex items-center justify-between text-xs text-gray-400 py-2 border-y border-gray-500/10">
{course.instructor && (
<span>{course.instructor}</span>
)}
{course.duration && (
<span>{course.duration}</span>
)}
{course.instructor && <span>{course.instructor}</span>}
{course.duration && <span>{course.duration}</span>}
</div>
{/* Progress */}
@ -138,16 +164,18 @@ export function CoursesWidget({
<div className="flex justify-between text-xs">
<span className="text-gray-400">Progress</span>
<span className="text-gray-300 font-semibold">
{course.lessons_completed || 0}/{course.lessons_total}
{course.lessons_completed || 0}/
{course.lessons_total}
</span>
</div>
<div className="w-full bg-black/50 rounded-full h-2">
<div
className="bg-gradient-to-r from-red-500 to-orange-500 h-2 rounded-full transition-all"
style={{
width: course.lessons_total > 0
? `${(((course.lessons_completed || 0) / course.lessons_total) * 100)}%`
: "0%"
width:
course.lessons_total > 0
? `${((course.lessons_completed || 0) / course.lessons_total) * 100}%`
: "0%",
}}
/>
</div>
@ -164,7 +192,9 @@ export function CoursesWidget({
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" />
</Button>
</div>

View file

@ -1,6 +1,13 @@
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 {
arm: ArmKey;
@ -105,11 +112,16 @@ interface DashboardThemeProviderProps {
children: ReactNode;
}
export function DashboardThemeProvider({ arm, children }: DashboardThemeProviderProps) {
export function DashboardThemeProvider({
arm,
children,
}: DashboardThemeProviderProps) {
const theme = getTheme(arm);
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}
</div>
);
@ -121,13 +133,19 @@ interface DashboardHeaderProps {
subtitle?: string;
}
export function DashboardHeader({ arm, title, subtitle }: DashboardHeaderProps) {
export function DashboardHeader({
arm,
title,
subtitle,
}: DashboardHeaderProps) {
const theme = getTheme(arm);
return (
<div className="space-y-4 animate-slide-down">
<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}
</h1>
{subtitle && <p className="text-gray-400 text-lg">{subtitle}</p>}
@ -141,7 +159,10 @@ interface ColorPaletteConfig {
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>>> = {
nexus: {
default: {

View file

@ -1,5 +1,11 @@
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 { Input } from "@/components/ui/input";
import { Users, Search, Phone, Mail, MapPin } from "lucide-react";
@ -48,8 +54,12 @@ export function DirectoryWidget({
return matchesSearch;
});
const employeeCount = members.filter(m => m.employment_type === "employee").length;
const contractorCount = members.filter(m => m.employment_type === "contractor").length;
const employeeCount = members.filter(
(m) => m.employment_type === "employee",
).length;
const contractorCount = members.filter(
(m) => m.employment_type === "contractor",
).length;
return (
<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 className="text-center">
<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>
@ -114,10 +126,14 @@ export function DirectoryWidget({
)}
<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>
{member.department && (
<p className="text-xs text-gray-500">{member.department}</p>
<p className="text-xs text-gray-500">
{member.department}
</p>
)}
</div>

View file

@ -1,8 +1,20 @@
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 { 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 {
for_hire: boolean;
@ -92,7 +104,9 @@ export function EthosStorefrontWidget({
<Music className="h-5 w-5" />
My ETHOS Storefront
</CardTitle>
<CardDescription>Your marketplace presence for services and tracks</CardDescription>
<CardDescription>
Your marketplace presence for services and tracks
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Profile Header */}
@ -106,13 +120,17 @@ export function EthosStorefrontWidget({
)}
<div className="flex-1">
{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 && (
<p className="text-sm text-gray-400 mt-1">{data.bio}</p>
)}
{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>
@ -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>
<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>
<button
onClick={() => handleToggleForHire(!isForHire)}

View file

@ -5,7 +5,15 @@ import { Badge } from "@/components/ui/badge";
export interface KanbanColumn {
id: string;
title: string;
color: "blue" | "yellow" | "green" | "red" | "purple" | "pink" | "cyan" | "amber";
color:
| "blue"
| "yellow"
| "green"
| "red"
| "purple"
| "pink"
| "cyan"
| "amber";
items: KanbanItem[];
count?: number;
}
@ -59,17 +67,25 @@ interface 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 (
<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) => (
<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">
<CardTitle className="text-lg flex items-center justify-between">
<span>{column.title}</span>
{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>
</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" : ""}`}
>
<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">
<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 && (
<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 && (
<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}
</Badge>
</div>
@ -102,9 +126,14 @@ export function KanbanBoard({ columns, gap = "medium" }: KanbanBoardProps) {
{item.metadata && (
<div className="mt-2 space-y-1">
{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 className="font-semibold text-white">{value}</span>
<span className="font-semibold text-white">
{value}
</span>
</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 { 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 {
id: string;
@ -59,7 +72,8 @@ export function MentorshipWidget({
accentColor = "red",
}: MentorshipWidgetProps) {
const colors = colorMap[accentColor];
const person = mentorship?.type === "mentor" ? mentorship.mentor : mentorship?.mentee;
const person =
mentorship?.type === "mentor" ? mentorship.mentor : mentorship?.mentee;
if (!mentorship) {
return (
@ -77,7 +91,7 @@ export function MentorshipWidget({
<div>
<p className="text-gray-400 mb-2">No active mentorship</p>
<p className="text-sm text-gray-500 mb-4">
{title.includes("Mentorship")
{title.includes("Mentorship")
? "Connect with an experienced mentor or become one yourself"
: ""}
</p>
@ -105,7 +119,9 @@ export function MentorshipWidget({
{title}
</CardTitle>
<CardDescription>
{mentorship.type === "mentor" ? "Mentoring someone" : "Being mentored by"}
{mentorship.type === "mentor"
? "Mentoring someone"
: "Being mentored by"}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
@ -116,12 +132,13 @@ export function MentorshipWidget({
mentorship.status === "active"
? "bg-green-600/50 text-green-100"
: mentorship.status === "paused"
? "bg-yellow-600/50 text-yellow-100"
: "bg-gray-600/50 text-gray-100"
? "bg-yellow-600/50 text-yellow-100"
: "bg-gray-600/50 text-gray-100"
}
>
{mentorship.status === "active" ? "🟢" : "⏸"}
{" "}{mentorship.status.charAt(0).toUpperCase() + mentorship.status.slice(1)}
{mentorship.status === "active" ? "🟢" : "⏸"}{" "}
{mentorship.status.charAt(0).toUpperCase() +
mentorship.status.slice(1)}
</Badge>
</div>
@ -151,10 +168,15 @@ export function MentorshipWidget({
{/* Specialties */}
{person.specialties && person.specialties.length > 0 && (
<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">
{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}
</Badge>
))}
@ -174,7 +196,9 @@ export function MentorshipWidget({
{mentorship.sessions_completed !== undefined && (
<div className="text-center">
<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>
)}
{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 { 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 {
id: string;
@ -112,7 +125,9 @@ export function OpportunitiesWidget({
{opp.title}
</h4>
{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>
<Badge className={statusBadge[opp.status]}>

View file

@ -1,8 +1,20 @@
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 { 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 {
available_for_payout: number;
@ -93,14 +105,12 @@ export function PayoutsWidget({
<div>
<p className="font-semibold text-white">Connect Stripe Account</p>
<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>
</div>
</div>
<Button
onClick={onConnectStripe}
className={colors.accent}
>
<Button onClick={onConnectStripe} className={colors.accent}>
<ExternalLink className="h-4 w-4 mr-2" />
Connect Stripe Account
</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">
<p className="text-sm text-gray-400">Available for Payout</p>
<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>
<div className="flex gap-2 pt-2">
<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">
<p className="text-sm text-gray-400">Pending (30-day Clearance)</p>
<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 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>
</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">
<p className="text-sm text-gray-400">Total Earned (All-Time)</p>
<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 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>
</div>
</div>
@ -186,10 +211,15 @@ export function PayoutsWidget({
)}
<div className="flex-1 min-w-0">
<p className="font-semibold text-white">
${payout.amount.toLocaleString('en-US', { minimumFractionDigits: 2 })}
$
{payout.amount.toLocaleString("en-US", {
minimumFractionDigits: 2,
})}
</p>
{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">
{new Date(payout.date).toLocaleDateString()}
@ -201,8 +231,8 @@ export function PayoutsWidget({
payout.status === "completed"
? "bg-green-600/50 text-green-100"
: payout.status === "pending"
? "bg-yellow-600/50 text-yellow-100"
: "bg-red-600/50 text-red-100"
? "bg-yellow-600/50 text-yellow-100"
: "bg-red-600/50 text-red-100"
}
>
{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 { 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 {
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="text-center">
<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 className="text-center">
<p className="text-xs text-gray-400">Open</p>
<p className="text-lg font-bold text-green-400">
{opportunities.filter(o => o.status === "open").length}
{opportunities.filter((o) => o.status === "open").length}
</p>
</div>
<div className="text-center">
<p className="text-xs text-gray-400">In Progress</p>
<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>
</div>
<div className="text-center">
<p className="text-xs text-gray-400">Total Applicants</p>
<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>
</div>
</div>
@ -132,7 +154,9 @@ export function PostedOpportunitiesWidget({
{opp.title}
</h4>
{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>
<Badge className={statusBadge[opp.status]}>
@ -183,13 +207,20 @@ export function PostedOpportunitiesWidget({
{opp.deadline && (
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
<span>Due {new Date(opp.deadline).toLocaleDateString()}</span>
<span>
Due {new Date(opp.deadline).toLocaleDateString()}
</span>
</div>
)}
{opp.applications_count !== undefined && (
<div className="flex items-center gap-1">
<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>

View file

@ -1,7 +1,13 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
@ -77,9 +83,16 @@ export function ProfileEditor({
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 [newWorkExp, setNewWorkExp] = useState({ company: "", title: "", duration: "" });
const [newWorkExp, setNewWorkExp] = useState({
company: "",
title: "",
duration: "",
});
const [newPortfolio, setNewPortfolio] = useState({ title: "", url: "" });
const [copied, setCopied] = useState(false);
@ -88,7 +101,9 @@ export function ProfileEditor({
const handleSubmit = async () => {
await onSave({
...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,
languages: formData.languages,
work_experience: formData.work_experience,
@ -206,7 +221,11 @@ export function ProfileEditor({
onClick={copyProfileUrl}
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>
</div>
</CardContent>
@ -221,7 +240,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Bio</label>
<textarea
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..."
className="w-full px-3 py-2 mt-1 border rounded-lg bg-background"
rows={4}
@ -233,7 +254,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Location</label>
<Input
value={formData.location}
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, location: e.target.value })
}
placeholder="City, Country"
/>
</div>
@ -241,7 +264,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Timezone</label>
<Input
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"
/>
</div>
@ -256,7 +281,9 @@ export function ProfileEditor({
<Input
type="number"
value={formData.hourly_rate}
onChange={(e) => setFormData({ ...formData, hourly_rate: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, hourly_rate: e.target.value })
}
placeholder="50"
/>
</div>
@ -268,7 +295,10 @@ export function ProfileEditor({
<select
value={formData.availability_status}
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"
>
@ -296,7 +326,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Twitter</label>
<Input
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"
/>
</div>
@ -304,7 +336,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">LinkedIn</label>
<Input
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"
/>
</div>
@ -312,7 +346,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">GitHub</label>
<Input
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"
/>
</div>
@ -320,7 +356,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Portfolio Website</label>
<Input
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"
/>
</div>
@ -328,7 +366,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">YouTube</label>
<Input
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"
/>
</div>
@ -336,7 +376,9 @@ export function ProfileEditor({
<label className="text-sm font-medium">Twitch</label>
<Input
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"
/>
</div>
@ -359,10 +401,15 @@ export function ProfileEditor({
<h3 className="font-semibold mb-3">Technical Skills</h3>
<div className="space-y-2 mb-4">
{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>
<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>
<Button
variant="ghost"
@ -378,7 +425,9 @@ export function ProfileEditor({
<div className="flex gap-2">
<Input
value={newSkill.name}
onChange={(e) => setNewSkill({ ...newSkill, name: e.target.value })}
onChange={(e) =>
setNewSkill({ ...newSkill, name: e.target.value })
}
placeholder="Add skill"
/>
<select
@ -386,7 +435,10 @@ export function ProfileEditor({
onChange={(e) =>
setNewSkill({
...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"
@ -406,7 +458,10 @@ export function ProfileEditor({
<h3 className="font-semibold mb-3">Languages</h3>
<div className="space-y-2 mb-4">
{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>
<Button
variant="ghost"
@ -453,8 +508,12 @@ export function ProfileEditor({
<div className="flex justify-between items-start">
<div>
<p className="font-medium">{exp.title}</p>
<p className="text-sm text-muted-foreground">{exp.company}</p>
<p className="text-xs text-muted-foreground">{exp.duration}</p>
<p className="text-sm text-muted-foreground">
{exp.company}
</p>
<p className="text-xs text-muted-foreground">
{exp.duration}
</p>
</div>
<Button
variant="ghost"
@ -466,7 +525,9 @@ export function ProfileEditor({
</Button>
</div>
{exp.description && (
<p className="text-sm text-muted-foreground">{exp.description}</p>
<p className="text-sm text-muted-foreground">
{exp.description}
</p>
)}
</div>
))}
@ -474,17 +535,23 @@ export function ProfileEditor({
<div className="space-y-2 p-3 border rounded-lg">
<Input
value={newWorkExp.title}
onChange={(e) => setNewWorkExp({ ...newWorkExp, title: e.target.value })}
onChange={(e) =>
setNewWorkExp({ ...newWorkExp, title: e.target.value })
}
placeholder="Job Title"
/>
<Input
value={newWorkExp.company}
onChange={(e) => setNewWorkExp({ ...newWorkExp, company: e.target.value })}
onChange={(e) =>
setNewWorkExp({ ...newWorkExp, company: e.target.value })
}
placeholder="Company"
/>
<Input
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)"
/>
<Button size="sm" onClick={addWorkExp} className="w-full">
@ -522,7 +589,9 @@ export function ProfileEditor({
</Button>
</div>
{item.description && (
<p className="text-sm text-muted-foreground">{item.description}</p>
<p className="text-sm text-muted-foreground">
{item.description}
</p>
)}
</div>
))}
@ -530,12 +599,16 @@ export function ProfileEditor({
<div className="space-y-2 p-3 border rounded-lg">
<Input
value={newPortfolio.title}
onChange={(e) => setNewPortfolio({ ...newPortfolio, title: e.target.value })}
onChange={(e) =>
setNewPortfolio({ ...newPortfolio, title: e.target.value })
}
placeholder="Project Title"
/>
<Input
value={newPortfolio.url}
onChange={(e) => setNewPortfolio({ ...newPortfolio, url: e.target.value })}
onChange={(e) =>
setNewPortfolio({ ...newPortfolio, url: e.target.value })
}
placeholder="Project URL"
/>
<Button size="sm" onClick={addPortfolio} className="w-full">
@ -557,7 +630,8 @@ export function ProfileEditor({
Arm Affiliations
</CardTitle>
<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>
</CardHeader>
<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 { CheckCircle, Clock, AlertCircle, Calendar, DollarSign } from "lucide-react";
import {
CheckCircle,
Clock,
AlertCircle,
Calendar,
DollarSign,
} from "lucide-react";
export interface Milestone {
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 completionPercentage = totalMilestones > 0 ? Math.round((completedMilestones / totalMilestones) * 100) : 0;
const completionPercentage =
totalMilestones > 0
? Math.round((completedMilestones / totalMilestones) * 100)
: 0;
const getStatusIcon = (status: string) => {
switch (status) {
@ -95,7 +111,9 @@ export function ProjectStatusWidget({
const getDaysRemaining = (dueDate: string) => {
const today = new Date();
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;
};
@ -112,32 +130,40 @@ export function ProjectStatusWidget({
{project.description && (
<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>
<p className="text-gray-400 text-xs uppercase">Start</p>
<div className="flex items-center gap-1 mt-1">
<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>
<p className="text-gray-400 text-xs uppercase">End</p>
<div className="flex items-center gap-1 mt-1">
<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>
<p className="text-gray-400 text-xs uppercase">Value</p>
<div className="flex items-center gap-1 mt-1">
<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>
<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>
@ -145,7 +171,9 @@ export function ProjectStatusWidget({
<div className="mt-4 space-y-2">
<div className="flex justify-between text-sm">
<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 className="w-full bg-black/50 rounded-full h-3">
<div
@ -153,7 +181,9 @@ export function ProjectStatusWidget({
style={{ width: `${completionPercentage}%` }}
/>
</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>
@ -164,7 +194,8 @@ export function ProjectStatusWidget({
<div className="space-y-3">
{project.milestones.map((milestone, idx) => {
const daysRemaining = getDaysRemaining(milestone.due_date);
const isOverdue = daysRemaining < 0 && milestone.status !== "completed";
const isOverdue =
daysRemaining < 0 && milestone.status !== "completed";
return (
<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 items-start justify-between gap-2">
<div>
<p className="font-semibold text-white">{milestone.title}</p>
<p className="font-semibold text-white">
{milestone.title}
</p>
{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 className="text-right flex-shrink-0">
{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"}`}>
{isOverdue ? `${Math.abs(daysRemaining)} days overdue` : `${daysRemaining} days remaining`}
<p
className={`text-xs ${isOverdue ? "text-red-400" : "text-gray-400"}`}
>
{isOverdue
? `${Math.abs(daysRemaining)} days overdue`
: `${daysRemaining} days remaining`}
</p>
</div>
</div>
@ -209,7 +250,9 @@ export function ProjectStatusWidget({
})}
</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>
</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 { 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 {
id: string;
@ -40,10 +52,26 @@ const colorMap = {
};
const statusMap = {
active: { label: "Active", color: "bg-green-600/50 text-green-100", 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 },
active: {
label: "Active",
color: "bg-green-600/50 text-green-100",
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({
@ -54,8 +82,8 @@ export function ResearchWidget({
accentColor = "yellow",
}: ResearchWidgetProps) {
const colors = colorMap[accentColor];
const activeCount = tracks.filter(t => t.status === "active").length;
const completedCount = tracks.filter(t => t.status === "completed").length;
const activeCount = tracks.filter((t) => t.status === "active").length;
const completedCount = tracks.filter((t) => t.status === "completed").length;
return (
<Card className={`${colors.bg} border ${colors.border}`}>
@ -82,11 +110,15 @@ export function ResearchWidget({
</div>
<div className="text-center">
<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 className="text-center">
<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>
@ -105,9 +137,13 @@ export function ResearchWidget({
{/* Header */}
<div className="flex items-start justify-between gap-4">
<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 && (
<p className="text-xs text-gray-400">{track.category}</p>
<p className="text-xs text-gray-400">
{track.category}
</p>
)}
</div>
<Badge className={statusInfo.color}>
@ -118,7 +154,9 @@ export function ResearchWidget({
{/* Description */}
{track.description && (
<p className="text-sm text-gray-400">{track.description}</p>
<p className="text-sm text-gray-400">
{track.description}
</p>
)}
{/* 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 { Clock, Zap, TrendingUp } from "lucide-react";
@ -66,11 +72,14 @@ export function SprintWidgetComponent({
const now = new 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 progress = sprint.tasks_total && sprint.tasks_total > 0
? Math.round(((sprint.tasks_completed || 0) / sprint.tasks_total) * 100)
: 0;
const progress =
sprint.tasks_total && sprint.tasks_total > 0
? Math.round(((sprint.tasks_completed || 0) / sprint.tasks_total) * 100)
: 0;
const formatCountdown = (days: number) => {
if (days < 0) return "Sprint Over";
@ -87,11 +96,21 @@ export function SprintWidgetComponent({
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5" />
{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>
<CardDescription>Sprint timeline and progress</CardDescription>
</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}
</Badge>
</div>
@ -106,12 +125,16 @@ export function SprintWidgetComponent({
<div className={`text-3xl font-bold ${colors.accent}`}>
{daysRemaining > 0 ? daysRemaining : 0} days
</div>
<p className="text-sm text-gray-400">{formatCountdown(daysRemaining)}</p>
<p className="text-sm text-gray-400">
{formatCountdown(daysRemaining)}
</p>
</div>
{/* Timeline */}
<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="p-2 bg-black/30 rounded border border-gray-500/10">
<p className="text-xs text-gray-400">Start</p>
@ -132,7 +155,9 @@ export function SprintWidgetComponent({
{sprint.tasks_total && sprint.tasks_total > 0 && (
<div className="space-y-2">
<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>
</div>
<div className="w-full bg-black/50 rounded-full h-3">
@ -142,7 +167,8 @@ export function SprintWidgetComponent({
/>
</div>
<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>
</div>
)}
@ -150,7 +176,9 @@ export function SprintWidgetComponent({
{/* Scope */}
{sprint.scope && (
<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>
</div>
)}
@ -159,8 +187,12 @@ export function SprintWidgetComponent({
{sprint.team_size && (
<div className="p-3 bg-black/30 rounded-lg border border-gray-500/10 flex items-center justify-between">
<div>
<p className="text-xs font-semibold text-gray-300 uppercase">Team Size</p>
<p className="text-sm text-gray-300 mt-1">{sprint.team_size} members</p>
<p className="text-xs font-semibold text-gray-300 uppercase">
Team Size
</p>
<p className="text-sm text-gray-300 mt-1">
{sprint.team_size} members
</p>
</div>
<TrendingUp className="h-5 w-5 text-gray-500" />
</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 { Button } from "@/components/ui/button";
import { Users, MessageCircle, ExternalLink } from "lucide-react";
@ -108,7 +114,9 @@ export function TeamWidget({
</div>
<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>
{member.title && (
<p className="text-xs text-gray-500">{member.title}</p>
@ -118,7 +126,9 @@ export function TeamWidget({
{/* 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 */}

View file

@ -14,7 +14,9 @@ export const armAffiliationService = {
/**
* 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 {
const { data: enrollments, error } = await supabase
.from("course_enrollments")
@ -23,7 +25,10 @@ export const armAffiliationService = {
.limit(1);
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) {
console.error("Error checking Foundation activity:", error);
return { detected: false, count: 0 };
@ -33,7 +38,9 @@ export const armAffiliationService = {
/**
* 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 {
const { data: projects, error: projectError } = await supabase
.from("gameforge_projects")
@ -62,7 +69,9 @@ export const armAffiliationService = {
/**
* 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 {
const { data: research, error } = await supabase
.from("labs_research_tracks")
@ -71,7 +80,10 @@ export const armAffiliationService = {
.limit(1);
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) {
console.error("Error checking Labs activity:", error);
return { detected: false, count: 0 };
@ -81,7 +93,9 @@ export const armAffiliationService = {
/**
* 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 {
// Corp activities could include partnerships, investments, or business accounts
const { data: accounts, error } = await supabase
@ -91,7 +105,10 @@ export const armAffiliationService = {
.limit(1);
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) {
// Corp table may not exist yet, return false
return { detected: false, count: 0 };
@ -101,7 +118,9 @@ export const armAffiliationService = {
/**
* 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 {
const { data: devLinkProfiles, error } = await supabase
.from("devlink_profiles")
@ -110,7 +129,10 @@ export const armAffiliationService = {
.limit(1);
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) {
// Table may not exist yet
return { detected: false, count: 0 };
@ -133,31 +155,41 @@ export const armAffiliationService = {
{
arm: "foundation",
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,
},
{
arm: "gameforge",
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,
},
{
arm: "labs",
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,
},
{
arm: "corp",
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,
},
{
arm: "devlink",
detected: devlink.detected,
reason: devlink.detected ? "DevLink profile created" : "No DevLink profile",
reason: devlink.detected
? "DevLink profile created"
: "No DevLink profile",
activityCount: devlink.count,
},
];
@ -172,22 +204,25 @@ export const armAffiliationService = {
for (const affiliation of detectedArms) {
if (affiliation.detected) {
await fetch(`${import.meta.env.VITE_API_BASE}/api/user/arm-affiliations`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
arm: affiliation.arm,
affiliation_type: "courses", // Generic type for auto-detected
affiliation_data: {
detected: true,
reason: affiliation.reason,
await fetch(
`${import.meta.env.VITE_API_BASE}/api/user/arm-affiliations`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
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) {

View file

@ -115,9 +115,20 @@ const ARMS = [
export default function Dashboard() {
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 [activeTab, setActiveTab] = useState(() => searchParams.get("tab") ?? "realms");
const [activeTab, setActiveTab] = useState(
() => searchParams.get("tab") ?? "realms",
);
const [displayName, setDisplayName] = useState("");
const [bio, setBio] = useState("");
const [website, setWebsite] = useState("");
@ -218,7 +229,10 @@ export default function Dashboard() {
Dashboard
</h1>
<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>
</div>
<div className="flex flex-wrap gap-2">
@ -229,7 +243,10 @@ export default function Dashboard() {
</Badge>
)}
{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" />
Level {profile.level}
</Badge>
@ -243,8 +260,13 @@ export default function Dashboard() {
<CardContent className="p-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<p className="font-semibold text-white">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>
<p className="font-semibold text-white">
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>
<Button
size="sm"
@ -260,7 +282,11 @@ export default function Dashboard() {
</div>
{/* 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">
<TabsTrigger value="realms" className="text-sm md:text-base">
<span className="hidden sm:inline">Realms</span>
@ -291,17 +317,26 @@ export default function Dashboard() {
}}
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">
<div className="flex items-start justify-between">
<div className="space-y-1">
<div className="flex items-center gap-3">
<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>
<h3 className="text-xl font-bold text-white">{arm.label}</h3>
<h3 className="text-xl font-bold text-white">
{arm.label}
</h3>
</div>
<p className="text-sm text-gray-400">{arm.description}</p>
<p className="text-sm text-gray-400">
{arm.description}
</p>
</div>
</div>
<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="absolute inset-0 rounded-full bg-gradient-to-r from-purple-500/30 to-blue-500/30 blur-lg" />
<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"
className="w-24 h-24 rounded-full ring-4 ring-purple-500/40 relative"
/>
</div>
<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>
<Badge className="bg-purple-600/50 text-purple-100 mx-auto">
<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">
<CardHeader>
<CardTitle>Edit Profile</CardTitle>
<CardDescription>Update your public profile information</CardDescription>
<CardDescription>
Update your public profile information
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
@ -429,14 +471,23 @@ export default function Dashboard() {
</TabsContent>
{/* 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">
<CardHeader>
<CardTitle>Connected Accounts</CardTitle>
<CardDescription>Link external accounts to your AeThex profile</CardDescription>
<CardDescription>
Link external accounts to your AeThex profile
</CardDescription>
</CardHeader>
<CardContent>
<OAuthConnections linkedProviders={linkedProviders} linkProvider={linkProvider} unlinkProvider={unlinkProvider} />
<OAuthConnections
linkedProviders={linkedProviders}
linkProvider={linkProvider}
unlinkProvider={unlinkProvider}
/>
</CardContent>
</Card>
</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">
<CardHeader>
<CardTitle>Primary Realm</CardTitle>
<CardDescription>Choose your primary area of focus</CardDescription>
<CardDescription>
Choose your primary area of focus
</CardDescription>
</CardHeader>
<CardContent>
<RealmSwitcher />

View file

@ -5,11 +5,26 @@ import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext";
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
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";
const API_BASE = import.meta.env.VITE_API_BASE || "";
@ -34,7 +49,9 @@ export default function DevLinkDashboard() {
const loadDashboardData = async () => {
try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_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`, {
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();
setProfile(data);
}
@ -54,7 +74,10 @@ export default function DevLinkDashboard() {
const oppRes = await fetch(`${API_BASE}/api/devlink/opportunities`, {
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();
setOpportunities(Array.isArray(data) ? data : []);
}
@ -66,7 +89,10 @@ export default function DevLinkDashboard() {
const teamsRes = await fetch(`${API_BASE}/api/devlink/teams`, {
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();
setTeams(Array.isArray(data) ? data : []);
}
@ -107,19 +133,33 @@ export default function DevLinkDashboard() {
return (
<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">
{/* Header */}
<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
</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>
{/* Tabs */}
<Tabs 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 }}>
<Tabs
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="profile">Profile Editor</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">
<CardContent className="p-6 space-y-2">
<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>
</Card>
<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">
<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>
</Card>
<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">
<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>
</Card>
</div>
@ -153,7 +199,12 @@ export default function DevLinkDashboard() {
<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">
<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
onClick={() => navigate("/dev-link/jobs")}
variant="outline"
@ -167,7 +218,12 @@ export default function DevLinkDashboard() {
</Card>
<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">
<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
onClick={() => navigate("/dev-link/teams")}
variant="outline"
@ -188,17 +244,30 @@ export default function DevLinkDashboard() {
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>My dev-link Profile Editor</span>
<Button 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" />}
<Button
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"}
</Button>
</CardTitle>
<CardDescription>Customize your Roblox portfolio</CardDescription>
<CardDescription>
Customize your Roblox portfolio
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Roblox Creations */}
<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 ? (
<textarea
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}
/>
) : (
<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>
{/* Experiences */}
<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 ? (
<textarea
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}
/>
) : (
<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>
{/* Certifications */}
<div className="space-y-3">
<h3 className="font-semibold text-white">EdTech Certifications</h3>
<h3 className="font-semibold text-white">
EdTech Certifications
</h3>
{isEditing ? (
<textarea
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}
/>
) : (
<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>
</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">
<CardHeader>
<CardTitle>Roblox Job Feed</CardTitle>
<CardDescription>Pre-filtered DEV-LINK opportunities</CardDescription>
<CardDescription>
Pre-filtered DEV-LINK opportunities
</CardDescription>
</CardHeader>
<CardContent>
{opportunities.length === 0 ? (
<div className="text-center py-12">
<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 className="space-y-3">
{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">
<h4 className="font-semibold text-white">{job.title}</h4>
<Badge className="bg-cyan-600/50 text-cyan-100">Roblox</Badge>
<h4 className="font-semibold text-white">
{job.title}
</h4>
<Badge className="bg-cyan-600/50 text-cyan-100">
Roblox
</Badge>
</div>
<p className="text-sm text-gray-400">{job.description?.substring(0, 100)}...</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">
<p className="text-sm text-gray-400">
{job.description?.substring(0, 100)}...
</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" />
</Button>
</div>
@ -281,14 +380,16 @@ export default function DevLinkDashboard() {
{/* Teams Tab */}
<TabsContent value="teams" className="space-y-4 animate-fade-in">
<TeamWidget
members={teams.flatMap((t: any) => (t.members || []).map((m: any) => ({
id: m.id,
name: m.full_name,
role: m.role || "Member",
type: m.role === "lead" ? "lead" : "member",
avatar: m.avatar_url,
team_name: t.name,
})))}
members={teams.flatMap((t: any) =>
(t.members || []).map((m: any) => ({
id: m.id,
name: m.full_name,
role: m.role || "Member",
type: m.role === "lead" ? "lead" : "member",
avatar: m.avatar_url,
team_name: t.name,
})),
)}
title="My dev-link Teams"
description="Find and manage Roblox development teams"
accentColor="cyan"

View file

@ -54,7 +54,9 @@ export default function FoundationDashboard() {
const loadDashboardData = async () => {
try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_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`, {
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();
setCourses(Array.isArray(data) ? data : []);
}
@ -71,10 +76,16 @@ export default function FoundationDashboard() {
}
try {
const mentorRes = await fetch(`${API_BASE}/api/foundation/mentorships`, {
headers: { Authorization: `Bearer ${token}` },
});
if (mentorRes.ok && mentorRes.headers.get("content-type")?.includes("application/json")) {
const mentorRes = await fetch(
`${API_BASE}/api/foundation/mentorships`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (
mentorRes.ok &&
mentorRes.headers.get("content-type")?.includes("application/json")
) {
const data = await mentorRes.json();
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">
Join FOUNDATION
</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
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"
@ -114,18 +127,25 @@ export default function FoundationDashboard() {
}
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");
return (
<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">
{/* Header */}
<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="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
</h1>
<p className="text-gray-400 text-lg">
@ -140,8 +160,12 @@ export default function FoundationDashboard() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Courses Enrolled</p>
<p className="text-2xl font-bold text-white mt-1">{enrolledCourses.length}</p>
<p className="text-xs text-gray-400 uppercase tracking-wider">
Courses Enrolled
</p>
<p className="text-2xl font-bold text-white mt-1">
{enrolledCourses.length}
</p>
</div>
<BookOpen className="h-6 w-6 text-red-500 opacity-50" />
</div>
@ -152,8 +176,12 @@ export default function FoundationDashboard() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Completed</p>
<p className="text-2xl font-bold text-white mt-1">{completedCourses.length}</p>
<p className="text-xs text-gray-400 uppercase tracking-wider">
Completed
</p>
<p className="text-2xl font-bold text-white mt-1">
{completedCourses.length}
</p>
</div>
<CheckCircle className="h-6 w-6 text-red-500 opacity-50" />
</div>
@ -164,27 +192,38 @@ export default function FoundationDashboard() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Achievements</p>
<p className="text-2xl font-bold text-white mt-1">{achievements.length}</p>
<p className="text-xs text-gray-400 uppercase tracking-wider">
Achievements
</p>
<p className="text-2xl font-bold text-white mt-1">
{achievements.length}
</p>
</div>
<Award className="h-6 w-6 text-red-500 opacity-50" />
</div>
</CardContent>
</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">
<div className="flex items-center justify-between">
<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">
{activeMentor ? '✓' : '—'}
{activeMentor ? "✓" : "—"}
</p>
</div>
<Users className="h-6 w-6" style={{
color: activeMentor ? '#22c55e' : '#666',
opacity: 0.5
}} />
<Users
className="h-6 w-6"
style={{
color: activeMentor ? "#22c55e" : "#666",
opacity: 0.5,
}}
/>
</div>
</CardContent>
</Card>
@ -192,8 +231,15 @@ export default function FoundationDashboard() {
</div>
{/* Tabs */}
<Tabs 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 }}>
<Tabs
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="courses">Courses</TabsTrigger>
<TabsTrigger value="mentorship">Mentorship</TabsTrigger>
@ -213,7 +259,9 @@ export default function FoundationDashboard() {
expertise: m.mentor?.role_title,
},
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,
nextSession: m.next_session_date,
}))}
@ -269,7 +317,10 @@ export default function FoundationDashboard() {
</TabsContent>
{/* Mentorship Tab */}
<TabsContent value="mentorship" className="space-y-4 animate-fade-in">
<TabsContent
value="mentorship"
className="space-y-4 animate-fade-in"
>
<MentorshipWidget
mentorships={mentorships.map((m: any) => ({
id: m.id,
@ -280,7 +331,9 @@ export default function FoundationDashboard() {
expertise: m.mentor?.role_title,
},
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,
nextSession: m.next_session_date,
}))}
@ -292,7 +345,10 @@ export default function FoundationDashboard() {
</TabsContent>
{/* Achievements Tab */}
<TabsContent value="achievements" className="space-y-4 animate-fade-in">
<TabsContent
value="achievements"
className="space-y-4 animate-fade-in"
>
<AchievementsWidget
achievements={achievements.map((a: any) => ({
id: a.id,

View file

@ -5,11 +5,26 @@ import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext";
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
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 { TeamWidget } from "@/components/TeamWidget";
@ -36,7 +51,9 @@ export default function GameForgeDashboard() {
const loadDashboardData = async () => {
try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_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`, {
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();
setSprint(data);
}
@ -56,7 +76,10 @@ export default function GameForgeDashboard() {
const teamRes = await fetch(`${API_BASE}/api/gameforge/team`, {
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();
setTeam(Array.isArray(data) ? data : []);
}
@ -68,7 +91,10 @@ export default function GameForgeDashboard() {
const tasksRes = await fetch(`${API_BASE}/api/gameforge/tasks`, {
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();
setTasks(Array.isArray(data) ? data : []);
}
@ -95,9 +121,9 @@ export default function GameForgeDashboard() {
}, [sprint]);
const tasksByStatus = {
todo: tasks.filter(t => t.status === "todo"),
inprogress: tasks.filter(t => t.status === "in_progress"),
done: tasks.filter(t => t.status === "done"),
todo: tasks.filter((t) => t.status === "todo"),
inprogress: tasks.filter((t) => t.status === "in_progress"),
done: tasks.filter((t) => t.status === "done"),
};
if (authLoading || loading) {
@ -127,13 +153,18 @@ export default function GameForgeDashboard() {
return (
<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">
{sprint ? (
<>
{/* Active Sprint Header */}
<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
</h1>
<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">
<CardContent className="p-6">
<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="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>
</div>
<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>
</div>
<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>
</div>
<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>
</div>
</div>
@ -169,8 +210,15 @@ export default function GameForgeDashboard() {
</div>
{/* Tabs */}
<Tabs 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 }}>
<Tabs
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="scope">Scope</TabsTrigger>
<TabsTrigger value="team">Team</TabsTrigger>
@ -178,24 +226,33 @@ export default function GameForgeDashboard() {
</TabsList>
{/* 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">
<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">
<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>
</Card>
<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">
<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>
</Card>
<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">
<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>
</Card>
</div>
@ -203,8 +260,15 @@ export default function GameForgeDashboard() {
{/* Submit Build CTA */}
<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">
<h3 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>
<h3
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
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"
@ -218,15 +282,22 @@ export default function GameForgeDashboard() {
</TabsContent>
{/* 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">
<CardHeader>
<CardTitle>The Scope Anchor (KND-001)</CardTitle>
<CardDescription>Your north star - prevent feature creep</CardDescription>
<CardDescription>
Your north star - prevent feature creep
</CardDescription>
</CardHeader>
<CardContent>
<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>
</CardContent>
</Card>
@ -255,21 +326,35 @@ export default function GameForgeDashboard() {
</TabsContent>
{/* 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">
{/* To Do */}
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20">
<CardHeader>
<CardTitle className="text-lg">To Do ({tasksByStatus.todo.length})</CardTitle>
<CardTitle className="text-lg">
To Do ({tasksByStatus.todo.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{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) => (
<div key={task.id} 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
key={task.id}
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>
))
)}
@ -279,16 +364,27 @@ export default function GameForgeDashboard() {
{/* In Progress */}
<Card className="bg-gradient-to-br from-yellow-950/40 to-yellow-900/20 border-yellow-500/20">
<CardHeader>
<CardTitle className="text-lg">In Progress ({tasksByStatus.inprogress.length})</CardTitle>
<CardTitle className="text-lg">
In Progress ({tasksByStatus.inprogress.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{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) => (
<div key={task.id} 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
key={task.id}
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>
))
)}
@ -298,19 +394,30 @@ export default function GameForgeDashboard() {
{/* Done */}
<Card className="bg-gradient-to-br from-green-950/40 to-green-900/20 border-green-500/20">
<CardHeader>
<CardTitle className="text-lg">Done ({tasksByStatus.done.length})</CardTitle>
<CardTitle className="text-lg">
Done ({tasksByStatus.done.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{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) => (
<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">
<CheckCircle className="h-4 w-4 text-green-500" />
<div className="flex-1">
<p className="font-semibold text-white text-sm line-through">{task.title}</p>
<p className="text-xs text-gray-400 mt-1">{task.assigned_to?.full_name}</p>
<p className="font-semibold text-white text-sm line-through">
{task.title}
</p>
<p className="text-xs text-gray-400 mt-1">
{task.assigned_to?.full_name}
</p>
</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">
<CardContent className="p-12 text-center space-y-4">
<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>
<Button onClick={() => navigate("/gameforge")}>Browse GAMEFORGE</Button>
<p className="text-gray-400">
No active sprint. Join one to get started!
</p>
<Button onClick={() => navigate("/gameforge")}>
Browse GAMEFORGE
</Button>
</CardContent>
</Card>
)}

View file

@ -5,11 +5,26 @@ import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext";
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
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";
const API_BASE = import.meta.env.VITE_API_BASE || "";
@ -35,7 +50,9 @@ export default function LabsDashboard() {
const loadDashboardData = async () => {
try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_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`, {
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();
setResearchTracks(Array.isArray(data) ? data : []);
}
@ -55,7 +75,10 @@ export default function LabsDashboard() {
const bountiesRes = await fetch(`${API_BASE}/api/labs/bounties`, {
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();
setBounties(Array.isArray(data) ? data : []);
}
@ -67,7 +90,10 @@ export default function LabsDashboard() {
const pubRes = await fetch(`${API_BASE}/api/labs/publications`, {
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();
setPublications(Array.isArray(data) ? data : []);
}
@ -79,7 +105,10 @@ export default function LabsDashboard() {
const ipRes = await fetch(`${API_BASE}/api/labs/ip-portfolio`, {
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();
setIpPortfolio(data);
setIsAdmin(data?.is_admin || false);
@ -121,19 +150,33 @@ export default function LabsDashboard() {
return (
<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">
{/* Header */}
<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
</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>
{/* Tabs */}
<Tabs 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 }}>
<Tabs
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="tracks">Tracks</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">
<CardContent className="p-6 space-y-2">
<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>
</Card>
<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">
<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>
</Card>
<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">
<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>
</Card>
</div>
@ -171,12 +220,24 @@ export default function LabsDashboard() {
</CardHeader>
<CardContent className="space-y-3">
{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">
<FileText className="h-5 w-5 text-amber-500 flex-shrink-0 mt-1" />
<div className="flex-1 min-w-0">
<p className="font-semibold text-white truncate">{pub.title}</p>
<p className="text-xs text-gray-400 mt-1">{new Date(pub.published_date).toLocaleDateString()}</p>
<p className="font-semibold text-white truncate">
{pub.title}
</p>
<p className="text-xs text-gray-400 mt-1">
{new Date(
pub.published_date,
).toLocaleDateString()}
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500 flex-shrink-0" />
</div>
@ -189,8 +250,15 @@ export default function LabsDashboard() {
{/* Submit Research Proposal CTA */}
<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">
<h3 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>
<h3
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
onClick={() => navigate("/labs/submit-proposal")}
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">
<CardHeader>
<CardTitle>Research Bounties</CardTitle>
<CardDescription>High-difficulty opportunities from NEXUS</CardDescription>
<CardDescription>
High-difficulty opportunities from NEXUS
</CardDescription>
</CardHeader>
<CardContent>
{bounties.length === 0 ? (
@ -238,13 +308,26 @@ export default function LabsDashboard() {
) : (
<div className="space-y-3">
{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">
<h4 className="font-semibold text-white">{bounty.title}</h4>
<p className="text-lg font-bold text-amber-400">${bounty.reward?.toLocaleString()}</p>
<h4 className="font-semibold text-white">
{bounty.title}
</h4>
<p className="text-lg font-bold text-amber-400">
${bounty.reward?.toLocaleString()}
</p>
</div>
<p className="text-sm text-gray-400">{bounty.description}</p>
<Button size="sm" variant="outline" className="mt-3 border-amber-500/30 text-amber-300 hover:bg-amber-500/10">
<p className="text-sm text-gray-400">
{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" />
</Button>
</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">
<CardHeader>
<CardTitle>Publication Pipeline</CardTitle>
<CardDescription>Upcoming whitepapers and technical blog posts</CardDescription>
<CardDescription>
Upcoming whitepapers and technical blog posts
</CardDescription>
</CardHeader>
<CardContent>
{publications.length === 0 ? (
@ -271,15 +356,33 @@ export default function LabsDashboard() {
) : (
<div className="space-y-3">
{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">
<h4 className="font-semibold text-white">{pub.title}</h4>
<Badge className={pub.status === "published" ? "bg-green-600/50 text-green-100" : "bg-blue-600/50 text-blue-100"}>
<h4 className="font-semibold text-white">
{pub.title}
</h4>
<Badge
className={
pub.status === "published"
? "bg-green-600/50 text-green-100"
: "bg-blue-600/50 text-blue-100"
}
>
{pub.status}
</Badge>
</div>
<p className="text-sm text-gray-400">{pub.description}</p>
<p className="text-xs text-gray-500 mt-2">{new Date(pub.published_date).toLocaleDateString()}</p>
<p className="text-sm text-gray-400">
{pub.description}
</p>
<p className="text-xs text-gray-500 mt-2">
{new Date(pub.published_date).toLocaleDateString()}
</p>
</a>
))}
</div>
@ -302,23 +405,33 @@ export default function LabsDashboard() {
<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">
<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 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-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 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-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 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-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>
) : (
<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>
</Card>

View file

@ -63,7 +63,9 @@ export default function NexusDashboard() {
const loadDashboardData = async () => {
try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token;
if (!token) throw new Error("No auth token");
@ -76,54 +78,72 @@ export default function NexusDashboard() {
}
// Load applications
const appRes = await fetch(`${API_BASE}/api/nexus/creator/applications?limit=10`, {
headers: { Authorization: `Bearer ${token}` },
});
const appRes = await fetch(
`${API_BASE}/api/nexus/creator/applications?limit=10`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (appRes.ok) {
const data = await appRes.json();
setApplications(data.applications || []);
}
// Load contracts
const contractRes = await fetch(`${API_BASE}/api/nexus/creator/contracts?limit=10`, {
headers: { Authorization: `Bearer ${token}` },
});
const contractRes = await fetch(
`${API_BASE}/api/nexus/creator/contracts?limit=10`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (contractRes.ok) {
const data = await contractRes.json();
setContracts(data.contracts || []);
}
// Load payout info
const payoutRes = await fetch(`${API_BASE}/api/nexus/creator/payouts?limit=10`, {
headers: { Authorization: `Bearer ${token}` },
});
const payoutRes = await fetch(
`${API_BASE}/api/nexus/creator/payouts?limit=10`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (payoutRes.ok) {
const data = await payoutRes.json();
setPayoutInfo(data.summary);
}
// Load client data (posted opportunities)
const oppRes = await fetch(`${API_BASE}/api/nexus/client/opportunities?limit=10`, {
headers: { Authorization: `Bearer ${token}` },
});
const oppRes = await fetch(
`${API_BASE}/api/nexus/client/opportunities?limit=10`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (oppRes.ok) {
const data = await oppRes.json();
setPostedOpportunities(data.opportunities || []);
}
// Load applicants
const appliRes = await fetch(`${API_BASE}/api/nexus/client/applicants?limit=50`, {
headers: { Authorization: `Bearer ${token}` },
});
const appliRes = await fetch(
`${API_BASE}/api/nexus/client/applicants?limit=50`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (appliRes.ok) {
const data = await appliRes.json();
setApplicants(data.applicants || []);
}
// Load payment history
const payHistRes = await fetch(`${API_BASE}/api/nexus/client/payment-history?limit=10`, {
headers: { Authorization: `Bearer ${token}` },
});
const payHistRes = await fetch(
`${API_BASE}/api/nexus/client/payment-history?limit=10`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (payHistRes.ok) {
const data = await payHistRes.json();
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">
Sign In to NEXUS
</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
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"
@ -163,10 +185,16 @@ export default function NexusDashboard() {
);
}
const isProfileComplete = creatorProfile?.verified || (creatorProfile?.headline && creatorProfile?.skills?.length > 0);
const pendingApplications = applications.filter((a) => a.status === "submitted").length;
const isProfileComplete =
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 openOpportunities = postedOpportunities.filter((o) => o.status === "open").length;
const openOpportunities = postedOpportunities.filter(
(o) => o.status === "open",
).length;
const applicantStats = {
applied: applicants.filter((a) => a.status === "applied").length,
interviewing: applicants.filter((a) => a.status === "interviewing").length,
@ -200,9 +228,17 @@ export default function NexusDashboard() {
setViewMode("creator");
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
</Button>
<Button
@ -212,9 +248,15 @@ export default function NexusDashboard() {
setViewMode("client");
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
</Button>
</div>
@ -231,7 +273,8 @@ export default function NexusDashboard() {
Complete Your NEXUS Profile
</p>
<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>
</div>
<Button
@ -251,7 +294,11 @@ export default function NexusDashboard() {
{viewMode === "creator" && (
<>
{/* 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">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="applications">Applications</TabsTrigger>
@ -260,17 +307,26 @@ export default function NexusDashboard() {
</TabsList>
{/* 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">
{/* Stat: Total Earnings */}
<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">
<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" />
</div>
<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>
</CardContent>
</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">
<CardContent className="p-6 space-y-2">
<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" />
</div>
<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>
</CardContent>
</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">
<CardContent className="p-6 space-y-2">
<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" />
</div>
<p className="text-3xl font-bold text-white">{pendingApplications}</p>
<p className="text-3xl font-bold text-white">
{pendingApplications}
</p>
</CardContent>
</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">
<CardContent className="p-6 space-y-2">
<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" />
</div>
<p className="text-3xl font-bold text-white">{activeContracts}</p>
<p className="text-3xl font-bold text-white">
{activeContracts}
</p>
</CardContent>
</Card>
</div>
@ -321,7 +391,10 @@ export default function NexusDashboard() {
{applications.length === 0 ? (
<div className="text-center py-8 space-y-4">
<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
onClick={() => navigate("/nexus")}
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">
{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">
<p className="font-semibold text-white">{app.opportunity?.title}</p>
<p className="text-sm text-gray-400">{app.opportunity?.category}</p>
<p className="font-semibold text-white">
{app.opportunity?.title}
</p>
<p className="text-sm text-gray-400">
{app.opportunity?.category}
</p>
</div>
<Badge variant={
app.status === "accepted" ? "default" :
app.status === "rejected" ? "destructive" :
"secondary"
}>
<Badge
variant={
app.status === "accepted"
? "default"
: app.status === "rejected"
? "destructive"
: "secondary"
}
>
{app.status}
</Badge>
</div>
@ -357,8 +441,13 @@ export default function NexusDashboard() {
<CardContent className="p-8">
<div className="space-y-6">
<div className="text-center space-y-2">
<h3 className="text-2xl font-bold text-white">Maximize Your Earnings</h3>
<p className="text-gray-300">Complete your profile and start bidding on opportunities</p>
<h3 className="text-2xl font-bold text-white">
Maximize Your Earnings
</h3>
<p className="text-gray-300">
Complete your profile and start bidding on
opportunities
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Button
@ -382,7 +471,10 @@ export default function NexusDashboard() {
</TabsContent>
{/* Applications Tab */}
<TabsContent value="applications" className="space-y-4 animate-fade-in">
<TabsContent
value="applications"
className="space-y-4 animate-fade-in"
>
<ApplicationsWidget
applications={applications.map((a: any) => ({
id: a.id,
@ -408,7 +500,10 @@ export default function NexusDashboard() {
</TabsContent>
{/* Contracts Tab */}
<TabsContent value="contracts" className="space-y-4 animate-fade-in">
<TabsContent
value="contracts"
className="space-y-4 animate-fade-in"
>
<ContractsWidget
contracts={contracts.map((c: any) => ({
id: c.id,
@ -416,7 +511,11 @@ export default function NexusDashboard() {
client_name: c.client?.full_name || "Client",
status: c.status || "active",
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,
end_date: c.end_date,
description: c.description,
@ -430,16 +529,23 @@ export default function NexusDashboard() {
</TabsContent>
{/* 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">
<CardHeader>
<CardTitle>Your NEXUS Profile</CardTitle>
<CardDescription>Your marketplace identity</CardDescription>
<CardDescription>
Your marketplace identity
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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
type="text"
value={creatorProfile?.headline || ""}
@ -450,9 +556,13 @@ export default function NexusDashboard() {
</div>
<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
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"
disabled
>
@ -464,7 +574,9 @@ export default function NexusDashboard() {
</div>
<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
type="number"
value={creatorProfile?.hourly_rate || ""}
@ -475,9 +587,13 @@ export default function NexusDashboard() {
</div>
<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
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"
disabled
>
@ -492,7 +608,9 @@ export default function NexusDashboard() {
{creatorProfile?.verified && (
<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" />
<span className="text-sm text-green-200">Profile Verified </span>
<span className="text-sm text-green-200">
Profile Verified
</span>
</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">
<CardHeader>
<CardTitle>Payout Information</CardTitle>
<CardDescription>Manage how you receive payments</CardDescription>
<CardDescription>
Manage how you receive payments
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
>
<Button className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
Connect Stripe Account
<ExternalLink className="h-4 w-4 ml-2" />
</Button>
<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>
</CardContent>
</Card>
@ -528,7 +647,11 @@ export default function NexusDashboard() {
{/* Client View */}
{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">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="opportunities">Opportunities</TabsTrigger>
@ -537,16 +660,23 @@ export default function NexusDashboard() {
</TabsList>
{/* 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">
{/* Stat: Open Opportunities */}
<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">
<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" />
</div>
<p className="text-3xl font-bold text-white">{openOpportunities}</p>
<p className="text-3xl font-bold text-white">
{openOpportunities}
</p>
</CardContent>
</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">
<CardContent className="p-6 space-y-2">
<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" />
</div>
<p className="text-3xl font-bold text-white">{applicants.length}</p>
<p className="text-3xl font-bold text-white">
{applicants.length}
</p>
</CardContent>
</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">
<CardContent className="p-6 space-y-2">
<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" />
</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>
</Card>
@ -580,7 +721,12 @@ export default function NexusDashboard() {
<DollarSign className="h-5 w-5 text-orange-500" />
</div>
<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>
</CardContent>
</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">
<CardContent className="p-4 text-center space-y-2">
<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>
</Card>
<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">
<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>
</Card>
<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">
<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>
</Card>
</div>
@ -611,9 +763,12 @@ export default function NexusDashboard() {
{/* CTA Section */}
<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">
<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">
Post opportunities and find the perfect creators for your projects
Post opportunities and find the perfect creators for
your projects
</p>
<Button
onClick={() => navigate("/opportunities/post")}
@ -627,7 +782,10 @@ export default function NexusDashboard() {
</TabsContent>
{/* Opportunities Tab */}
<TabsContent value="opportunities" className="space-y-4 animate-fade-in">
<TabsContent
value="opportunities"
className="space-y-4 animate-fade-in"
>
<PostedOpportunitiesWidget
opportunities={postedOpportunities.map((o: any) => ({
id: o.id,
@ -659,8 +817,13 @@ export default function NexusDashboard() {
{postedOpportunities.length === 0 && (
<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">
<h3 className="text-2xl font-bold text-white">Start Hiring Talent</h3>
<p className="text-gray-300">Post opportunities and find the perfect creators for your projects</p>
<h3 className="text-2xl font-bold text-white">
Start Hiring Talent
</h3>
<p className="text-gray-300">
Post opportunities and find the perfect creators for
your projects
</p>
<Button
onClick={() => navigate("/opportunities/post")}
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>
{/* 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
applicants={applicants.map((a: any) => ({
id: a.id,
@ -704,7 +870,10 @@ export default function NexusDashboard() {
</TabsContent>
{/* Contracts Tab */}
<TabsContent value="contracts" className="space-y-4 animate-fade-in">
<TabsContent
value="contracts"
className="space-y-4 animate-fade-in"
>
<ContractsWidget
contracts={contracts.map((c: any) => ({
id: c.id,
@ -712,7 +881,11 @@ export default function NexusDashboard() {
creator_name: c.creator?.full_name || "Creator",
status: c.status || "active",
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,
end_date: c.end_date,
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">
<CardHeader>
<CardTitle>Payment History</CardTitle>
<CardDescription>Recent payments made to creators</CardDescription>
<CardDescription>
Recent payments made to creators
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{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">
<p className="font-semibold text-white text-sm">{payment.description}</p>
<p className="text-xs text-gray-400">{new Date(payment.created_at).toLocaleDateString()}</p>
<p className="font-semibold text-white text-sm">
{payment.description}
</p>
<p className="text-xs text-gray-400">
{new Date(
payment.created_at,
).toLocaleDateString()}
</p>
</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>

View file

@ -5,11 +5,29 @@ import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext";
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
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";
const API_BASE = import.meta.env.VITE_API_BASE || "";
@ -35,7 +53,9 @@ export default function StaffDashboard() {
const loadDashboardData = async () => {
try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_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`, {
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();
setStaffMember(data);
}
@ -55,7 +78,10 @@ export default function StaffDashboard() {
const okrRes = await fetch(`${API_BASE}/api/staff/okrs`, {
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();
setOkrs(Array.isArray(data) ? data : []);
}
@ -67,7 +93,10 @@ export default function StaffDashboard() {
const invRes = await fetch(`${API_BASE}/api/staff/invoices`, {
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();
setInvoices(Array.isArray(data) ? data : []);
}
@ -79,7 +108,10 @@ export default function StaffDashboard() {
const dirRes = await fetch(`${API_BASE}/api/staff/directory`, {
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();
setDirectory(Array.isArray(data) ? data : []);
}
@ -95,9 +127,10 @@ export default function StaffDashboard() {
const isEmployee = staffMember?.employment_type === "employee";
const isContractor = staffMember?.employment_type === "contractor";
const filteredDirectory = directory.filter(member =>
member.full_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
member.role?.toLowerCase().includes(searchQuery.toLowerCase())
const filteredDirectory = directory.filter(
(member) =>
member.full_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
member.role?.toLowerCase().includes(searchQuery.toLowerCase()),
);
if (authLoading || loading) {
@ -127,23 +160,41 @@ export default function StaffDashboard() {
return (
<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">
{/* Header */}
<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
</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>
{/* Tabs */}
<Tabs 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 }}>
<Tabs
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>
{isEmployee && <TabsTrigger value="okrs">OKRs</TabsTrigger>}
{isEmployee && <TabsTrigger value="benefits">Pay & Benefits</TabsTrigger>}
{isContractor && <TabsTrigger value="invoices">Invoices</TabsTrigger>}
{isEmployee && (
<TabsTrigger value="benefits">Pay & Benefits</TabsTrigger>
)}
{isContractor && (
<TabsTrigger value="invoices">Invoices</TabsTrigger>
)}
<TabsTrigger value="directory">Directory</TabsTrigger>
</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">
<CardContent className="p-6 space-y-2">
<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>
</Card>
<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">
<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>
</Card>
<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">
<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>
</Card>
</div>
@ -173,7 +232,9 @@ export default function StaffDashboard() {
{/* Quick Links */}
<Card className="bg-gradient-to-br from-purple-950/40 to-purple-900/20 border-purple-500/20">
<CardHeader>
<CardTitle style={{ fontFamily: theme.fontFamily }}>Quick Actions</CardTitle>
<CardTitle style={{ fontFamily: theme.fontFamily }}>
Quick Actions
</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-3">
<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">
<CardHeader>
<CardTitle>My OKRs</CardTitle>
<CardDescription>Quarterly Objectives & Key Results</CardDescription>
<CardDescription>
Quarterly Objectives & Key Results
</CardDescription>
</CardHeader>
<CardContent>
{okrs.length === 0 ? (
<div className="text-center py-12">
<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 className="space-y-4">
{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-1">
<h4 className="font-semibold text-white">{okr.objective}</h4>
<p className="text-sm text-gray-400 mt-1">{okr.description}</p>
<h4 className="font-semibold text-white">
{okr.objective}
</h4>
<p className="text-sm text-gray-400 mt-1">
{okr.description}
</p>
</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}
</Badge>
</div>
<div className="space-y-2">
{okr.key_results?.map((kr: any) => (
<div key={kr.id} className="flex items-start gap-3 text-sm">
<span className="text-purple-400 mt-1"></span>
<div
key={kr.id}
className="flex items-start gap-3 text-sm"
>
<span className="text-purple-400 mt-1">
</span>
<div className="flex-1">
<p className="text-white">{kr.title}</p>
<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 */}
{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">
<CardHeader>
<CardTitle>Pay & Benefits</CardTitle>
<CardDescription>Payroll and compensation information</CardDescription>
<CardDescription>
Payroll and compensation information
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<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-3xl font-bold text-white">${staffMember?.salary?.toLocaleString() || "—"}</p>
<p className="text-3xl font-bold text-white">
${staffMember?.salary?.toLocaleString() || "—"}
</p>
</div>
<Button className="w-full bg-purple-600 hover:bg-purple-700">
<ExternalLink className="h-4 w-4 mr-2" />
Open Rippling (Payroll System)
</Button>
<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>
</CardContent>
</Card>
@ -290,11 +381,16 @@ export default function StaffDashboard() {
{/* Invoices Tab - Contractor Only */}
{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">
<CardHeader>
<CardTitle>My Invoices</CardTitle>
<CardDescription>SOP-301: Contractor Invoice Portal</CardDescription>
<CardDescription>
SOP-301: Contractor Invoice Portal
</CardDescription>
</CardHeader>
<CardContent>
{invoices.length === 0 ? (
@ -305,21 +401,32 @@ export default function StaffDashboard() {
) : (
<div className="space-y-3">
{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-1">
<p className="font-semibold text-white">{invoice.invoice_number}</p>
<p className="text-xs text-gray-400 mt-1">{new Date(invoice.date).toLocaleDateString()}</p>
<p className="font-semibold text-white">
{invoice.invoice_number}
</p>
<p className="text-xs text-gray-400 mt-1">
{new Date(invoice.date).toLocaleDateString()}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-white">${invoice.amount?.toLocaleString()}</p>
<Badge className={
invoice.status === "paid"
? "bg-green-600/50 text-green-100"
: invoice.status === "pending"
? "bg-yellow-600/50 text-yellow-100"
: "bg-blue-600/50 text-blue-100"
}>
<p className="font-semibold text-white">
${invoice.amount?.toLocaleString()}
</p>
<Badge
className={
invoice.status === "paid"
? "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}
</Badge>
</div>
@ -334,7 +441,10 @@ export default function StaffDashboard() {
)}
{/* Directory Tab */}
<TabsContent value="directory" className="space-y-4 animate-fade-in">
<TabsContent
value="directory"
className="space-y-4 animate-fade-in"
>
<DirectoryWidget
members={directory.map((m: any) => ({
id: m.id,
@ -345,7 +455,10 @@ export default function StaffDashboard() {
phone: m.phone,
location: m.location,
avatar_url: m.avatar_url,
employment_type: m.employment_type === "employee" ? "employee" : "contractor",
employment_type:
m.employment_type === "employee"
? "employee"
: "contractor",
}))}
title="Internal Directory"
description="Find employees and contractors"

View file

@ -54,14 +54,19 @@ export default function ClientHub() {
const loadDashboardData = async () => {
try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token;
if (!token) throw new Error("No auth token");
// Load contracts for milestone tracking
const contractRes = await fetch(`${API_BASE}/api/corp/contracts?limit=10`, {
headers: { Authorization: `Bearer ${token}` },
});
const contractRes = await fetch(
`${API_BASE}/api/corp/contracts?limit=10`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (contractRes.ok) {
const data = await contractRes.json();
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">
CORP Client Portal
</h1>
<p className="text-gray-400">Enterprise solutions for your business</p>
<p className="text-gray-400">
Enterprise solutions for your business
</p>
<Button
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"
@ -120,13 +127,22 @@ export default function ClientHub() {
);
}
const activeContract = contracts.find(c => c.status === "active");
const completedMilestones = activeContract?.milestones?.filter((m: any) => m.status === "completed").length || 0;
const activeContract = contracts.find((c) => c.status === "active");
const completedMilestones =
activeContract?.milestones?.filter((m: any) => m.status === "completed")
.length || 0;
const totalMilestones = activeContract?.milestones?.length || 0;
const outstandingInvoices = invoices.filter((i: any) => i.status === "pending" || i.status === "overdue").length;
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");
const outstandingInvoices = invoices.filter(
(i: any) => i.status === "pending" || i.status === "overdue",
).length;
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 (
<Layout>
@ -151,8 +167,12 @@ export default function ClientHub() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Active Projects</p>
<p className="text-2xl font-bold text-white mt-1">{contracts.filter(c => c.status === "active").length}</p>
<p className="text-xs text-gray-400 uppercase tracking-wider">
Active Projects
</p>
<p className="text-2xl font-bold text-white mt-1">
{contracts.filter((c) => c.status === "active").length}
</p>
</div>
<Briefcase className="h-6 w-6 text-blue-500 opacity-50" />
</div>
@ -163,25 +183,38 @@ export default function ClientHub() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Total Invoices</p>
<p className="text-2xl font-bold text-white mt-1">${(totalInvoiceValue / 1000).toFixed(0)}k</p>
<p className="text-xs text-gray-400 uppercase tracking-wider">
Total Invoices
</p>
<p className="text-2xl font-bold text-white mt-1">
${(totalInvoiceValue / 1000).toFixed(0)}k
</p>
</div>
<DollarSign className="h-6 w-6 text-cyan-500 opacity-50" />
</div>
</CardContent>
</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">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Outstanding</p>
<p className="text-2xl font-bold text-white mt-1">{outstandingInvoices}</p>
<p className="text-xs text-gray-400 uppercase tracking-wider">
Outstanding
</p>
<p className="text-2xl font-bold text-white mt-1">
{outstandingInvoices}
</p>
</div>
<FileText className="h-6 w-6" style={{
color: outstandingInvoices > 0 ? "#f97316" : "#22c55e",
opacity: 0.5
}} />
<FileText
className="h-6 w-6"
style={{
color: outstandingInvoices > 0 ? "#f97316" : "#22c55e",
opacity: 0.5,
}}
/>
</div>
</CardContent>
</Card>
@ -190,8 +223,12 @@ export default function ClientHub() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-400 uppercase tracking-wider">Team Members</p>
<p className="text-2xl font-bold text-white mt-1">{teamMembers.length}</p>
<p className="text-xs text-gray-400 uppercase tracking-wider">
Team Members
</p>
<p className="text-2xl font-bold text-white mt-1">
{teamMembers.length}
</p>
</div>
<Users className="h-6 w-6 text-purple-500 opacity-50" />
</div>
@ -201,7 +238,11 @@ export default function ClientHub() {
</div>
{/* 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">
<TabsTrigger value="overview">Overview</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">
<CardHeader>
<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>
<CardContent>
<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>
<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>
</CardContent>
</Card>
@ -242,16 +286,31 @@ export default function ClientHub() {
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="space-y-1">
<p className="text-xs text-gray-400 uppercase">Total Value</p>
<p className="text-2xl font-bold text-white">${(activeContract.total_value / 1000).toFixed(0)}k</p>
<p className="text-xs text-gray-400 uppercase">
Total Value
</p>
<p className="text-2xl font-bold text-white">
${(activeContract.total_value / 1000).toFixed(0)}k
</p>
</div>
<div className="space-y-1">
<p className="text-xs text-gray-400 uppercase">Status</p>
<Badge className="bg-blue-600/50 text-blue-100">{activeContract.status}</Badge>
<p className="text-xs text-gray-400 uppercase">
Status
</p>
<Badge className="bg-blue-600/50 text-blue-100">
{activeContract.status}
</Badge>
</div>
<div className="space-y-1">
<p className="text-xs text-gray-400 uppercase">Completion</p>
<p className="text-2xl font-bold text-cyan-400">{Math.round((completedMilestones / totalMilestones) * 100)}%</p>
<p className="text-xs text-gray-400 uppercase">
Completion
</p>
<p className="text-2xl font-bold text-cyan-400">
{Math.round(
(completedMilestones / totalMilestones) * 100,
)}
%
</p>
</div>
</div>
@ -259,12 +318,16 @@ export default function ClientHub() {
<div className="space-y-2">
<div className="flex justify-between text-xs text-gray-400">
<span>Milestone Progress</span>
<span>{completedMilestones} of {totalMilestones}</span>
<span>
{completedMilestones} of {totalMilestones}
</span>
</div>
<div className="w-full bg-black/50 rounded-full h-3">
<div
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>
@ -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">
<CardContent className="p-8 text-center space-y-4">
<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>
</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">
<CardHeader>
<CardTitle>Recent Invoices</CardTitle>
<CardDescription>Your latest billing activity</CardDescription>
<CardDescription>
Your latest billing activity
</CardDescription>
</CardHeader>
<CardContent>
{invoices.length === 0 ? (
@ -305,18 +372,34 @@ export default function ClientHub() {
) : (
<div className="space-y-3">
{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">
<p className="font-semibold text-white text-sm">{invoice.invoice_number}</p>
<p className="text-xs text-gray-400">{new Date(invoice.created_at).toLocaleDateString()}</p>
<p className="font-semibold text-white text-sm">
{invoice.invoice_number}
</p>
<p className="text-xs text-gray-400">
{new Date(
invoice.created_at,
).toLocaleDateString()}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-white">${invoice.amount?.toLocaleString()}</p>
<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" :
""
}>
<p className="font-semibold text-white">
${invoice.amount?.toLocaleString()}
</p>
<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}
</Badge>
</div>
@ -331,16 +414,20 @@ export default function ClientHub() {
{/* Project Status Tab - Gantt Style */}
<TabsContent value="project" className="space-y-4 animate-fade-in">
<ProjectStatusWidget
project={activeContract ? {
id: activeContract.id,
title: activeContract.title,
description: activeContract.description,
status: activeContract.status || "active",
start_date: activeContract.start_date,
end_date: activeContract.end_date,
total_value: activeContract.total_value,
milestones: activeContract.milestones || [],
} : null}
project={
activeContract
? {
id: activeContract.id,
title: activeContract.title,
description: activeContract.description,
status: activeContract.status || "active",
start_date: activeContract.start_date,
end_date: activeContract.end_date,
total_value: activeContract.total_value,
milestones: activeContract.milestones || [],
}
: null
}
accentColor="cyan"
/>
</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">
<CardHeader>
<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>
<CardContent className="space-y-6">
{/* Billing Summary */}
<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">
<p className="text-xs text-gray-400 uppercase">Total Invoices</p>
<p className="text-3xl font-bold text-white">${(totalInvoiceValue / 1000).toFixed(0)}k</p>
<p className="text-xs text-gray-400 uppercase">
Total Invoices
</p>
<p className="text-3xl font-bold text-white">
${(totalInvoiceValue / 1000).toFixed(0)}k
</p>
</div>
<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-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>
</div>
<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">
${(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>
</div>
</div>
@ -382,21 +489,32 @@ export default function ClientHub() {
) : (
<div className="space-y-3">
{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-1">
<h4 className="font-semibold text-white">{invoice.invoice_number}</h4>
<p className="text-sm text-gray-400">{invoice.description}</p>
<h4 className="font-semibold text-white">
{invoice.invoice_number}
</h4>
<p className="text-sm text-gray-400">
{invoice.description}
</p>
</div>
<div className="text-right">
<p className="text-lg font-semibold text-white">${invoice.amount?.toLocaleString()}</p>
<Badge 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"
: "bg-yellow-500/20 border-yellow-500/30 text-yellow-300"
}>
<p className="text-lg font-semibold text-white">
${invoice.amount?.toLocaleString()}
</p>
<Badge
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"
: "bg-yellow-500/20 border-yellow-500/30 text-yellow-300"
}
>
{invoice.status}
</Badge>
</div>
@ -404,15 +522,27 @@ export default function ClientHub() {
<div className="grid grid-cols-2 gap-4 text-sm text-gray-400">
<div>
<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>
<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>
{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
<ArrowRight className="h-4 w-4 ml-2" />
</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">
<CardHeader>
<CardTitle>Your Dedicated AeThex Team</CardTitle>
<CardDescription>White-glove service with personalized support</CardDescription>
<CardDescription>
White-glove service with personalized support
</CardDescription>
</CardHeader>
<CardContent>
<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="flex items-start gap-4">
<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}
className="w-16 h-16 rounded-full border-2 border-blue-500/40 object-cover"
/>
<div className="flex-1">
<Badge className="bg-blue-600/50 text-blue-100 mb-2">Account Manager</Badge>
<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>
<Badge className="bg-blue-600/50 text-blue-100 mb-2">
Account Manager
</Badge>
<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">
<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" />
Message
</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" />
Call
</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" />
Book Meeting
</Button>
@ -467,11 +621,13 @@ export default function ClientHub() {
{accountManager.contact_info && (
<div className="pt-4 border-t border-blue-500/20 space-y-2 text-sm">
<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>
{accountManager.contact_info.phone && (
<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>
)}
</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="flex items-start gap-4">
<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}
className="w-16 h-16 rounded-full border-2 border-purple-500/40 object-cover"
/>
<div className="flex-1">
<Badge className="bg-purple-600/50 text-purple-100 mb-2">Solutions Architect</Badge>
<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>
<Badge className="bg-purple-600/50 text-purple-100 mb-2">
Solutions Architect
</Badge>
<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">
<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" />
Message
</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" />
Call
</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" />
Book Meeting
</Button>
@ -517,11 +695,13 @@ export default function ClientHub() {
{solutionsArchitect.contact_info && (
<div className="pt-4 border-t border-purple-500/20 space-y-2 text-sm">
<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>
{solutionsArchitect.contact_info.phone && (
<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>
)}
</div>
@ -547,7 +727,10 @@ export default function ClientHub() {
<MessageSquare className="h-4 w-4 mr-2" />
Open Support Ticket
</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" />
Schedule Support Call
</Button>