Update teams features.

This commit is contained in:
Mike Cao 2023-02-01 18:39:54 -08:00
parent 89f2fd601e
commit 656df4f846
23 changed files with 278 additions and 113 deletions

View File

@ -1,14 +1,17 @@
import { FormattedMessage } from 'react-intl'; import { useIntl } from 'react-intl';
import { Icon, Icons } from 'react-basics'; import { Icon, Icons, Text } from 'react-basics';
import { messages } from 'components/messages';
import styles from './ErrorMessage.module.css'; import styles from './ErrorMessage.module.css';
export default function ErrorMessage() { export default function ErrorMessage() {
const { formatMessage } = useIntl();
return ( return (
<div className={styles.error}> <div className={styles.error}>
<Icon className={styles.icon} size="large"> <Icon className={styles.icon} size="large">
<Icons.Alert /> <Icons.Alert />
</Icon> </Icon>
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." /> <Text>{formatMessage(messages.error)}</Text>
</div> </div>
); );
} }

View File

@ -1,4 +1,5 @@
import { Icons } from 'react-basics'; import { Icons } from 'react-basics';
import AddUser from 'assets/add-user.svg';
import Bolt from 'assets/bolt.svg'; import Bolt from 'assets/bolt.svg';
import Calendar from 'assets/calendar.svg'; import Calendar from 'assets/calendar.svg';
import Clock from 'assets/clock.svg'; import Clock from 'assets/clock.svg';
@ -15,6 +16,7 @@ import Users from 'assets/users.svg';
const icons = { const icons = {
...Icons, ...Icons,
AddUser,
Bolt, Bolt,
Calendar, Calendar,
Clock, Clock,

View File

@ -26,8 +26,11 @@ export const labels = defineMessages({
team: { id: 'label.team', defaultMessage: 'Team' }, team: { id: 'label.team', defaultMessage: 'Team' },
regenerate: { id: 'label.regenerate', defaultMessage: 'Regenerate' }, regenerate: { id: 'label.regenerate', defaultMessage: 'Regenerate' },
remove: { id: 'label.remove', defaultMessage: 'Remove' }, remove: { id: 'label.remove', defaultMessage: 'Remove' },
join: { id: 'label.join', defaultMessage: 'Join' },
createTeam: { id: 'label.create-team', defaultMessage: 'Create team' }, createTeam: { id: 'label.create-team', defaultMessage: 'Create team' },
joinTeam: { id: 'label.join-team', defaultMessage: 'Join team' },
settings: { id: 'label.settings', defaultMessage: 'Settings' }, settings: { id: 'label.settings', defaultMessage: 'Settings' },
owner: { id: 'label.owner', defaultMessage: 'Owner' },
teamOwner: { id: 'label.team-owner', defaultMessage: 'Team owner' }, teamOwner: { id: 'label.team-owner', defaultMessage: 'Team owner' },
teamMember: { id: 'label.team-member', defaultMessage: 'Team member' }, teamMember: { id: 'label.team-member', defaultMessage: 'Team member' },
teamGuest: { id: 'label.team-guest', defaultMessage: 'Team guest' }, teamGuest: { id: 'label.team-guest', defaultMessage: 'Team guest' },
@ -61,6 +64,7 @@ export const labels = defineMessages({
logout: { id: 'label.logout', defaultMessage: 'Logout' }, logout: { id: 'label.logout', defaultMessage: 'Logout' },
singleDay: { id: 'label.single-day', defaultMessage: 'Single day' }, singleDay: { id: 'label.single-day', defaultMessage: 'Single day' },
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' }, dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
viewDetails: { id: 'label.view-details', defaultMessage: 'View details' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({
@ -79,7 +83,7 @@ export const messages = defineMessages({
}, },
noTeams: { noTeams: {
id: 'message.no-teams', id: 'message.no-teams',
defaultMessage: 'You have no created any teams.', defaultMessage: 'You have not created any teams.',
}, },
shareUrl: { shareUrl: {
id: 'message.share-url', id: 'message.share-url',
@ -120,6 +124,14 @@ export const messages = defineMessages({
id: 'message.go-to-settings', id: 'message.go-to-settings',
defaultMessage: 'Go to settings', defaultMessage: 'Go to settings',
}, },
activeUsers: {
id: 'message.active-users',
defaultMessage: '{x} current {x, plural, one {visitor} other {visitors}}',
},
teamNotFound: {
id: 'message.team-not-found',
defaultMessage: 'Team not found.',
},
}); });
export const devices = defineMessages({ export const devices = defineMessages({
@ -129,6 +141,12 @@ export const devices = defineMessages({
mobile: { id: 'metrics.device.mobile', defaultMessage: 'Mobile' }, mobile: { id: 'metrics.device.mobile', defaultMessage: 'Mobile' },
}); });
export function getMessage(id, formatMessage) {
const message = Object.values(messages).find(value => value.id === id);
return message ? formatMessage(message) : id;
}
export function getDeviceMessage(device) { export function getDeviceMessage(device) {
return devices[device] || labels.unknown; return devices[device] || labels.unknown;
} }

View File

@ -1,16 +1,20 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { StatusLight } from 'react-basics'; import { StatusLight } from 'react-basics';
import { FormattedMessage } from 'react-intl'; import { useIntl } from 'react-intl';
import classNames from 'classnames';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { messages } from 'components/messages';
import styles from './ActiveUsers.module.css'; import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, className, value, refetchInterval = 60000 }) { export default function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) {
const url = websiteId ? `/websites/${websiteId}/active` : null; const { formatMessage } = useIntl();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data } = useQuery(['websites:active', websiteId], () => get(url), { const { data } = useQuery(
['websites:active', websiteId],
() => get(`/websites/${websiteId}/active`),
{
refetchInterval, refetchInterval,
}); },
);
const count = useMemo(() => { const count = useMemo(() => {
if (websiteId) { if (websiteId) {
@ -25,17 +29,8 @@ export default function ActiveUsers({ websiteId, className, value, refetchInterv
} }
return ( return (
<div className={classNames(styles.container, className)}> <StatusLight variant="success">
<StatusLight variant="success" /> <div className={styles.text}>{formatMessage(messages.activeUsers, { x: count })}</div>
<div className={styles.text}> </StatusLight>
<div>
<FormattedMessage
id="message.active-users"
defaultMessage="{x} current {x, plural, one {visitor} other {visitors}}"
values={{ x: count }}
/>
</div>
</div>
</div>
); );
} }

View File

@ -15,8 +15,9 @@ export default function ChartTooltip({ chartId, tooltip }) {
<div className={styles.content}> <div className={styles.content}>
<div className={styles.title}>{title}</div> <div className={styles.title}>{title}</div>
<div className={styles.metric}> <div className={styles.metric}>
<StatusLight color={labelColor} /> <StatusLight color={labelColor}>
{value} {label} {value} {label}
</StatusLight>
</div> </div>
</div> </div>
</div> </div>

View File

@ -34,8 +34,9 @@ export default function Legend({ chart }) {
className={classNames(styles.label, { [styles.hidden]: hidden })} className={classNames(styles.label, { [styles.hidden]: hidden })}
onClick={() => handleClick(datasetIndex)} onClick={() => handleClick(datasetIndex)}
> >
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()} /> <StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
<span className={locale}>{text}</span> <span className={locale}>{text}</span>
</StatusLight>
</div> </div>
); );
})} })}

View File

@ -1,5 +1,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Row, Column, Loading } from 'react-basics'; import { useIntl } from 'react-intl';
import { Button, Icon, Text, Row, Column, Loading } from 'react-basics';
import Link from 'next/link';
import PageviewsChart from './PageviewsChart'; import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar'; import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader'; import WebsiteHeader from './WebsiteHeader';
@ -12,6 +14,8 @@ import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone'; import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date'; import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date';
import Icons from 'components/icons';
import { labels } from 'components/messages';
import styles from './WebsiteChart.module.css'; import styles from './WebsiteChart.module.css';
export default function WebsiteChart({ export default function WebsiteChart({
@ -20,8 +24,10 @@ export default function WebsiteChart({
domain, domain,
stickyHeader = false, stickyHeader = false,
showChart = true, showChart = true,
showDetailsButton = false,
onDataLoad = () => {}, onDataLoad = () => {},
}) { }) {
const { formatMessage } = useIntl();
const [dateRange, setDateRange] = useDateRange(websiteId); const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange; const { startDate, endDate, unit, value, modified } = dateRange;
const [timezone] = useTimezone(); const [timezone] = useTimezone();
@ -82,7 +88,20 @@ export default function WebsiteChart({
return ( return (
<> <>
<WebsiteHeader websiteId={websiteId} title={title} domain={domain} /> <WebsiteHeader websiteId={websiteId} title={title} domain={domain}>
{showDetailsButton && (
<Link href={`/websites/${websiteId}`}>
<a>
<Button>
<Text>{formatMessage(labels.viewDetails)}</Text>
<Icon>
<Icons.ArrowRight />
</Icon>
</Button>
</a>
</Link>
)}
</WebsiteHeader>
<StickyHeader <StickyHeader
className={styles.metrics} className={styles.metrics}
stickyClassName={styles.sticky} stickyClassName={styles.sticky}

View File

@ -1,22 +1,20 @@
import { Row, Column } from 'react-basics'; import { Row, Column } from 'react-basics';
import Favicon from 'components/common/Favicon'; import Favicon from 'components/common/Favicon';
import OverflowText from 'components/common/OverflowText'; import OverflowText from 'components/common/OverflowText';
import PageHeader from 'components/layout/PageHeader';
import ActiveUsers from './ActiveUsers'; import ActiveUsers from './ActiveUsers';
import styles from './WebsiteHeader.module.css'; import styles from './WebsiteHeader.module.css';
export default function WebsiteHeader({ websiteId, title, domain }) { export default function WebsiteHeader({ websiteId, title, domain, children }) {
return ( return (
<PageHeader> <Row className={styles.header} justifyContent="center">
<Row>
<Column className={styles.title} variant="two"> <Column className={styles.title} variant="two">
<Favicon domain={domain} /> <Favicon domain={domain} />
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText> <OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
</Column> </Column>
<Column className={styles.active} variant="two"> <Column className={styles.body} variant="two">
<ActiveUsers websiteId={websiteId} /> <ActiveUsers websiteId={websiteId} />
{children}
</Column> </Column>
</Row> </Row>
</PageHeader>
); );
} }

View File

@ -1,15 +1,21 @@
.header {
height: 100px;
}
.title { .title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
font-size: var(--font-size-lg); font-size: 24px;
font-weight: 700;
overflow: hidden; overflow: hidden;
} }
.active { .body {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-end;
gap: 30px;
} }

View File

@ -0,0 +1,43 @@
import { useRef } from 'react';
import { useIntl } from 'react-intl';
import {
Form,
FormRow,
FormInput,
FormButtons,
TextField,
Button,
SubmitButton,
} from 'react-basics';
import useApi from 'hooks/useApi';
import { labels, getMessage } from 'components/messages';
export default function TeamJoinForm({ onSave, onClose }) {
const { formatMessage } = useIntl();
const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data => post('/teams/join', data));
const ref = useRef(null);
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
onClose();
},
});
};
return (
<Form ref={ref} onSubmit={handleSubmit} error={error && getMessage(error, formatMessage)}>
<FormRow label={formatMessage(labels.accessCode)}>
<FormInput name="accessCode" rules={{ required: formatMessage(labels.required) }}>
<TextField autoComplete="off" />
</FormInput>
</FormRow>
<FormButtons flex>
<SubmitButton variant="primary">{formatMessage(labels.join)}</SubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</FormButtons>
</Form>
);
}

View File

@ -4,7 +4,7 @@ import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
export default function TeamMembers({ teamId }) { export default function TeamMembers({ teamId }) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['team-members', teamId], () => const { data, isLoading } = useQuery(['teams:users', teamId], () =>
get(`/teams/${teamId}/users`), get(`/teams/${teamId}/users`),
); );

View File

@ -40,14 +40,16 @@ export default function TeamSettings({ teamId }) {
return ( return (
<Page loading={isLoading || !values}> <Page loading={isLoading || !values}>
{toast} {toast}
<PageHeader> <PageHeader
title={
<Breadcrumbs> <Breadcrumbs>
<Item> <Item>
<Link href="/settings/teams">Teams</Link> <Link href="/settings/teams">Teams</Link>
</Item> </Item>
<Item>{values?.name}</Item> <Item>{values?.name}</Item>
</Breadcrumbs> </Breadcrumbs>
</PageHeader> }
/>
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}> <Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
<Item key="details">{formatMessage(labels.details)}</Item> <Item key="details">{formatMessage(labels.details)}</Item>
<Item key="members">{formatMessage(labels.members)}</Item> <Item key="members">{formatMessage(labels.members)}</Item>

View File

@ -7,7 +7,7 @@ import { messages } from 'components/messages';
export default function TeamWebsites({ teamId }) { export default function TeamWebsites({ teamId }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['teams/websites', teamId], () => const { data, isLoading } = useQuery(['teams:websites', teamId], () =>
get(`/teams/${teamId}/websites`), get(`/teams/${teamId}/websites`),
); );
const hasData = data && data.length !== 0; const hasData = data && data.length !== 0;

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, Icon, Modal, ModalTrigger, useToast, Icons, Text } from 'react-basics'; import { Button, Icon, Modal, ModalTrigger, useToast, Text, Flexbox } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
@ -8,6 +8,8 @@ import PageHeader from 'components/layout/PageHeader';
import TeamsTable from 'components/pages/settings/teams/TeamsTable'; import TeamsTable from 'components/pages/settings/teams/TeamsTable';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import { labels, messages } from 'components/messages'; import { labels, messages } from 'components/messages';
import Icons from 'components/icons';
import TeamJoinForm from './JoinTeamForm';
export default function TeamsList() { export default function TeamsList() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -22,6 +24,24 @@ export default function TeamsList() {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
}; };
const handleJoin = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
const joinButton = (
<ModalTrigger>
<Button variant="secondary">
<Icon>
<Icons.AddUser />
</Icon>
<Text>{formatMessage(labels.joinTeam)}</Text>
</Button>
<Modal title={formatMessage(labels.joinTeam)}>
{close => <TeamJoinForm onSave={handleJoin} onClose={close} />}
</Modal>
</ModalTrigger>
);
const createButton = ( const createButton = (
<ModalTrigger> <ModalTrigger>
<Button variant="primary"> <Button variant="primary">
@ -39,7 +59,14 @@ export default function TeamsList() {
return ( return (
<Page loading={isLoading} error={error}> <Page loading={isLoading} error={error}>
{toast} {toast}
<PageHeader title={formatMessage(labels.team)}>{createButton}</PageHeader> <PageHeader title={formatMessage(labels.team)}>
{hasData && (
<Flexbox gap={10}>
{joinButton}
{createButton}
</Flexbox>
)}
</PageHeader>
{hasData && <TeamsTable data={data} />} {hasData && <TeamsTable data={data} />}
{!hasData && ( {!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noTeams)}> <EmptyPlaceholder message={formatMessage(messages.noTeams)}>

View File

@ -14,12 +14,14 @@ import {
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { labels } from 'components/messages'; import { labels } from 'components/messages';
import { ROLES } from 'lib/constants';
export default function TeamsTable({ data = [] }) { export default function TeamsTable({ data = [] }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const columns = [ const columns = [
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } }, { name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
{ name: 'owner', label: formatMessage(labels.owner) },
{ name: 'action', label: ' ' }, { name: 'action', label: ' ' },
]; ];
@ -38,7 +40,10 @@ export default function TeamsTable({ data = [] }) {
{(row, keys, rowIndex) => { {(row, keys, rowIndex) => {
const { id } = row; const { id } = row;
row.action = ( const rowData = {
...row,
owner: row.teamUsers.find(({ role }) => role === ROLES.teamOwner)?.user?.username,
action: (
<Flexbox flex={1} justifyContent="end"> <Flexbox flex={1} justifyContent="end">
<Link href={`/settings/teams/${id}`}> <Link href={`/settings/teams/${id}`}>
<a> <a>
@ -51,10 +56,11 @@ export default function TeamsTable({ data = [] }) {
</a> </a>
</Link> </Link>
</Flexbox> </Flexbox>
); ),
};
return ( return (
<TableRow key={rowIndex} data={row} keys={keys}> <TableRow key={rowIndex} data={rowData} keys={keys}>
{(data, key, colIndex) => { {(data, key, colIndex) => {
return ( return (
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}> <TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>

View File

@ -46,14 +46,16 @@ export default function UserSettings({ userId }) {
return ( return (
<Page loading={isLoading || !values}> <Page loading={isLoading || !values}>
{toast} {toast}
<PageHeader> <PageHeader
title={
<Breadcrumbs> <Breadcrumbs>
<Item> <Item>
<Link href="/settings/users">{formatMessage(labels.users)}</Link> <Link href="/settings/users">{formatMessage(labels.users)}</Link>
</Item> </Item>
<Item>{values?.username}</Item> <Item>{values?.username}</Item>
</Breadcrumbs> </Breadcrumbs>
</PageHeader> }
/>
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}> <Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
<Item key="details">{formatMessage(labels.details)}</Item> <Item key="details">{formatMessage(labels.details)}</Item>
<Item key="websites">{formatMessage(labels.websites)}</Item> <Item key="websites">{formatMessage(labels.websites)}</Item>

View File

@ -7,7 +7,7 @@ import { messages } from 'components/messages';
export default function UserWebsites({ userId }) { export default function UserWebsites({ userId }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['user/websites', userId], () => const { data, isLoading } = useQuery(['user:websites', userId], () =>
get(`/users/${userId}/websites`), get(`/users/${userId}/websites`),
); );
const hasData = data && data.length !== 0; const hasData = data && data.length !== 0;

View File

@ -49,13 +49,16 @@ export default function WebsiteSettings({ websiteId }) {
return ( return (
<Page loading={isLoading || !values}> <Page loading={isLoading || !values}>
{toast} {toast}
<PageHeader> <PageHeader
title={
<Breadcrumbs> <Breadcrumbs>
<Item> <Item>
<Link href="/settings/websites">{formatMessage(labels.websites)}</Link> <Link href="/settings/websites">{formatMessage(labels.websites)}</Link>
</Item> </Item>
<Item>{values?.name}</Item> <Item>{values?.name}</Item>
</Breadcrumbs> </Breadcrumbs>
}
>
<Link href={`/websites/${websiteId}`}> <Link href={`/websites/${websiteId}`}>
<a> <a>
<Button variant="primary"> <Button variant="primary">

View File

@ -1,10 +1,10 @@
import WebsiteChart from 'components/metrics/WebsiteChart';
import styles from './WebsiteList.module.css';
import useDashboard from 'store/dashboard';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
import WebsiteChart from 'components/metrics/WebsiteChart';
import useDashboard from 'store/dashboard';
import styles from './WebsiteList.module.css';
export default function WebsiteList({ websites, showCharts, limit }) { export default function WebsiteChartList({ websites, showCharts, limit }) {
const { websiteOrder } = useDashboard(); const { websiteOrder } = useDashboard();
const ordered = useMemo( const ordered = useMemo(
@ -17,19 +17,19 @@ export default function WebsiteList({ websites, showCharts, limit }) {
return ( return (
<div> <div>
{ordered.map(({ id, name, domain }, index) => {ordered.map(({ id, name, domain }, index) => {
index < limit ? ( return index < limit ? (
<div key={id} className={styles.website}> <div key={id} className={styles.website}>
<WebsiteChart <WebsiteChart
websiteId={id} websiteId={id}
title={name} title={name}
domain={domain} domain={domain}
showChart={showCharts} showChart={showCharts}
showLink showDetailsButton={true}
/> />
</div> </div>
) : null, ) : null;
)} })}
</div> </div>
); );
} }

View File

@ -38,11 +38,11 @@ export default async (
if (user && checkPassword(password, user.password)) { if (user && checkPassword(password, user.password)) {
if (redis.enabled) { if (redis.enabled) {
const key = `auth:${getRandomChars(32)}`; const authKey = `auth:${getRandomChars(32)}`;
await redis.set(key, user); await redis.set(authKey, user);
const token = createSecureToken({ key }, secret()); const token = createSecureToken({ authKey }, secret());
return ok(res, { token, user }); return ok(res, { token, user });
} }

View File

@ -4,7 +4,7 @@ import { canCreateTeam } from 'lib/auth';
import { uuid } from 'lib/crypto'; import { uuid } from 'lib/crypto';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeam, getUserTeams } from 'queries'; import { createTeam, getUserTeams } from 'queries';
export interface TeamsRequestBody { export interface TeamsRequestBody {
@ -34,13 +34,14 @@ export default async (
const { name } = req.body; const { name } = req.body;
const created = await createTeam({ const team = await createTeam({
id: uuid(), id: uuid(),
name, name,
userId, userId,
accessCode: getRandomChars(16),
}); });
return ok(res, created); return ok(res, team);
} }
return methodNotAllowed(res); return methodNotAllowed(res);

34
pages/api/teams/join.ts Normal file
View File

@ -0,0 +1,34 @@
import { Team } from '@prisma/client';
import { NextApiRequestQueryBody } from 'lib/types';
import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, notFound } from 'next-basics';
import { createTeamUser, getTeam } from 'queries';
import { ROLES } from 'lib/constants';
export interface TeamsRequestBody {
accessCode: string;
}
export default async (
req: NextApiRequestQueryBody<any, TeamsRequestBody>,
res: NextApiResponse<Team[] | Team>,
) => {
await useAuth(req, res);
if (req.method === 'POST') {
const { accessCode } = req.body;
const team = await getTeam({ accessCode });
if (!team) {
return notFound(res, 'message.team-not-found');
}
await createTeamUser(req.auth.user.id, team.id, ROLES.teamMember);
return ok(res, team);
}
return methodNotAllowed(res);
};

View File

@ -47,17 +47,21 @@ export async function getUsers(): Promise<User[]> {
} }
export async function getUserTeams(userId: string): Promise<Team[]> { export async function getUserTeams(userId: string): Promise<Team[]> {
return prisma.client.teamUser return prisma.client.team.findMany({
.findMany({
where: { where: {
teamUsers: {
some: {
userId, userId,
}, },
include: {
team: true,
}, },
}) },
.then(data => { include: {
return data.map(a => a.team); teamUsers: {
include: {
user: true,
},
},
},
}); });
} }