aethex-forge/client/components/ProfileEditor.tsx
Claude b640b0d2ad
Mobile optimization pass for responsive layouts
- TabsList: Add responsive grid columns (grid-cols-2/3 on mobile)
- Headers: Stack vertically on mobile with responsive text sizes
- Dialogs: Use viewport-relative heights (70-80vh on mobile)
- Grids: Add sm: breakpoints for single-column mobile layouts
- Tables: Add overflow-x-auto for horizontal scrolling
- Buttons: Full-width on mobile with flex-1 sm:flex-none
- Select triggers: Full-width on mobile

Files updated: 21 component and page files across admin,
staff, dashboards, and hub sections.
2026-01-26 22:46:26 +00:00

664 lines
22 KiB
TypeScript

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 { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Globe,
Briefcase,
Award,
Clock,
MapPin,
DollarSign,
Zap,
Plus,
Trash2,
Copy,
CheckCircle2,
} from "lucide-react";
import { type AethexUserProfile } from "@/lib/aethex-database-adapter";
interface SkillItem {
name: string;
level: "beginner" | "intermediate" | "expert";
}
interface WorkExperienceItem {
company: string;
title: string;
duration: string;
description?: string;
}
interface PortfolioItem {
title: string;
url: string;
description?: string;
}
interface ProfileEditorProps {
profile: AethexUserProfile;
onSave: (updates: Partial<AethexUserProfile>) => Promise<void>;
isSaving?: boolean;
username?: string;
}
const ARMS = [
{ id: "foundation", label: "Foundation", color: "bg-red-500" },
{ id: "gameforge", label: "GameForge", color: "bg-green-500" },
{ id: "labs", label: "Labs", color: "bg-yellow-500" },
{ id: "corp", label: "Corp", color: "bg-blue-500" },
];
export function ProfileEditor({
profile,
onSave,
isSaving = false,
username,
}: ProfileEditorProps) {
const [formData, setFormData] = useState({
bio_detailed: profile.bio_detailed || "",
twitter_url: profile.twitter_url || "",
linkedin_url: profile.linkedin_url || "",
github_url: profile.github_url || "",
portfolio_url: profile.portfolio_url || "",
youtube_url: profile.youtube_url || "",
twitch_url: profile.twitch_url || "",
hourly_rate: profile.hourly_rate?.toString() || "",
availability_status: profile.availability_status || "available",
timezone: profile.timezone || "",
location: profile.location || "",
languages: (profile.languages as string[]) || [],
skills_detailed: (profile.skills_detailed as SkillItem[]) || [],
work_experience: (profile.work_experience as WorkExperienceItem[]) || [],
portfolio_items: (profile.portfolio_items as PortfolioItem[]) || [],
arm_affiliations: (profile.arm_affiliations as string[]) || [],
});
const [newSkill, setNewSkill] = useState({
name: "",
level: "intermediate" as const,
});
const [newLanguage, setNewLanguage] = useState("");
const [newWorkExp, setNewWorkExp] = useState({
company: "",
title: "",
duration: "",
});
const [newPortfolio, setNewPortfolio] = useState({ title: "", url: "" });
const [copied, setCopied] = useState(false);
const profileUrl = username ? `https://${username}.aethex.me` : "";
const handleSubmit = async () => {
await onSave({
...formData,
hourly_rate: formData.hourly_rate
? parseFloat(formData.hourly_rate)
: undefined,
skills_detailed: formData.skills_detailed,
languages: formData.languages,
work_experience: formData.work_experience,
portfolio_items: formData.portfolio_items,
arm_affiliations: formData.arm_affiliations,
});
};
const addSkill = () => {
if (newSkill.name.trim()) {
setFormData({
...formData,
skills_detailed: [...formData.skills_detailed, newSkill],
});
setNewSkill({ name: "", level: "intermediate" });
}
};
const removeSkill = (index: number) => {
setFormData({
...formData,
skills_detailed: formData.skills_detailed.filter((_, i) => i !== index),
});
};
const addLanguage = () => {
if (newLanguage.trim()) {
setFormData({
...formData,
languages: [...formData.languages, newLanguage],
});
setNewLanguage("");
}
};
const removeLanguage = (index: number) => {
setFormData({
...formData,
languages: formData.languages.filter((_, i) => i !== index),
});
};
const addWorkExp = () => {
if (newWorkExp.company.trim() && newWorkExp.title.trim()) {
setFormData({
...formData,
work_experience: [...formData.work_experience, newWorkExp],
});
setNewWorkExp({ company: "", title: "", duration: "" });
}
};
const removeWorkExp = (index: number) => {
setFormData({
...formData,
work_experience: formData.work_experience.filter((_, i) => i !== index),
});
};
const addPortfolio = () => {
if (newPortfolio.title.trim() && newPortfolio.url.trim()) {
setFormData({
...formData,
portfolio_items: [...formData.portfolio_items, newPortfolio],
});
setNewPortfolio({ title: "", url: "" });
}
};
const removePortfolio = (index: number) => {
setFormData({
...formData,
portfolio_items: formData.portfolio_items.filter((_, i) => i !== index),
});
};
const toggleArmAffiliation = (armId: string) => {
setFormData({
...formData,
arm_affiliations: formData.arm_affiliations.includes(armId)
? formData.arm_affiliations.filter((a) => a !== armId)
: [...formData.arm_affiliations, armId],
});
};
const copyProfileUrl = () => {
navigator.clipboard.writeText(profileUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-5">
<TabsTrigger value="basic">Basic</TabsTrigger>
<TabsTrigger value="social">Social</TabsTrigger>
<TabsTrigger value="skills">Skills</TabsTrigger>
<TabsTrigger value="experience">Experience</TabsTrigger>
<TabsTrigger value="arms">Arms</TabsTrigger>
</TabsList>
{/* BASIC INFO TAB */}
<TabsContent value="basic" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Profile Link</CardTitle>
<CardDescription>Your public profile URL</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2">
<Input value={profileUrl} disabled className="bg-muted" />
<Button
variant="outline"
size="icon"
onClick={copyProfileUrl}
title={copied ? "Copied!" : "Copy"}
>
{copied ? (
<CheckCircle2 className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>About You</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">Bio</label>
<textarea
value={formData.bio_detailed}
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}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">Location</label>
<Input
value={formData.location}
onChange={(e) =>
setFormData({ ...formData, location: e.target.value })
}
placeholder="City, Country"
/>
</div>
<div>
<label className="text-sm font-medium">Timezone</label>
<Input
value={formData.timezone}
onChange={(e) =>
setFormData({ ...formData, timezone: e.target.value })
}
placeholder="UTC-8 or America/Los_Angeles"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium flex items-center gap-2">
<DollarSign className="h-4 w-4" />
Hourly Rate
</label>
<Input
type="number"
value={formData.hourly_rate}
onChange={(e) =>
setFormData({ ...formData, hourly_rate: e.target.value })
}
placeholder="50"
/>
</div>
<div>
<label className="text-sm font-medium flex items-center gap-2">
<Clock className="h-4 w-4" />
Availability
</label>
<select
value={formData.availability_status}
onChange={(e) =>
setFormData({
...formData,
availability_status: e.target.value,
})
}
className="w-full px-3 py-2 border rounded-lg bg-background"
>
<option value="available">Available Now</option>
<option value="limited">Limited Availability</option>
<option value="unavailable">Not Available</option>
</select>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* SOCIAL TAB */}
<TabsContent value="social" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Globe className="h-5 w-5" />
Social & Web Links
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">Twitter</label>
<Input
value={formData.twitter_url}
onChange={(e) =>
setFormData({ ...formData, twitter_url: e.target.value })
}
placeholder="https://twitter.com/username"
/>
</div>
<div>
<label className="text-sm font-medium">LinkedIn</label>
<Input
value={formData.linkedin_url}
onChange={(e) =>
setFormData({ ...formData, linkedin_url: e.target.value })
}
placeholder="https://linkedin.com/in/username"
/>
</div>
<div>
<label className="text-sm font-medium">GitHub</label>
<Input
value={formData.github_url}
onChange={(e) =>
setFormData({ ...formData, github_url: e.target.value })
}
placeholder="https://github.com/username"
/>
</div>
<div>
<label className="text-sm font-medium">Portfolio Website</label>
<Input
value={formData.portfolio_url}
onChange={(e) =>
setFormData({ ...formData, portfolio_url: e.target.value })
}
placeholder="https://yourportfolio.com"
/>
</div>
<div>
<label className="text-sm font-medium">YouTube</label>
<Input
value={formData.youtube_url}
onChange={(e) =>
setFormData({ ...formData, youtube_url: e.target.value })
}
placeholder="https://youtube.com/@username"
/>
</div>
<div>
<label className="text-sm font-medium">Twitch</label>
<Input
value={formData.twitch_url}
onChange={(e) =>
setFormData({ ...formData, twitch_url: e.target.value })
}
placeholder="https://twitch.tv/username"
/>
</div>
</CardContent>
</Card>
</TabsContent>
{/* SKILLS TAB */}
<TabsContent value="skills" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5" />
Skills & Languages
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Skills */}
<div>
<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>
<p className="font-medium">{skill.name}</p>
<p className="text-xs text-muted-foreground capitalize">
{skill.level}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeSkill(idx)}
className="h-6 w-6 p-0"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
<div className="flex gap-2">
<Input
value={newSkill.name}
onChange={(e) =>
setNewSkill({ ...newSkill, name: e.target.value })
}
placeholder="Add skill"
/>
<select
value={newSkill.level}
onChange={(e) =>
setNewSkill({
...newSkill,
level: e.target.value as
| "beginner"
| "intermediate"
| "expert",
})
}
className="px-2 border rounded-lg bg-background"
>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="expert">Expert</option>
</select>
<Button size="sm" onClick={addSkill}>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
{/* Languages */}
<div>
<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"
>
<p className="font-medium">{lang}</p>
<Button
variant="ghost"
size="sm"
onClick={() => removeLanguage(idx)}
className="h-6 w-6 p-0"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
<div className="flex gap-2">
<Input
value={newLanguage}
onChange={(e) => setNewLanguage(e.target.value)}
placeholder="Add language (e.g., English, Spanish)"
/>
<Button size="sm" onClick={addLanguage}>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* EXPERIENCE TAB */}
<TabsContent value="experience" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Briefcase className="h-5 w-5" />
Work Experience & Portfolio
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Work Experience */}
<div>
<h3 className="font-semibold mb-3">Work Experience</h3>
<div className="space-y-2 mb-4">
{formData.work_experience.map((exp, idx) => (
<div key={idx} className="p-3 bg-muted rounded space-y-1">
<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>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeWorkExp(idx)}
className="h-6 w-6 p-0"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
{exp.description && (
<p className="text-sm text-muted-foreground">
{exp.description}
</p>
)}
</div>
))}
</div>
<div className="space-y-2 p-3 border rounded-lg">
<Input
value={newWorkExp.title}
onChange={(e) =>
setNewWorkExp({ ...newWorkExp, title: e.target.value })
}
placeholder="Job Title"
/>
<Input
value={newWorkExp.company}
onChange={(e) =>
setNewWorkExp({ ...newWorkExp, company: e.target.value })
}
placeholder="Company"
/>
<Input
value={newWorkExp.duration}
onChange={(e) =>
setNewWorkExp({ ...newWorkExp, duration: e.target.value })
}
placeholder="Duration (e.g., 2020-2023)"
/>
<Button size="sm" onClick={addWorkExp} className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Experience
</Button>
</div>
</div>
{/* Portfolio Items */}
<div>
<h3 className="font-semibold mb-3">Portfolio Items</h3>
<div className="space-y-2 mb-4">
{formData.portfolio_items.map((item, idx) => (
<div key={idx} className="p-3 bg-muted rounded space-y-1">
<div className="flex justify-between items-start">
<div>
<p className="font-medium">{item.title}</p>
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-500 hover:underline"
>
{item.url}
</a>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removePortfolio(idx)}
className="h-6 w-6 p-0"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
{item.description && (
<p className="text-sm text-muted-foreground">
{item.description}
</p>
)}
</div>
))}
</div>
<div className="space-y-2 p-3 border rounded-lg">
<Input
value={newPortfolio.title}
onChange={(e) =>
setNewPortfolio({ ...newPortfolio, title: e.target.value })
}
placeholder="Project Title"
/>
<Input
value={newPortfolio.url}
onChange={(e) =>
setNewPortfolio({ ...newPortfolio, url: e.target.value })
}
placeholder="Project URL"
/>
<Button size="sm" onClick={addPortfolio} className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Portfolio Item
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* ARMS TAB */}
<TabsContent value="arms" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Award className="h-5 w-5" />
Arm Affiliations
</CardTitle>
<CardDescription>
Select the arms you're part of. They can also be auto-detected
from your activities.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-3">
{ARMS.map((arm) => (
<button
key={arm.id}
onClick={() => toggleArmAffiliation(arm.id)}
className={`p-4 rounded-lg border-2 transition-all ${
formData.arm_affiliations.includes(arm.id)
? `border-${arm.color.split("-")[1]}-500 bg-${arm.color.split("-")[1]}-50`
: "border-muted hover:border-muted-foreground/50"
}`}
>
<Badge className={arm.color}>{arm.label}</Badge>
</button>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<div className="flex justify-end gap-2 mt-6">
<Button variant="outline">Cancel</Button>
<Button onClick={handleSubmit} disabled={isSaving}>
{isSaving ? "Saving..." : "Save Profile"}
</Button>
</div>
</Tabs>
);
}