Updated website, team and user save.

This commit is contained in:
Mike Cao 2024-01-29 03:15:22 -08:00
parent 2fa50892d8
commit fec81695e8
15 changed files with 128 additions and 117 deletions

View File

@ -4,6 +4,7 @@ import { Item, Loading, Tabs, Flexbox } from 'react-basics';
import TeamsContext from 'app/(main)/teams/TeamsContext';
import PageHeader from 'components/layout/PageHeader';
import { ROLES } from 'lib/constants';
import Icons from 'components/icons';
import { useLogin, useTeam, useMessages } from 'components/hooks';
import TeamEditForm from './TeamEditForm';
import TeamMembers from './TeamMembers';
@ -27,7 +28,7 @@ export function TeamSettings({ teamId }: { teamId: string }) {
return (
<TeamsContext.Provider value={team}>
<Flexbox direction="column">
<PageHeader title={team?.name} />
<PageHeader title={team?.name} icon={<Icons.Users />} />
<Tabs
selectedKey={tab}
onSelect={(value: any) => setTab(value)}

View File

@ -1,7 +1,7 @@
'use client';
import DataTable from 'components/common/DataTable';
import { useUsers } from 'components/hooks';
import UsersTable from './UsersTable';
import useUsers from 'components/hooks/queries/useUsers';
export function UsersDataTable({ showActions }: { showActions?: boolean }) {
const queryResult = useUsers();

View File

@ -1,6 +1,7 @@
'use client';
import { Key, useState } from 'react';
import { Item, Loading, Tabs } from 'react-basics';
import Icons from 'components/icons';
import UserEditForm from '../UserEditForm';
import PageHeader from 'components/layout/PageHeader';
import { useMessages, useUser } from 'components/hooks';
@ -17,7 +18,7 @@ export function UserSettings({ userId }: { userId: string }) {
return (
<>
<PageHeader title={user?.username} />
<PageHeader title={user?.username} icon={<Icons.User />} />
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
<Item key="details">{formatMessage(labels.details)}</Item>
<Item key="websites">{formatMessage(labels.websites)}</Item>

View File

@ -10,8 +10,6 @@ import {
import { useApi } from 'components/hooks';
import { DOMAIN_REGEX } from 'lib/constants';
import { useMessages } from 'components/hooks';
import { useContext } from 'react';
import SettingsContext from '../SettingsContext';
export function WebsiteAddForm({
teamId,
@ -23,10 +21,9 @@ export function WebsiteAddForm({
onClose?: () => void;
}) {
const { formatMessage, labels, messages } = useMessages();
const { websitesUrl } = useContext(SettingsContext);
const { post, useMutation } = useApi();
const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) => post(websitesUrl, { ...data, teamId }),
mutationFn: (data: any) => post('/websites', { ...data, teamId }),
});
const handleSubmit = async (data: any) => {

View File

@ -1,39 +1,23 @@
'use client';
import { useState, Key } from 'react';
import { Item, Tabs, useToasts, Button, Text, Icon, Icons, Loading } from 'react-basics';
import { useRouter } from 'next/navigation';
import { Item, Tabs, Button, Text, Icon, Loading } from 'react-basics';
import Link from 'next/link';
import Icons from 'components/icons';
import PageHeader from 'components/layout/PageHeader';
import WebsiteEditForm from './[id]/WebsiteEditForm';
import WebsiteData from './[id]/WebsiteData';
import TrackingCode from './[id]/TrackingCode';
import ShareUrl from './[id]/ShareUrl';
import { useWebsite, useMessages } from 'components/hooks';
import { touch } from 'store/cache';
import WebsiteContext from 'app/(main)/websites/[id]/WebsiteContext';
export function WebsiteSettings({ websiteId, openExternal = false }) {
const router = useRouter();
const { formatMessage, labels, messages } = useMessages();
const { showToast } = useToasts();
const { data: website, isLoading } = useWebsite(websiteId, { gcTime: 0 });
const { formatMessage, labels } = useMessages();
const { data: website, isLoading, refetch } = useWebsite(websiteId, { gcTime: 0 });
const [tab, setTab] = useState<Key>('details');
const showSuccess = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
const handleSave = () => {
showSuccess();
touch('websites');
};
const handleReset = async (value: string) => {
if (value === 'delete') {
router.push('/settings/websites');
} else if (value === 'reset') {
showSuccess();
}
refetch();
};
if (isLoading) {
@ -41,8 +25,8 @@ export function WebsiteSettings({ websiteId, openExternal = false }) {
}
return (
<>
<PageHeader title={website?.name}>
<WebsiteContext.Provider value={website}>
<PageHeader title={website?.name} icon={<Icons.Globe />}>
<Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}>
<Button variant="primary">
<Icon>
@ -58,13 +42,11 @@ export function WebsiteSettings({ websiteId, openExternal = false }) {
<Item key="share">{formatMessage(labels.shareUrl)}</Item>
<Item key="data">{formatMessage(labels.data)}</Item>
</Tabs>
{tab === 'details' && (
<WebsiteEditForm websiteId={websiteId} data={website} onSave={handleSave} />
)}
{tab === 'details' && <WebsiteEditForm website={website} onSave={handleSave} />}
{tab === 'tracking' && <TrackingCode websiteId={websiteId} />}
{tab === 'share' && <ShareUrl websiteId={websiteId} data={website} onSave={handleSave} />}
{tab === 'data' && <WebsiteData websiteId={websiteId} onSave={handleReset} />}
</>
{tab === 'share' && <ShareUrl website={website} onSave={handleSave} />}
{tab === 'data' && <WebsiteData websiteId={websiteId} />}
</WebsiteContext.Provider>
);
}

View File

@ -2,16 +2,7 @@
import { ReactNode } from 'react';
import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable';
import DataTable from 'components/common/DataTable';
import useWebsites from 'components/hooks/queries/useWebsites';
export interface WebsitesDataTableProps {
userId?: string;
teamId?: string;
allowEdit?: boolean;
allowView?: boolean;
showActions?: boolean;
children?: ReactNode;
}
import { useWebsites } from 'components/hooks';
export function WebsitesDataTable({
userId,
@ -20,13 +11,21 @@ export function WebsitesDataTable({
allowView = true,
showActions = true,
children,
}: WebsitesDataTableProps) {
}: {
userId?: string;
teamId?: string;
allowEdit?: boolean;
allowView?: boolean;
showActions?: boolean;
children?: ReactNode;
}) {
const queryResult = useWebsites({ userId, teamId });
return (
<DataTable queryResult={queryResult}>
{({ data }) => (
<WebsitesTable
teamId={teamId}
data={data}
showActions={showActions}
allowEdit={allowEdit}

View File

@ -46,7 +46,7 @@ export function WebsitesTable({
</Link>
)}
{allowView && (
<Link href={teamId ? `/team/${teamId}/websites/${id}` : `/websites/${id}`}>
<Link href={teamId ? `/teams/${teamId}/websites/${id}` : `/websites/${id}`}>
<Button>
<Icon>
<Icons.External />

View File

@ -1,68 +1,63 @@
import { Website } from '@prisma/client';
import {
Form,
FormRow,
FormButtons,
Flexbox,
TextField,
SubmitButton,
Button,
Toggle,
LoadingButton,
useToasts,
} from 'react-basics';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useContext, useState } from 'react';
import { getRandomChars } from 'next-basics';
import { useApi, useMessages } from 'components/hooks';
import SettingsContext from '../../SettingsContext';
import SettingsContext from 'app/(main)/settings/SettingsContext';
const generateId = () => getRandomChars(16);
export function ShareUrl({ websiteId, data, onSave }) {
const ref = useRef(null);
const { shareUrl } = useContext(SettingsContext);
export function ShareUrl({ website, onSave }: { website: Website; onSave?: () => void }) {
const { domain, shareId } = website;
const { hostUrl } = useContext(SettingsContext);
const { formatMessage, labels, messages } = useMessages();
const { name, shareId } = data;
const [id, setId] = useState(shareId);
const { showToast } = useToasts();
const { post, useMutation } = useApi();
const { mutate, error } = useMutation({
mutationFn: (data: any) => post(`/websites/${websiteId}`, data),
const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) => post(`/websites/${website.id}`, data),
});
const url = useMemo(
() => `${shareUrl}${process.env.basePath}/share/${id}/${encodeURIComponent(name)}`,
[id, name],
);
const handleSubmit = async (data: any) => {
mutate(data, {
onSuccess: async () => {
onSave(data);
ref.current.reset(data);
},
});
};
const url = `${hostUrl || location.origin}${
process.env.basePath
}/share/${id}/${encodeURIComponent(domain)}`;
const handleGenerate = () => {
const id = generateId();
ref.current.setValue('shareId', id, {
shouldValidate: true,
shouldDirty: true,
});
setId(id);
setId(generateId());
};
const handleCheck = (checked: boolean) => {
const data = { shareId: checked ? generateId() : null };
mutate(data, {
onSuccess: async () => {
onSave(data);
onSave?.();
showToast({ message: formatMessage(messages.saved), variant: 'success' });
},
});
setId(data.shareId);
};
useEffect(() => {
if (id && id !== shareId) {
ref.current.setValue('shareId', id);
}
}, [id, shareId]);
const handleSave = () => {
mutate(
{ shareId: id },
{
onSuccess: async () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
onSave?.();
},
},
);
};
return (
<>
@ -70,7 +65,7 @@ export function ShareUrl({ websiteId, data, onSave }) {
{formatMessage(labels.enableShareUrl)}
</Toggle>
{id && (
<Form key={websiteId} ref={ref} onSubmit={handleSubmit} error={error} values={data}>
<Form error={error}>
<FormRow>
<p>{formatMessage(messages.shareUrl)}</p>
<Flexbox gap={10}>
@ -79,7 +74,14 @@ export function ShareUrl({ websiteId, data, onSave }) {
</Flexbox>
</FormRow>
<FormButtons>
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
<LoadingButton
variant="primary"
disabled={id === shareId}
isLoading={isPending}
onClick={handleSave}
>
{formatMessage(labels.save)}
</LoadingButton>
</FormButtons>
</Form>
)}

View File

@ -1,23 +1,23 @@
import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics';
import { Button, Modal, ModalTrigger, ActionForm, useToasts } from 'react-basics';
import { useRouter } from 'next/navigation';
import { useMessages } from 'components/hooks';
import WebsiteDeleteForm from './WebsiteDeleteForm';
import WebsiteResetForm from './WebsiteResetForm';
import { useMessages } from 'components/hooks';
import { touch } from 'store/cache';
export function WebsiteData({
websiteId,
onSave,
}: {
websiteId: string;
onSave?: (value: string) => void;
}) {
export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
const { formatMessage, labels, messages } = useMessages();
const router = useRouter();
const { showToast } = useToasts();
const handleReset = async () => {
onSave('reset');
showToast({ message: formatMessage(messages.saved), variant: 'success' });
onSave?.();
};
const handleDelete = async () => {
onSave('delete');
touch('websites');
router.push('/settings/websites');
};
return (

View File

@ -1,6 +1,4 @@
import { useApi, useMessages } from 'components/hooks';
import { useContext } from 'react';
import SettingsContext from '../../SettingsContext';
import TypeConfirmationForm from 'components/common/TypeConfirmationForm';
const CONFIRM_VALUE = 'DELETE';
@ -15,10 +13,9 @@ export function WebsiteDeleteForm({
onClose?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { websitesUrl } = useContext(SettingsContext);
const { del, useMutation } = useApi();
const { mutate, isPending, error } = useMutation({
mutationFn: (data: any) => del(`${websitesUrl}/${websiteId}`, data),
mutationFn: (data: any) => del(`/websites/${websiteId}`, data),
});
const handleConfirm = async () => {

View File

@ -1,40 +1,46 @@
import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics';
import { useContext, useRef } from 'react';
import { useApi } from 'components/hooks';
import { Website } from '@prisma/client';
import { useRef } from 'react';
import {
SubmitButton,
Form,
FormInput,
FormRow,
FormButtons,
TextField,
useToasts,
} from 'react-basics';
import { useApi, useMessages } from 'components/hooks';
import { DOMAIN_REGEX } from 'lib/constants';
import { useMessages } from 'components/hooks';
import SettingsContext from '../../SettingsContext';
export function WebsiteEditForm({
websiteId,
data,
website,
onSave,
}: {
websiteId: string;
data: any[];
website: Website;
onSave?: (data: any) => void;
}) {
const { formatMessage, labels, messages } = useMessages();
const { websitesUrl } = useContext(SettingsContext);
const { post, useMutation } = useApi();
const { mutate, error } = useMutation({
mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}`, data),
mutationFn: (data: any) => post(`/websites/${website.id}`, data),
});
const ref = useRef(null);
const { showToast } = useToasts();
const handleSubmit = async (data: any) => {
mutate(data, {
onSuccess: async () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
ref.current.reset(data);
onSave(data);
onSave?.(data);
},
});
};
return (
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}>
<Form ref={ref} onSubmit={handleSubmit} error={error} values={website}>
<FormRow label={formatMessage(labels.websiteId)}>
<TextField value={websiteId} readOnly allowCopy />
<TextField value={website.id} readOnly allowCopy />
</FormRow>
<FormRow label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>

View File

@ -0,0 +1,5 @@
import WebsiteDetails from 'app/(main)/websites/[id]/WebsiteDetails';
export default function TeamWebsitePage({ params: { websiteId } }) {
return <WebsiteDetails websiteId={websiteId} />;
}

View File

@ -0,0 +1,6 @@
'use client';
import { createContext } from 'react';
export const WebsiteContext = createContext(null);
export default WebsiteContext;

View File

@ -27,6 +27,11 @@
flex: 1;
}
.icon {
color: var(--base700);
margin-right: 1rem;
}
.actions {
display: flex;
justify-content: flex-end;

View File

@ -1,16 +1,26 @@
import classNames from 'classnames';
import React, { ReactNode } from 'react';
import { Icon } from 'react-basics';
import styles from './PageHeader.module.css';
export interface PageHeaderProps {
export function PageHeader({
title,
icon,
className,
children,
}: {
title?: ReactNode;
icon?: ReactNode;
className?: string;
children?: ReactNode;
}
export function PageHeader({ title, className, children }: PageHeaderProps) {
}) {
return (
<div className={classNames(styles.header, className)}>
{icon && (
<Icon size="lg" className={styles.icon}>
{icon}
</Icon>
)}
{title && <div className={styles.title}>{title}</div>}
<div className={styles.actions}>{children}</div>
</div>