From 7e79674b5f50ebc3409fb9485ae2b17273591857 Mon Sep 17 00:00:00 2001 From: Art Moskvin Date: Wed, 12 Feb 2025 20:58:35 +0100 Subject: [PATCH] draft user account --- src/components/AccountDialog.tsx | 144 +++++++++++++++++++++++++++++++ src/components/AppSidebar.tsx | 19 +++- src/components/UserSwitcher.tsx | 48 +++++++---- src/global.d.ts | 5 ++ src/main/db.ts | 85 ++++++++++++++++++ src/preload.ts | 8 ++ src/types/account.ts | 6 ++ 7 files changed, 299 insertions(+), 16 deletions(-) create mode 100644 src/components/AccountDialog.tsx create mode 100644 src/types/account.ts diff --git a/src/components/AccountDialog.tsx b/src/components/AccountDialog.tsx new file mode 100644 index 0000000..085cca0 --- /dev/null +++ b/src/components/AccountDialog.tsx @@ -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(null); + const [draftSettings, setDraftSettings] = React.useState | null>(null); + const [isSaving, setIsSaving] = React.useState(false); + const [error, setError] = React.useState(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 ( + + + + Account Settings + + Manage your account details and preferences. + + +
+
+
+ + handleSettingChange("email", e.target.value)} + className="col-span-3" + /> +
+
+ + handleSettingChange("username", e.target.value)} + className="col-span-3" + /> +
+
+ {(error || externalError) && ( +
+ {error || externalError} +
+ )} + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 787aa00..d1e73a8 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -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 { @@ -54,6 +55,8 @@ export function AppSidebar({ const [projectToDelete, setProjectToDelete] = React.useState(null); const [chatToEdit, setChatToEdit] = React.useState(null); const [chatToDelete, setChatToDelete] = React.useState(null); + const [isAccountOpen, setIsAccountOpen] = React.useState(false); + const [accountVersion, setAccountVersion] = React.useState(0); return ( @@ -93,9 +96,23 @@ export function AppSidebar({ }} onDelete={onDeleteConversation} /> + { + setIsAccountOpen(open); + if (!open) { + // Increment version to trigger UserSwitcher refresh + setAccountVersion(v => v + 1); + } + }} + /> - + setIsAccountOpen(true)} + version={accountVersion} + /> diff --git a/src/components/UserSwitcher.tsx b/src/components/UserSwitcher.tsx index b7da166..033132c 100644 --- a/src/components/UserSwitcher.tsx +++ b/src/components/UserSwitcher.tsx @@ -1,3 +1,4 @@ +import * as React from "react" import { ChevronsUpDown } from "lucide-react" import { DropdownMenu, @@ -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 ( @@ -37,12 +51,14 @@ export function UserSwitcher({ onSettingsClick }: UserSwitcherProps) { className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > - - AM + + + {accountSettings.username.slice(0, 2).toUpperCase()} +
- {user.name} - {user.email} + {accountSettings.username} + {accountSettings.email}
@@ -56,17 +72,19 @@ export function UserSwitcher({ onSettingsClick }: UserSwitcherProps) {
- - AM + + + {accountSettings.username.slice(0, 2).toUpperCase()} +
- {user.name} - {user.email} + {accountSettings.username} + {accountSettings.email}
console.log("Account clicked")} + onClick={onAccountClick} className="gap-2 p-2" > Account diff --git a/src/global.d.ts b/src/global.d.ts index 002ae31..6255ac0 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -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 { @@ -20,6 +21,10 @@ declare global { update: (conversation: Conversation) => Promise; delete: (id: string) => Promise; }; + account: { + get: () => Promise; + update: (settings: Omit) => Promise; + }; settings: { get: () => Promise; update: (settings: Omit) => Promise; diff --git a/src/main/db.ts b/src/main/db.ts index a6ad0dc..b92aaf9 100644 --- a/src/main/db.ts +++ b/src/main/db.ts @@ -5,6 +5,7 @@ import { homedir } from 'os'; import { v4 as uuidv4 } from 'uuid'; import { Conversation, Project, Task } from '../types'; import { UserSettings } from '../types/settings'; +import { AccountSettings } from '../types/account'; let db: Database.Database; @@ -49,6 +50,17 @@ export const initializeDatabase = () => { ) `); + // Create account_settings table if it doesn't exist + db.exec(` + CREATE TABLE IF NOT EXISTS account_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + email TEXT NOT NULL, + username TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + `); + // Create tasks table if it doesn't exist db.exec(` CREATE TABLE IF NOT EXISTS tasks ( @@ -297,6 +309,59 @@ export const deleteConversationTasks = (conversationId: string): void => { stmt.run(conversationId); }; +export const getAccountSettings = (): AccountSettings | null => { + const stmt = db.prepare('SELECT * FROM account_settings WHERE id = 1'); + const row = stmt.get(); + + if (!row) return null; + + return { + email: row.email, + username: row.username, + created_at: row.created_at, + updated_at: row.updated_at + }; +}; + +export const updateAccountSettings = async (settings: Omit): Promise => { + const currentSettings = db.prepare('SELECT created_at FROM account_settings WHERE id = 1').get(); + + if (currentSettings) { + // Update existing settings + const stmt = db.prepare(` + UPDATE account_settings + SET email = ?, + username = ?, + updated_at = ? + WHERE id = 1 + `); + stmt.run( + settings.email, + settings.username, + Date.now() + ); + } else { + // Insert new settings + const stmt = db.prepare(` + INSERT INTO account_settings ( + id, + email, + username, + created_at, + updated_at + ) VALUES (?, ?, ?, ?, ?) + `); + const now = Date.now(); + stmt.run( + 1, + settings.email, + settings.username, + now, + now + ); + } +}; + export const setupDbHandlers = () => { // Settings handlers ipcMain.handle('settings:get', async () => { @@ -318,6 +383,26 @@ export const setupDbHandlers = () => { } }); + // Account handlers + ipcMain.handle('account:get', async () => { + try { + return getAccountSettings(); + } catch (err) { + console.error('Error getting account settings:', err); + throw err; + } + }); + + ipcMain.handle('account:update', async (_, settings: Omit) => { + try { + await updateAccountSettings(settings); + return getAccountSettings(); + } catch (err) { + console.error('Error updating account settings:', err); + throw err; + } + }); + // Project handlers ipcMain.handle('projects:getAll', async () => { try { diff --git a/src/preload.ts b/src/preload.ts index 8f06a70..fed6543 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -3,6 +3,7 @@ import { contextBridge, ipcRenderer } from 'electron'; import { Project, Conversation } from '@/types'; import { Message } from '@/types/message'; import { UserSettings } from '@/types/settings'; +import { AccountSettings } from '@/types/account'; // Expose file dialog API and other electron features contextBridge.exposeInMainWorld('electron', { @@ -30,6 +31,13 @@ contextBridge.exposeInMainWorld('conversations', { delete: (id: string) => ipcRenderer.invoke('conversations:delete', { id }) }); +// Expose account API +contextBridge.exposeInMainWorld('account', { + get: () => ipcRenderer.invoke('account:get'), + update: (settings: Omit) => + ipcRenderer.invoke('account:update', settings) +}); + // Expose settings API contextBridge.exposeInMainWorld('settings', { get: () => ipcRenderer.invoke('settings:get'), diff --git a/src/types/account.ts b/src/types/account.ts new file mode 100644 index 0000000..c5abe38 --- /dev/null +++ b/src/types/account.ts @@ -0,0 +1,6 @@ +export interface AccountSettings { + email: string; + username: string; + created_at: number; + updated_at: number; +} \ No newline at end of file