From 2f85973fb9b78478007324426ac160e3a708eb98 Mon Sep 17 00:00:00 2001 From: saknarajapakshe Date: Fri, 17 Jul 2026 16:05:38 +0530 Subject: [PATCH 1/3] feat: add ManageAccountDialog component for managing user accounts --- src/view/atoms/ConfirmDialog.tsx | 10 +- src/view/moles/Header.tsx | 19 -- src/view/moles/ManageAccountDialog.tsx | 314 +++++++++++++++++++++++++ 3 files changed, 321 insertions(+), 22 deletions(-) create mode 100644 src/view/moles/ManageAccountDialog.tsx diff --git a/src/view/atoms/ConfirmDialog.tsx b/src/view/atoms/ConfirmDialog.tsx index 083c210..4761d98 100644 --- a/src/view/atoms/ConfirmDialog.tsx +++ b/src/view/atoms/ConfirmDialog.tsx @@ -40,11 +40,15 @@ const ConfirmDialog = ({ return ( - {title} + + {title} + - {message} + + {message} + - + diff --git a/src/view/moles/Header.tsx b/src/view/moles/Header.tsx index 4266722..25353b4 100644 --- a/src/view/moles/Header.tsx +++ b/src/view/moles/Header.tsx @@ -10,9 +10,6 @@ import { Avatar, Tooltip, } from "@mui/material"; -import { useColorScheme } from "@mui/material/styles"; -import DarkModeOutlinedIcon from "@mui/icons-material/DarkModeOutlined"; -import LightModeOutlinedIcon from "@mui/icons-material/LightModeOutlined"; import { useNavigate } from "react-router-dom"; import { useAccount } from "../../nonview/core/AccountContext"; import { getInitials } from "../_constants/avatarUtils"; @@ -31,8 +28,6 @@ const Header = ({ const isMobile = useMediaQuery(theme.breakpoints.down("md")); const navigate = useNavigate(); const { activeAccount } = useAccount(); - const { mode, systemMode, setMode } = useColorScheme(); - const resolvedMode = (mode === "system" ? systemMode : mode) || "light"; const TitleIcon = titleIcon; @@ -124,20 +119,6 @@ const Header = ({ ); })} - - setMode(resolvedMode === "dark" ? "light" : "dark")} - aria-label="toggle color scheme" - sx={{ color: "text.secondary" }} - > - {resolvedMode === "dark" ? ( - - ) : ( - - )} - - - navigate("/profile")} aria-label="profile" diff --git a/src/view/moles/ManageAccountDialog.tsx b/src/view/moles/ManageAccountDialog.tsx new file mode 100644 index 0000000..e677585 --- /dev/null +++ b/src/view/moles/ManageAccountDialog.tsx @@ -0,0 +1,314 @@ +import React, { useState } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + IconButton, + List, + ListItemButton, + ListItemIcon, + ListItemText, + Box, + Avatar, + TextField, + Button, + Divider, + Typography, + Link, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useColorScheme } from "@mui/material/styles"; +import CloseIcon from "@mui/icons-material/Close"; +import PersonOutlineIcon from "@mui/icons-material/PersonOutline"; +import PaletteOutlinedIcon from "@mui/icons-material/PaletteOutlined"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { useAccount } from "../../nonview/core/AccountContext"; +import { getInitials } from "../_constants/avatarUtils"; +import ErrorMessage from "../atoms/ErrorMessage"; +import ConfirmDialog from "../atoms/ConfirmDialog"; + +interface ManageAccountDialogProps { + open: boolean; + onClose: () => void; +} + +const THEME_OPTIONS = ["light", "dark"] as const; +const NAV_ITEMS = [ + { key: "profile", label: "Profile", icon: PersonOutlineIcon }, + { key: "appearance", label: "Appearance", icon: PaletteOutlinedIcon }, +] as const; + +const ManageAccountDialog = ({ open, onClose }: ManageAccountDialogProps) => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("md")); + const { activeAccount, updateAccount, removeAccount } = useAccount(); + const { mode, systemMode, setMode } = useColorScheme(); + const resolvedMode = (mode === "system" ? systemMode : mode) || "light"; + + const [section, setSection] = useState<(typeof NAV_ITEMS)[number]["key"]>("profile"); + const [name, setName] = useState(activeAccount?.name || ""); + const [nameError, setNameError] = useState(""); + const [saved, setSaved] = useState(false); + const [confirmOpen, setConfirmOpen] = useState(false); + const [removing, setRemoving] = useState(false); + + if (!activeAccount) return null; + + const handleSave = () => { + if (!name.trim()) { + setNameError("Name is required"); + return; + } + updateAccount(activeAccount.id, { name: name.trim() }); + setSaved(true); + }; + + const handleRemove = async () => { + setRemoving(true); + await removeAccount(activeAccount.id); + setRemoving(false); + setConfirmOpen(false); + onClose(); + }; + + const filledFieldSx = { + "& .MuiFilledInput-root": { + borderRadius: 2, + bgcolor: "action.hover", + "&::before, &::after": { display: "none" }, + }, + }; + + return ( + <> + + + + {getInitials(activeAccount.name)} + + + + {activeAccount.name} + + + {activeAccount.email} + + + + + + + + + + + + {NAV_ITEMS.map(({ key, label, icon: Icon }) => ( + setSection(key)} + sx={fullScreen ? { flex: 1, justifyContent: "center", mx: 0.5, borderRadius: 1.5 } : { mx: 1, borderRadius: 1.5 }} + > + + + + + + ))} + + + + {section === "profile" && ( + + + + {getInitials(activeAccount.name)} + + + + Profile photo + + {/* No avatar-upload backend yet — inert placeholder. */} + + Change photo + + + + + {saved && ( + + setSaved(false)} + /> + + )} + + { + setName(e.target.value); + setNameError(""); + setSaved(false); + }} + error={!!nameError} + helperText={nameError} + sx={{ mb: 2, ...filledFieldSx }} + /> + + + + + + Account management + + setConfirmOpen(true)} + sx={{ + border: 1, + borderColor: "divider", + borderRadius: 2, + }} + > + + + + + + + + )} + + {section === "appearance" && ( + + + Theme + + + {THEME_OPTIONS.map((option) => ( + setMode(option)} + sx={{ + flex: 1, + cursor: "pointer", + border: 2, + borderColor: resolvedMode === option ? "primary.main" : "divider", + borderRadius: 2, + p: 2, + textAlign: "center", + bgcolor: option === "dark" ? "grey.900" : "grey.50", + color: option === "dark" ? "grey.50" : "grey.900", + }} + > + + {option} + + + ))} + + + )} + + + + + setConfirmOpen(false)} + /> + + ); +}; + +export default ManageAccountDialog; From 42a4d61460b7563f05c70a29383379e6c3f0ec96 Mon Sep 17 00:00:00 2001 From: saknarajapakshe Date: Mon, 20 Jul 2026 09:23:02 +0530 Subject: [PATCH 2/3] feat: add AddAccountModal component for adding accounts feat: enhance AddAccountModal with step tracking and improved layout --- src/view/moles/AddAccountForm.tsx | 121 +++++++++++++++++++---------- src/view/moles/AddAccountModal.tsx | 89 +++++++++++++++++++++ 2 files changed, 167 insertions(+), 43 deletions(-) create mode 100644 src/view/moles/AddAccountModal.tsx diff --git a/src/view/moles/AddAccountForm.tsx b/src/view/moles/AddAccountForm.tsx index 79e2213..7eeb57e 100644 --- a/src/view/moles/AddAccountForm.tsx +++ b/src/view/moles/AddAccountForm.tsx @@ -1,14 +1,16 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Box, Stack, TextField, Button, + IconButton, CircularProgress, MenuItem, Typography, MobileStepper, } from "@mui/material"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ErrorMessage from "../atoms/ErrorMessage"; import type { LinkedAccount, RegistrationData } from "../../nonview/core/accountStorage"; @@ -57,6 +59,14 @@ const EMAIL_PROVIDERS = { }, }; +const FILLED_FIELD_SX = { + "& .MuiFilledInput-root": { + borderRadius: 2, + bgcolor: "action.hover", + "&::before, &::after": { display: "none" }, + }, +}; + type Step = 1 | 2 | "success"; interface AddAccountFormProps { @@ -64,6 +74,7 @@ interface AddAccountFormProps { loading?: boolean; onSuccess: (account: LinkedAccount) => void; successCtaLabel?: string; + onStepChange?: (step: Step) => void; } // Two-step "add another account" form @@ -72,6 +83,7 @@ const AddAccountForm = ({ loading = false, onSuccess, successCtaLabel = "Continue", + onStepChange, }: AddAccountFormProps) => { const [step, setStep] = useState(1); const [formData, setFormData] = useState({ @@ -94,6 +106,10 @@ const AddAccountForm = ({ const isBusy = loading || submitting; + useEffect(() => { + onStepChange?.(step); + }, [step]); + const handleChange = (field: string) => (e: React.ChangeEvent) => { const value = e.target.value; let updates: Record = { [field]: value }; @@ -195,20 +211,20 @@ const AddAccountForm = ({ } }; + const dots = ( + } + nextButton={} + sx={{ justifyContent: "center", bgcolor: "transparent", px: 0 }} + /> + ); + return ( - {step !== "success" && ( - } - nextButton={} - sx={{ justifyContent: "center", bgcolor: "transparent", px: 0, mb: 3 }} - /> - )} - {formError && step !== "success" && ( setFormError("")} /> @@ -231,8 +247,9 @@ const AddAccountForm = ({ autoComplete="name" required fullWidth - variant="outlined" + variant="filled" disabled={isBusy} + sx={FILLED_FIELD_SX} /> + {dots} + - - + {dots} + + )} diff --git a/src/view/moles/AddAccountModal.tsx b/src/view/moles/AddAccountModal.tsx new file mode 100644 index 0000000..d334b23 --- /dev/null +++ b/src/view/moles/AddAccountModal.tsx @@ -0,0 +1,89 @@ +import { useState } from "react"; +import { Dialog, DialogTitle, DialogContent, Divider, IconButton, Box, Typography, useMediaQuery, useTheme } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import { useAccount } from "../../nonview/core/AccountContext"; +import { openAccountInNewTab } from "../../nonview/core/openAccountInNewTab"; +import AddAccountForm from "./AddAccountForm"; +import type { LinkedAccount } from "../../nonview/core/accountStorage"; + +interface AddAccountModalProps { + open: boolean; + onClose: () => void; +} + +const AddAccountModal = ({ open, onClose }: AddAccountModalProps) => { + const theme = useTheme(); + // This media query is to check whether the screen size is small (mobile) or not. + // (whether to display the dialog in full-screen mode or not) + const fullScreen = useMediaQuery(theme.breakpoints.down("md")); + const { addAccount } = useAccount(); + const [step, setStep] = useState<1 | 2 | "success">(1); + + // Fires synchronously from the button click (no await) + const handleSuccess = (account: LinkedAccount) => { + openAccountInNewTab(account.id); + onClose(); + }; + + return ( + + + + Add Account + + + + + + + + + + {step === 1 && ( + + + + Quicksilver + + + Welcome to your unified inbox + + + )} + + + + + ); +}; + +export default AddAccountModal; From bd2573622310714aeada32b0ea31cbf467f4e713 Mon Sep 17 00:00:00 2001 From: saknarajapakshe Date: Mon, 20 Jul 2026 12:33:09 +0530 Subject: [PATCH 3/3] feat: integrate AccountSwitcher component into Header for account management --- src/view/moles/AccountSwitcher.tsx | 250 +++++++++++++++++++++++++++++ src/view/moles/Header.tsx | 27 +--- 2 files changed, 254 insertions(+), 23 deletions(-) create mode 100644 src/view/moles/AccountSwitcher.tsx diff --git a/src/view/moles/AccountSwitcher.tsx b/src/view/moles/AccountSwitcher.tsx new file mode 100644 index 0000000..b03a631 --- /dev/null +++ b/src/view/moles/AccountSwitcher.tsx @@ -0,0 +1,250 @@ +import React, { useState } from "react"; +import { + IconButton, + Avatar, + Menu, + MenuItem, + Button, + Typography, + Box, + Divider, + Collapse, + Badge, +} from "@mui/material"; +import { alpha } from "@mui/material/styles"; +import CloseIcon from "@mui/icons-material/Close"; +import CameraAltOutlinedIcon from "@mui/icons-material/CameraAltOutlined"; +import AddIcon from "@mui/icons-material/Add"; +import LogoutOutlinedIcon from "@mui/icons-material/LogoutOutlined"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; +import { useAccount } from "../../nonview/core/AccountContext"; +import { useData } from "../../nonview/core/DataContext"; +import { openAccountInNewTab } from "../../nonview/core/openAccountInNewTab"; +import { getInitials, getAvatarColor } from "../_constants/avatarUtils"; +import ManageAccountDialog from "./ManageAccountDialog"; +import AddAccountModal from "./AddAccountModal"; +import ConfirmDialog from "../atoms/ConfirmDialog"; + +const AccountSwitcher = () => { + const { activeAccount, accounts, logoutAll } = useAccount(); + const { unreadCount } = useData(); + + const [anchorEl, setAnchorEl] = useState(null); + const [othersOpen, setOthersOpen] = useState(true); + const [manageOpen, setManageOpen] = useState(false); + const [addOpen, setAddOpen] = useState(false); + const [signOutOpen, setSignOutOpen] = useState(false); + const [signingOut, setSigningOut] = useState(false); + + if (!activeAccount) return null; + + const otherAccounts = accounts.filter((a) => a.id !== activeAccount.id); + + const closeMenu = () => setAnchorEl(null); + + const openManage = () => { + closeMenu(); + setManageOpen(true); + }; + + const handleSignOutAll = async () => { + setSigningOut(true); + await logoutAll(); + setSigningOut(false); + setSignOutOpen(false); + }; + + return ( + <> + setAnchorEl(e.currentTarget)} aria-label="account menu"> + + + {getInitials(activeAccount.name)} + + + + + + + + {activeAccount.email} + + + + + + + + + + {getInitials(activeAccount.name)} + + + + + + + + Hi, {activeAccount.name.split(" ")[0]}! + + {unreadCount > 0 && ( + + {unreadCount} unread message{unreadCount === 1 ? "" : "s"} + + )} + + + + + + + {otherAccounts.length > 0 && ( + <> + setOthersOpen((prev) => !prev)} + sx={{ justifyContent: "space-between" }} + > + + {othersOpen ? "Hide more accounts" : "Show more accounts"} + + {othersOpen ? : } + + + {otherAccounts.map((account) => ( + { + closeMenu(); + openAccountInNewTab(account.id); + }} + sx={{ gap: 1.5, py: 1 }} + > + + {getInitials(account.name)} + + + + {account.name} + + + {account.email} + + + + ))} + + + )} + + { + closeMenu(); + setAddOpen(true); + }} + sx={{ gap: 1.5 }} + > + + + + + Add another account + + + + + + { + closeMenu(); + setSignOutOpen(true); + }} + sx={{ gap: 1.5, color: "error.main" }} + > + ({ + width: 32, + height: 32, + borderRadius: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + flexShrink: 0, + bgcolor: alpha(theme.palette.error.main, 0.12), + })} + > + + + + Sign out of all accounts + + + + + setManageOpen(false)} /> + setAddOpen(false)} /> + setSignOutOpen(false)} + /> + + ); +}; + +export default AccountSwitcher; diff --git a/src/view/moles/Header.tsx b/src/view/moles/Header.tsx index 25353b4..23ad6de 100644 --- a/src/view/moles/Header.tsx +++ b/src/view/moles/Header.tsx @@ -7,13 +7,10 @@ import { useMediaQuery, useTheme, IconButton, - Avatar, Tooltip, } from "@mui/material"; -import { useNavigate } from "react-router-dom"; -import { useAccount } from "../../nonview/core/AccountContext"; -import { getInitials } from "../_constants/avatarUtils"; import SearchBar from "./SearchBar"; +import AccountSwitcher from "./AccountSwitcher"; const Header = ({ title, @@ -26,8 +23,6 @@ const Header = ({ }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("md")); - const navigate = useNavigate(); - const { activeAccount } = useAccount(); const TitleIcon = titleIcon; @@ -119,23 +114,9 @@ const Header = ({ ); })} - navigate("/profile")} - aria-label="profile" - sx={{ ml: isMobile ? 0.5 : 1 }} - > - - {getInitials(activeAccount?.name || "User")} - - + + +