aethex-forge/client/pages/Network.tsx
MrPiglr 0623521374
feat: complete Phase 2 design system rollout
Applied max-w-6xl standard across all remaining pages:

**Internal Tools & Admin (6 files):**
- Teams.tsx, Squads.tsx, Network.tsx, Portal.tsx
- Admin.tsx, BotPanel.tsx, Arms.tsx

**Hub/Client Pages (6 files):**
- ClientHub.tsx, ClientProjects.tsx (all 3 instances)
- ClientDashboard.tsx, ClientSettings.tsx
- ClientContracts.tsx, ClientInvoices.tsx, ClientReports.tsx

**Dashboard Pages (5 files):**
- FoundationDashboard.tsx, GameForgeDashboard.tsx
- StaffDashboard.tsx, NexusDashboard.tsx, LabsDashboard.tsx

**Community & Creator Pages (6 files):**
- Directory.tsx, Projects.tsx
- CreatorDirectory.tsx, MentorProfile.tsx, EthosGuild.tsx
- FoundationDownloadCenter.tsx, OpportunitiesHub.tsx

**Result:** Zero instances of max-w-7xl remaining in client/pages
All pages now use consistent max-w-6xl width for optimal readability
2026-01-11 01:57:16 +00:00

406 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Layout from "@/components/Layout";
import Layout from "@/components/Layout";
import { useAuth } from "@/contexts/AuthContext";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import LoadingScreen from "@/components/LoadingScreen";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { useToast } from "@/hooks/use-toast";
import { aethexSocialService } from "@/lib/aethex-social-service";
import { UserPlus, UserCheck } from "lucide-react";
export default function Network() {
const { user, profile, loading } = useAuth();
const navigate = useNavigate();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(true);
const [recommended, setRecommended] = useState<any[]>([]);
const [following, setFollowing] = useState<string[]>([]);
const [connections, setConnections] = useState<any[]>([]);
const [endorsements, setEndorsements] = useState<any[]>([]);
const [inviteEmail, setInviteEmail] = useState("");
const [inviteSending, setInviteSending] = useState(false);
useEffect(() => {
if (!loading && !user) {
navigate("/login", { replace: true });
return;
}
if (!user) return;
const load = async () => {
setIsLoading(true);
try {
const recs = await aethexSocialService.listRecommended(user.id, 12);
setRecommended(recs);
const flw = await aethexSocialService.getFollowing(user.id);
setFollowing(flw);
const conns = await aethexSocialService.getConnections(user.id);
setConnections(conns);
const ends = await aethexSocialService.getEndorsements(user.id);
setEndorsements(ends);
} finally {
setIsLoading(false);
}
};
load();
}, [user, loading, navigate]);
const isFollowing = (id: string) => following.includes(id);
const toggleFollow = async (targetId: string) => {
if (!user) return;
if (isFollowing(targetId)) {
await aethexSocialService.unfollowUser(user.id, targetId);
setFollowing((s) => s.filter((x) => x !== targetId));
} else {
await aethexSocialService.followUser(user.id, targetId);
setFollowing((s) => Array.from(new Set([...s, targetId])));
}
};
const handleInvite = async () => {
if (!user) return;
const email = inviteEmail.trim();
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
toast({ variant: "destructive", title: "Invalid email" });
return;
}
setInviteSending(true);
try {
await aethexSocialService.sendInvite(user.id, email, null);
toast({ description: "Invitation sent" });
setInviteEmail("");
} catch (e: any) {
toast({
variant: "destructive",
title: "Failed to send invite",
description: e?.message || "Try again later",
});
} finally {
setInviteSending(false);
}
};
const handleEndorse = async (targetId: string, skill: string) => {
if (!user) return;
try {
await aethexSocialService.endorseSkill(user.id, targetId, skill);
toast({ description: `Endorsed for ${skill}` });
} catch (e: any) {
toast({
variant: "destructive",
title: "Failed to endorse",
description: e?.message || "Try again later",
});
}
};
if (loading || isLoading) {
return (
<LoadingScreen
message="Loading your network..."
showProgress
duration={1000}
/>
);
}
if (!user) return null;
return (
<Layout>
<div className="min-h-screen bg-aethex-gradient py-8">
<div className="container mx-auto px-4 max-w-6xl grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Public Profile */}
<div className="lg:col-span-4 space-y-6">
<Card className="bg-card/50 border-border/50">
<CardContent className="p-6 space-y-4">
<div className="flex items-center gap-4">
<Avatar className="h-16 w-16">
<AvatarImage src={profile?.avatar_url} />
<AvatarFallback>
{profile?.full_name?.[0] ||
user.email?.[0]?.toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<h2 className="text-xl font-semibold">
{profile?.full_name || user.email?.split("@")[0]}
</h2>
<p className="text-sm text-muted-foreground">
{profile?.role || "Member"}
</p>
<div className="mt-2 flex gap-2">
<Badge
variant="outline"
className="border-aethex-400/50 text-aethex-400"
>
Level {profile?.level || 1}
</Badge>
<Badge variant="outline">
{(profile as any)?.experience_level || "beginner"}
</Badge>
</div>
</div>
</div>
{profile?.bio && (
<p className="text-sm text-muted-foreground">{profile.bio}</p>
)}
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => navigate("/dashboard")}
>
Edit Profile
</Button>
<Button
variant="outline"
onClick={() => navigate("/onboarding")}
>
Improve Profile
</Button>
</div>
</CardContent>
</Card>
<Card className="bg-card/50 border-border/50">
<CardHeader>
<CardTitle>Invite collaborator</CardTitle>
<CardDescription>Grow your network</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-2">
<Input
placeholder="name@company.com"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
/>
<Button onClick={handleInvite} disabled={inviteSending}>
Send
</Button>
</div>
<p className="text-xs text-muted-foreground">
Accepted invites boost your loyalty, XP, and reputation.
</p>
</CardContent>
</Card>
<Card className="bg-card/50 border-border/50">
<CardHeader>
<CardTitle>Recommendations</CardTitle>
<CardDescription>
People who align with your interests
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{recommended.slice(0, 3).map((r) => (
<div key={r.id} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarImage src={r.avatar_url} />
<AvatarFallback>
{(r.full_name || r.username || "U")[0]}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">
{r.full_name || r.username}
</div>
<div className="text-xs text-muted-foreground">
{r.bio?.slice(0, 40) || "Member"}
</div>
</div>
</div>
<Button
size="sm"
variant={isFollowing(r.id) ? "outline" : "default"}
onClick={() => toggleFollow(r.id)}
>
{isFollowing(r.id) ? (
<span className="flex items-center gap-1">
<UserCheck className="h-4 w-4" /> Following
</span>
) : (
<span className="flex items-center gap-1">
<UserPlus className="h-4 w-4" /> Follow
</span>
)}
</Button>
</div>
))}
{recommended.length === 0 && (
<div className="text-sm text-muted-foreground">
No recommendations yet.
</div>
)}
</CardContent>
</Card>
</div>
{/* Connections + Discover + Endorsements */}
<div className="lg:col-span-8 space-y-6">
<Card className="bg-card/50 border-border/50">
<CardHeader>
<CardTitle>My connections</CardTitle>
<CardDescription>People youre connected with</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{connections.map((c) => {
const p = (c as any).user_profiles || {};
const display = p.full_name || p.username || c.connection_id;
return (
<div
key={c.connection_id}
className="flex items-center justify-between p-3 rounded-lg border border-border/50"
>
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarImage src={p.avatar_url || undefined} />
<AvatarFallback>{(display || "U")[0]}</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{display}</div>
<div className="text-xs text-muted-foreground">
Connected
</div>
</div>
</div>
<Button size="sm" variant="outline">
Message
</Button>
</div>
);
})}
{connections.length === 0 && (
<div className="text-sm text-muted-foreground">
No connections yet.
</div>
)}
</CardContent>
</Card>
<Card className="bg-card/50 border-border/50">
<CardHeader>
<CardTitle>Discover People</CardTitle>
<CardDescription>
Connect with creators, clients, and members
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{recommended.map((r) => (
<div
key={r.id}
className="flex items-center justify-between p-3 rounded-lg border border-border/50 hover:border-aethex-400/50 transition-all"
>
<div className="flex items-center gap-3">
<Avatar className="h-12 w-12">
<AvatarImage src={r.avatar_url} />
<AvatarFallback>
{(r.full_name || r.username || "U")[0]}
</AvatarFallback>
</Avatar>
<div>
<div className="font-semibold">
{r.full_name || r.username}
</div>
<div className="text-xs text-muted-foreground">
{r.bio?.slice(0, 80) || "Member"}
</div>
</div>
</div>
<Button
size="sm"
variant={isFollowing(r.id) ? "outline" : "default"}
onClick={() => toggleFollow(r.id)}
>
{isFollowing(r.id) ? (
<span className="flex items-center gap-1">
<UserCheck className="h-4 w-4" /> Following
</span>
) : (
<span className="flex items-center gap-1">
<UserPlus className="h-4 w-4" /> Follow
</span>
)}
</Button>
</div>
))}
{recommended.length === 0 && (
<div className="text-sm text-muted-foreground">
No people found yet.
</div>
)}
</CardContent>
</Card>
<Card className="bg-card/50 border-border/50">
<CardHeader>
<CardTitle>Endorsements</CardTitle>
<CardDescription>
Recognize skills of your peers
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{connections.slice(0, 6).map((c) => {
const p = (c as any).user_profiles || {};
const display = p.full_name || p.username || c.connection_id;
return (
<div
key={`endorse-${c.connection_id}`}
className="flex items-center justify-between p-3 rounded-lg border border-border/50"
>
<div className="flex items-center gap-3">
<Avatar className="h-8 w-8">
<AvatarImage src={p.avatar_url || undefined} />
<AvatarFallback>{(display || "U")[0]}</AvatarFallback>
</Avatar>
<div className="text-sm">{display}</div>
</div>
<div className="flex gap-2 flex-wrap">
{(
[
"Leadership",
"Systems",
"Frontend",
"Backend",
] as const
).map((skill) => (
<Button
key={skill}
size="xs"
variant="outline"
onClick={() =>
handleEndorse(c.connection_id, skill)
}
>
{skill}
</Button>
))}
</div>
</div>
);
})}
{endorsements.length > 0 && (
<div className="text-xs text-muted-foreground">
You have received {endorsements.length} endorsements.
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
</Layout>
);
}