Files
multica/packages/views/settings/components/account-tab.tsx
Naiyuan Qing d911cdf5ac refactor: extract all shared logic to packages — apps are now thin routing shells
- 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>
2026-04-09 16:45:41 +08:00

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>
);
}