feat: configurable invites & disable_media_preview config
This commit is contained in:
parent
ec0e7e5ec7
commit
d31371eb6c
19 changed files with 72 additions and 20 deletions
|
@ -5,6 +5,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env REACT_EDITOR=code NODE_ENV=development tsx src/server",
|
"dev": "cross-env REACT_EDITOR=code NODE_ENV=development tsx src/server",
|
||||||
"build": "npm-run-all build:schema build:next",
|
"build": "npm-run-all build:schema build:next",
|
||||||
|
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:schema build:next",
|
||||||
"build:next": "next build",
|
"build:next": "next build",
|
||||||
"build:schema": "prisma generate --schema=prisma/schema.prisma",
|
"build:schema": "prisma generate --schema=prisma/schema.prisma",
|
||||||
"migrate:dev": "prisma migrate dev --create-only",
|
"migrate:dev": "prisma migrate dev --create-only",
|
||||||
|
|
|
@ -4,9 +4,10 @@ import { showNotification } from '@mantine/notifications';
|
||||||
import { relativeTime } from 'lib/utils/client';
|
import { relativeTime } from 'lib/utils/client';
|
||||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons';
|
import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, ExternalLinkIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons';
|
||||||
import MutedText from './MutedText';
|
import MutedText from './MutedText';
|
||||||
import Type from './Type';
|
import Type from './Type';
|
||||||
|
import Link from './Link';
|
||||||
|
|
||||||
export function FileMeta({ Icon, title, subtitle, ...other }) {
|
export function FileMeta({ Icon, title, subtitle, ...other }) {
|
||||||
return other.tooltip ? (
|
return other.tooltip ? (
|
||||||
|
@ -30,7 +31,7 @@ export function FileMeta({ Icon, title, subtitle, ...other }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function File({ image, updateImages }) {
|
export default function File({ image, updateImages, disableMediaPreview }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const deleteFile = useFileDelete();
|
const deleteFile = useFileDelete();
|
||||||
const favoriteFile = useFileFavorite();
|
const favoriteFile = useFileFavorite();
|
||||||
|
@ -112,6 +113,7 @@ export default function File({ image, updateImages }) {
|
||||||
popup
|
popup
|
||||||
sx={{ minHeight: 200 }}
|
sx={{ minHeight: 200 }}
|
||||||
style={{ minHeight: 200 }}
|
style={{ minHeight: 200 }}
|
||||||
|
disableMediaPreview={false}
|
||||||
/>
|
/>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
||||||
|
@ -128,7 +130,10 @@ export default function File({ image, updateImages }) {
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Group position='right' mt={22}>
|
<Group position='right' mt={22}>
|
||||||
<Button onClick={handleCopy}>Copy</Button>
|
<Link href={image.url} target='_blank'>
|
||||||
|
<Button rightIcon={<ExternalLinkIcon />}>Open</Button>
|
||||||
|
</Link>
|
||||||
|
<Button onClick={handleCopy}>Copy URL</Button>
|
||||||
<Button onClick={handleDelete}>Delete</Button>
|
<Button onClick={handleDelete}>Delete</Button>
|
||||||
<Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
|
<Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
@ -143,6 +148,7 @@ export default function File({ image, updateImages }) {
|
||||||
src={image.url}
|
src={image.url}
|
||||||
alt={image.file}
|
alt={image.file}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
|
disableMediaPreview={disableMediaPreview}
|
||||||
/>
|
/>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -101,15 +101,18 @@ const admin_items = [
|
||||||
icon: <UserIcon size={18} />,
|
icon: <UserIcon size={18} />,
|
||||||
text: 'Users',
|
text: 'Users',
|
||||||
link: '/dashboard/users',
|
link: '/dashboard/users',
|
||||||
|
if: props => true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <TagIcon size={18} />,
|
icon: <TagIcon size={18} />,
|
||||||
text: 'Invites',
|
text: 'Invites',
|
||||||
link: '/dashboard/invites',
|
link: '/dashboard/invites',
|
||||||
|
if: props => props.invites,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Layout({ children, props }) {
|
export default function Layout({ children, props }) {
|
||||||
|
console.log(props);
|
||||||
const [user, setUser] = useRecoilState(userSelector);
|
const [user, setUser] = useRecoilState(userSelector);
|
||||||
|
|
||||||
const { title } = props;
|
const { title } = props;
|
||||||
|
@ -230,7 +233,7 @@ export default function Layout({ children, props }) {
|
||||||
childrenOffset={28}
|
childrenOffset={28}
|
||||||
defaultOpened={admin_items.map(x => x.link).includes(router.pathname)}
|
defaultOpened={admin_items.map(x => x.link).includes(router.pathname)}
|
||||||
>
|
>
|
||||||
{admin_items.map(({ icon, text, link }) => (
|
{admin_items.filter(x => x.if(props)).map(({ icon, text, link }) => (
|
||||||
<Link href={link} key={text} passHref>
|
<Link href={link} key={text} passHref>
|
||||||
<NavLink
|
<NavLink
|
||||||
component='a'
|
component='a'
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { useEffect, useState } from 'react';
|
||||||
import { AudioIcon, FileIcon, PlayIcon } from './icons';
|
import { AudioIcon, FileIcon, PlayIcon } from './icons';
|
||||||
|
|
||||||
function Placeholder({ text, Icon, ...props }) {
|
function Placeholder({ text, Icon, ...props }) {
|
||||||
|
if (props.disableResolve) props.src = null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Image height={200} withPlaceholder placeholder={
|
<Image height={200} withPlaceholder placeholder={
|
||||||
<Group>
|
<Group>
|
||||||
|
@ -14,7 +16,7 @@ function Placeholder({ text, Icon, ...props }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Type({ file, popup = false, ...props }){
|
export default function Type({ file, popup = false, disableMediaPreview, ...props }){
|
||||||
const type = (file.type || file.mimetype).split('/')[0];
|
const type = (file.type || file.mimetype).split('/')[0];
|
||||||
const name = (file.name || file.file);
|
const name = (file.name || file.file);
|
||||||
|
|
||||||
|
@ -33,6 +35,10 @@ export default function Type({ file, popup = false, ...props }){
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (media && disableMediaPreview) {
|
||||||
|
return <Placeholder Icon={FileIcon} text={`Click to view file (${name})`} disableResolve={true} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
return popup ? (media ? {
|
return popup ? (media ? {
|
||||||
'video': <video width='100%' autoPlay controls {...props} />,
|
'video': <video width='100%' autoPlay controls {...props} />,
|
||||||
'image': <Image {...props} />,
|
'image': <Image {...props} />,
|
||||||
|
|
|
@ -11,6 +11,7 @@ export function FilePreview({ file }: { file: File }) {
|
||||||
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||||
src={URL.createObjectURL(file)}
|
src={URL.createObjectURL(file)}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
|
disableMediaPreview={false}
|
||||||
popup
|
popup
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import MutedText from 'components/MutedText';
|
||||||
import { invalidateFiles, useRecent } from 'lib/queries/files';
|
import { invalidateFiles, useRecent } from 'lib/queries/files';
|
||||||
import { UploadCloud } from 'react-feather';
|
import { UploadCloud } from 'react-feather';
|
||||||
|
|
||||||
export default function RecentFiles() {
|
export default function RecentFiles({ disableMediaPreview }) {
|
||||||
const recent = useRecent('media');
|
const recent = useRecent('media');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -24,7 +24,7 @@ export default function RecentFiles() {
|
||||||
recent.data.length > 0
|
recent.data.length > 0
|
||||||
? (
|
? (
|
||||||
recent.data.map(image => (
|
recent.data.map(image => (
|
||||||
<File key={randomId()} image={image} updateImages={invalidateFiles} />
|
<File key={randomId()} image={image} updateImages={invalidateFiles} disableMediaPreview={disableMediaPreview} />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<MantineCard shadow='md'>
|
<MantineCard shadow='md'>
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { useRecoilValue } from 'recoil';
|
||||||
import RecentFiles from './RecentFiles';
|
import RecentFiles from './RecentFiles';
|
||||||
import { StatCards } from './StatCards';
|
import { StatCards } from './StatCards';
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard({ disableMediaPreview }) {
|
||||||
const user = useRecoilValue(userSelector);
|
const user = useRecoilValue(userSelector);
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ export default function Dashboard() {
|
||||||
|
|
||||||
<StatCards />
|
<StatCards />
|
||||||
|
|
||||||
<RecentFiles />
|
<RecentFiles disableMediaPreview={disableMediaPreview} />
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<Title>Files</Title>
|
<Title>Files</Title>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import MutedText from 'components/MutedText';
|
||||||
import { usePaginatedFiles } from 'lib/queries/files';
|
import { usePaginatedFiles } from 'lib/queries/files';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function FilePagation() {
|
export default function FilePagation({ disableMediaPreview }) {
|
||||||
const [checked, setChecked] = useState(false);
|
const [checked, setChecked] = useState(false);
|
||||||
|
|
||||||
const pages = usePaginatedFiles(!checked ? { filter: 'media' } : {});
|
const pages = usePaginatedFiles(!checked ? { filter: 'media' } : {});
|
||||||
|
@ -42,7 +42,7 @@ export default function FilePagation() {
|
||||||
? (
|
? (
|
||||||
pages.data[(page - 1) ?? 0].map(image => (
|
pages.data[(page - 1) ?? 0].map(image => (
|
||||||
<div key={image.id}>
|
<div key={image.id}>
|
||||||
<File image={image} updateImages={() => pages.refetch()} />
|
<File image={image} updateImages={() => pages.refetch()} disableMediaPreview={disableMediaPreview} />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import FilePagation from './FilePagation';
|
import FilePagation from './FilePagation';
|
||||||
|
|
||||||
export default function Files() {
|
export default function Files({ disableMediaPreview }) {
|
||||||
const pages = usePaginatedFiles({ filter: 'media' });
|
const pages = usePaginatedFiles({ filter: 'media' });
|
||||||
const favoritePages = usePaginatedFiles({ favorite: 'media' });
|
const favoritePages = usePaginatedFiles({ favorite: 'media' });
|
||||||
const [favoritePage, setFavoritePage] = useState(1);
|
const [favoritePage, setFavoritePage] = useState(1);
|
||||||
|
@ -46,7 +46,7 @@ export default function Files() {
|
||||||
>
|
>
|
||||||
{(favoritePages.isSuccess && favoritePages.data.length) ? favoritePages.data[(favoritePage - 1) ?? 0].map(image => (
|
{(favoritePages.isSuccess && favoritePages.data.length) ? favoritePages.data[(favoritePage - 1) ?? 0].map(image => (
|
||||||
<div key={image.id}>
|
<div key={image.id}>
|
||||||
<File image={image} updateImages={() => updatePages(true)} />
|
<File image={image} updateImages={() => updatePages(true)} disableMediaPreview={disableMediaPreview} />
|
||||||
</div>
|
</div>
|
||||||
)) : null}
|
)) : null}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
@ -67,7 +67,7 @@ export default function Files() {
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
<FilePagation />
|
<FilePagation disableMediaPreview={disableMediaPreview} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -63,6 +63,7 @@ export interface ConfigWebsite {
|
||||||
title: string;
|
title: string;
|
||||||
show_files_per_user: boolean;
|
show_files_per_user: boolean;
|
||||||
show_version: boolean;
|
show_version: boolean;
|
||||||
|
disable_media_preview: boolean;
|
||||||
|
|
||||||
external_links: ConfigWebsiteExternalLinks[];
|
external_links: ConfigWebsiteExternalLinks[];
|
||||||
}
|
}
|
||||||
|
@ -96,6 +97,10 @@ export interface ConfigDiscordEmbed {
|
||||||
image: boolean;
|
image: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigFeatures {
|
||||||
|
invites: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
core: ConfigCore;
|
core: ConfigCore;
|
||||||
uploader: ConfigUploader;
|
uploader: ConfigUploader;
|
||||||
|
@ -104,4 +109,5 @@ export interface Config {
|
||||||
datasource: ConfigDatasource;
|
datasource: ConfigDatasource;
|
||||||
website: ConfigWebsite;
|
website: ConfigWebsite;
|
||||||
discord: ConfigDiscord;
|
discord: ConfigDiscord;
|
||||||
|
features: ConfigFeatures;
|
||||||
}
|
}
|
|
@ -86,6 +86,7 @@ export default function readConfig() {
|
||||||
map('WEBSITE_TITLE', 'string', 'website.title'),
|
map('WEBSITE_TITLE', 'string', 'website.title'),
|
||||||
map('WEBSITE_SHOW_FILES_PER_USER', 'boolean', 'website.show_files_per_user'),
|
map('WEBSITE_SHOW_FILES_PER_USER', 'boolean', 'website.show_files_per_user'),
|
||||||
map('WEBSITE_SHOW_VERSION', 'boolean', 'website.show_version'),
|
map('WEBSITE_SHOW_VERSION', 'boolean', 'website.show_version'),
|
||||||
|
map('WEBSITE_DISABLE_MEDIA_PREVIEW', 'boolean', 'website.disable_media_preview'),
|
||||||
map('WEBSITE_EXTERNAL_LINKS', 'json-array', 'website.external_links'),
|
map('WEBSITE_EXTERNAL_LINKS', 'json-array', 'website.external_links'),
|
||||||
|
|
||||||
map('DISCORD_URL', 'string', 'discord.url'),
|
map('DISCORD_URL', 'string', 'discord.url'),
|
||||||
|
@ -109,6 +110,8 @@ export default function readConfig() {
|
||||||
map('DISCORD_SHORTEN_EMBED_IMAGE', 'boolean', 'discord.shorten.embed.image'),
|
map('DISCORD_SHORTEN_EMBED_IMAGE', 'boolean', 'discord.shorten.embed.image'),
|
||||||
map('DISCORD_SHORTEN_EMBED_THUMBNAIL', 'boolean', 'discord.shorten.embed.thumbnail'),
|
map('DISCORD_SHORTEN_EMBED_THUMBNAIL', 'boolean', 'discord.shorten.embed.thumbnail'),
|
||||||
map('DISCORD_SHORTEN_EMBED_TIMESTAMP', 'boolean', 'discord.shorten.embed.timestamp'),
|
map('DISCORD_SHORTEN_EMBED_TIMESTAMP', 'boolean', 'discord.shorten.embed.timestamp'),
|
||||||
|
|
||||||
|
map('FEATURES_INVITES', 'boolean', 'features.invites'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const config = {};
|
const config = {};
|
||||||
|
|
|
@ -69,6 +69,8 @@ const validator = object({
|
||||||
title: string().default('Zipline'),
|
title: string().default('Zipline'),
|
||||||
show_files_per_user: boolean().default(true),
|
show_files_per_user: boolean().default(true),
|
||||||
show_version: boolean().default(true),
|
show_version: boolean().default(true),
|
||||||
|
disable_media_preview: boolean().default(false),
|
||||||
|
|
||||||
external_links: array(object({
|
external_links: array(object({
|
||||||
label: string(),
|
label: string(),
|
||||||
link: string(),
|
link: string(),
|
||||||
|
@ -84,6 +86,9 @@ const validator = object({
|
||||||
upload: discord_content,
|
upload: discord_content,
|
||||||
shorten: discord_content,
|
shorten: discord_content,
|
||||||
}).optional().nullable().default(null),
|
}).optional().nullable().default(null),
|
||||||
|
features: object({
|
||||||
|
invites: boolean().default(true),
|
||||||
|
}).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function validate(config): Config {
|
export default function validate(config): Config {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import config from 'lib/config';
|
import config from 'lib/config';
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async context => {
|
export const getServerSideProps: GetServerSideProps = async () => {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
title: config.website.title,
|
title: config.website.title,
|
||||||
external_links: JSON.stringify(config.website.external_links),
|
external_links: JSON.stringify(config.website.external_links),
|
||||||
|
disable_media_preview: config.website.disable_media_preview,
|
||||||
|
invites: config.features.invites,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -2,9 +2,12 @@ import prisma from 'lib/prisma';
|
||||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||||
import { createToken, hashPassword } from 'lib/util';
|
import { createToken, hashPassword } from 'lib/util';
|
||||||
import Logger from 'lib/logger';
|
import Logger from 'lib/logger';
|
||||||
|
import config from 'lib/config';
|
||||||
|
|
||||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (req.method === 'POST' && req.body && req.body.code) {
|
if (req.method === 'POST' && req.body && req.body.code) {
|
||||||
|
if (!config.features.invites) return res.forbid('invites are disabled');
|
||||||
|
|
||||||
const { code, username, password } = req.body as { code: string; username: string, password: string };
|
const { code, username, password } = req.body as { code: string; username: string, password: string };
|
||||||
const invite = await prisma.invite.findUnique({
|
const invite = await prisma.invite.findUnique({
|
||||||
where: { code },
|
where: { code },
|
||||||
|
|
|
@ -2,8 +2,11 @@ import prisma from 'lib/prisma';
|
||||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||||
import { randomChars } from 'lib/util';
|
import { randomChars } from 'lib/util';
|
||||||
import Logger from 'lib/logger';
|
import Logger from 'lib/logger';
|
||||||
|
import config from 'lib/config';
|
||||||
|
|
||||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
|
if (!config.features.invites) return res.forbid('invites are disabled');
|
||||||
|
|
||||||
const user = await req.user();
|
const user = await req.user();
|
||||||
if (!user) return res.forbid('not logged in');
|
if (!user) return res.forbid('not logged in');
|
||||||
if (!user.administrator) return res.forbid('you arent an administrator');
|
if (!user.administrator) return res.forbid('you arent an administrator');
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default function FilesPage(props) {
|
||||||
<Layout
|
<Layout
|
||||||
props={props}
|
props={props}
|
||||||
>
|
>
|
||||||
<Files />
|
<Files disableMediaPreview={props.disable_media_preview} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default function DashboardPage(props) {
|
||||||
<Layout
|
<Layout
|
||||||
props={props}
|
props={props}
|
||||||
>
|
>
|
||||||
<Dashboard />
|
<Dashboard disableMediaPreview={props.disable_media_preview} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import useLogin from 'hooks/useLogin';
|
import useLogin from 'hooks/useLogin';
|
||||||
import Layout from 'components/Layout';
|
import Layout from 'components/Layout';
|
||||||
import Invites from 'components/pages/Invites';
|
import Invites from 'components/pages/Invites';
|
||||||
import { LoadingOverlay } from '@mantine/core';
|
import { LoadingOverlay } from '@mantine/core';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||||
|
|
||||||
export default function InvitesPage(props) {
|
export default function InvitesPage(props) {
|
||||||
|
const router = useRouter();
|
||||||
|
if (!props.invites) {
|
||||||
|
useEffect(() => {
|
||||||
|
router.push('/dashboard');
|
||||||
|
}, []);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const { loading } = useLogin();
|
const { loading } = useLogin();
|
||||||
|
|
||||||
if (loading) return <LoadingOverlay visible={loading} />;
|
if (loading) return <LoadingOverlay visible={loading} />;
|
||||||
|
|
|
@ -132,6 +132,10 @@ export default function Invite({ code, title }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async context => {
|
export const getServerSideProps: GetServerSideProps = async context => {
|
||||||
|
if (!config.features.invites) return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
|
||||||
const { code } = context.query as { code: string };
|
const { code } = context.query as { code: string };
|
||||||
|
|
||||||
const invite = await prisma.invite.findUnique({
|
const invite = await prisma.invite.findUnique({
|
||||||
|
|
Loading…
Add table
Reference in a new issue