aboutsummaryrefslogtreecommitdiff
path: root/src/app/(main)/settings/profile
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-24 13:09:50 +0000
committerFuwn <[email protected]>2026-01-24 13:09:50 +0000
commit396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch)
treeb9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/app/(main)/settings/profile
downloadumami-396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b.tar.xz
umami-396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b.zip
Initial commitHEADmain
Created from https://vercel.com/new
Diffstat (limited to 'src/app/(main)/settings/profile')
-rw-r--r--src/app/(main)/settings/profile/PasswordChangeButton.tsx29
-rw-r--r--src/app/(main)/settings/profile/PasswordEditForm.tsx67
-rw-r--r--src/app/(main)/settings/profile/ProfileHeader.tsx8
-rw-r--r--src/app/(main)/settings/profile/ProfilePage.tsx22
-rw-r--r--src/app/(main)/settings/profile/ProfileSettings.tsx51
-rw-r--r--src/app/(main)/settings/profile/page.tsx10
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',
+};