mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 11:10:23 +02:00
- Add CoreProvider to @multica/core/platform — single component for API/stores/WS/QueryClient init - Delete 13 platform files across web (6) and desktop (7), each app keeps only navigation.tsx - Extract AppSidebar + DashboardLayout to @multica/views/layout - Extract LoginPage to @multica/views/auth - Extract AgentsPage (1,279 lines) to @multica/views/agents (11 files) - Extract InboxPage (468 lines) to @multica/views/inbox (5 files) - Extract SettingsPage + 6 tabs (1,277 lines) to @multica/views/settings (9 files) - Fix AppLink to use forwardRef for Base UI render prop compatibility - Fix Tailwind @source to scan .ts files (status config with bg-info/bg-warning) - Suppress next-themes React 19 script tag warning - Add WebProviders wrapper for Server→Client function passing - Wire all desktop routes to shared views, remove PlaceholderPage - Net: +106 / -4,094 lines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { Camera, Loader2, Save } from "lucide-react";
|
|
import { Input } from "@multica/ui/components/ui/input";
|
|
import { Label } from "@multica/ui/components/ui/label";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import { Card, CardContent } from "@multica/ui/components/ui/card";
|
|
import { toast } from "sonner";
|
|
import { useAuthStore } from "@multica/core/auth";
|
|
import { api } from "@multica/core/api";
|
|
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
|
|
|
|
export function AccountTab() {
|
|
const user = useAuthStore((s) => s.user);
|
|
const setUser = useAuthStore((s) => s.setUser);
|
|
|
|
const [profileName, setProfileName] = useState(user?.name ?? "");
|
|
const [profileSaving, setProfileSaving] = useState(false);
|
|
const { upload, uploading } = useFileUpload(api);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
setProfileName(user?.name ?? "");
|
|
}, [user]);
|
|
|
|
const initials = (user?.name ?? "")
|
|
.split(" ")
|
|
.map((w) => w[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.slice(0, 2);
|
|
|
|
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
// Reset input so the same file can be re-selected
|
|
e.target.value = "";
|
|
try {
|
|
const result = await upload(file);
|
|
if (!result) return;
|
|
const updated = await api.updateMe({ avatar_url: result.link });
|
|
setUser(updated);
|
|
toast.success("Avatar updated");
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "Failed to upload avatar");
|
|
}
|
|
};
|
|
|
|
const handleProfileSave = async () => {
|
|
setProfileSaving(true);
|
|
try {
|
|
const updated = await api.updateMe({ name: profileName });
|
|
setUser(updated);
|
|
toast.success("Profile updated");
|
|
} catch (e) {
|
|
toast.error(e instanceof Error ? e.message : "Failed to update profile");
|
|
} finally {
|
|
setProfileSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<section className="space-y-4">
|
|
<h2 className="text-sm font-semibold">Profile</h2>
|
|
|
|
<Card>
|
|
<CardContent className="space-y-4">
|
|
{/* Avatar upload */}
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
type="button"
|
|
className="group relative h-16 w-16 shrink-0 rounded-full bg-muted overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
onClick={() => fileInputRef.current?.click()}
|
|
disabled={uploading}
|
|
>
|
|
{user?.avatar_url ? (
|
|
<img
|
|
src={user.avatar_url}
|
|
alt={user.name}
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
) : (
|
|
<span className="flex h-full w-full items-center justify-center text-lg font-semibold text-muted-foreground">
|
|
{initials}
|
|
</span>
|
|
)}
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 transition-opacity group-hover:opacity-100">
|
|
{uploading ? (
|
|
<Loader2 className="h-5 w-5 animate-spin text-white" />
|
|
) : (
|
|
<Camera className="h-5 w-5 text-white" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
className="hidden"
|
|
onChange={handleAvatarUpload}
|
|
/>
|
|
<div className="text-xs text-muted-foreground">
|
|
Click to upload avatar
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-xs text-muted-foreground">Name</Label>
|
|
<Input
|
|
type="search"
|
|
value={profileName}
|
|
onChange={(e) => setProfileName(e.target.value)}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-end gap-2 pt-1">
|
|
<Button
|
|
size="sm"
|
|
onClick={handleProfileSave}
|
|
disabled={profileSaving || !profileName.trim()}
|
|
>
|
|
<Save className="h-3 w-3" />
|
|
{profileSaving ? "Updating..." : "Update Profile"}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|