Prettier format pending files
This commit is contained in:
parent
271b8b7ccd
commit
8a94eb1785
54 changed files with 2409 additions and 779 deletions
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 || [])
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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]}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue