Added website and team providers.

This commit is contained in:
Mike Cao 2024-02-04 19:53:06 -08:00
parent dbb3801e66
commit cc273092d5
25 changed files with 123 additions and 98 deletions

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
import ConfirmationForm from 'components/common/ConfirmationForm'; import ConfirmationForm from 'components/common/ConfirmationForm';
export function ReportDeleteButton({ export function ReportDeleteButton({

View File

@ -8,7 +8,7 @@ import {
Button, Button,
SubmitButton, SubmitButton,
} from 'react-basics'; } from 'react-basics';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; import TypeConfirmationForm from 'components/common/TypeConfirmationForm';
const CONFIRM_VALUE = 'DELETE'; const CONFIRM_VALUE = 'DELETE';

View File

@ -10,7 +10,7 @@ import {
SubmitButton, SubmitButton,
} from 'react-basics'; } from 'react-basics';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
const { formatMessage, labels, getMessage } = useMessages(); const { formatMessage, labels, getMessage } = useMessages();

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
import ConfirmationForm from 'components/common/ConfirmationForm'; import ConfirmationForm from 'components/common/ConfirmationForm';
export function TeamLeaveForm({ export function TeamLeaveForm({

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import { Icon, Icons, LoadingButton, Text } from 'react-basics'; import { Icon, Icons, LoadingButton, Text } from 'react-basics';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
export function TeamMemberRemoveButton({ export function TeamMemberRemoveButton({
teamId, teamId,

View File

@ -2,7 +2,7 @@
import { Button, Icon, Text, Modal, Icons, ModalTrigger, useToasts } from 'react-basics'; import { Button, Icon, Text, Modal, Icons, ModalTrigger, useToasts } from 'react-basics';
import UserAddForm from './UserAddForm'; import UserAddForm from './UserAddForm';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
export function UserAddButton({ onSave }: { onSave?: () => void }) { export function UserAddButton({ onSave }: { onSave?: () => void }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import ConfirmationForm from 'components/common/ConfirmationForm'; import ConfirmationForm from 'components/common/ConfirmationForm';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
export function UserDeleteForm({ userId, username, onSave, onClose }) { export function UserDeleteForm({ userId, username, onSave, onClose }) {
const { FormattedMessage, messages, labels, formatMessage } = useMessages(); const { FormattedMessage, messages, labels, formatMessage } = useMessages();

View File

@ -2,7 +2,7 @@
import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
import WebsiteAddForm from './WebsiteAddForm'; import WebsiteAddForm from './WebsiteAddForm';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?: () => void }) { export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?: () => void }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();

View File

@ -1,5 +1,4 @@
'use client'; 'use client';
import { Website } from '@prisma/client';
import { import {
Form, Form,
FormRow, FormRow,
@ -11,21 +10,22 @@ import {
LoadingButton, LoadingButton,
useToasts, useToasts,
} from 'react-basics'; } from 'react-basics';
import { useState } from 'react'; import { useContext, useState } from 'react';
import { getRandomChars } from 'next-basics'; import { getRandomChars } from 'next-basics';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider';
import { touch } from 'store/modified';
const generateId = () => getRandomChars(16); const generateId = () => getRandomChars(16);
export function ShareUrl({ export function ShareUrl({
website,
hostUrl, hostUrl,
onSave,
}: { }: {
website: Website; websiteId: string;
hostUrl?: string; hostUrl?: string;
onSave?: () => void; onSave?: () => void;
}) { }) {
const website = useContext(WebsiteContext);
const { domain, shareId } = website; const { domain, shareId } = website;
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const [id, setId] = useState(shareId); const [id, setId] = useState(shareId);
@ -47,7 +47,6 @@ export function ShareUrl({
const data = { shareId: checked ? generateId() : null }; const data = { shareId: checked ? generateId() : null };
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave?.();
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
}, },
}); });
@ -60,7 +59,7 @@ export function ShareUrl({
{ {
onSuccess: async () => { onSuccess: async () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
onSave?.(); touch(`website:${website?.id}`);
}, },
}, },
); );

View File

@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import WebsiteDeleteForm from './WebsiteDeleteForm'; import WebsiteDeleteForm from './WebsiteDeleteForm';
import WebsiteResetForm from './WebsiteResetForm'; import WebsiteResetForm from './WebsiteResetForm';
import { touch } from 'store/cache'; import { touch } from 'store/modified';
export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) { export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();

View File

@ -1,6 +1,5 @@
'use client'; 'use client';
import { Website } from '@prisma/client'; import { useContext, useRef } from 'react';
import { useRef } from 'react';
import { import {
SubmitButton, SubmitButton,
Form, Form,
@ -12,18 +11,20 @@ import {
} from 'react-basics'; } from 'react-basics';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import { touch } from 'store/modified';
import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider';
export function WebsiteEditForm({ export function WebsiteEditForm({
website, websiteId,
onSave,
}: { }: {
website: Website; websiteId: string;
onSave?: (data: any) => void; onSave?: (data: any) => void;
}) { }) {
const website = useContext(WebsiteContext);
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation({ const { mutate, error } = useMutation({
mutationFn: (data: any) => post(`/websites/${website.id}`, data), mutationFn: (data: any) => post(`/websites/${websiteId}`, data),
}); });
const ref = useRef(null); const ref = useRef(null);
const { showToast } = useToasts(); const { showToast } = useToasts();
@ -33,7 +34,7 @@ export function WebsiteEditForm({
onSuccess: async () => { onSuccess: async () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
ref.current.reset(data); ref.current.reset(data);
onSave?.(data); touch(`website:${website?.id}`);
}, },
}); });
}; };
@ -41,7 +42,7 @@ export function WebsiteEditForm({
return ( return (
<Form ref={ref} onSubmit={handleSubmit} error={error} values={website}> <Form ref={ref} onSubmit={handleSubmit} error={error} values={website}>
<FormRow label={formatMessage(labels.websiteId)}> <FormRow label={formatMessage(labels.websiteId)}>
<TextField value={website.id} readOnly allowCopy /> <TextField value={website?.id} readOnly allowCopy />
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.name)}> <FormRow label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}> <FormInput name="name" rules={{ required: formatMessage(labels.required) }}>

View File

@ -1,31 +1,23 @@
'use client'; 'use client';
import { useState, Key } from 'react'; import { useState, Key, useContext } from 'react';
import { Item, Tabs, Button, Text, Icon, Loading } from 'react-basics'; import { Item, Tabs, Button, Text, Icon } from 'react-basics';
import Link from 'next/link'; import Link from 'next/link';
import Icons from 'components/icons'; import Icons from 'components/icons';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import WebsiteContext from 'app/(main)/websites/[websiteId]/WebsiteContext'; import WebsiteEditForm from './WebsiteEditForm';
import WebsiteEditForm from './[websiteId]/WebsiteEditForm'; import WebsiteData from './WebsiteData';
import WebsiteData from './[websiteId]/WebsiteData'; import TrackingCode from './TrackingCode';
import TrackingCode from './[websiteId]/TrackingCode'; import ShareUrl from './ShareUrl';
import ShareUrl from './[websiteId]/ShareUrl'; import { useMessages } from 'components/hooks';
import { useWebsite, useMessages } from 'components/hooks'; import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider';
export function WebsiteSettings({ websiteId, openExternal = false }) { export function WebsiteSettings({ websiteId, openExternal = false }) {
const website = useContext(WebsiteContext);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data: website, isLoading, refetch } = useWebsite(websiteId);
const [tab, setTab] = useState<Key>('details'); const [tab, setTab] = useState<Key>('details');
const handleSave = () => {
refetch();
};
if (isLoading) {
return <Loading position="page" />;
}
return ( return (
<WebsiteContext.Provider value={website}> <>
<PageHeader title={website?.name} icon={<Icons.Globe />}> <PageHeader title={website?.name} icon={<Icons.Globe />}>
<Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}> <Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}>
<Button variant="primary"> <Button variant="primary">
@ -42,11 +34,11 @@ export function WebsiteSettings({ websiteId, openExternal = false }) {
<Item key="share">{formatMessage(labels.shareUrl)}</Item> <Item key="share">{formatMessage(labels.shareUrl)}</Item>
<Item key="data">{formatMessage(labels.data)}</Item> <Item key="data">{formatMessage(labels.data)}</Item>
</Tabs> </Tabs>
{tab === 'details' && <WebsiteEditForm website={website} onSave={handleSave} />} {tab === 'details' && <WebsiteEditForm websiteId={websiteId} />}
{tab === 'tracking' && <TrackingCode websiteId={websiteId} />} {tab === 'tracking' && <TrackingCode websiteId={websiteId} />}
{tab === 'share' && <ShareUrl website={website} onSave={handleSave} />} {tab === 'share' && <ShareUrl websiteId={websiteId} />}
{tab === 'data' && <WebsiteData websiteId={websiteId} />} {tab === 'data' && <WebsiteData websiteId={websiteId} />}
</WebsiteContext.Provider> </>
); );
} }

View File

@ -1,5 +1,10 @@
import WebsiteSettings from '../WebsiteSettings'; import WebsiteProvider from 'app/(main)/websites/[websiteId]/WebsiteProvider';
import WebsiteSettings from './WebsiteSettings';
export default async function WebsiteSettingsPage({ params: { websiteId } }) { export default async function WebsiteSettingsPage({ params: { websiteId } }) {
return <WebsiteSettings websiteId={websiteId} />; return (
<WebsiteProvider websiteId={websiteId}>
<WebsiteSettings websiteId={websiteId} />
</WebsiteProvider>
);
} }

View File

@ -1,21 +0,0 @@
'use client';
import { useTeam, useTeamUrl } from 'components/hooks';
import { Loading } from 'react-basics';
import TeamContext from './TeamContext';
export function Team({ children }) {
const { teamId } = useTeamUrl();
const { data: team, isLoading } = useTeam(teamId);
if (isLoading) {
return <Loading />;
}
if (!team) {
return null;
}
return <TeamContext.Provider value={team}>{children}</TeamContext.Provider>;
}
export default Team;

View File

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

View File

@ -0,0 +1,30 @@
'use client';
import { createContext, ReactNode, useEffect } from 'react';
import { useTeam } from 'components/hooks';
import { Loading } from 'react-basics';
import useModified from 'store/modified';
export const TeamContext = createContext(null);
export function TeamProvider({ teamId, children }: { teamId: string; children: ReactNode }) {
const modified = useModified(state => state?.[`team:${teamId}`]);
const { data: team, isLoading, isFetching, refetch } = useTeam(teamId);
useEffect(() => {
if (modified) {
refetch();
}
}, [modified]);
if (isFetching && isLoading) {
return <Loading />;
}
if (!team) {
return null;
}
return <TeamContext.Provider value={team}>{children}</TeamContext.Provider>;
}
export default TeamProvider;

View File

@ -1,5 +1,5 @@
import Team from './Team'; import TeamProvider from './TeamProvider';
export default function ({ children }) { export default function ({ children }) {
return <Team>{children}</Team>; return <TeamProvider>{children}</TeamProvider>;
} }

View File

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

View File

@ -0,0 +1,32 @@
'use client';
import { createContext, ReactNode, useEffect } from 'react';
import { useWebsite } from 'components/hooks';
import { Loading } from 'react-basics';
import useModified from 'store/modified';
export const WebsiteContext = createContext(null);
export function WebsiteProvider({
websiteId,
children,
}: {
websiteId: string;
children: ReactNode;
}) {
const modified = useModified(state => state?.[`website:${websiteId}`]);
const { data: website, isFetching, isLoading, refetch } = useWebsite(websiteId);
useEffect(() => {
if (modified) {
refetch();
}
}, [modified]);
if (isFetching && isLoading) {
return <Loading position="page" />;
}
return <WebsiteContext.Provider value={website}>{children}</WebsiteContext.Provider>;
}
export default WebsiteProvider;

View File

@ -5,7 +5,7 @@ export function useWebsite(websiteId: string, options?: { [key: string]: any })
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
return useQuery({ return useQuery({
queryKey: ['websites', { websiteId }], queryKey: ['website', { websiteId }],
queryFn: () => get(`/websites/${websiteId}`), queryFn: () => get(`/websites/${websiteId}`),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,

View File

@ -22,13 +22,16 @@ export * from 'app/(main)/settings/websites/[websiteId]/TrackingCode';
export * from 'app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm'; export * from 'app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm';
export * from 'app/(main)/settings/websites/[websiteId]/WebsiteEditForm'; export * from 'app/(main)/settings/websites/[websiteId]/WebsiteEditForm';
export * from 'app/(main)/settings/websites/[websiteId]/WebsiteResetForm'; export * from 'app/(main)/settings/websites/[websiteId]/WebsiteResetForm';
export * from 'app/(main)/settings/websites/[websiteId]/WebsiteSettings';
export * from 'app/(main)/settings/websites/WebsiteAddForm'; export * from 'app/(main)/settings/websites/WebsiteAddForm';
export * from 'app/(main)/settings/websites/WebsitesHeader'; export * from 'app/(main)/settings/websites/WebsitesHeader';
export * from 'app/(main)/settings/websites/WebsiteSettings';
export * from 'app/(main)/settings/websites/WebsitesDataTable'; export * from 'app/(main)/settings/websites/WebsitesDataTable';
export * from 'app/(main)/settings/websites/WebsitesTable'; export * from 'app/(main)/settings/websites/WebsitesTable';
export * from 'app/(main)/teams/[teamId]/TeamProvider';
export * from 'app/(main)/websites/[websiteId]/WebsiteProvider';
export * from 'components/common/TypeConfirmationForm'; export * from 'components/common/TypeConfirmationForm';
export * from 'components/common/DataTable'; export * from 'components/common/DataTable';
export * from 'components/common/Empty'; export * from 'components/common/Empty';

View File

@ -57,17 +57,17 @@ export default async (
const { name, domain, shareId } = req.body; const { name, domain, shareId } = req.body;
let website;
try { try {
website = await updateWebsite(websiteId, { name, domain, shareId }); const website = await updateWebsite(websiteId, { name, domain, shareId });
return ok(res, website);
} catch (e: any) { } catch (e: any) {
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
return serverError(res, 'That share ID is already taken.'); return serverError(res, 'That share ID is already taken.');
} }
}
return ok(res, website); return serverError(res, e);
}
} }
if (req.method === 'DELETE') { if (req.method === 'DELETE') {

View File

@ -124,7 +124,7 @@ export async function updateWebsite(
): Promise<Website> { ): Promise<Website> {
return prisma.client.website.update({ return prisma.client.website.update({
where: { where: {
websiteId, id: websiteId,
}, },
data, data,
}); });
@ -146,7 +146,7 @@ export async function resetWebsite(
where: { websiteId }, where: { websiteId },
}), }),
client.website.update({ client.website.update({
where: { websiteId }, where: { id: websiteId },
data: { data: {
resetAt: new Date(), resetAt: new Date(),
}, },
@ -186,10 +186,10 @@ export async function deleteWebsite(
data: { data: {
deletedAt: new Date(), deletedAt: new Date(),
}, },
where: { websiteId }, where: { id: websiteId },
}) })
: client.website.delete({ : client.website.delete({
where: { websiteId }, where: { id: websiteId },
}), }),
]).then(async data => { ]).then(async data => {
if (cache.enabled) { if (cache.enabled) {

View File

@ -6,8 +6,4 @@ export function setValue(key: string, value: any) {
store.setState({ [key]: value }); store.setState({ [key]: value });
} }
export function touch(key: string) {
setValue(key, Date.now());
}
export default store; export default store;