diff options
| author | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
| commit | 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch) | |
| tree | b9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/app/(main)/settings/profile | |
| download | umami-396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b.tar.xz umami-396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b.zip | |
Created from https://vercel.com/new
Diffstat (limited to 'src/app/(main)/settings/profile')
| -rw-r--r-- | src/app/(main)/settings/profile/PasswordChangeButton.tsx | 29 | ||||
| -rw-r--r-- | src/app/(main)/settings/profile/PasswordEditForm.tsx | 67 | ||||
| -rw-r--r-- | src/app/(main)/settings/profile/ProfileHeader.tsx | 8 | ||||
| -rw-r--r-- | src/app/(main)/settings/profile/ProfilePage.tsx | 22 | ||||
| -rw-r--r-- | src/app/(main)/settings/profile/ProfileSettings.tsx | 51 | ||||
| -rw-r--r-- | src/app/(main)/settings/profile/page.tsx | 10 |
6 files changed, 187 insertions, 0 deletions
diff --git a/src/app/(main)/settings/profile/PasswordChangeButton.tsx b/src/app/(main)/settings/profile/PasswordChangeButton.tsx new file mode 100644 index 0000000..6ce8ef8 --- /dev/null +++ b/src/app/(main)/settings/profile/PasswordChangeButton.tsx @@ -0,0 +1,29 @@ +import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen'; +import { useMessages } from '@/components/hooks'; +import { LockKeyhole } from '@/components/icons'; +import { PasswordEditForm } from './PasswordEditForm'; + +export function PasswordChangeButton() { + const { formatMessage, labels, messages } = useMessages(); + const { toast } = useToast(); + + const handleSave = () => { + toast(formatMessage(messages.saved)); + }; + + return ( + <DialogTrigger> + <Button> + <Icon> + <LockKeyhole /> + </Icon> + <Text>{formatMessage(labels.changePassword)}</Text> + </Button> + <Modal> + <Dialog title={formatMessage(labels.changePassword)} style={{ width: 400 }}> + {({ close }) => <PasswordEditForm onSave={handleSave} onClose={close} />} + </Dialog> + </Modal> + </DialogTrigger> + ); +} diff --git a/src/app/(main)/settings/profile/PasswordEditForm.tsx b/src/app/(main)/settings/profile/PasswordEditForm.tsx new file mode 100644 index 0000000..6f782e4 --- /dev/null +++ b/src/app/(main)/settings/profile/PasswordEditForm.tsx @@ -0,0 +1,67 @@ +import { + Button, + Form, + FormButtons, + FormField, + FormSubmitButton, + PasswordField, +} from '@umami/react-zen'; +import { useMessages, useUpdateQuery } from '@/components/hooks'; + +export function PasswordEditForm({ onSave, onClose }) { + const { formatMessage, labels, messages, getErrorMessage } = useMessages(); + const { mutateAsync, error, isPending } = useUpdateQuery('/me/password'); + + const handleSubmit = async (data: any) => { + await mutateAsync(data, { + onSuccess: async () => { + onSave(); + onClose(); + }, + }); + }; + + const samePassword = (value: string, values: Record<string, any>) => { + if (value !== values.newPassword) { + return formatMessage(messages.noMatchPassword); + } + return true; + }; + + return ( + <Form onSubmit={handleSubmit} error={getErrorMessage(error)}> + <FormField + label={formatMessage(labels.currentPassword)} + name="currentPassword" + rules={{ required: 'Required' }} + > + <PasswordField autoComplete="current-password" /> + </FormField> + <FormField + name="newPassword" + label={formatMessage(labels.newPassword)} + rules={{ + required: 'Required', + minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: '8' }) }, + }} + > + <PasswordField autoComplete="new-password" /> + </FormField> + <FormField + name="confirmPassword" + label={formatMessage(labels.confirmPassword)} + rules={{ + required: formatMessage(labels.required), + minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: '8' }) }, + validate: samePassword, + }} + > + <PasswordField autoComplete="confirm-password" /> + </FormField> + <FormButtons> + <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button> + <FormSubmitButton isDisabled={isPending}>{formatMessage(labels.save)}</FormSubmitButton> + </FormButtons> + </Form> + ); +} diff --git a/src/app/(main)/settings/profile/ProfileHeader.tsx b/src/app/(main)/settings/profile/ProfileHeader.tsx new file mode 100644 index 0000000..05f7996 --- /dev/null +++ b/src/app/(main)/settings/profile/ProfileHeader.tsx @@ -0,0 +1,8 @@ +import { SectionHeader } from '@/components/common/SectionHeader'; +import { useMessages } from '@/components/hooks'; + +export function ProfileHeader() { + const { formatMessage, labels } = useMessages(); + + return <SectionHeader title={formatMessage(labels.profile)}></SectionHeader>; +} diff --git a/src/app/(main)/settings/profile/ProfilePage.tsx b/src/app/(main)/settings/profile/ProfilePage.tsx new file mode 100644 index 0000000..f03499a --- /dev/null +++ b/src/app/(main)/settings/profile/ProfilePage.tsx @@ -0,0 +1,22 @@ +'use client'; +import { Column } from '@umami/react-zen'; +import { PageBody } from '@/components/common/PageBody'; +import { PageHeader } from '@/components/common/PageHeader'; +import { Panel } from '@/components/common/Panel'; +import { useMessages } from '@/components/hooks'; +import { ProfileSettings } from './ProfileSettings'; + +export function ProfilePage() { + const { formatMessage, labels } = useMessages(); + + return ( + <PageBody> + <Column gap="6"> + <PageHeader title={formatMessage(labels.profile)} /> + <Panel> + <ProfileSettings /> + </Panel> + </Column> + </PageBody> + ); +} diff --git a/src/app/(main)/settings/profile/ProfileSettings.tsx b/src/app/(main)/settings/profile/ProfileSettings.tsx new file mode 100644 index 0000000..fae73a5 --- /dev/null +++ b/src/app/(main)/settings/profile/ProfileSettings.tsx @@ -0,0 +1,51 @@ +import { Column, Label, Row } from '@umami/react-zen'; +import { useConfig, useLoginQuery, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; +import { PasswordChangeButton } from './PasswordChangeButton'; + +export function ProfileSettings() { + const { user } = useLoginQuery(); + const { formatMessage, labels } = useMessages(); + const { cloudMode } = useConfig(); + + if (!user) { + return null; + } + + const { username, role } = user; + + const renderRole = (value: string) => { + if (value === ROLES.user) { + return formatMessage(labels.user); + } + if (value === ROLES.admin) { + return formatMessage(labels.admin); + } + if (value === ROLES.viewOnly) { + return formatMessage(labels.viewOnly); + } + + return formatMessage(labels.unknown); + }; + + return ( + <Column width="400px" gap="6"> + <Column> + <Label>{formatMessage(labels.username)}</Label> + {username} + </Column> + <Column> + <Label>{formatMessage(labels.role)}</Label> + {renderRole(role)} + </Column> + {!cloudMode && ( + <Column> + <Label>{formatMessage(labels.password)}</Label> + <Row> + <PasswordChangeButton /> + </Row> + </Column> + )} + </Column> + ); +} diff --git a/src/app/(main)/settings/profile/page.tsx b/src/app/(main)/settings/profile/page.tsx new file mode 100644 index 0000000..6060b91 --- /dev/null +++ b/src/app/(main)/settings/profile/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from 'next'; +import { ProfilePage } from './ProfilePage'; + +export default function () { + return <ProfilePage />; +} + +export const metadata: Metadata = { + title: 'Profile', +}; |