New admin section.

This commit is contained in:
Mike Cao 2025-07-06 08:22:29 -07:00
parent cdf391d5c2
commit b78ff3b477
28 changed files with 161 additions and 100 deletions

View file

@ -0,0 +1,88 @@
import {
Select,
ListItem,
Form,
FormField,
FormButtons,
TextField,
FormSubmitButton,
PasswordField,
useToast,
} from '@umami/react-zen';
import { useApi, useLoginQuery, useMessages, useModified } from '@/components/hooks';
import { ROLES } from '@/lib/constants';
import { useContext } from 'react';
import { UserContext } from './UserProvider';
export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () => void }) {
const { formatMessage, labels, messages, getMessage } = useMessages();
const { post, useMutation } = useApi();
const user = useContext(UserContext);
const { user: login } = useLoginQuery();
const { toast } = useToast();
const { touch } = useModified();
const { mutate, error } = useMutation({
mutationFn: ({
username,
password,
role,
}: {
username: string;
password: string;
role: string;
}) => post(`/users/${userId}`, { username, password, role }),
});
const handleSubmit = async (data: any) => {
mutate(data, {
onSuccess: async () => {
toast(formatMessage(messages.saved));
touch(`user:${user.id}`);
onSave?.();
},
});
};
return (
<Form onSubmit={handleSubmit} error={getMessage(error)} values={user} style={{ width: 300 }}>
<FormField name="username" label={formatMessage(labels.username)}>
<TextField data-test="input-username" />
</FormField>
<FormField
name="password"
label={formatMessage(labels.password)}
rules={{
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
}}
>
<PasswordField autoComplete="new-password" data-test="input-password" />
</FormField>
{user.id !== login.id && (
<FormField
name="role"
label={formatMessage(labels.role)}
rules={{ required: formatMessage(labels.required) }}
>
<Select defaultSelectedKey={user.role}>
<ListItem id={ROLES.viewOnly} data-test="dropdown-item-viewOnly">
{formatMessage(labels.viewOnly)}
</ListItem>
<ListItem id={ROLES.user} data-test="dropdown-item-user">
{formatMessage(labels.user)}
</ListItem>
<ListItem id={ROLES.admin} data-test="dropdown-item-admin">
{formatMessage(labels.admin)}
</ListItem>
</Select>
</FormField>
)}
<FormButtons>
<FormSubmitButton data-test="button-submit" variant="primary">
{formatMessage(labels.save)}
</FormSubmitButton>
</FormButtons>
</Form>
);
}

View file

@ -0,0 +1,11 @@
'use client';
import { UserSettings } from './UserSettings';
import { UserProvider } from './UserProvider';
export function UserPage({ userId }: { userId: string }) {
return (
<UserProvider userId={userId}>
<UserSettings userId={userId} />
</UserProvider>
);
}

View file

@ -0,0 +1,22 @@
import { createContext, ReactNode, useEffect } from 'react';
import { Loading } from '@umami/react-zen';
import { useModified, useUserQuery } from '@/components/hooks';
export const UserContext = createContext(null);
export function UserProvider({ userId, children }: { userId: string; children: ReactNode }) {
const { modified } = useModified(`user:${userId}`);
const { data: user, isFetching, isLoading, refetch } = useUserQuery(userId);
useEffect(() => {
if (modified) {
refetch();
}
}, [modified]);
if (isFetching && isLoading) {
return <Loading position="page" />;
}
return <UserContext.Provider value={{ ...user, modified }}>{children}</UserContext.Provider>;
}

View file

@ -0,0 +1,31 @@
import { useContext } from 'react';
import { Tabs, Tab, TabList, TabPanel } from '@umami/react-zen';
import { User } from '@/components/icons';
import { UserEditForm } from './UserEditForm';
import { SectionHeader } from '@/components/common/SectionHeader';
import { useMessages } from '@/components/hooks';
import { UserWebsites } from './UserWebsites';
import { UserContext } from './UserProvider';
export function UserSettings({ userId }: { userId: string }) {
const { formatMessage, labels } = useMessages();
const user = useContext(UserContext);
return (
<>
<SectionHeader title={user?.username} icon={<User />} />
<Tabs>
<TabList>
<Tab id="details">{formatMessage(labels.details)}</Tab>
<Tab id="websites">{formatMessage(labels.websites)}</Tab>
</TabList>
<TabPanel id="details">
<UserEditForm userId={userId} />
</TabPanel>
<TabPanel id="websites">
<UserWebsites userId={userId} />
</TabPanel>
</Tabs>
</>
);
}

View file

@ -0,0 +1,15 @@
import { DataGrid } from '@/components/common/DataGrid';
import { useWebsitesQuery } from '@/components/hooks';
import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable';
export function UserWebsites({ userId }) {
const queryResult = useWebsitesQuery({ userId });
return (
<DataGrid queryResult={queryResult}>
{({ data }) => (
<WebsitesTable data={data} showActions={true} allowEdit={true} allowView={true} />
)}
</DataGrid>
);
}

View file

@ -0,0 +1,12 @@
import { UserPage } from './UserPage';
import { Metadata } from 'next';
export default async function ({ params }: { params: Promise<{ userId: string }> }) {
const { userId } = await params;
return <UserPage userId={userId} />;
}
export const metadata: Metadata = {
title: 'User Settings',
};