Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions src/components/AccountDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { AccountSettings } from "@/types/account"

interface AccountDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
error?: string | null;
}

export function AccountDialog({ open, onOpenChange, error: externalError }: AccountDialogProps) {
const [settings, setSettings] = React.useState<AccountSettings | null>(null);
const [draftSettings, setDraftSettings] = React.useState<Omit<AccountSettings, 'created_at' | 'updated_at'> | null>(null);
const [isSaving, setIsSaving] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);

// Load account settings when dialog opens
React.useEffect(() => {
if (open) {
window.account.get().then(settings => {
setSettings(settings);
if (settings) {
setDraftSettings({
email: settings.email,
username: settings.username,
});
} else {
setDraftSettings({
email: "",
username: "",
});
}
});
}
}, [open]);

const handleSettingChange = (key: keyof AccountSettings, value: string) => {
if (!draftSettings) return;
setDraftSettings({
...draftSettings,
[key]: value,
});
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!draftSettings) return;

try {
setError(null);
setIsSaving(true);

// Validate inputs
if (!draftSettings.email?.trim()) {
throw new Error("Please enter an email address");
}
if (!draftSettings.username?.trim()) {
throw new Error("Please enter a username");
}

const updatedSettings = await window.account.update(draftSettings);
setSettings(updatedSettings);
onOpenChange(false);
} catch (error) {
console.error('Error saving account settings:', error);
setError(error instanceof Error ? error.message : 'Failed to save account settings');
} finally {
setIsSaving(false);
}
};

// When dialog closes without saving, reset draft to current settings
const handleOpenChange = (open: boolean) => {
if (!open) {
setDraftSettings(settings);
setError(null);
}
onOpenChange(open);
};

return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Account Settings</DialogTitle>
<DialogDescription>
Manage your account details and preferences.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="space-y-4 py-2">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="email" className="text-right">
Email
</Label>
<Input
id="email"
type="email"
value={draftSettings?.email || ''}
onChange={(e) => handleSettingChange("email", e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="username" className="text-right">
Username
</Label>
<Input
id="username"
type="text"
value={draftSettings?.username || ''}
onChange={(e) => handleSettingChange("username", e.target.value)}
className="col-span-3"
/>
</div>
</div>
{(error || externalError) && (
<div className="mt-4 text-sm text-red-500">
{error || externalError}
</div>
)}
<DialogFooter className="mt-4">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={isSaving}>
{isSaving ? "Saving..." : "Save"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
19 changes: 18 additions & 1 deletion src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ProjectDialog } from "./ProjectDialog"
import { DeleteProjectDialog } from "./DeleteProjectDialog"
import { ChatDialog } from "./ChatDialog"
import { DeleteChatDialog } from "./DeleteChatDialog"
import { AccountDialog } from "./AccountDialog"


interface AppSidebarProps {
Expand Down Expand Up @@ -54,6 +55,8 @@ export function AppSidebar({
const [projectToDelete, setProjectToDelete] = React.useState<Project | null>(null);
const [chatToEdit, setChatToEdit] = React.useState<Conversation | null>(null);
const [chatToDelete, setChatToDelete] = React.useState<Conversation | null>(null);
const [isAccountOpen, setIsAccountOpen] = React.useState(false);
const [accountVersion, setAccountVersion] = React.useState(0);

return (
<Sidebar>
Expand Down Expand Up @@ -93,9 +96,23 @@ export function AppSidebar({
}}
onDelete={onDeleteConversation}
/>
<AccountDialog
open={isAccountOpen}
onOpenChange={(open) => {
setIsAccountOpen(open);
if (!open) {
// Increment version to trigger UserSwitcher refresh
setAccountVersion(v => v + 1);
}
}}
/>

<SidebarHeader className="space-y-4 p-4">
<UserSwitcher onSettingsClick={onSettingsClick} />
<UserSwitcher
onSettingsClick={onSettingsClick}
onAccountClick={() => setIsAccountOpen(true)}
version={accountVersion}
/>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
Expand Down
48 changes: 33 additions & 15 deletions src/components/UserSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from "react"
import { ChevronsUpDown } from "lucide-react"
import {
DropdownMenu,
Expand All @@ -17,15 +18,28 @@ import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"

interface UserSwitcherProps {
onSettingsClick: () => void;
onAccountClick: () => void;
version?: number; // Changes to trigger a refresh
}

export function UserSwitcher({ onSettingsClick }: UserSwitcherProps) {
export function UserSwitcher({ onSettingsClick, onAccountClick, version }: UserSwitcherProps) {
const { isMobile } = useSidebar()
const user = {
name: "artm",
email: "art@hide.sh",
avatar: "/user-avatar.png",
}
const [accountSettings, setAccountSettings] = React.useState({
username: "",
email: "",
})

// Load account settings when component mounts
React.useEffect(() => {
window.account.get().then(settings => {
if (settings) {
setAccountSettings({
username: settings.username,
email: settings.email,
});
}
});
}, [version]) // Reload when version changes

return (
<SidebarMenu>
Expand All @@ -37,12 +51,14 @@ export function UserSwitcher({ onSettingsClick }: UserSwitcherProps) {
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">AM</AvatarFallback>
<AvatarImage src="/user-avatar.png" alt={accountSettings.username} />
<AvatarFallback className="rounded-lg">
{accountSettings.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
<span className="truncate font-semibold">{accountSettings.username}</span>
<span className="truncate text-xs">{accountSettings.email}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
Expand All @@ -56,17 +72,19 @@ export function UserSwitcher({ onSettingsClick }: UserSwitcherProps) {
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">AM</AvatarFallback>
<AvatarImage src="/user-avatar.png" alt={accountSettings.username} />
<AvatarFallback className="rounded-lg">
{accountSettings.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
<span className="truncate font-semibold">{accountSettings.username}</span>
<span className="truncate text-xs">{accountSettings.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => console.log("Account clicked")}
onClick={onAccountClick}
className="gap-2 p-2"
>
Account
Expand Down
5 changes: 5 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Project, Conversation } from '@/types';
import { Message } from '@/types/message';
import { UserSettings } from '@/types/settings';
import { AccountSettings } from '@/types/account';

declare global {
interface Window {
Expand All @@ -20,6 +21,10 @@ declare global {
update: (conversation: Conversation) => Promise<Conversation>;
delete: (id: string) => Promise<void>;
};
account: {
get: () => Promise<AccountSettings | null>;
update: (settings: Omit<AccountSettings, 'created_at' | 'updated_at'>) => Promise<AccountSettings>;
};
settings: {
get: () => Promise<UserSettings | null>;
update: (settings: Omit<UserSettings, 'created_at' | 'updated_at'>) => Promise<UserSettings>;
Expand Down
Loading