- Applied all 31 pending Supabase migrations successfully
- Fixed 100+ policy/trigger/index duplication errors for shared database
- Resolved foundation_contributions schema mismatch (added user_id, contribution_type, resource_id, points columns)
- Added DROP IF EXISTS statements for all policies, triggers, and indexes
- Wrapped storage.objects operations in permission-safe DO blocks
Developer Platform (10 Phases Complete):
- API key management dashboard with RLS and SHA-256 hashing
- Complete API documentation (8 endpoint categories)
- 9 template starters + 9 marketplace products + 12 code examples
- Quick start guide and SDK distribution
- Testing framework and QA checklist
Database Schema Now Includes:
- Ethos: Artist/guild tracking, verification, tracks, storage
- GameForge: Games, assets, monetization
- Foundation: Courses, mentorship, resources, contributions
- Nexus: Creator marketplace, portfolios, contracts, escrow
- Corp Hub: Invoices, contracts, team management, projects
- Developer: API keys, usage logs, profiles
Platform Status: Production Ready ✅
208 lines
7.5 KiB
TypeScript
208 lines
7.5 KiB
TypeScript
import Layout from "@/components/Layout";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardDescription,
|
|
CardContent,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { aethexCollabService } from "@/lib/aethex-collab-service";
|
|
import LoadingScreen from "@/components/LoadingScreen";
|
|
import { aethexToast } from "@/lib/aethex-toast";
|
|
|
|
export default function Teams() {
|
|
const { user, profile, loading } = useAuth();
|
|
const navigate = useNavigate();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [teams, setTeams] = useState<any[]>([]);
|
|
const [name, setName] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [creating, setCreating] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!loading && !user) navigate("/login", { replace: true });
|
|
}, [loading, user, navigate]);
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
if (!user?.id) return;
|
|
setIsLoading(true);
|
|
try {
|
|
const myTeams = await aethexCollabService.listMyTeams(user.id);
|
|
setTeams(myTeams);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
load();
|
|
}, [user?.id]);
|
|
|
|
const canCreate = useMemo(
|
|
() => name.trim().length > 2 && !creating,
|
|
[name, creating],
|
|
);
|
|
|
|
const handleCreate = async () => {
|
|
if (!user?.id) return;
|
|
if (!canCreate) return;
|
|
setCreating(true);
|
|
const tempId = `temp-${Date.now()}`;
|
|
const optimistic = {
|
|
team_id: tempId,
|
|
teams: {
|
|
id: tempId,
|
|
name: name.trim(),
|
|
description: description.trim() || null,
|
|
visibility: "private",
|
|
},
|
|
} as any;
|
|
setTeams((prev) => [optimistic, ...prev]);
|
|
setName("");
|
|
setDescription("");
|
|
let created: any | null = null;
|
|
try {
|
|
created = await aethexCollabService.createTeam(
|
|
user.id,
|
|
optimistic.teams.name,
|
|
optimistic.teams.description,
|
|
"private",
|
|
);
|
|
setTeams((prev) =>
|
|
prev.map((t: any) =>
|
|
t.team_id === tempId ? { team_id: created.id, teams: created } : t,
|
|
),
|
|
);
|
|
aethexToast.success({ title: "Team created" });
|
|
} catch (e: any) {
|
|
setTeams((prev) => prev.filter((t: any) => t.team_id !== tempId));
|
|
aethexToast.error({
|
|
title: "Failed to create team",
|
|
description: e?.message || "Try again later.",
|
|
});
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
if (loading || isLoading)
|
|
return (
|
|
<LoadingScreen message="Loading teams..." showProgress duration={800} />
|
|
);
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(110,141,255,0.12),transparent_60%)]">
|
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-7xl space-y-8">
|
|
<section className="rounded-3xl border border-border/40 bg-background/80 p-6 shadow-2xl backdrop-blur">
|
|
<h1 className="text-3xl font-semibold text-foreground">Teams</h1>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
Create a team and collaborate with members across projects.
|
|
</p>
|
|
</section>
|
|
|
|
<div className="grid lg:grid-cols-[2fr,1fr] gap-8">
|
|
<div className="space-y-6">
|
|
<Card className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Your teams</CardTitle>
|
|
<CardDescription>Teams you belong to</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{teams.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">
|
|
No teams yet. Create one to get started.
|
|
</p>
|
|
) : (
|
|
teams.map((t) => {
|
|
const team = (t as any).teams || t;
|
|
return (
|
|
<div
|
|
key={team.id}
|
|
className="flex items-center justify-between rounded-2xl border border-border/30 bg-background/60 p-4"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Avatar className="h-10 w-10">
|
|
<AvatarImage src={undefined} />
|
|
<AvatarFallback>
|
|
{team.name?.[0]?.toUpperCase() || "T"}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<div className="font-medium text-foreground">
|
|
{team.name}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{team.visibility || "private"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Badge
|
|
variant="outline"
|
|
className="border-border/50"
|
|
>
|
|
Team
|
|
</Badge>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigate(`/projects/new`)}
|
|
>
|
|
New project
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<aside className="space-y-6">
|
|
<Card className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Create a team</CardTitle>
|
|
<CardDescription>
|
|
Private by default; you can invite members later.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<Input
|
|
placeholder="Team name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
/>
|
|
<Textarea
|
|
placeholder="Short description (optional)"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
/>
|
|
<div className="flex justify-end">
|
|
<Button
|
|
onClick={handleCreate}
|
|
disabled={!canCreate}
|
|
className="rounded-full bg-gradient-to-r from-aethex-500 to-neon-blue text-white"
|
|
>
|
|
{creating ? "Creating..." : "Create team"}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|