Prettier format pending files
This commit is contained in:
parent
b573ff8070
commit
bf4234348c
10 changed files with 495 additions and 198 deletions
|
|
@ -124,8 +124,14 @@ const DEMO_POSTS: DemoPost[] = [
|
|||
];
|
||||
|
||||
const FOLLOW_PAIRS: Array<{ followerEmail: string; followingEmail: string }> = [
|
||||
{ followerEmail: "mrpiglr+demo@aethex.dev", followingEmail: "updates@aethex.dev" },
|
||||
{ followerEmail: "mrpiglr+demo@aethex.dev", followingEmail: "labs@aethex.dev" },
|
||||
{
|
||||
followerEmail: "mrpiglr+demo@aethex.dev",
|
||||
followingEmail: "updates@aethex.dev",
|
||||
},
|
||||
{
|
||||
followerEmail: "mrpiglr+demo@aethex.dev",
|
||||
followingEmail: "labs@aethex.dev",
|
||||
},
|
||||
{ followerEmail: "labs@aethex.dev", followingEmail: "updates@aethex.dev" },
|
||||
];
|
||||
|
||||
|
|
@ -140,8 +146,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
const seededUsers: any[] = [];
|
||||
|
||||
for (const demoUser of DEMO_USERS) {
|
||||
const { data: searchResult, error: searchError } = await admin.auth.admin
|
||||
.listUsers({ email: demoUser.email });
|
||||
const { data: searchResult, error: searchError } =
|
||||
await admin.auth.admin.listUsers({ email: demoUser.email });
|
||||
if (searchError) throw searchError;
|
||||
|
||||
let authUser = searchResult.users?.[0];
|
||||
|
|
@ -226,7 +232,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
continue;
|
||||
}
|
||||
|
||||
const createdAt = new Date(now - post.hoursAgo * 3600 * 1000).toISOString();
|
||||
const createdAt = new Date(
|
||||
now - post.hoursAgo * 3600 * 1000,
|
||||
).toISOString();
|
||||
const { data: insertedPost, error: insertPostError } = await admin
|
||||
.from("community_posts")
|
||||
.insert({
|
||||
|
|
@ -257,18 +265,20 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
seededPosts.push(insertedPost);
|
||||
}
|
||||
|
||||
const followRows = FOLLOW_PAIRS.flatMap(({ followerEmail, followingEmail }) => {
|
||||
const followerId = userMap.get(followerEmail);
|
||||
const followingId = userMap.get(followingEmail);
|
||||
if (!followerId || !followingId) return [];
|
||||
return [
|
||||
{
|
||||
follower_id: followerId,
|
||||
following_id: followingId,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
});
|
||||
const followRows = FOLLOW_PAIRS.flatMap(
|
||||
({ followerEmail, followingEmail }) => {
|
||||
const followerId = userMap.get(followerEmail);
|
||||
const followingId = userMap.get(followingEmail);
|
||||
if (!followerId || !followingId) return [];
|
||||
return [
|
||||
{
|
||||
follower_id: followerId,
|
||||
following_id: followingId,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
if (followRows.length) {
|
||||
const { error: followError } = await admin
|
||||
|
|
@ -288,9 +298,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
);
|
||||
const sanitizedPosts = Array.from(
|
||||
new Map(
|
||||
seededPosts
|
||||
.filter(Boolean)
|
||||
.map((post: any) => [post.id, post]),
|
||||
seededPosts.filter(Boolean).map((post: any) => [post.id, post]),
|
||||
).values(),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -308,9 +308,9 @@ export default function CodeLayout({ children }: LayoutProps) {
|
|||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/profile" className="cursor-pointer">
|
||||
<UserCircle className="mr-2 h-4 w-4" />
|
||||
My Profile
|
||||
</Link>
|
||||
<UserCircle className="mr-2 h-4 w-4" />
|
||||
My Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={passportHref} className="cursor-pointer">
|
||||
|
|
|
|||
|
|
@ -256,7 +256,8 @@ const AdminMemberManager = ({
|
|||
} catch (error: any) {
|
||||
console.error("Failed to update profile", error);
|
||||
const extractErrorMessage = (err: any) => {
|
||||
if (!err) return "Supabase rejected the update. Review payload and RLS policies.";
|
||||
if (!err)
|
||||
return "Supabase rejected the update. Review payload and RLS policies.";
|
||||
if (typeof err === "string") return err;
|
||||
if (err.message) return err.message;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -382,7 +382,10 @@ export const aethexUserService = {
|
|||
let err = anyResp.error;
|
||||
if (err) {
|
||||
const message = String(err?.message || err);
|
||||
if (message.includes("relationship") || message.includes("schema cache")) {
|
||||
if (
|
||||
message.includes("relationship") ||
|
||||
message.includes("schema cache")
|
||||
) {
|
||||
// Fallback: fetch profiles, then batch-fetch achievements and map xp rewards
|
||||
const { data: profilesOnly, error: profilesErr } = await supabase
|
||||
.from("user_profiles")
|
||||
|
|
@ -399,7 +402,9 @@ export const aethexUserService = {
|
|||
throw new Error(profilesErr?.message || String(profilesErr));
|
||||
}
|
||||
|
||||
const ids = Array.isArray(profilesOnly) ? profilesOnly.map((p: any) => p.id).filter(Boolean) : [];
|
||||
const ids = Array.isArray(profilesOnly)
|
||||
? profilesOnly.map((p: any) => p.id).filter(Boolean)
|
||||
: [];
|
||||
|
||||
let uaRows: any[] = [];
|
||||
if (ids.length) {
|
||||
|
|
@ -414,7 +419,9 @@ export const aethexUserService = {
|
|||
uaRows = Array.isArray(uaData) ? uaData : [];
|
||||
}
|
||||
|
||||
const achievementIds = Array.from(new Set(uaRows.map((r) => r.achievement_id).filter(Boolean)));
|
||||
const achievementIds = Array.from(
|
||||
new Set(uaRows.map((r) => r.achievement_id).filter(Boolean)),
|
||||
);
|
||||
let achievementMap: Record<string, number> = {};
|
||||
if (achievementIds.length) {
|
||||
const { data: achData, error: achErr } = await supabase
|
||||
|
|
@ -433,7 +440,11 @@ export const aethexUserService = {
|
|||
const enrichedProfiles = (profilesOnly || []).map((p: any) => {
|
||||
const userAchievements = uaRows
|
||||
.filter((r) => r.user_id === p.id)
|
||||
.map((r) => ({ achievements: { xp_reward: achievementMap[r.achievement_id] || 0 } }));
|
||||
.map((r) => ({
|
||||
achievements: {
|
||||
xp_reward: achievementMap[r.achievement_id] || 0,
|
||||
},
|
||||
}));
|
||||
return { ...p, user_achievements: userAchievements };
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -86,8 +86,9 @@ export default function Admin() {
|
|||
specialties: ["Simulation", "AI/ML", "Economy"],
|
||||
},
|
||||
]);
|
||||
const [projectApplications, setProjectApplications] =
|
||||
useState<ProjectApplication[]>([]);
|
||||
const [projectApplications, setProjectApplications] = useState<
|
||||
ProjectApplication[]
|
||||
>([]);
|
||||
const [projectApplicationsLoading, setProjectApplicationsLoading] =
|
||||
useState(false);
|
||||
type OpportunityApplication = {
|
||||
|
|
@ -103,8 +104,9 @@ export default function Admin() {
|
|||
submitted_at?: string | null;
|
||||
message?: string | null;
|
||||
};
|
||||
const [opportunityApplications, setOpportunityApplications] =
|
||||
useState<OpportunityApplication[]>([]);
|
||||
const [opportunityApplications, setOpportunityApplications] = useState<
|
||||
OpportunityApplication[]
|
||||
>([]);
|
||||
const [opportunityApplicationsLoading, setOpportunityApplicationsLoading] =
|
||||
useState(false);
|
||||
const [selectedMemberId, setSelectedMemberId] = useState<string | null>(null);
|
||||
|
|
@ -311,7 +313,8 @@ export default function Admin() {
|
|||
value: opportunityApplicationsLoading
|
||||
? "…"
|
||||
: opportunityApplications.length.toString(),
|
||||
description: "Contributor & career submissions captured via Opportunities.",
|
||||
description:
|
||||
"Contributor & career submissions captured via Opportunities.",
|
||||
trend: opportunityApplicationsLoading
|
||||
? "Syncing applicant data…"
|
||||
: `${opportunityApplications.filter((app) => (app.status ?? "new").toLowerCase() === "new").length} awaiting review`,
|
||||
|
|
@ -1016,7 +1019,10 @@ export default function Admin() {
|
|||
<div className="grid gap-2">
|
||||
{projectApplications.slice(0, 6).map((app) => (
|
||||
<div
|
||||
key={app.id || `${app.applicant_email ?? "applicant"}-${app.projects?.id ?? "project"}`}
|
||||
key={
|
||||
app.id ||
|
||||
`${app.applicant_email ?? "applicant"}-${app.projects?.id ?? "project"}`
|
||||
}
|
||||
className="space-y-1 rounded border border-border/30 bg-background/40 p-3"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
|
|
@ -1043,7 +1049,8 @@ export default function Admin() {
|
|||
</div>
|
||||
) : (
|
||||
<p>
|
||||
No project applications on file. Encourage partners to apply via briefs.
|
||||
No project applications on file. Encourage partners to
|
||||
apply via briefs.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
@ -1059,7 +1066,8 @@ export default function Admin() {
|
|||
<CardTitle>Opportunity applications</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
View contributor and career submissions captured on the Opportunities page.
|
||||
View contributor and career submissions captured on the
|
||||
Opportunities page.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
|
|
@ -1092,7 +1100,10 @@ export default function Admin() {
|
|||
<div className="grid gap-2">
|
||||
{opportunityApplications.slice(0, 6).map((app) => (
|
||||
<div
|
||||
key={app.id || `${app.email ?? "candidate"}-${app.submitted_at ?? "time"}`}
|
||||
key={
|
||||
app.id ||
|
||||
`${app.email ?? "candidate"}-${app.submitted_at ?? "time"}`
|
||||
}
|
||||
className="space-y-2 rounded border border-border/30 bg-background/40 p-3"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
|
|
@ -1165,7 +1176,8 @@ export default function Admin() {
|
|||
</div>
|
||||
) : (
|
||||
<p>
|
||||
No opportunity applications yet. Share the Opportunities page to grow the pipeline.
|
||||
No opportunity applications yet. Share the Opportunities
|
||||
page to grow the pipeline.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -109,19 +109,21 @@ interface DeveloperCardProps {
|
|||
const DeveloperCard = ({ profile }: DeveloperCardProps) => {
|
||||
const realmStyle =
|
||||
realmBadgeStyles[profile.user_type] || "bg-aethex-500 text-white";
|
||||
const fallbackBanner = realmBannerFallbacks[profile.user_type] ||
|
||||
const fallbackBanner =
|
||||
realmBannerFallbacks[profile.user_type] ||
|
||||
"from-slate-900 via-slate-800 to-slate-900";
|
||||
const isGodMode = (profile.level ?? 1) >= 100;
|
||||
const passportHref = profile.username
|
||||
? `/passport/${profile.username}`
|
||||
: `/passport/${profile.id}`;
|
||||
const name = profile.full_name || profile.username || "AeThex Explorer";
|
||||
const initials = name
|
||||
.split(" ")
|
||||
.filter(Boolean)
|
||||
.map((segment) => segment[0]?.toUpperCase())
|
||||
.join("")
|
||||
.slice(0, 2) || "AE";
|
||||
const initials =
|
||||
name
|
||||
.split(" ")
|
||||
.filter(Boolean)
|
||||
.map((segment) => segment[0]?.toUpperCase())
|
||||
.join("")
|
||||
.slice(0, 2) || "AE";
|
||||
const totalXp = Math.max(0, Math.floor(Number(profile.total_xp ?? 0)));
|
||||
const levelValue = Math.max(1, Math.floor(Number(profile.level ?? 1)));
|
||||
const loyaltyPoints = Math.max(
|
||||
|
|
@ -225,9 +227,7 @@ const DeveloperCard = ({ profile }: DeveloperCardProps) => {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-0">
|
||||
{profile.bio && (
|
||||
<p className="text-sm text-slate-300 line-clamp-3">
|
||||
{profile.bio}
|
||||
</p>
|
||||
<p className="text-sm text-slate-300 line-clamp-3">{profile.bio}</p>
|
||||
)}
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900/70 p-3 text-slate-200">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,13 @@ import {
|
|||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useAethexToast } from "@/hooks/use-aethex-toast";
|
||||
|
|
@ -208,7 +214,8 @@ const Opportunities = () => {
|
|||
useEffect(() => {
|
||||
setContributorForm((prev) => ({
|
||||
...prev,
|
||||
fullName: prev.fullName || profile?.full_name || user?.email?.split("@")[0] || "",
|
||||
fullName:
|
||||
prev.fullName || profile?.full_name || user?.email?.split("@")[0] || "",
|
||||
email: prev.email || user?.email || "",
|
||||
location: prev.location || profile?.location || "",
|
||||
portfolioUrl: prev.portfolioUrl || profileWebsite || "",
|
||||
|
|
@ -216,7 +223,8 @@ const Opportunities = () => {
|
|||
|
||||
setCareerForm((prev) => ({
|
||||
...prev,
|
||||
fullName: prev.fullName || profile?.full_name || user?.email?.split("@")[0] || "",
|
||||
fullName:
|
||||
prev.fullName || profile?.full_name || user?.email?.split("@")[0] || "",
|
||||
email: prev.email || user?.email || "",
|
||||
location: prev.location || profile?.location || "",
|
||||
portfolioUrl: prev.portfolioUrl || profileWebsite || "",
|
||||
|
|
@ -236,23 +244,36 @@ const Opportunities = () => {
|
|||
};
|
||||
|
||||
const submitApplication = async (
|
||||
payload: Omit<AethexApplicationSubmission, "type"> & { type: "contributor" | "career" },
|
||||
payload: Omit<AethexApplicationSubmission, "type"> & {
|
||||
type: "contributor" | "career";
|
||||
},
|
||||
) => {
|
||||
await aethexApplicationService.submitApplication(payload);
|
||||
};
|
||||
|
||||
const handleContributorSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleContributorSubmit = async (
|
||||
event: React.FormEvent<HTMLFormElement>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
if (!contributorForm.fullName.trim()) {
|
||||
toast.error({ title: "Name required", description: "Please tell us who you are." });
|
||||
toast.error({
|
||||
title: "Name required",
|
||||
description: "Please tell us who you are.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!validateEmail(contributorForm.email)) {
|
||||
toast.error({ title: "Valid email required", description: "Share an email so we can follow up." });
|
||||
toast.error({
|
||||
title: "Valid email required",
|
||||
description: "Share an email so we can follow up.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!contributorForm.primarySkill) {
|
||||
toast.error({ title: "Select your primary skill", description: "Choose the area where you create the most impact." });
|
||||
toast.error({
|
||||
title: "Select your primary skill",
|
||||
description: "Choose the area where you create the most impact.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSubmittingContributor(true);
|
||||
|
|
@ -270,10 +291,12 @@ const Opportunities = () => {
|
|||
});
|
||||
toast.success({
|
||||
title: "Application received",
|
||||
description: "Thank you! Our contributor success team will reach out shortly.",
|
||||
description:
|
||||
"Thank you! Our contributor success team will reach out shortly.",
|
||||
});
|
||||
setContributorForm({
|
||||
fullName: profile?.full_name || contributorForm.email.split("@")[0] || "",
|
||||
fullName:
|
||||
profile?.full_name || contributorForm.email.split("@")[0] || "",
|
||||
email: contributorForm.email,
|
||||
location: profile?.location || "",
|
||||
primarySkill: "",
|
||||
|
|
@ -285,17 +308,23 @@ const Opportunities = () => {
|
|||
} catch (error: any) {
|
||||
toast.error({
|
||||
title: "Submission failed",
|
||||
description: error?.message || "We could not submit your contributor application.",
|
||||
description:
|
||||
error?.message || "We could not submit your contributor application.",
|
||||
});
|
||||
} finally {
|
||||
setSubmittingContributor(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCareerSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleCareerSubmit = async (
|
||||
event: React.FormEvent<HTMLFormElement>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
if (!careerForm.fullName.trim()) {
|
||||
toast.error({ title: "Name required", description: "Please add your full name." });
|
||||
toast.error({
|
||||
title: "Name required",
|
||||
description: "Please add your full name.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!validateEmail(careerForm.email)) {
|
||||
|
|
@ -327,7 +356,8 @@ const Opportunities = () => {
|
|||
});
|
||||
toast.success({
|
||||
title: "Thanks for applying",
|
||||
description: "Our talent team will review your experience and reach out soon.",
|
||||
description:
|
||||
"Our talent team will review your experience and reach out soon.",
|
||||
});
|
||||
setCareerForm({
|
||||
fullName: profile?.full_name || careerForm.email.split("@")[0] || "",
|
||||
|
|
@ -342,7 +372,8 @@ const Opportunities = () => {
|
|||
} catch (error: any) {
|
||||
toast.error({
|
||||
title: "Submission failed",
|
||||
description: error?.message || "We could not submit your career application.",
|
||||
description:
|
||||
error?.message || "We could not submit your career application.",
|
||||
});
|
||||
} finally {
|
||||
setSubmittingCareer(false);
|
||||
|
|
@ -356,20 +387,35 @@ const Opportunities = () => {
|
|||
<section className="relative z-10 border-b border-border/30">
|
||||
<div className="container mx-auto grid gap-12 px-4 py-20 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<div className="space-y-6">
|
||||
<Badge variant="outline" className="border-neon-blue/60 bg-neon-blue/10 text-neon-blue">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-neon-blue/60 bg-neon-blue/10 text-neon-blue"
|
||||
>
|
||||
Build the future at AeThex
|
||||
</Badge>
|
||||
<h1 className="text-4xl font-bold tracking-tight text-white sm:text-5xl">
|
||||
Contribute. Collaborate. Craft the next era of AeThex.
|
||||
</h1>
|
||||
<p className="max-w-2xl text-lg text-muted-foreground">
|
||||
Whether you are a community contributor or exploring full-time roles, this is your gateway to ship meaningfully with the AeThex team. We unite game makers, storytellers, engineers, and strategists around bold ideas.
|
||||
Whether you are a community contributor or exploring full-time
|
||||
roles, this is your gateway to ship meaningfully with the AeThex
|
||||
team. We unite game makers, storytellers, engineers, and
|
||||
strategists around bold ideas.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button asChild size="lg" className="bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90"
|
||||
>
|
||||
<a href="#apply">Start Application</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="border-border/60 bg-background/40 backdrop-blur hover:bg-background/70">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="border-border/60 bg-background/40 backdrop-blur hover:bg-background/70"
|
||||
>
|
||||
<a href="#open-roles">
|
||||
Browse open roles
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
|
|
@ -380,8 +426,12 @@ const Opportunities = () => {
|
|||
<Card className="border-border/40 bg-background/50 backdrop-blur">
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Contributor network</CardTitle>
|
||||
<CardDescription>Mentors, maintainers, and shipmates.</CardDescription>
|
||||
<CardTitle className="text-lg">
|
||||
Contributor network
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Mentors, maintainers, and shipmates.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Users className="h-8 w-8 text-aethex-400" />
|
||||
</CardHeader>
|
||||
|
|
@ -392,13 +442,19 @@ const Opportunities = () => {
|
|||
<Card className="border-border/40 bg-background/50 backdrop-blur">
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Teams hiring now</CardTitle>
|
||||
<CardDescription>Across Labs, Platform, and Community.</CardDescription>
|
||||
<CardTitle className="text-lg">
|
||||
Teams hiring now
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Across Labs, Platform, and Community.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Briefcase className="h-8 w-8 text-aethex-400" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold text-white">12 squads</p>
|
||||
<p className="text-2xl font-semibold text-white">
|
||||
12 squads
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
@ -410,7 +466,8 @@ const Opportunities = () => {
|
|||
What makes AeThex different?
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Cross-functional teams, high-trust culture, and shipped outcomes over vanity metrics.
|
||||
Cross-functional teams, high-trust culture, and shipped
|
||||
outcomes over vanity metrics.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
|
|
@ -419,16 +476,20 @@ const Opportunities = () => {
|
|||
<div>
|
||||
<p className="font-medium text-white">Hybrid squads</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Contributors and full-time teammates collaborate inside the same rituals, tooling, and roadmap.
|
||||
Contributors and full-time teammates collaborate inside
|
||||
the same rituals, tooling, and roadmap.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle2 className="mt-1 h-5 w-5 text-yellow-400" />
|
||||
<div>
|
||||
<p className="font-medium text-white">Impact-first onboarding</p>
|
||||
<p className="font-medium text-white">
|
||||
Impact-first onboarding
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Ship something real during your first sprint with a dedicated mentor or squad lead.
|
||||
Ship something real during your first sprint with a
|
||||
dedicated mentor or squad lead.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -437,7 +498,8 @@ const Opportunities = () => {
|
|||
<div>
|
||||
<p className="font-medium text-white">Transparent growth</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Clear expectations, async updates, and opportunities to move from contributor to core team.
|
||||
Clear expectations, async updates, and opportunities to
|
||||
move from contributor to core team.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -446,18 +508,26 @@ const Opportunities = () => {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative z-10 border-b border-border/30" id="open-roles">
|
||||
<section
|
||||
className="relative z-10 border-b border-border/30"
|
||||
id="open-roles"
|
||||
>
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="mb-10 grid gap-4 md:grid-cols-[0.6fr_1fr] md:items-end">
|
||||
<div>
|
||||
<Badge variant="outline" className="border-purple-400/60 bg-purple-500/10 text-purple-200">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-purple-400/60 bg-purple-500/10 text-purple-200"
|
||||
>
|
||||
Explore opportunities
|
||||
</Badge>
|
||||
<h2 className="mt-4 text-3xl font-semibold text-white">
|
||||
Contributor paths & immediate hiring needs
|
||||
</h2>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Choose how you want to collaborate with AeThex. We empower flexible contributor engagements alongside full-time roles across labs, platform, and community.
|
||||
Choose how you want to collaborate with AeThex. We empower
|
||||
flexible contributor engagements alongside full-time roles
|
||||
across labs, platform, and community.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/40 bg-background/40 p-4 backdrop-blur">
|
||||
|
|
@ -466,7 +536,8 @@ const Opportunities = () => {
|
|||
<div>
|
||||
<p className="font-medium text-white">Rolling reviews</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Contributor cohorts are evaluated weekly. Urgent hiring roles receive priority outreach within 72 hours.
|
||||
Contributor cohorts are evaluated weekly. Urgent hiring
|
||||
roles receive priority outreach within 72 hours.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -480,15 +551,25 @@ const Opportunities = () => {
|
|||
Core Contributors
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Shape feature roadmaps, ship code, design experiences, or lead community programs.
|
||||
Shape feature roadmaps, ship code, design experiences, or
|
||||
lead community programs.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<p>Typical commitment: 5-15 hrs / week</p>
|
||||
<ul className="list-disc space-y-2 pl-4">
|
||||
<li>Join a squad shipping AeThex platform, docs, or live services</li>
|
||||
<li>Collaborate directly with PMs, producers, and staff engineers</li>
|
||||
<li>Earn AeThex recognition, mentorship, and prioritized hiring pathways</li>
|
||||
<li>
|
||||
Join a squad shipping AeThex platform, docs, or live
|
||||
services
|
||||
</li>
|
||||
<li>
|
||||
Collaborate directly with PMs, producers, and staff
|
||||
engineers
|
||||
</li>
|
||||
<li>
|
||||
Earn AeThex recognition, mentorship, and prioritized
|
||||
hiring pathways
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -499,15 +580,24 @@ const Opportunities = () => {
|
|||
Fellows & Apprentices
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Guided programs for emerging builders to gain portfolio-ready experience.
|
||||
Guided programs for emerging builders to gain
|
||||
portfolio-ready experience.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<p>Typical commitment: 10-20 hrs / week for 12 weeks</p>
|
||||
<ul className="list-disc space-y-2 pl-4">
|
||||
<li>Structured mentorship with clear milestones and feedback</li>
|
||||
<li>Contribute to lab prototypes, live events, or curriculum builds</li>
|
||||
<li>Ideal for rising professionals breaking into games or platform engineering</li>
|
||||
<li>
|
||||
Structured mentorship with clear milestones and feedback
|
||||
</li>
|
||||
<li>
|
||||
Contribute to lab prototypes, live events, or curriculum
|
||||
builds
|
||||
</li>
|
||||
<li>
|
||||
Ideal for rising professionals breaking into games or
|
||||
platform engineering
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -518,14 +608,24 @@ const Opportunities = () => {
|
|||
Full-time roles
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Join AeThex as a core teammate with benefits, equity pathways, and ownership of initiatives.
|
||||
Join AeThex as a core teammate with benefits, equity
|
||||
pathways, and ownership of initiatives.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<p>Immediate needs across engineering, design, product, and community.</p>
|
||||
<p>
|
||||
Immediate needs across engineering, design, product, and
|
||||
community.
|
||||
</p>
|
||||
<ul className="list-disc space-y-2 pl-4">
|
||||
<li>Global-first, remote-friendly culture with async collaboration</li>
|
||||
<li>Cross-functional squads aligned to measurable player and creator outcomes</li>
|
||||
<li>
|
||||
Global-first, remote-friendly culture with async
|
||||
collaboration
|
||||
</li>
|
||||
<li>
|
||||
Cross-functional squads aligned to measurable player and
|
||||
creator outcomes
|
||||
</li>
|
||||
<li>Opportunities to incubate new AeThex Labs ventures</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
|
|
@ -533,23 +633,37 @@ const Opportunities = () => {
|
|||
</div>
|
||||
<div className="mt-8 grid gap-6 lg:grid-cols-3">
|
||||
{openOpportunities.map((role) => (
|
||||
<Card key={role.title} className="border-border/40 bg-background/40 backdrop-blur">
|
||||
<Card
|
||||
key={role.title}
|
||||
className="border-border/40 bg-background/40 backdrop-blur"
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-white">{role.title}</CardTitle>
|
||||
<CardTitle className="text-white">
|
||||
{role.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{role.type}</CardDescription>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-aethex-400/60 text-aethex-200">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-aethex-400/60 text-aethex-200"
|
||||
>
|
||||
Priority
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">{role.summary}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{role.summary}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{role.tags.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="border-border/50 bg-background/60 text-xs uppercase tracking-wide">
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="outline"
|
||||
className="border-border/50 bg-background/60 text-xs uppercase tracking-wide"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
|
|
@ -565,39 +679,59 @@ const Opportunities = () => {
|
|||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="mb-10 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<Badge variant="outline" className="border-aethex-400/60 bg-aethex-500/10 text-aethex-200">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-aethex-400/60 bg-aethex-500/10 text-aethex-200"
|
||||
>
|
||||
Apply now
|
||||
</Badge>
|
||||
<h2 className="mt-4 text-3xl font-semibold text-white">
|
||||
Tell us how you want to build with AeThex
|
||||
</h2>
|
||||
<p className="mt-2 max-w-2xl text-muted-foreground">
|
||||
Share a snapshot of your experience, interests, and availability. We calibrate opportunities weekly and match you with the best-fit squad or role.
|
||||
Share a snapshot of your experience, interests, and
|
||||
availability. We calibrate opportunities weekly and match you
|
||||
with the best-fit squad or role.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/40 bg-background/40 p-4 text-sm text-muted-foreground backdrop-blur">
|
||||
<p className="font-medium text-white">Need help?</p>
|
||||
<p>
|
||||
Email <a className="text-neon-blue underline" href="mailto:opportunities@aethex.com">opportunities@aethex.com</a> if you want to talk through the right track before applying.
|
||||
Email{" "}
|
||||
<a
|
||||
className="text-neon-blue underline"
|
||||
href="mailto:opportunities@aethex.com"
|
||||
>
|
||||
opportunities@aethex.com
|
||||
</a>{" "}
|
||||
if you want to talk through the right track before applying.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="contributor" className="w-full">
|
||||
<TabsList className="bg-background/40">
|
||||
<TabsTrigger value="contributor">Contributor journey</TabsTrigger>
|
||||
<TabsTrigger value="contributor">
|
||||
Contributor journey
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="career">Careers at AeThex</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="contributor" className="mt-6">
|
||||
<Card className="border-border/40 bg-background/50 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Contributor application</CardTitle>
|
||||
<CardTitle className="text-white">
|
||||
Contributor application
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Ideal for open-source, part-time, or fellowship-style collaborations.
|
||||
Ideal for open-source, part-time, or fellowship-style
|
||||
collaborations.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="grid gap-6" onSubmit={handleContributorSubmit}>
|
||||
<form
|
||||
className="grid gap-6"
|
||||
onSubmit={handleContributorSubmit}
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contributor-name">Full name</Label>
|
||||
|
|
@ -631,7 +765,9 @@ const Opportunities = () => {
|
|||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contributor-location">Location (optional)</Label>
|
||||
<Label htmlFor="contributor-location">
|
||||
Location (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="contributor-location"
|
||||
value={contributorForm.location}
|
||||
|
|
@ -694,7 +830,9 @@ const Opportunities = () => {
|
|||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contributor-portfolio">Portfolio or profile link</Label>
|
||||
<Label htmlFor="contributor-portfolio">
|
||||
Portfolio or profile link
|
||||
</Label>
|
||||
<Input
|
||||
id="contributor-portfolio"
|
||||
value={contributorForm.portfolioUrl}
|
||||
|
|
@ -717,8 +855,12 @@ const Opportunities = () => {
|
|||
className="flex cursor-pointer items-start gap-3 rounded-lg border border-border/50 bg-background/40 p-3 text-sm transition hover:border-aethex-400/60"
|
||||
>
|
||||
<Checkbox
|
||||
checked={contributorForm.interests.includes(interest)}
|
||||
onCheckedChange={() => toggleContributorInterest(interest)}
|
||||
checked={contributorForm.interests.includes(
|
||||
interest,
|
||||
)}
|
||||
onCheckedChange={() =>
|
||||
toggleContributorInterest(interest)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span>{interest}</span>
|
||||
|
|
@ -727,7 +869,10 @@ const Opportunities = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contributor-message">Tell us about a project you are proud of or what you want to build here</Label>
|
||||
<Label htmlFor="contributor-message">
|
||||
Tell us about a project you are proud of or what you
|
||||
want to build here
|
||||
</Label>
|
||||
<Textarea
|
||||
id="contributor-message"
|
||||
value={contributorForm.message}
|
||||
|
|
@ -741,7 +886,11 @@ const Opportunities = () => {
|
|||
placeholder="Share context, links, or ideas you want to explore with AeThex."
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full sm:w-auto" disabled={submittingContributor}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full sm:w-auto"
|
||||
disabled={submittingContributor}
|
||||
>
|
||||
{submittingContributor ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
|
|
@ -758,9 +907,12 @@ const Opportunities = () => {
|
|||
<TabsContent value="career" className="mt-6">
|
||||
<Card className="border-border/40 bg-background/50 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Career application</CardTitle>
|
||||
<CardTitle className="text-white">
|
||||
Career application
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Ideal for full-time or long-term contract positions within AeThex.
|
||||
Ideal for full-time or long-term contract positions within
|
||||
AeThex.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
@ -798,7 +950,9 @@ const Opportunities = () => {
|
|||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="career-location">Location (optional)</Label>
|
||||
<Label htmlFor="career-location">
|
||||
Location (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="career-location"
|
||||
value={careerForm.location}
|
||||
|
|
@ -861,7 +1015,9 @@ const Opportunities = () => {
|
|||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="career-portfolio">Portfolio or work samples</Label>
|
||||
<Label htmlFor="career-portfolio">
|
||||
Portfolio or work samples
|
||||
</Label>
|
||||
<Input
|
||||
id="career-portfolio"
|
||||
value={careerForm.portfolioUrl}
|
||||
|
|
@ -876,7 +1032,9 @@ const Opportunities = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="career-resume">Resume, CV, or LinkedIn</Label>
|
||||
<Label htmlFor="career-resume">
|
||||
Resume, CV, or LinkedIn
|
||||
</Label>
|
||||
<Input
|
||||
id="career-resume"
|
||||
value={careerForm.resumeUrl}
|
||||
|
|
@ -890,7 +1048,9 @@ const Opportunities = () => {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="career-message">Why AeThex, why now?</Label>
|
||||
<Label htmlFor="career-message">
|
||||
Why AeThex, why now?
|
||||
</Label>
|
||||
<Textarea
|
||||
id="career-message"
|
||||
value={careerForm.message}
|
||||
|
|
@ -904,7 +1064,11 @@ const Opportunities = () => {
|
|||
placeholder="Share recent accomplishments, what motivates you, or ideas you want to ship with AeThex."
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full sm:w-auto" disabled={submittingCareer}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full sm:w-auto"
|
||||
disabled={submittingCareer}
|
||||
>
|
||||
{submittingCareer ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
|
|
@ -925,30 +1089,43 @@ const Opportunities = () => {
|
|||
<section className="relative z-10 border-b border-border/30">
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="mb-10 text-center">
|
||||
<Badge variant="outline" className="border-border/50 bg-background/40 text-muted-foreground">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-border/50 bg-background/40 text-muted-foreground"
|
||||
>
|
||||
From hello to shipped impact
|
||||
</Badge>
|
||||
<h2 className="mt-4 text-3xl font-semibold text-white">
|
||||
What happens after you apply
|
||||
</h2>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
A guided experience that helps you connect with squads, evaluate fit, and start shipping meaningful work.
|
||||
A guided experience that helps you connect with squads, evaluate
|
||||
fit, and start shipping meaningful work.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{applicationMilestones.map(({ icon: Icon, title, description }) => (
|
||||
<Card key={title} className="border-border/40 bg-background/40 backdrop-blur">
|
||||
<CardHeader className="space-y-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-aethex-500/15 text-aethex-200">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
<CardTitle className="text-xl text-white">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{applicationMilestones.map(
|
||||
({ icon: Icon, title, description }) => (
|
||||
<Card
|
||||
key={title}
|
||||
className="border-border/40 bg-background/40 backdrop-blur"
|
||||
>
|
||||
<CardHeader className="space-y-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-aethex-500/15 text-aethex-200">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
<CardTitle className="text-xl text-white">
|
||||
{title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -956,22 +1133,35 @@ const Opportunities = () => {
|
|||
<section className="relative z-10">
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="mb-10 text-center">
|
||||
<Badge variant="outline" className="border-border/50 bg-background/40 text-muted-foreground">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-border/50 bg-background/40 text-muted-foreground"
|
||||
>
|
||||
Frequently asked questions
|
||||
</Badge>
|
||||
<h2 className="mt-4 text-3xl font-semibold text-white">Everything you need to know</h2>
|
||||
<h2 className="mt-4 text-3xl font-semibold text-white">
|
||||
Everything you need to know
|
||||
</h2>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
From application timelines to cross-team collaboration, here are answers to what most applicants ask.
|
||||
From application timelines to cross-team collaboration, here are
|
||||
answers to what most applicants ask.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto grid max-w-4xl gap-4">
|
||||
{faqs.map((item) => (
|
||||
<Card key={item.question} className="border-border/40 bg-background/50 backdrop-blur">
|
||||
<Card
|
||||
key={item.question}
|
||||
className="border-border/40 bg-background/50 backdrop-blur"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg text-white">{item.question}</CardTitle>
|
||||
<CardTitle className="text-lg text-white">
|
||||
{item.question}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{item.answer}</p>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{item.answer}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ import {
|
|||
aethexAchievementService,
|
||||
type AethexAchievement,
|
||||
} from "@/lib/aethex-database-adapter";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/ui/avatar";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -81,7 +77,9 @@ const Profile = () => {
|
|||
if (!user?.id) return;
|
||||
setLoadingAchievements(true);
|
||||
try {
|
||||
const data = await aethexAchievementService.getUserAchievements(user.id);
|
||||
const data = await aethexAchievementService.getUserAchievements(
|
||||
user.id,
|
||||
);
|
||||
setAchievements(data.slice(0, 6));
|
||||
} catch (error) {
|
||||
console.warn("Failed to load achievements for profile overview", error);
|
||||
|
|
@ -182,12 +180,18 @@ const Profile = () => {
|
|||
</CardTitle>
|
||||
<CardDescription className="flex flex-wrap items-center justify-center gap-2 text-sm">
|
||||
{profile.user_type ? (
|
||||
<Badge variant="outline" className="capitalize border-aethex-400/60 text-aethex-200">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="capitalize border-aethex-400/60 text-aethex-200"
|
||||
>
|
||||
{profile.user_type.replace(/_/g, " ")}
|
||||
</Badge>
|
||||
) : null}
|
||||
{profile.experience_level ? (
|
||||
<Badge variant="outline" className="capitalize border-purple-400/50 text-purple-200">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="capitalize border-purple-400/50 text-purple-200"
|
||||
>
|
||||
{profile.experience_level}
|
||||
</Badge>
|
||||
) : null}
|
||||
|
|
@ -224,10 +228,19 @@ const Profile = () => {
|
|||
</div>
|
||||
<Separator className="bg-border/40" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button asChild className="bg-gradient-to-r from-aethex-500 to-neon-blue">
|
||||
<Link to={dashboardSettingsHref}>Edit profile in Dashboard</Link>
|
||||
<Button
|
||||
asChild
|
||||
className="bg-gradient-to-r from-aethex-500 to-neon-blue"
|
||||
>
|
||||
<Link to={dashboardSettingsHref}>
|
||||
Edit profile in Dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="border-border/50">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="border-border/50"
|
||||
>
|
||||
<Link to={passportHref}>View AeThex Passport</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -241,7 +254,9 @@ const Profile = () => {
|
|||
<Shield className="h-5 w-5 text-aethex-300" />
|
||||
Progress snapshot
|
||||
</CardTitle>
|
||||
<CardDescription>Where you stand across AeThex programs.</CardDescription>
|
||||
<CardDescription>
|
||||
Where you stand across AeThex programs.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
|
|
@ -251,12 +266,18 @@ const Profile = () => {
|
|||
className="flex flex-col gap-2 rounded-lg border border-border/40 bg-background/50 p-4 transition hover:border-aethex-400/60"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">{label}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{label}
|
||||
</p>
|
||||
<Icon className="h-5 w-5 text-aethex-300" />
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-white">{value}</p>
|
||||
<p className="text-xl font-semibold text-white">
|
||||
{value}
|
||||
</p>
|
||||
{helper ? (
|
||||
<p className="text-xs text-muted-foreground">{helper}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{helper}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -271,13 +292,16 @@ const Profile = () => {
|
|||
About {profile.full_name || username}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Community presence, specialties, and how to collaborate with you.
|
||||
Community presence, specialties, and how to collaborate with
|
||||
you.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-border/40 bg-background/50 p-4">
|
||||
<p className="text-sm font-medium text-foreground/80">Role focus</p>
|
||||
<p className="text-sm font-medium text-foreground/80">
|
||||
Role focus
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{profile.user_type
|
||||
? profile.user_type.replace(/_/g, " ")
|
||||
|
|
@ -285,15 +309,20 @@ const Profile = () => {
|
|||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border/40 bg-background/50 p-4">
|
||||
<p className="text-sm font-medium text-foreground/80">Experience level</p>
|
||||
<p className="text-sm font-medium text-foreground/80">
|
||||
Experience level
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{profile.experience_level || "Let collaborators know your seniority."}
|
||||
{profile.experience_level ||
|
||||
"Let collaborators know your seniority."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-foreground/80">Links & presence</h3>
|
||||
<h3 className="text-sm font-medium text-foreground/80">
|
||||
Links & presence
|
||||
</h3>
|
||||
{socialLinks.length ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{socialLinks.map((link) => (
|
||||
|
|
@ -329,7 +358,8 @@ const Profile = () => {
|
|||
Recent achievements
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Milestones you've unlocked across AeThex games and programs.
|
||||
Milestones you've unlocked across AeThex games and
|
||||
programs.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
@ -349,7 +379,10 @@ const Profile = () => {
|
|||
<p className="font-medium text-foreground">
|
||||
{achievement.name}
|
||||
</p>
|
||||
<Badge variant="outline" className="border-amber-400/60 text-amber-200">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-amber-400/60 text-amber-200"
|
||||
>
|
||||
+{achievement.xp_reward} XP
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -361,7 +394,8 @@ const Profile = () => {
|
|||
</div>
|
||||
) : (
|
||||
<p className="rounded border border-dashed border-border/40 bg-background/40 p-4 text-sm text-muted-foreground">
|
||||
Start contributing to unlock your first AeThex achievement.
|
||||
Start contributing to unlock your first AeThex
|
||||
achievement.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@ const curriculumModules: CurriculumModule[] = [
|
|||
"Establish core mastery of the AeThex platform, from environment setup to shipping your first interactive experience.",
|
||||
duration: "2.5 hrs",
|
||||
level: "foundation",
|
||||
focus: ["Workspace onboarding", "Project scaffolding", "Passport + identity"],
|
||||
focus: [
|
||||
"Workspace onboarding",
|
||||
"Project scaffolding",
|
||||
"Passport + identity",
|
||||
],
|
||||
learningGoals: [
|
||||
"Configure a production-ready AeThex project",
|
||||
"Understand AeThex Passport, identity, and role models",
|
||||
|
|
@ -242,12 +246,14 @@ const curriculumHighlights = [
|
|||
},
|
||||
{
|
||||
title: "Project-based milestones",
|
||||
description: "Each module culminates in a capstone aligned to AeThex deliverables.",
|
||||
description:
|
||||
"Each module culminates in a capstone aligned to AeThex deliverables.",
|
||||
icon: Target,
|
||||
},
|
||||
{
|
||||
title: "Built with real data",
|
||||
description: "Lessons reference live dashboards, Passport identities, and Supabase schemas.",
|
||||
description:
|
||||
"Lessons reference live dashboards, Passport identities, and Supabase schemas.",
|
||||
icon: Lightbulb,
|
||||
},
|
||||
];
|
||||
|
|
@ -273,19 +279,22 @@ const curriculumStats = [
|
|||
const supplementalResources = [
|
||||
{
|
||||
title: "AeThex Playbooks",
|
||||
description: "Download ready-made GTM, community, and growth playbooks to complement each module.",
|
||||
description:
|
||||
"Download ready-made GTM, community, and growth playbooks to complement each module.",
|
||||
cta: "Browse playbooks",
|
||||
href: "/docs/examples#playbooks",
|
||||
},
|
||||
{
|
||||
title: "Live Mentorship Sessions",
|
||||
description: "Join weekly office hours with AeThex engineers and producers to review your progress.",
|
||||
description:
|
||||
"Join weekly office hours with AeThex engineers and producers to review your progress.",
|
||||
cta: "Reserve a seat",
|
||||
href: "/mentorship",
|
||||
},
|
||||
{
|
||||
title: "Certification Exams",
|
||||
description: "Validate mastery with AeThex Builder and Operator certifications once you finish the track.",
|
||||
description:
|
||||
"Validate mastery with AeThex Builder and Operator certifications once you finish the track.",
|
||||
cta: "View certification guide",
|
||||
href: "/docs/platform#certification",
|
||||
},
|
||||
|
|
@ -305,9 +314,9 @@ export default function DocsCurriculum() {
|
|||
Structured learning paths for builders, operators, and labs teams
|
||||
</h1>
|
||||
<p className="max-w-3xl text-base text-slate-200 sm:text-lg">
|
||||
Progress through sequenced modules that combine documentation, interactive labs, and
|
||||
project-based assignments. Graduate with deployment-ready AeThex experiences and
|
||||
certification badges.
|
||||
Progress through sequenced modules that combine documentation,
|
||||
interactive labs, and project-based assignments. Graduate with
|
||||
deployment-ready AeThex experiences and certification badges.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
|
|
@ -343,8 +352,9 @@ export default function DocsCurriculum() {
|
|||
<Compass className="h-6 w-6 text-purple-300" /> Curriculum roadmap
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-300">
|
||||
Expand each module to view lessons, formats, and key objectives. Every module ends with a
|
||||
capstone milestone that prepares you for certification.
|
||||
Expand each module to view lessons, formats, and key objectives.
|
||||
Every module ends with a capstone milestone that prepares you for
|
||||
certification.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
|
|
@ -379,7 +389,9 @@ export default function DocsCurriculum() {
|
|||
<h3 className="text-lg font-semibold text-white sm:text-xl">
|
||||
{module.title}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-300">{module.description}</p>
|
||||
<p className="text-sm text-slate-300">
|
||||
{module.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
|
|
@ -439,7 +451,8 @@ export default function DocsCurriculum() {
|
|||
className="mt-3 h-8 w-fit gap-2 rounded-full border border-slate-800/60 bg-slate-900/50 px-3 text-xs text-slate-200 hover:border-purple-500/50 hover:text-white"
|
||||
>
|
||||
<Link to={lesson.path}>
|
||||
Review lesson <ArrowRight className="h-3.5 w-3.5" />
|
||||
Review lesson{" "}
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -464,7 +477,8 @@ export default function DocsCurriculum() {
|
|||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-purple-100/80">
|
||||
Coming soon — the AeThex team is curating the next advanced mission.
|
||||
Coming soon — the AeThex team is curating the next
|
||||
advanced mission.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -483,7 +497,8 @@ export default function DocsCurriculum() {
|
|||
Why this curriculum works
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-300">
|
||||
Blending documentation, live labs, and project work keeps teams aligned and measurable.
|
||||
Blending documentation, live labs, and project work keeps teams
|
||||
aligned and measurable.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
|
|
@ -514,10 +529,12 @@ export default function DocsCurriculum() {
|
|||
<Card className="border-slate-800 bg-slate-900/70 shadow-xl">
|
||||
<CardHeader className="space-y-2">
|
||||
<CardTitle className="flex items-center gap-2 text-xl text-white">
|
||||
<BookOpenCheck className="h-5 w-5 text-purple-300" /> Supplemental resources
|
||||
<BookOpenCheck className="h-5 w-5 text-purple-300" />{" "}
|
||||
Supplemental resources
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-300">
|
||||
Extend the curriculum with live mentorship, playbooks, and certification tracks.
|
||||
Extend the curriculum with live mentorship, playbooks, and
|
||||
certification tracks.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
|
|
@ -526,8 +543,12 @@ export default function DocsCurriculum() {
|
|||
key={resource.title}
|
||||
className="space-y-2 rounded-2xl border border-slate-800/60 bg-slate-950/70 p-4"
|
||||
>
|
||||
<p className="text-sm font-semibold text-white">{resource.title}</p>
|
||||
<p className="text-sm text-slate-300">{resource.description}</p>
|
||||
<p className="text-sm font-semibold text-white">
|
||||
{resource.title}
|
||||
</p>
|
||||
<p className="text-sm text-slate-300">
|
||||
{resource.description}
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
|
|
@ -544,36 +565,48 @@ export default function DocsCurriculum() {
|
|||
|
||||
<Card className="border-slate-800 bg-slate-900/70 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl text-white">Team readiness checklist</CardTitle>
|
||||
<CardTitle className="text-xl text-white">
|
||||
Team readiness checklist
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-300">
|
||||
Confirm prerequisites before starting the Builder or Advanced tracks.
|
||||
Confirm prerequisites before starting the Builder or Advanced
|
||||
tracks.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex items-start gap-3 rounded-2xl border border-slate-800/60 bg-slate-950/70 p-4">
|
||||
<Calculator className="mt-1 h-5 w-5 text-purple-200" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">Account + billing configured</p>
|
||||
<p className="text-sm font-semibold text-white">
|
||||
Account + billing configured
|
||||
</p>
|
||||
<p className="text-sm text-slate-300">
|
||||
Ensure Supabase, billing providers, and environment variables are ready for production load.
|
||||
Ensure Supabase, billing providers, and environment
|
||||
variables are ready for production load.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 rounded-2xl border border-slate-800/60 bg-slate-950/70 p-4">
|
||||
<Sparkles className="mt-1 h-5 w-5 text-purple-200" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">Core team enrolled</p>
|
||||
<p className="text-sm font-semibold text-white">
|
||||
Core team enrolled
|
||||
</p>
|
||||
<p className="text-sm text-slate-300">
|
||||
Identify product, engineering, and operations owners to steward each module.
|
||||
Identify product, engineering, and operations owners to
|
||||
steward each module.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 rounded-2xl border border-slate-800/60 bg-slate-950/70 p-4">
|
||||
<Layers className="mt-1 h-5 w-5 text-purple-200" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">Backlog aligned to modules</p>
|
||||
<p className="text-sm font-semibold text-white">
|
||||
Backlog aligned to modules
|
||||
</p>
|
||||
<p className="text-sm text-slate-300">
|
||||
Map existing sprints to module milestones so curriculum progress mirrors roadmap delivery.
|
||||
Map existing sprints to module milestones so curriculum
|
||||
progress mirrors roadmap delivery.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -585,13 +618,19 @@ export default function DocsCurriculum() {
|
|||
<Separator className="border-slate-800/70" />
|
||||
|
||||
<section className="rounded-3xl border border-slate-800/60 bg-slate-900/70 p-8 text-center">
|
||||
<h2 className="text-2xl font-semibold text-white">Ready to certify your AeThex team?</h2>
|
||||
<h2 className="text-2xl font-semibold text-white">
|
||||
Ready to certify your AeThex team?
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-2xl text-sm text-slate-300">
|
||||
Track your completion inside the AeThex admin panel, then schedule a certification review with the
|
||||
AeThex Labs crew. We award badges directly to your Passport once requirements are verified.
|
||||
Track your completion inside the AeThex admin panel, then schedule a
|
||||
certification review with the AeThex Labs crew. We award badges
|
||||
directly to your Passport once requirements are verified.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap justify-center gap-3">
|
||||
<Button asChild className="gap-2 rounded-full bg-purple-600 text-white hover:bg-purple-500">
|
||||
<Button
|
||||
asChild
|
||||
className="gap-2 rounded-full bg-purple-600 text-white hover:bg-purple-500"
|
||||
>
|
||||
<Link to="/mentorship">
|
||||
Join mentorship cohort <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -19,12 +19,16 @@ export function createServer() {
|
|||
|
||||
// Admin-backed API (service role)
|
||||
try {
|
||||
const ownerEmail = (process.env.AETHEX_OWNER_EMAIL || "mrpiglr@gmail.com").toLowerCase();
|
||||
const ownerEmail = (
|
||||
process.env.AETHEX_OWNER_EMAIL || "mrpiglr@gmail.com"
|
||||
).toLowerCase();
|
||||
const isTableMissing = (err: any) => {
|
||||
const code = err?.code;
|
||||
const message = String(err?.message || err?.hint || err?.details || "");
|
||||
return (
|
||||
code === "42P01" || message.includes("relation") || message.includes("does not exist")
|
||||
code === "42P01" ||
|
||||
message.includes("relation") ||
|
||||
message.includes("does not exist")
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -391,9 +395,7 @@ export function createServer() {
|
|||
}
|
||||
return res.json(data || []);
|
||||
} catch (e: any) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: e?.message || String(e) });
|
||||
return res.status(500).json({ error: e?.message || String(e) });
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue