Converted user and website settings.

This commit is contained in:
Mike Cao 2025-02-21 16:55:05 -08:00
parent 4c24e54fdd
commit b5c6194f36
59 changed files with 363 additions and 554 deletions

View file

@ -1,11 +1,11 @@
import { ReactNode } from 'react';
import { Button, LoadingButton, Form, FormButtons } from 'react-basics';
import { Row, Button, FormSubmitButton, Form, FormButtons } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
export interface ConfirmationFormProps {
message: ReactNode;
buttonLabel?: ReactNode;
buttonVariant?: 'none' | 'primary' | 'secondary' | 'quiet' | 'danger';
buttonVariant?: 'primary' | 'quiet' | 'danger';
isLoading?: boolean;
error?: string | Error;
onConfirm?: () => void;
@ -24,13 +24,13 @@ export function ConfirmationForm({
const { formatMessage, labels } = useMessages();
return (
<Form error={error}>
<p>{message}</p>
<FormButtons flex>
<LoadingButton isLoading={isLoading} onClick={onConfirm} variant={buttonVariant}>
<Form onSubmit={onConfirm} error={error}>
<Row marginY="4">{message}</Row>
<FormButtons>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
<FormSubmitButton isLoading={isLoading} variant={buttonVariant}>
{buttonLabel || formatMessage(labels.ok)}
</LoadingButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</FormSubmitButton>
</FormButtons>
</Form>
);

View file

@ -1,12 +1,10 @@
import { ReactNode } from 'react';
import classNames from 'classnames';
import { Loading, SearchField } from 'react-basics';
import { Loading, SearchField, Row, Column } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { Pager } from '@/components/common/Pager';
import { PagedQueryResult } from '@/lib/types';
import styles from './DataTable.module.css';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { PagedQueryResult } from '@/lib/types';
const DEFAULT_SEARCH_DELAY = 600;
@ -20,7 +18,7 @@ export interface DataTableProps {
children: ReactNode | ((data: any) => ReactNode);
}
export function DataTable({
export function DataGrid({
queryResult,
searchDelay = 600,
allowSearch = true,
@ -30,12 +28,8 @@ export function DataTable({
children,
}: DataTableProps) {
const { formatMessage, labels, messages } = useMessages();
const {
result,
params,
setParams,
query: { error, isLoading, isFetched },
} = queryResult || {};
const { result, params, setParams, query } = queryResult || {};
const { error, isLoading, isFetched } = query || {};
const { page, pageSize, count, data } = result || {};
const { search } = params || {};
const hasData = Boolean(!isLoading && data?.length);
@ -43,45 +37,38 @@ export function DataTable({
const { router, renderUrl } = useNavigation();
const handleSearch = (search: string) => {
setParams({ ...params, search, page: params.page ? page : 1 });
setParams({ ...params, search });
};
const handlePageChange = (page: number) => {
setParams({ ...params, search, page });
setParams({ ...params, page });
router.push(renderUrl({ page }));
};
return (
<>
{allowSearch && (hasData || search) && (
<SearchField
className={styles.search}
value={search}
onSearch={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={autoFocus}
placeholder={formatMessage(labels.search)}
/>
<Row width="280px" alignItems="center" marginBottom="6">
<SearchField
value={search}
onSearch={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={autoFocus}
placeholder={formatMessage(labels.search)}
/>
</Row>
)}
<LoadingPanel data={data} isLoading={isLoading} isFetched={isFetched} error={error}>
<div
className={classNames(styles.body, {
[styles.status]: isLoading || noResults || !hasData,
})}
>
<Column>
{hasData ? (typeof children === 'function' ? children(result) : children) : null}
{isLoading && <Loading position="page" />}
{!isLoading && !hasData && !search && (renderEmpty ? renderEmpty() : <Empty />)}
{!isLoading && noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
</div>
</Column>
{allowPaging && hasData && (
<Pager
className={styles.pager}
page={page}
pageSize={pageSize}
count={count}
onPageChange={handlePageChange}
/>
<Row marginTop="6">
<Pager page={page} pageSize={pageSize} count={count} onPageChange={handlePageChange} />
</Row>
)}
</LoadingPanel>
</>

View file

@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import classNames from 'classnames';
import { Loading } from 'react-basics';
import { Loading } from '@umami/react-zen';
import { ErrorMessage } from '@/components/common/ErrorMessage';
import { Empty } from '@/components/common/Empty';
import styles from './LoadingPanel.module.css';

View file

@ -1,7 +1,5 @@
import classNames from 'classnames';
import { Button, Icon, Icons } from 'react-basics';
import { Button, Icon, Icons, Row, Text } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import styles from './Pager.module.css';
export interface PagerProps {
page: string | number;
@ -11,7 +9,7 @@ export interface PagerProps {
className?: string;
}
export function Pager({ page, pageSize, count, onPageChange, className }: PagerProps) {
export function Pager({ page, pageSize, count, onPageChange }: PagerProps) {
const { formatMessage, labels } = useMessages();
const maxPage = pageSize && count ? Math.ceil(+count / +pageSize) : 0;
const lastPage = page === maxPage;
@ -34,24 +32,21 @@ export function Pager({ page, pageSize, count, onPageChange, className }: PagerP
}
return (
<div className={classNames(styles.pager, className)}>
<div className={styles.count}>{formatMessage(labels.numberOfRecords, { x: count })}</div>
<div className={styles.nav}>
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
<Icon rotate={90}>
<Icons.ChevronDown />
<Row alignItems="center" justifyContent="space-between" gap="3" flexGrow={1}>
<Text>{formatMessage(labels.numberOfRecords, { x: count })}</Text>
<Row alignItems="center" justifyContent="flex-end" gap="3">
<Text>{formatMessage(labels.pageOf, { current: page, total: maxPage })}</Text>
<Button onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
<Icon size="sm" rotate={180}>
<Icons.Chevron />
</Icon>
</Button>
<div className={styles.text}>
{formatMessage(labels.pageOf, { current: page, total: maxPage })}
</div>
<Button onClick={() => handlePageChange(1)} disabled={lastPage}>
<Icon rotate={270}>
<Icons.ChevronDown />
<Button onPress={() => handlePageChange(1)} isDisabled={lastPage}>
<Icon size="sm">
<Icons.Chevron />
</Icon>
</Button>
</div>
<div></div>
</div>
</Row>
</Row>
);
}

View file

@ -8,7 +8,7 @@ import { useApp } from '@/store/app';
const selector = (state: { shareToken: { token?: string } }) => state.shareToken;
async function handleResponse(res: FetchResponse): Promise<any> {
if (!res.ok) {
if (res.error) {
const { message, code } = res?.error?.error || {};
return Promise.reject(new Error(code || message || 'Unexpectd error.'));
}

View file

@ -24,6 +24,7 @@
}
.selected {
color: var(--font-color);
font-weight: 700;
background: var(--blue100);
}

View file

@ -1,22 +1,18 @@
import { ReactNode } from 'react';
import { usePathname } from 'next/navigation';
import { SideNav } from '@/components/layout/SideNav';
import styles from './MenuLayout.module.css';
import { Grid, Column } from '@umami/react-zen';
import { MenuNav } from '@/components/layout/MenuNav';
export function MenuLayout({ items = [], children }: { items: any[]; children: ReactNode }) {
const pathname = usePathname();
const cloudMode = !!process.env.cloudMode;
const getKey = () => items.find(({ url }) => pathname === url)?.key;
return (
<div className={styles.layout}>
<Grid columns="auto 1fr" gap="5">
{!cloudMode && (
<div className={styles.menu}>
<SideNav items={items} shallow={true} selectedKey={getKey()} />
</div>
<Column width="240px">
<MenuNav items={items} shallow={true} />
</Column>
)}
<div className={styles.content}>{children}</div>
</div>
<Column>{children}</Column>
</Grid>
);
}

View file

@ -0,0 +1,29 @@
import { List, ListItem, Text } from '@umami/react-zen';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
export interface SideNavProps {
items: any[];
shallow?: boolean;
scroll?: boolean;
}
export function MenuNav({ items, shallow = true, scroll = false }: SideNavProps) {
const pathname = usePathname();
return (
<List>
{items.map(({ key, label, url }) => {
const isSelected = pathname.startsWith(url);
return (
<ListItem key={key}>
<Link href={url} shallow={shallow} scroll={scroll}>
<Text weight={isSelected ? 'bold' : 'regular'}>{label}</Text>
</Link>
</ListItem>
);
})}
</List>
);
}

View file

@ -1,80 +0,0 @@
.group {
display: flex;
flex-direction: column;
width: 100%;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--base600);
font-size: 11px;
font-weight: 600;
padding: 10px 20px;
text-transform: uppercase;
cursor: pointer;
}
.body {
display: none;
}
.expanded .body {
display: block;
}
.item {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
border-inline-end: 2px solid var(--base200);
padding: 1rem 2rem;
gap: var(--size500);
font-weight: 600;
width: 200px;
margin-inline-end: -2px;
}
a.item {
color: var(--base700);
}
.item.selected {
color: var(--base900);
border-inline-end-color: var(--primary400);
background: var(--blue100);
}
.item:hover {
color: var(--base900);
}
.minimized .text,
.minimized .header {
display: none;
}
.minimized .item {
width: 60px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.divider:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
margin: auto;
border-top: 1px solid var(--base300);
width: 160px;
}
.minimized .divider:before {
width: 60px;
}

View file

@ -1,64 +0,0 @@
import { useState } from 'react';
import { Icon, Text, TooltipPopup } from 'react-basics';
import classNames from 'classnames';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { Icons } from '@/components/icons';
import styles from './NavGroup.module.css';
export interface NavGroupProps {
title: string;
items: any[];
defaultExpanded?: boolean;
allowExpand?: boolean;
minimized?: boolean;
}
export function NavGroup({
title,
items,
defaultExpanded = true,
allowExpand = true,
minimized = false,
}: NavGroupProps) {
const pathname = usePathname();
const [expanded, setExpanded] = useState(defaultExpanded);
const handleExpand = () => setExpanded(state => !state);
return (
<div
className={classNames(styles.group, {
[styles.expanded]: expanded,
[styles.minimized]: minimized,
})}
>
{title && (
<div className={styles.header} onClick={allowExpand ? handleExpand : undefined}>
<Text>{title}</Text>
<Icon size="sm" rotate={expanded ? 0 : -90}>
<Icons.ChevronDown />
</Icon>
</div>
)}
<div className={styles.body}>
{items.map(({ label, url, icon, divider }) => {
return (
<TooltipPopup key={label} label={label} position="right" disabled={!minimized}>
<Link
href={url}
className={classNames(styles.item, {
[styles.divider]: divider,
[styles.selected]: pathname.startsWith(url),
})}
>
<Icon>{icon}</Icon>
<Text className={styles.text}>{label}</Text>
</Link>
</TooltipPopup>
);
})}
</div>
</div>
);
}

View file

@ -1,29 +1,23 @@
import { ReactNode } from 'react';
import { Heading, Icon, Breadcrumbs, Breadcrumb, Row } from '@umami/react-zen';
import { Heading, Icon, Row } from '@umami/react-zen';
export function PageHeader({
title,
icon,
breadcrumb,
children,
}: {
title?: ReactNode;
icon?: ReactNode;
className?: string;
breadcrumb?: ReactNode;
children?: ReactNode;
}) {
return (
<>
<Breadcrumbs>
<Breadcrumb>{breadcrumb}</Breadcrumb>
</Breadcrumbs>
<Row justifyContent="space-between" paddingY="6">
<Row justifyContent="space-between" alignItems="center" paddingBottom="6">
<Row gap="3">
{icon && <Icon size="lg">{icon}</Icon>}
{title && <Heading>{title}</Heading>}
<Row justifyContent="flex-end">{children}</Row>
</Row>
</>
<Row justifyContent="flex-end">{children}</Row>
</Row>
);
}

View file

@ -1,44 +0,0 @@
import classNames from 'classnames';
import { Menu, Item } from 'react-basics';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import styles from './SideNav.module.css';
export interface SideNavProps {
selectedKey: string;
items: any[];
shallow?: boolean;
scroll?: boolean;
className?: string;
onSelect?: () => void;
}
export function SideNav({
selectedKey,
items,
shallow = true,
scroll = false,
className,
onSelect = () => {},
}: SideNavProps) {
const pathname = usePathname();
return (
<Menu
items={items}
selectedKey={selectedKey}
className={classNames(styles.menu, className)}
onSelect={onSelect}
>
{({ key, label, url }) => (
<Item
key={key}
className={classNames(styles.item, { [styles.selected]: pathname.startsWith(url) })}
>
<Link href={url} shallow={shallow} scroll={scroll}>
{label}
</Link>
</Item>
)}
</Menu>
);
}