From 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b Mon Sep 17 00:00:00 2001
From: Fuwn <50817549+Fuwn@users.noreply.github.com>
Date: Sat, 24 Jan 2026 13:09:50 +0000
Subject: Initial commit
Created from https://vercel.com/new
---
src/app/(main)/settings/SettingsLayout.tsx | 26 +++++++++
src/app/(main)/settings/SettingsNav.tsx | 53 +++++++++++++++++
src/app/(main)/settings/layout.tsx | 17 ++++++
.../settings/preferences/DateRangeSetting.tsx | 28 +++++++++
.../settings/preferences/LanguageSetting.tsx | 48 ++++++++++++++++
.../settings/preferences/PreferenceSettings.tsx | 36 ++++++++++++
.../settings/preferences/PreferencesPage.tsx | 22 +++++++
.../(main)/settings/preferences/ThemeSetting.tsx | 21 +++++++
.../settings/preferences/TimezoneSetting.tsx | 44 ++++++++++++++
src/app/(main)/settings/preferences/page.tsx | 10 ++++
.../settings/profile/PasswordChangeButton.tsx | 29 ++++++++++
.../(main)/settings/profile/PasswordEditForm.tsx | 67 ++++++++++++++++++++++
src/app/(main)/settings/profile/ProfileHeader.tsx | 8 +++
src/app/(main)/settings/profile/ProfilePage.tsx | 22 +++++++
.../(main)/settings/profile/ProfileSettings.tsx | 51 ++++++++++++++++
src/app/(main)/settings/profile/page.tsx | 10 ++++
.../(main)/settings/teams/TeamsSettingsPage.tsx | 16 ++++++
.../settings/teams/[teamId]/TeamSettingsPage.tsx | 11 ++++
src/app/(main)/settings/teams/[teamId]/page.tsx | 12 ++++
src/app/(main)/settings/teams/page.tsx | 10 ++++
.../settings/websites/WebsitesSettingsPage.tsx | 16 ++++++
.../websites/[websiteId]/WebsiteSettingsPage.tsx | 16 ++++++
.../(main)/settings/websites/[websiteId]/page.tsx | 12 ++++
src/app/(main)/settings/websites/page.tsx | 12 ++++
24 files changed, 597 insertions(+)
create mode 100644 src/app/(main)/settings/SettingsLayout.tsx
create mode 100644 src/app/(main)/settings/SettingsNav.tsx
create mode 100644 src/app/(main)/settings/layout.tsx
create mode 100644 src/app/(main)/settings/preferences/DateRangeSetting.tsx
create mode 100644 src/app/(main)/settings/preferences/LanguageSetting.tsx
create mode 100644 src/app/(main)/settings/preferences/PreferenceSettings.tsx
create mode 100644 src/app/(main)/settings/preferences/PreferencesPage.tsx
create mode 100644 src/app/(main)/settings/preferences/ThemeSetting.tsx
create mode 100644 src/app/(main)/settings/preferences/TimezoneSetting.tsx
create mode 100644 src/app/(main)/settings/preferences/page.tsx
create mode 100644 src/app/(main)/settings/profile/PasswordChangeButton.tsx
create mode 100644 src/app/(main)/settings/profile/PasswordEditForm.tsx
create mode 100644 src/app/(main)/settings/profile/ProfileHeader.tsx
create mode 100644 src/app/(main)/settings/profile/ProfilePage.tsx
create mode 100644 src/app/(main)/settings/profile/ProfileSettings.tsx
create mode 100644 src/app/(main)/settings/profile/page.tsx
create mode 100644 src/app/(main)/settings/teams/TeamsSettingsPage.tsx
create mode 100644 src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx
create mode 100644 src/app/(main)/settings/teams/[teamId]/page.tsx
create mode 100644 src/app/(main)/settings/teams/page.tsx
create mode 100644 src/app/(main)/settings/websites/WebsitesSettingsPage.tsx
create mode 100644 src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx
create mode 100644 src/app/(main)/settings/websites/[websiteId]/page.tsx
create mode 100644 src/app/(main)/settings/websites/page.tsx
(limited to 'src/app/(main)/settings')
diff --git a/src/app/(main)/settings/SettingsLayout.tsx b/src/app/(main)/settings/SettingsLayout.tsx
new file mode 100644
index 0000000..f658872
--- /dev/null
+++ b/src/app/(main)/settings/SettingsLayout.tsx
@@ -0,0 +1,26 @@
+'use client';
+import { Column, Grid } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { PageBody } from '@/components/common/PageBody';
+import { SettingsNav } from './SettingsNav';
+
+export function SettingsLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/(main)/settings/SettingsNav.tsx b/src/app/(main)/settings/SettingsNav.tsx
new file mode 100644
index 0000000..4b35c82
--- /dev/null
+++ b/src/app/(main)/settings/SettingsNav.tsx
@@ -0,0 +1,53 @@
+import { SideMenu } from '@/components/common/SideMenu';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { Settings2, UserCircle, Users } from '@/components/icons';
+
+export function SettingsNav({ onItemClick }: { onItemClick?: () => void }) {
+ const { formatMessage, labels } = useMessages();
+ const { renderUrl, pathname } = useNavigation();
+
+ const items = [
+ {
+ label: formatMessage(labels.application),
+ items: [
+ {
+ id: 'preferences',
+ label: formatMessage(labels.preferences),
+ path: renderUrl('/settings/preferences'),
+ icon: ,
+ },
+ ],
+ },
+ {
+ label: formatMessage(labels.account),
+ items: [
+ {
+ id: 'profile',
+ label: formatMessage(labels.profile),
+ path: renderUrl('/settings/profile'),
+ icon: ,
+ },
+ {
+ id: 'teams',
+ label: formatMessage(labels.teams),
+ path: renderUrl('/settings/teams'),
+ icon: ,
+ },
+ ],
+ },
+ ];
+
+ const selectedKey = items
+ .flatMap(e => e.items)
+ .find(({ path }) => path && pathname.includes(path.split('?')[0]))?.id;
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/settings/layout.tsx b/src/app/(main)/settings/layout.tsx
new file mode 100644
index 0000000..4e773a3
--- /dev/null
+++ b/src/app/(main)/settings/layout.tsx
@@ -0,0 +1,17 @@
+import type { Metadata } from 'next';
+import { SettingsLayout } from './SettingsLayout';
+
+export default function ({ children }) {
+ if (process.env.cloudMode) {
+ return null;
+ }
+
+ return {children};
+}
+
+export const metadata: Metadata = {
+ title: {
+ template: '%s | Settings | Umami',
+ default: 'Settings | Umami',
+ },
+};
diff --git a/src/app/(main)/settings/preferences/DateRangeSetting.tsx b/src/app/(main)/settings/preferences/DateRangeSetting.tsx
new file mode 100644
index 0000000..3f5e664
--- /dev/null
+++ b/src/app/(main)/settings/preferences/DateRangeSetting.tsx
@@ -0,0 +1,28 @@
+import { Button, Row } from '@umami/react-zen';
+import { useState } from 'react';
+import { useMessages } from '@/components/hooks';
+import { DateFilter } from '@/components/input/DateFilter';
+import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
+import { getItem, setItem } from '@/lib/storage';
+
+export function DateRangeSetting() {
+ const { formatMessage, labels } = useMessages();
+ const [date, setDate] = useState(getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE);
+
+ const handleChange = (value: string) => {
+ setItem(DATE_RANGE_CONFIG, value);
+ setDate(value);
+ };
+
+ const handleReset = () => {
+ setItem(DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE);
+ setDate(DEFAULT_DATE_RANGE_VALUE);
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/preferences/LanguageSetting.tsx b/src/app/(main)/settings/preferences/LanguageSetting.tsx
new file mode 100644
index 0000000..00a2d74
--- /dev/null
+++ b/src/app/(main)/settings/preferences/LanguageSetting.tsx
@@ -0,0 +1,48 @@
+import { Button, ListItem, Row, Select } from '@umami/react-zen';
+import { useState } from 'react';
+import { useLocale, useMessages } from '@/components/hooks';
+import { DEFAULT_LOCALE } from '@/lib/constants';
+import { languages } from '@/lib/lang';
+
+export function LanguageSetting() {
+ const [search, setSearch] = useState('');
+ const { formatMessage, labels } = useMessages();
+ const { locale, saveLocale } = useLocale();
+ const items = search
+ ? Object.keys(languages).filter(n => {
+ return (
+ n.toLowerCase().includes(search.toLowerCase()) ||
+ languages[n].label.toLowerCase().includes(search.toLowerCase())
+ );
+ })
+ : Object.keys(languages);
+
+ const handleReset = () => saveLocale(DEFAULT_LOCALE);
+
+ const handleOpen = (isOpen: boolean) => {
+ if (isOpen) {
+ setSearch('');
+ }
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/preferences/PreferenceSettings.tsx b/src/app/(main)/settings/preferences/PreferenceSettings.tsx
new file mode 100644
index 0000000..a2890ce
--- /dev/null
+++ b/src/app/(main)/settings/preferences/PreferenceSettings.tsx
@@ -0,0 +1,36 @@
+import { Column, Label } from '@umami/react-zen';
+import { useLoginQuery, useMessages } from '@/components/hooks';
+import { DateRangeSetting } from './DateRangeSetting';
+import { LanguageSetting } from './LanguageSetting';
+import { ThemeSetting } from './ThemeSetting';
+import { TimezoneSetting } from './TimezoneSetting';
+
+export function PreferenceSettings() {
+ const { user } = useLoginQuery();
+ const { formatMessage, labels } = useMessages();
+
+ if (!user) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/preferences/PreferencesPage.tsx b/src/app/(main)/settings/preferences/PreferencesPage.tsx
new file mode 100644
index 0000000..61e2669
--- /dev/null
+++ b/src/app/(main)/settings/preferences/PreferencesPage.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 { PreferenceSettings } from './PreferenceSettings';
+
+export function PreferencesPage() {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/preferences/ThemeSetting.tsx b/src/app/(main)/settings/preferences/ThemeSetting.tsx
new file mode 100644
index 0000000..03bd6a6
--- /dev/null
+++ b/src/app/(main)/settings/preferences/ThemeSetting.tsx
@@ -0,0 +1,21 @@
+import { Button, Icon, Row, useTheme } from '@umami/react-zen';
+import { Moon, Sun } from '@/components/icons';
+
+export function ThemeSetting() {
+ const { theme, setTheme } = useTheme();
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/preferences/TimezoneSetting.tsx b/src/app/(main)/settings/preferences/TimezoneSetting.tsx
new file mode 100644
index 0000000..cf20b20
--- /dev/null
+++ b/src/app/(main)/settings/preferences/TimezoneSetting.tsx
@@ -0,0 +1,44 @@
+import { Button, ListItem, Row, Select } from '@umami/react-zen';
+import { useState } from 'react';
+import { useMessages, useTimezone } from '@/components/hooks';
+import { getTimezone } from '@/lib/date';
+
+const timezones = Intl.supportedValuesOf('timeZone');
+
+export function TimezoneSetting() {
+ const [search, setSearch] = useState('');
+ const { formatMessage, labels } = useMessages();
+ const { timezone, saveTimezone } = useTimezone();
+ const items = search
+ ? timezones.filter(n => n.toLowerCase().includes(search.toLowerCase()))
+ : timezones;
+
+ const handleReset = () => saveTimezone(getTimezone());
+
+ const handleOpen = isOpen => {
+ if (isOpen) {
+ setSearch('');
+ }
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/preferences/page.tsx b/src/app/(main)/settings/preferences/page.tsx
new file mode 100644
index 0000000..dd16870
--- /dev/null
+++ b/src/app/(main)/settings/preferences/page.tsx
@@ -0,0 +1,10 @@
+import type { Metadata } from 'next';
+import { PreferencesPage } from './PreferencesPage';
+
+export default function () {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Preferences',
+};
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 (
+
+
+
+
+
+
+ );
+}
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) => {
+ if (value !== values.newPassword) {
+ return formatMessage(messages.noMatchPassword);
+ }
+ return true;
+ };
+
+ return (
+
+ );
+}
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 ;
+}
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 (
+
+
+
+
+
+
+
+
+ );
+}
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 (
+
+
+
+ {username}
+
+
+
+ {renderRole(role)}
+
+ {!cloudMode && (
+
+
+
+
+
+
+ )}
+
+ );
+}
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 ;
+}
+
+export const metadata: Metadata = {
+ title: 'Profile',
+};
diff --git a/src/app/(main)/settings/teams/TeamsSettingsPage.tsx b/src/app/(main)/settings/teams/TeamsSettingsPage.tsx
new file mode 100644
index 0000000..dc3e3bc
--- /dev/null
+++ b/src/app/(main)/settings/teams/TeamsSettingsPage.tsx
@@ -0,0 +1,16 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { TeamsDataTable } from '@/app/(main)/teams/TeamsDataTable';
+import { TeamsHeader } from '@/app/(main)/teams/TeamsHeader';
+import { Panel } from '@/components/common/Panel';
+
+export function TeamsSettingsPage() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx b/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx
new file mode 100644
index 0000000..9539625
--- /dev/null
+++ b/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx
@@ -0,0 +1,11 @@
+'use client';
+import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings';
+import { TeamProvider } from '@/app/(main)/teams/TeamProvider';
+
+export function TeamSettingsPage({ teamId }: { teamId: string }) {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/teams/[teamId]/page.tsx b/src/app/(main)/settings/teams/[teamId]/page.tsx
new file mode 100644
index 0000000..58a380b
--- /dev/null
+++ b/src/app/(main)/settings/teams/[teamId]/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { TeamSettingsPage } from './TeamSettingsPage';
+
+export default async function ({ params }: { params: Promise<{ teamId: string }> }) {
+ const { teamId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Teams',
+};
diff --git a/src/app/(main)/settings/teams/page.tsx b/src/app/(main)/settings/teams/page.tsx
new file mode 100644
index 0000000..a0913f4
--- /dev/null
+++ b/src/app/(main)/settings/teams/page.tsx
@@ -0,0 +1,10 @@
+import type { Metadata } from 'next';
+import { TeamsSettingsPage } from './TeamsSettingsPage';
+
+export default function () {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Teams',
+};
diff --git a/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx b/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx
new file mode 100644
index 0000000..5009ec6
--- /dev/null
+++ b/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx
@@ -0,0 +1,16 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { WebsitesDataTable } from '@/app/(main)/websites/WebsitesDataTable';
+import { SectionHeader } from '@/components/common/SectionHeader';
+import { useMessages } from '@/components/hooks';
+
+export function WebsitesSettingsPage({ teamId }: { teamId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx
new file mode 100644
index 0000000..53b4cd9
--- /dev/null
+++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx
@@ -0,0 +1,16 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
+import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader';
+import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
+
+export function WebsiteSettingsPage({ websiteId }: { websiteId: string }) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/websites/[websiteId]/page.tsx b/src/app/(main)/settings/websites/[websiteId]/page.tsx
new file mode 100644
index 0000000..9adfc91
--- /dev/null
+++ b/src/app/(main)/settings/websites/[websiteId]/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { WebsiteSettingsPage } from './WebsiteSettingsPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Website',
+};
diff --git a/src/app/(main)/settings/websites/page.tsx b/src/app/(main)/settings/websites/page.tsx
new file mode 100644
index 0000000..19c14fd
--- /dev/null
+++ b/src/app/(main)/settings/websites/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { WebsitesSettingsPage } from './WebsitesSettingsPage';
+
+export default async function ({ params }: { params: Promise<{ teamId: string }> }) {
+ const { teamId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Websites',
+};
--
cgit v1.2.3