feat: remove appwrite and add nextjs backend
This commit is contained in:
15
frontend/src/components/Footer.tsx
Normal file
15
frontend/src/components/Footer.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Anchor, Center, Footer as MFooter, Text } from "@mantine/core";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<MFooter height="auto" p={10}>
|
||||
<Center>
|
||||
<Text size="xs" color="dimmed">
|
||||
Made with 🖤 by <Anchor size="xs" href="https://eliasschneider.com" target="_blank">Elias Schneider</Anchor>
|
||||
</Text>
|
||||
</Center>
|
||||
</MFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
27
frontend/src/components/Meta.tsx
Normal file
27
frontend/src/components/Meta.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import Head from "next/head";
|
||||
|
||||
const Meta = ({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Head>
|
||||
{/* TODO: Doesn't work because script get only executed on client side */}
|
||||
<title>{title} - Pingvin Share</title>
|
||||
<meta name="og:title" content={`${title} - Pingvin Share`} />
|
||||
<meta
|
||||
name="og:description"
|
||||
content={
|
||||
description ?? "An open-source and self-hosted sharing platform."
|
||||
}
|
||||
/>
|
||||
<meta name="twitter:title" content={`${title} - Pingvin Share`} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
||||
export default Meta;
|
||||
95
frontend/src/components/auth/AuthForm.tsx
Normal file
95
frontend/src/components/auth/AuthForm.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Container,
|
||||
Paper,
|
||||
PasswordInput,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import getConfig from "next/config";
|
||||
import * as yup from "yup";
|
||||
import authService from "../../services/auth.service";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
||||
const validationSchema = yup.object().shape({
|
||||
email: yup.string().email().required(),
|
||||
password: yup.string().min(8).required(),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
schema: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const signIn = (email: string, password: string) => {
|
||||
authService
|
||||
.signIn(email, password)
|
||||
.then(() => window.location.replace("/upload"))
|
||||
.catch((e) => toast.error(e.response.data.message));
|
||||
};
|
||||
const signUp = (email: string, password: string) => {
|
||||
authService
|
||||
.signUp(email, password)
|
||||
.then(() => signIn(email, password))
|
||||
.catch((e) => toast.error(e.response.data.message));
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size={420} my={40}>
|
||||
<Title
|
||||
align="center"
|
||||
sx={(theme) => ({
|
||||
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
|
||||
fontWeight: 900,
|
||||
})}
|
||||
>
|
||||
{mode == "signUp" ? "Sign up" : "Welcome back"}
|
||||
</Title>
|
||||
{publicRuntimeConfig.ALLOW_REGISTRATION == "true" && (
|
||||
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||
{mode == "signUp"
|
||||
? "You have an account already?"
|
||||
: "You don't have an account yet?"}{" "}
|
||||
<Anchor href={mode == "signUp" ? "signIn" : "signUp"} size="sm">
|
||||
{mode == "signUp" ? "Sign in" : "Sign up"}
|
||||
</Anchor>
|
||||
</Text>
|
||||
)}
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) =>
|
||||
mode == "signIn"
|
||||
? signIn(values.email, values.password)
|
||||
: signUp(values.email, values.password)
|
||||
)}
|
||||
>
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="you@email.com"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button fullWidth mt="xl" type="submit">
|
||||
{mode == "signUp" ? "Let's get started" : "Sign in"}
|
||||
</Button>
|
||||
</form>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthForm;
|
||||
41
frontend/src/components/mantine/ThemeProvider.tsx
Normal file
41
frontend/src/components/mantine/ThemeProvider.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
ColorScheme,
|
||||
ColorSchemeProvider,
|
||||
MantineProvider,
|
||||
} from "@mantine/core";
|
||||
import { ModalsProvider } from "@mantine/modals";
|
||||
import { setCookies } from "cookies-next";
|
||||
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import mantineTheme from "../../styles/mantine.style";
|
||||
|
||||
const ThemeProvider = ({
|
||||
children,
|
||||
colorScheme,
|
||||
setColorScheme,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
colorScheme: ColorScheme;
|
||||
setColorScheme: Dispatch<SetStateAction<ColorScheme>>;
|
||||
}) => {
|
||||
const toggleColorScheme = (value?: ColorScheme) => {
|
||||
const nextColorScheme =
|
||||
value || (colorScheme === "dark" ? "light" : "dark");
|
||||
setColorScheme(nextColorScheme);
|
||||
setCookies("mantine-color-scheme", nextColorScheme, {
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<MantineProvider theme={{ colorScheme, ...mantineTheme }} withGlobalStyles>
|
||||
<ColorSchemeProvider
|
||||
colorScheme={colorScheme}
|
||||
toggleColorScheme={toggleColorScheme}
|
||||
>
|
||||
<ModalsProvider>{children}</ModalsProvider>
|
||||
</ColorSchemeProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeProvider;
|
||||
47
frontend/src/components/navBar/ActionAvatar.tsx
Normal file
47
frontend/src/components/navBar/ActionAvatar.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ActionIcon, Avatar, Menu } from "@mantine/core";
|
||||
import { NextLink } from "@mantine/next";
|
||||
import { DoorExit, Link, Moon } from "tabler-icons-react";
|
||||
import authService from "../../services/auth.service";
|
||||
import ToggleThemeButton from "./ToggleThemeButton";
|
||||
|
||||
const ActionAvatar = () => {
|
||||
return (
|
||||
<Menu
|
||||
control={
|
||||
<ActionIcon>
|
||||
<Avatar size={28} radius="xl" />
|
||||
</ActionIcon>
|
||||
}
|
||||
>
|
||||
<Menu.Label>My account</Menu.Label>
|
||||
<Menu.Item
|
||||
component={NextLink}
|
||||
href="/account/shares"
|
||||
icon={<Link size={14} />}
|
||||
>
|
||||
Shares
|
||||
</Menu.Item>
|
||||
{/* <Menu.Item
|
||||
component={NextLink}
|
||||
href="/account/shares"
|
||||
icon={<Settings size={14} />}
|
||||
>
|
||||
Settings
|
||||
</Menu.Item> */}
|
||||
<Menu.Item
|
||||
onClick={async () => {
|
||||
await authService.signOut();
|
||||
}}
|
||||
icon={<DoorExit size={14} />}
|
||||
>
|
||||
Sign out
|
||||
</Menu.Item>
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Item icon={<Moon size={14} />}>
|
||||
<ToggleThemeButton />
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionAvatar;
|
||||
131
frontend/src/components/navBar/NavBar.tsx
Normal file
131
frontend/src/components/navBar/NavBar.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
Burger,
|
||||
Container,
|
||||
Group,
|
||||
Header as MantineHeader,
|
||||
Paper,
|
||||
Text,
|
||||
Transition,
|
||||
} from "@mantine/core";
|
||||
import { useBooleanToggle } from "@mantine/hooks";
|
||||
import { NextLink } from "@mantine/next";
|
||||
import getConfig from "next/config";
|
||||
import Image from "next/image";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import headerStyle from "../../styles/header.style";
|
||||
import ActionAvatar from "./ActionAvatar";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
type Link = {
|
||||
link?: string;
|
||||
label?: string;
|
||||
component?: ReactNode;
|
||||
action?: () => Promise<void>;
|
||||
};
|
||||
|
||||
const Header = () => {
|
||||
const [opened, toggleOpened] = useBooleanToggle(false);
|
||||
const [active, setActive] = useState<string>();
|
||||
const user = useUser();
|
||||
|
||||
const { classes, cx } = headerStyle();
|
||||
|
||||
const authenticatedLinks: Link[] = [
|
||||
{
|
||||
link: "/upload",
|
||||
label: "Upload",
|
||||
},
|
||||
{
|
||||
component: <ActionAvatar />,
|
||||
},
|
||||
];
|
||||
|
||||
const unauthenticatedLinks: Link[] | undefined = [
|
||||
{
|
||||
link: "/auth/signIn",
|
||||
label: "Sign in",
|
||||
},
|
||||
];
|
||||
|
||||
if (publicRuntimeConfig.SHOW_HOME_PAGE == "true")
|
||||
unauthenticatedLinks.unshift({
|
||||
link: "/",
|
||||
label: "Home",
|
||||
});
|
||||
|
||||
if (publicRuntimeConfig.ALLOW_REGISTRATION == "true")
|
||||
unauthenticatedLinks.push({
|
||||
link: "/auth/signUp",
|
||||
label: "Sign up",
|
||||
});
|
||||
|
||||
const links = user ? authenticatedLinks : unauthenticatedLinks;
|
||||
|
||||
const items = links.map((link, i) => {
|
||||
if (link.component) {
|
||||
return (
|
||||
<Container key={i} pl={5} py={15}>
|
||||
{link.component}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
if (link) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
if (window.location.pathname == link.link) {
|
||||
setActive(link.link);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<NextLink
|
||||
key={link.label}
|
||||
href={link.link ?? ""}
|
||||
onClick={link.action}
|
||||
className={cx(classes.link, {
|
||||
[classes.linkActive]: link.link && active === link.link,
|
||||
})}
|
||||
>
|
||||
{link.label}
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MantineHeader height={60} mb={20} className={classes.root}>
|
||||
<Container className={classes.header}>
|
||||
<NextLink href="/">
|
||||
<Group>
|
||||
<Image
|
||||
src="/img/logo.svg"
|
||||
alt="Pinvgin Share Logo"
|
||||
height={40}
|
||||
width={40}
|
||||
/>
|
||||
<Text weight={600}>Pingvin Share</Text>
|
||||
</Group>
|
||||
</NextLink>
|
||||
<Group spacing={5} className={classes.links}>
|
||||
{items}
|
||||
</Group>
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={() => toggleOpened()}
|
||||
className={classes.burger}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<Transition transition="pop-top-right" duration={200} mounted={opened}>
|
||||
{(styles) => (
|
||||
<Paper className={classes.dropdown} withBorder style={styles}>
|
||||
{items}
|
||||
</Paper>
|
||||
)}
|
||||
</Transition>
|
||||
</Container>
|
||||
</MantineHeader>
|
||||
);
|
||||
};
|
||||
export default Header;
|
||||
19
frontend/src/components/navBar/ToggleThemeButton.tsx
Normal file
19
frontend/src/components/navBar/ToggleThemeButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Switch, useMantineColorScheme } from "@mantine/core";
|
||||
|
||||
const ToggleThemeButton = () => {
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
|
||||
return (
|
||||
<Switch
|
||||
size="sm"
|
||||
checked={colorScheme == "dark"}
|
||||
onClick={(v) =>
|
||||
toggleColorScheme(v.currentTarget.checked ? "dark" : "light")
|
||||
}
|
||||
onLabel="ON"
|
||||
offLabel="OFF"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleThemeButton;
|
||||
146
frontend/src/components/share/CreateUploadModalBody.tsx
Normal file
146
frontend/src/components/share/CreateUploadModalBody.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
Col,
|
||||
Grid,
|
||||
Group,
|
||||
NumberInput,
|
||||
PasswordInput,
|
||||
Select,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import * as yup from "yup";
|
||||
import shareService from "../../services/share.service";
|
||||
import { ShareSecurity } from "../../types/share.type";
|
||||
|
||||
const CreateUploadModalBody = ({
|
||||
uploadCallback,
|
||||
}: {
|
||||
uploadCallback: (
|
||||
id: string,
|
||||
expiration: string,
|
||||
security: ShareSecurity
|
||||
) => void;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const validationSchema = yup.object().shape({
|
||||
link: yup
|
||||
.string()
|
||||
.required()
|
||||
.min(3)
|
||||
.max(100)
|
||||
.matches(new RegExp("^[a-zA-Z0-9_-]*$"), {
|
||||
message: "Can only contain letters, numbers, underscores and hyphens",
|
||||
}),
|
||||
password: yup.string().min(3).max(30),
|
||||
maxViews: yup.number().min(1),
|
||||
});
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
link: "",
|
||||
|
||||
password: undefined,
|
||||
maxViews: undefined,
|
||||
expiration: "1-day",
|
||||
},
|
||||
schema: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
if (!(await shareService.isShareIdAvailable(values.link))) {
|
||||
form.setFieldError("link", "This link is already in use");
|
||||
} else {
|
||||
uploadCallback(values.link, values.expiration, {
|
||||
password: values.password,
|
||||
maxViews: values.maxViews,
|
||||
});
|
||||
modals.closeAll();
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Group direction="column" grow>
|
||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||
<Col xs={9}>
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label="Link"
|
||||
placeholder="myAwesomeShare"
|
||||
{...form.getInputProps("link")}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
form.setFieldValue(
|
||||
"link",
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7)
|
||||
)
|
||||
}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</Col>
|
||||
</Grid>
|
||||
|
||||
<Text
|
||||
size="xs"
|
||||
sx={(theme) => ({
|
||||
color: theme.colors.gray[6],
|
||||
})}
|
||||
>
|
||||
{window.location.origin}/share/
|
||||
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
|
||||
</Text>
|
||||
<Select
|
||||
label="Expiration"
|
||||
{...form.getInputProps("expiration")}
|
||||
data={[
|
||||
{
|
||||
value: "10-minutes",
|
||||
label: "10 Minutes",
|
||||
},
|
||||
{ value: "1-hour", label: "1 Hour" },
|
||||
{ value: "1-day", label: "1 Day" },
|
||||
{ value: "1-week".toString(), label: "1 Week" },
|
||||
{ value: "1-month", label: "1 Month" },
|
||||
]}
|
||||
/>
|
||||
<Accordion>
|
||||
<Accordion.Item
|
||||
label="Security options"
|
||||
sx={{ borderBottom: "none" }}
|
||||
>
|
||||
<Group direction="column" grow>
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
placeholder="No password"
|
||||
label="Password protection"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
min={1}
|
||||
type="number"
|
||||
variant="filled"
|
||||
placeholder="No limit"
|
||||
label="Maximal views"
|
||||
{...form.getInputProps("maxViews")}
|
||||
/>
|
||||
</Group>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
<Button type="submit">Share</Button>
|
||||
</Group>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUploadModalBody;
|
||||
53
frontend/src/components/share/DownloadAllButton.tsx
Normal file
53
frontend/src/components/share/DownloadAllButton.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Button, Tooltip } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import shareService from "../../services/share.service";
|
||||
|
||||
const DownloadAllButton = ({ shareId }: { shareId: string }) => {
|
||||
const [isZipReady, setIsZipReady] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const downloadAll = async () => {
|
||||
setIsLoading(true);
|
||||
await shareService
|
||||
.downloadFile(shareId, "zip")
|
||||
.then(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
shareService
|
||||
.getMetaData(shareId)
|
||||
.then((share) => setIsZipReady(share.isZipReady))
|
||||
.catch(() => {});
|
||||
|
||||
const timer = setInterval(() => {
|
||||
shareService.getMetaData(shareId).then((share) => {
|
||||
setIsZipReady(share.isZipReady);
|
||||
if (share.isZipReady) clearInterval(timer);
|
||||
}).catch(() => {});
|
||||
}, 5000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isZipReady)
|
||||
return (
|
||||
<Tooltip
|
||||
wrapLines
|
||||
position="bottom"
|
||||
width={220}
|
||||
withArrow
|
||||
label="The share is preparing. This can take a few minutes."
|
||||
>
|
||||
<Button variant="outline" onClick={downloadAll} disabled>
|
||||
Download all
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<Button variant="outline" loading={isLoading} onClick={downloadAll}>
|
||||
Download all
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadAllButton;
|
||||
88
frontend/src/components/share/FileList.tsx
Normal file
88
frontend/src/components/share/FileList.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ActionIcon, Loader, Skeleton, Table } from "@mantine/core";
|
||||
import { useRouter } from "next/router";
|
||||
import { CircleCheck, Download } from "tabler-icons-react";
|
||||
import shareService from "../../services/share.service";
|
||||
|
||||
import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
|
||||
|
||||
const FileList = ({
|
||||
files,
|
||||
shareId,
|
||||
isLoading,
|
||||
}: {
|
||||
files: any[];
|
||||
shareId: string;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const skeletonRows = [...Array(5)].map((c, i) => (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<Skeleton height={30} width={30} />
|
||||
</td>
|
||||
<td>
|
||||
<Skeleton height={14} />
|
||||
</td>
|
||||
<td>
|
||||
<Skeleton height={14} />
|
||||
</td>
|
||||
<td>
|
||||
<Skeleton height={25} width={25} />
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
const rows = files.map((file) => (
|
||||
<tr key={file.name}>
|
||||
<td>
|
||||
{/* <Image
|
||||
width={30}
|
||||
height={30}
|
||||
alt={file.name}
|
||||
objectFit="cover"
|
||||
style={{ borderRadius: 3 }}
|
||||
src={`data:image/png;base64,${new Buffer(file.preview).toString(
|
||||
"base64"
|
||||
)}`}
|
||||
></Image> */}
|
||||
</td>
|
||||
<td>{file.name}</td>
|
||||
<td>{byteStringToHumanSizeString(file.size)}</td>
|
||||
<td>
|
||||
{file.uploadingState ? (
|
||||
file.uploadingState != "finished" ? (
|
||||
<Loader size={22} />
|
||||
) : (
|
||||
<CircleCheck color="green" size={22} />
|
||||
)
|
||||
) : (
|
||||
<ActionIcon
|
||||
size={25}
|
||||
onClick={async () => {
|
||||
await shareService.downloadFile(shareId, file.id);
|
||||
}}
|
||||
>
|
||||
<Download />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{isLoading ? skeletonRows : rows}</tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileList;
|
||||
58
frontend/src/components/share/showEnterPasswordModal.tsx
Normal file
58
frontend/src/components/share/showEnterPasswordModal.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Button, Group, PasswordInput, Text, Title } from "@mantine/core";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { useState } from "react";
|
||||
|
||||
const showEnterPasswordModal = (
|
||||
modals: ModalsContextProps,
|
||||
submitCallback: any
|
||||
) => {
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: (
|
||||
<>
|
||||
<Title order={4}>Password required</Title>
|
||||
<Text size="sm">
|
||||
This access this share please enter the password for the share.
|
||||
</Text>
|
||||
</>
|
||||
),
|
||||
children: <Body submitCallback={submitCallback} />,
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({ submitCallback }: { submitCallback: any }) => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordWrong, setPasswordWrong] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Group grow direction="column">
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
placeholder="Password"
|
||||
error={passwordWrong && "Wrong password"}
|
||||
onFocus={() => setPasswordWrong(false)}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
value={password}
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
submitCallback(password)
|
||||
.then((res: any) => res)
|
||||
.catch((e: any) => {
|
||||
const error = e.response.data.message;
|
||||
if (error == "Wrong password") {
|
||||
setPasswordWrong(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default showEnterPasswordModal;
|
||||
41
frontend/src/components/share/showErrorModal.tsx
Normal file
41
frontend/src/components/share/showErrorModal.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Button, Group, Text, Title } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const showErrorModal = (
|
||||
modals: ModalsContextProps,
|
||||
title: string,
|
||||
text: string
|
||||
) => {
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: <Title order={4}>{title}</Title>,
|
||||
|
||||
children: <Body text={text} />,
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({ text }: { text: string }) => {
|
||||
const modals = useModals();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
<Group grow direction="column">
|
||||
<Text size="sm">{text}</Text>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.closeAll();
|
||||
router.back();
|
||||
}}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default showErrorModal;
|
||||
115
frontend/src/components/upload/Dropzone.tsx
Normal file
115
frontend/src/components/upload/Dropzone.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
createStyles,
|
||||
Group,
|
||||
MantineTheme,
|
||||
Text,
|
||||
useMantineTheme,
|
||||
} from "@mantine/core";
|
||||
import { Dropzone as MantineDropzone, DropzoneStatus } from "@mantine/dropzone";
|
||||
import getConfig from "next/config";
|
||||
import React, { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
|
||||
import { CloudUpload, Upload } from "tabler-icons-react";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
wrapper: {
|
||||
position: "relative",
|
||||
marginBottom: 30,
|
||||
},
|
||||
|
||||
dropzone: {
|
||||
borderWidth: 1,
|
||||
paddingBottom: 50,
|
||||
},
|
||||
|
||||
icon: {
|
||||
color:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.dark[3]
|
||||
: theme.colors.gray[4],
|
||||
},
|
||||
|
||||
control: {
|
||||
position: "absolute",
|
||||
bottom: -20,
|
||||
},
|
||||
}));
|
||||
|
||||
function getActiveColor(status: DropzoneStatus, theme: MantineTheme) {
|
||||
return status.accepted
|
||||
? theme.colors[theme.primaryColor][6]
|
||||
: theme.colorScheme === "dark"
|
||||
? theme.colors.dark[2]
|
||||
: theme.black;
|
||||
}
|
||||
|
||||
const Dropzone = ({
|
||||
isUploading,
|
||||
setFiles,
|
||||
}: {
|
||||
isUploading: boolean;
|
||||
setFiles: Dispatch<SetStateAction<File[]>>;
|
||||
}) => {
|
||||
const theme = useMantineTheme();
|
||||
const { classes } = useStyles();
|
||||
const openRef = useRef<() => void>();
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<MantineDropzone
|
||||
maxSize={parseInt(publicRuntimeConfig.MAX_FILE_SIZE!)}
|
||||
onReject={(e) => {
|
||||
toast.error(e[0].errors[0].message);
|
||||
}}
|
||||
disabled={isUploading}
|
||||
openRef={openRef as ForwardedRef<() => void>}
|
||||
onDrop={(files) => {
|
||||
if (files.length > 100) {
|
||||
toast.error("You can't upload more than 100 files per share.");
|
||||
} else {
|
||||
setFiles(files);
|
||||
}
|
||||
}}
|
||||
className={classes.dropzone}
|
||||
radius="md"
|
||||
>
|
||||
{(status) => (
|
||||
<div style={{ pointerEvents: "none" }}>
|
||||
<Group position="center">
|
||||
<CloudUpload size={50} color={getActiveColor(status, theme)} />
|
||||
</Group>
|
||||
<Text
|
||||
align="center"
|
||||
weight={700}
|
||||
size="lg"
|
||||
mt="xl"
|
||||
sx={{ color: getActiveColor(status, theme) }}
|
||||
>
|
||||
{status.accepted ? "Drop files here" : "Upload files"}
|
||||
</Text>
|
||||
<Text align="center" size="sm" mt="xs" color="dimmed">
|
||||
Drag and drop your files or use the upload button to start your
|
||||
share.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</MantineDropzone>
|
||||
<Center>
|
||||
<Button
|
||||
className={classes.control}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="xl"
|
||||
disabled={isUploading}
|
||||
onClick={() => openRef.current && openRef.current()}
|
||||
>
|
||||
{<Upload />}
|
||||
</Button>
|
||||
</Center>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Dropzone;
|
||||
60
frontend/src/components/upload/FileList.tsx
Normal file
60
frontend/src/components/upload/FileList.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { ActionIcon, Loader, Table } from "@mantine/core";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { CircleCheck, Trash } from "tabler-icons-react";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
|
||||
|
||||
const FileList = ({
|
||||
files,
|
||||
setFiles,
|
||||
}: {
|
||||
files: FileUpload[];
|
||||
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
||||
}) => {
|
||||
const remove = (index: number) => {
|
||||
files.splice(index, 1);
|
||||
setFiles([...files]);
|
||||
};
|
||||
|
||||
const rows = files.map((file, i) => (
|
||||
<tr key={i}>
|
||||
<td>{file.name}</td>
|
||||
<td>{file.type}</td>
|
||||
<td>{byteStringToHumanSizeString(file.size.toString())}</td>
|
||||
<td>
|
||||
{file.uploadingState ? (
|
||||
file.uploadingState != "finished" ? (
|
||||
<Loader size={22} />
|
||||
) : (
|
||||
<CircleCheck color="green" size={22} />
|
||||
)
|
||||
) : (
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => remove(i)}
|
||||
>
|
||||
<Trash />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Size</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileList;
|
||||
77
frontend/src/components/upload/showCompletedUploadModal.tsx
Normal file
77
frontend/src/components/upload/showCompletedUploadModal.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Group,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import moment from "moment";
|
||||
import { useRouter } from "next/router";
|
||||
import { Copy } from "tabler-icons-react";
|
||||
import { Share } from "../../types/share.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const showCompletedUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
share: Share,
|
||||
) => {
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: (
|
||||
<Group grow direction="column" spacing={0}>
|
||||
<Title order={4}>Share ready</Title>
|
||||
</Group>
|
||||
),
|
||||
children: <Body share={share} />,
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({ share }: { share: Share }) => {
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const modals = useModals();
|
||||
const router = useRouter();
|
||||
const link = `${window.location.origin}/share/${share.id}`;
|
||||
return (
|
||||
<Group grow direction="column">
|
||||
<TextInput
|
||||
variant="filled"
|
||||
value={link}
|
||||
rightSection={
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
clipboard.copy(link);
|
||||
toast.success("Your link was copied to the keyboard.");
|
||||
}}
|
||||
>
|
||||
<Copy />
|
||||
</ActionIcon>
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
size="xs"
|
||||
sx={(theme) => ({
|
||||
color: theme.colors.gray[6],
|
||||
})}
|
||||
>
|
||||
Your share expires at {moment(share.expiration).format("LLL")}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.closeAll();
|
||||
router.push("/upload");
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default showCompletedUploadModal;
|
||||
22
frontend/src/components/upload/showCreateUploadModal.tsx
Normal file
22
frontend/src/components/upload/showCreateUploadModal.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Title } from "@mantine/core";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { ShareSecurity } from "../../types/share.type";
|
||||
import CreateUploadModalBody from "../share/CreateUploadModalBody";
|
||||
|
||||
const showCreateUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
uploadCallback: (
|
||||
id: string,
|
||||
expiration: string,
|
||||
security: ShareSecurity,
|
||||
) => void
|
||||
) => {
|
||||
return modals.openModal({
|
||||
title: <Title order={4}>Share</Title>,
|
||||
children: (
|
||||
<CreateUploadModalBody uploadCallback={uploadCallback} />
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export default showCreateUploadModal;
|
||||
10
frontend/src/hooks/user.hook.ts
Normal file
10
frontend/src/hooks/user.hook.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import { CurrentUser } from "../types/user.type";
|
||||
|
||||
export const UserContext = createContext<CurrentUser | null>(null);
|
||||
|
||||
const useUser = () => {
|
||||
return useContext(UserContext);
|
||||
};
|
||||
|
||||
export default useUser;
|
||||
64
frontend/src/pages/404.tsx
Normal file
64
frontend/src/pages/404.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
import {
|
||||
createStyles,
|
||||
Title,
|
||||
Text,
|
||||
Button,
|
||||
Container,
|
||||
Group,
|
||||
} from "@mantine/core";
|
||||
import Meta from "../components/Meta";
|
||||
import { NextLink } from "@mantine/next";
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
paddingTop: 80,
|
||||
paddingBottom: 80,
|
||||
},
|
||||
|
||||
label: {
|
||||
textAlign: "center",
|
||||
fontWeight: 900,
|
||||
fontSize: 220,
|
||||
lineHeight: 1,
|
||||
marginBottom: theme.spacing.xl * 1.5,
|
||||
color: theme.colors.gray[2],
|
||||
|
||||
[theme.fn.smallerThan("sm")]: {
|
||||
fontSize: 120,
|
||||
},
|
||||
},
|
||||
|
||||
description: {
|
||||
maxWidth: 500,
|
||||
margin: "auto",
|
||||
marginBottom: theme.spacing.xl * 1.5,
|
||||
},
|
||||
}));
|
||||
|
||||
const ErrorNotFound = () => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Not found" />
|
||||
<Container className={classes.root}>
|
||||
<div className={classes.label}>404</div>
|
||||
<Title align="center" order={3}>
|
||||
Oops this page doesn't exist.
|
||||
</Title>
|
||||
<Text
|
||||
color="dimmed"
|
||||
align="center"
|
||||
className={classes.description}
|
||||
></Text>
|
||||
<Group position="center">
|
||||
<Button component={NextLink} href="/" variant="light">
|
||||
Bring me back
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ErrorNotFound;
|
||||
86
frontend/src/pages/_app.tsx
Normal file
86
frontend/src/pages/_app.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
ColorScheme,
|
||||
Container,
|
||||
LoadingOverlay,
|
||||
MantineProvider,
|
||||
Stack,
|
||||
} from "@mantine/core";
|
||||
import { ModalsProvider } from "@mantine/modals";
|
||||
import { NotificationsProvider } from "@mantine/notifications";
|
||||
import { getCookie } from "cookies-next";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import type { AppProps } from "next/app";
|
||||
import getConfig from "next/config";
|
||||
import { useEffect, useState } from "react";
|
||||
import Footer from "../components/Footer";
|
||||
import ThemeProvider from "../components/mantine/ThemeProvider";
|
||||
import Header from "../components/navBar/NavBar";
|
||||
import { UserContext } from "../hooks/user.hook";
|
||||
import authService from "../services/auth.service";
|
||||
import userService from "../services/user.service";
|
||||
import GlobalStyle from "../styles/global.style";
|
||||
import globalStyle from "../styles/mantine.style";
|
||||
import { CurrentUser } from "../types/user.type";
|
||||
import { GlobalLoadingContext } from "../utils/loading.util";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
|
||||
function App(
|
||||
props: AppProps & { colorScheme: ColorScheme; environmentVariables: any }
|
||||
) {
|
||||
const { Component, pageProps } = props;
|
||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(
|
||||
props.colorScheme
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [user, setUser] = useState<CurrentUser | null>(null);
|
||||
|
||||
const getInitalData = async () => {
|
||||
console.log(publicRuntimeConfig)
|
||||
setIsLoading(true);
|
||||
setUser(await userService.getCurrentUser());
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
||||
getInitalData();
|
||||
}, []);
|
||||
return (
|
||||
<MantineProvider withGlobalStyles withNormalizeCSS theme={globalStyle}>
|
||||
<ThemeProvider colorScheme={colorScheme} setColorScheme={setColorScheme}>
|
||||
<GlobalStyle />
|
||||
<NotificationsProvider>
|
||||
<ModalsProvider>
|
||||
<GlobalLoadingContext.Provider value={{ isLoading, setIsLoading }}>
|
||||
{isLoading ? (
|
||||
<LoadingOverlay visible overlayOpacity={1} />
|
||||
) : (
|
||||
<UserContext.Provider value={user}>
|
||||
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
|
||||
<Stack justify="space-between" sx={{ minHeight: "100vh" }}>
|
||||
<div>
|
||||
<Header />
|
||||
<Container>
|
||||
<Component {...pageProps} />
|
||||
</Container>
|
||||
</div>
|
||||
<Footer />
|
||||
</Stack>
|
||||
</UserContext.Provider>
|
||||
)}
|
||||
</GlobalLoadingContext.Provider>
|
||||
</ModalsProvider>
|
||||
</NotificationsProvider>
|
||||
</ThemeProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
return {
|
||||
colorScheme: getCookie("mantine-color-scheme", ctx) || "light",
|
||||
};
|
||||
};
|
||||
29
frontend/src/pages/_document.tsx
Normal file
29
frontend/src/pages/_document.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createGetInitialProps } from "@mantine/next";
|
||||
import Document, { Head, Html, Main, NextScript } from "next/document";
|
||||
|
||||
const getInitialProps = createGetInitialProps();
|
||||
|
||||
export default class _Document extends Document {
|
||||
static getInitialProps = getInitialProps;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/icons/icon-white-128x128.png" />
|
||||
|
||||
<meta property="og:image" content="/img/opengraph-default.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content="/img/opengraph-default.png" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="theme-color" content="#46509e" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
122
frontend/src/pages/account/shares.tsx
Normal file
122
frontend/src/pages/account/shares.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Space,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { NextLink } from "@mantine/next";
|
||||
import moment from "moment";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, Trash } from "tabler-icons-react";
|
||||
import Meta from "../../components/Meta";
|
||||
import shareService from "../../services/share.service";
|
||||
import { MyShare } from "../../types/share.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const MyShares = () => {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const [shares, setShares] = useState<MyShare[]>();
|
||||
|
||||
useEffect(() => {
|
||||
shareService.getMyShares().then((shares) => setShares(shares));
|
||||
}, []);
|
||||
|
||||
if (!shares) return <LoadingOverlay visible />;
|
||||
return (
|
||||
<>
|
||||
<Meta title="My shares" />
|
||||
<Title mb={30} order={3}>
|
||||
My shares
|
||||
</Title>
|
||||
{shares.length == 0 ? (
|
||||
<Center style={{ height: "70vh" }}>
|
||||
<Group direction="column" align="center" spacing={10}>
|
||||
<Title order={3}>It's empty here 👀</Title>
|
||||
<Text>You don't have any shares.</Text>
|
||||
<Space h={5} />
|
||||
<Button component={NextLink} href="/upload" variant="light">
|
||||
Create one
|
||||
</Button>
|
||||
</Group>
|
||||
</Center>
|
||||
) : (
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Visitors</th>
|
||||
<th>Expires at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{shares.map((share) => (
|
||||
<tr key={share.id}>
|
||||
<td>{share.id}</td>
|
||||
<td>{share.views}</td>
|
||||
<td>
|
||||
{moment(share.expiration).format("MMMM DD YYYY, HH:mm")}
|
||||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<ActionIcon
|
||||
color="victoria"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
clipboard.copy(
|
||||
`${window.location.origin}/share/${share.id}`
|
||||
);
|
||||
toast.success("Your link was copied to the keyboard.");
|
||||
}}
|
||||
>
|
||||
<Link />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete share ${share.id}`,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete this share?
|
||||
</Text>
|
||||
),
|
||||
confirmProps: {
|
||||
color: "red",
|
||||
},
|
||||
labels: { confirm: "Confirm", cancel: "Cancel" },
|
||||
onConfirm: () => {
|
||||
shareService.remove(share.id);
|
||||
setShares(
|
||||
shares.filter((item) => item.id !== share.id)
|
||||
);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyShares;
|
||||
19
frontend/src/pages/api/[...all].tsx
Normal file
19
frontend/src/pages/api/[...all].tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import httpProxyMiddleware from "next-http-proxy-middleware";
|
||||
import getConfig from "next/config";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
externalResolver: true,
|
||||
},
|
||||
};
|
||||
|
||||
// This function can be marked `async` if using `await` inside
|
||||
export default (req: NextApiRequest, res: NextApiResponse) =>
|
||||
httpProxyMiddleware(req, res, {
|
||||
// You can use the `http-proxy` option
|
||||
target: publicRuntimeConfig.BACKEND_URL,
|
||||
});
|
||||
20
frontend/src/pages/auth/signIn.tsx
Normal file
20
frontend/src/pages/auth/signIn.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useRouter } from "next/router";
|
||||
import AuthForm from "../../components/auth/AuthForm";
|
||||
import Meta from "../../components/Meta";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
|
||||
const SignIn = () => {
|
||||
const user = useUser();
|
||||
const router = useRouter();
|
||||
if (user) {
|
||||
router.replace("/");
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Sign In" />
|
||||
<AuthForm mode="signIn" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default SignIn;
|
||||
22
frontend/src/pages/auth/signUp.tsx
Normal file
22
frontend/src/pages/auth/signUp.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useRouter } from "next/router";
|
||||
import AuthForm from "../../components/auth/AuthForm";
|
||||
import Meta from "../../components/Meta";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
|
||||
const SignUp = () => {
|
||||
const user = useUser();
|
||||
const router = useRouter();
|
||||
if (user) {
|
||||
router.replace("/");
|
||||
} else if (process.env.NEXT_PUBLIC_DISABLE_REGISTRATION) {
|
||||
router.replace("/auth/signIn");
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Sign Up" />
|
||||
<AuthForm mode="signUp" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default SignUp;
|
||||
157
frontend/src/pages/index.tsx
Normal file
157
frontend/src/pages/index.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
createStyles,
|
||||
Group,
|
||||
List,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { NextLink } from "@mantine/next";
|
||||
import getConfig from "next/config";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { Check } from "tabler-icons-react";
|
||||
import Meta from "../components/Meta";
|
||||
import useUser from "../hooks/user.hook";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
inner: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
paddingTop: theme.spacing.xl * 4,
|
||||
paddingBottom: theme.spacing.xl * 4,
|
||||
},
|
||||
|
||||
content: {
|
||||
maxWidth: 480,
|
||||
marginRight: theme.spacing.xl * 3,
|
||||
|
||||
[theme.fn.smallerThan("md")]: {
|
||||
maxWidth: "100%",
|
||||
marginRight: 0,
|
||||
},
|
||||
},
|
||||
|
||||
title: {
|
||||
color: theme.colorScheme === "dark" ? theme.white : theme.black,
|
||||
fontSize: 44,
|
||||
lineHeight: 1.2,
|
||||
fontWeight: 900,
|
||||
|
||||
[theme.fn.smallerThan("xs")]: {
|
||||
fontSize: 28,
|
||||
},
|
||||
},
|
||||
|
||||
control: {
|
||||
[theme.fn.smallerThan("xs")]: {
|
||||
flex: 1,
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
[theme.fn.smallerThan("md")]: {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
|
||||
highlight: {
|
||||
position: "relative",
|
||||
backgroundColor:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.fn.rgba(theme.colors[theme.primaryColor][6], 0.55)
|
||||
: theme.colors[theme.primaryColor][0],
|
||||
borderRadius: theme.radius.sm,
|
||||
padding: "4px 12px",
|
||||
},
|
||||
}));
|
||||
|
||||
export default function Home() {
|
||||
const user = useUser();
|
||||
|
||||
const { classes } = useStyles();
|
||||
const router = useRouter();
|
||||
if (user) {
|
||||
router.replace("/upload");
|
||||
} else if (publicRuntimeConfig.SHOW_HOME_PAGE == "false") {
|
||||
router.replace("/auth/signIn");
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Home" />
|
||||
<Container>
|
||||
<div className={classes.inner}>
|
||||
<div className={classes.content}>
|
||||
<Title className={classes.title}>
|
||||
A <span className={classes.highlight}>self-hosted</span> <br />{" "}
|
||||
file sharing platform.
|
||||
</Title>
|
||||
<Text color="dimmed" mt="md">
|
||||
Do you really want to give your personal files in the hand of
|
||||
third parties like WeTransfer?
|
||||
</Text>
|
||||
|
||||
<List
|
||||
mt={30}
|
||||
spacing="sm"
|
||||
size="sm"
|
||||
icon={
|
||||
<ThemeIcon size={20} radius="xl">
|
||||
<Check size={12} />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
<List.Item>
|
||||
<b>Self-Hosted</b> - Host Pingvin Share on your own machine.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<b>Privacy</b> - Your files are your files and should never
|
||||
get into the hands of third parties.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<b>No annoying file size limit</b> - Upload as big files as
|
||||
you want. Only your hard drive will be your limit.
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
<Group mt={30}>
|
||||
<Button
|
||||
component={NextLink}
|
||||
href="/auth/signUp"
|
||||
radius="xl"
|
||||
size="md"
|
||||
className={classes.control}
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
<Button
|
||||
component={NextLink}
|
||||
href="https://github.com/stonith404/pingvin-share"
|
||||
target="_blank"
|
||||
variant="default"
|
||||
radius="xl"
|
||||
size="md"
|
||||
className={classes.control}
|
||||
>
|
||||
Source code
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
<Group className={classes.image} align="center">
|
||||
<Image
|
||||
src="/img/logo.svg"
|
||||
alt="Pingvin Share Logo"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
85
frontend/src/pages/share/[shareId].tsx
Normal file
85
frontend/src/pages/share/[shareId].tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Group } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Meta from "../../components/Meta";
|
||||
import DownloadAllButton from "../../components/share/DownloadAllButton";
|
||||
import FileList from "../../components/share/FileList";
|
||||
import showEnterPasswordModal from "../../components/share/showEnterPasswordModal";
|
||||
import showErrorModal from "../../components/share/showErrorModal";
|
||||
import shareService from "../../services/share.service";
|
||||
|
||||
const Share = () => {
|
||||
const router = useRouter();
|
||||
const modals = useModals();
|
||||
const shareId = router.query.shareId as string;
|
||||
const [fileList, setFileList] = useState<any[]>([]);
|
||||
|
||||
const submitPassword = async (password: string) => {
|
||||
await shareService
|
||||
.exchangeSharePasswordWithToken(shareId, password)
|
||||
.then(() => {
|
||||
modals.closeAll();
|
||||
getFiles();
|
||||
});
|
||||
};
|
||||
|
||||
const getFiles = async () => {
|
||||
shareService
|
||||
.get(shareId)
|
||||
.then((share) => {
|
||||
setFileList(share.files);
|
||||
})
|
||||
.catch((e) => {
|
||||
const { error } = e.response.data;
|
||||
if (e.response.status == 404) {
|
||||
showErrorModal(
|
||||
modals,
|
||||
"Not found",
|
||||
"This share can't be found. Please check your link."
|
||||
);
|
||||
} else if (error == "share_token_required") {
|
||||
showEnterPasswordModal(modals, submitPassword);
|
||||
} else if (error == "share_max_views_exceeded") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
"Visitor limit exceeded",
|
||||
"The visitor limit from this share has been exceeded."
|
||||
);
|
||||
} else if (error == "forbidden") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
"Forbidden",
|
||||
"You're not allowed to see this share. Are you logged in with the correct account?"
|
||||
);
|
||||
} else {
|
||||
showErrorModal(modals, "Error", "An unknown error occurred.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getFiles();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={`Share ${shareId}`}
|
||||
description="Look what I've shared with you."
|
||||
/>
|
||||
<Group position="right" mb="lg">
|
||||
<DownloadAllButton
|
||||
shareId={shareId}
|
||||
/>
|
||||
</Group>
|
||||
<FileList
|
||||
files={fileList}
|
||||
shareId={shareId}
|
||||
isLoading={fileList.length == 0}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Share;
|
||||
81
frontend/src/pages/upload.tsx
Normal file
81
frontend/src/pages/upload.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Button, Group } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import Meta from "../components/Meta";
|
||||
import Dropzone from "../components/upload/Dropzone";
|
||||
import FileList from "../components/upload/FileList";
|
||||
import showCompletedUploadModal from "../components/upload/showCompletedUploadModal";
|
||||
import showCreateUploadModal from "../components/upload/showCreateUploadModal";
|
||||
import useUser from "../hooks/user.hook";
|
||||
import shareService from "../services/share.service";
|
||||
import { FileUpload } from "../types/File.type";
|
||||
import { ShareSecurity } from "../types/share.type";
|
||||
|
||||
const Upload = () => {
|
||||
const router = useRouter();
|
||||
const modals = useModals();
|
||||
|
||||
const user = useUser();
|
||||
const [files, setFiles] = useState<FileUpload[]>([]);
|
||||
const [isUploading, setisUploading] = useState(false);
|
||||
|
||||
const uploadFiles = async (
|
||||
id: string,
|
||||
expiration: string,
|
||||
security: ShareSecurity
|
||||
) => {
|
||||
setisUploading(true);
|
||||
|
||||
try {
|
||||
files.forEach((file) => {
|
||||
file.uploadingState = "inProgress";
|
||||
});
|
||||
setFiles([...files]);
|
||||
const share = await shareService.create(id, expiration, security);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
await shareService.uploadFile(share.id, files[i]);
|
||||
|
||||
files[i].uploadingState = "finished";
|
||||
setFiles([...files]);
|
||||
if (!files.some((f) => f.uploadingState == "inProgress")) {
|
||||
await shareService.completeShare(share.id);
|
||||
setisUploading(false);
|
||||
showCompletedUploadModal(
|
||||
modals,
|
||||
|
||||
share
|
||||
);
|
||||
setFiles([]);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
files.forEach((file) => {
|
||||
file.uploadingState = undefined;
|
||||
});
|
||||
setFiles([...files]);
|
||||
setisUploading(false);
|
||||
}
|
||||
};
|
||||
if (!user) {
|
||||
router.replace("/");
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Upload" />
|
||||
<Group position="right" mb={20}>
|
||||
<Button
|
||||
loading={isUploading}
|
||||
disabled={files.length <= 0}
|
||||
onClick={() => showCreateUploadModal(modals, uploadFiles)}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
</Group>
|
||||
<Dropzone setFiles={setFiles} isUploading={isUploading} />
|
||||
{files.length > 0 && <FileList files={files} setFiles={setFiles} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default Upload;
|
||||
6
frontend/src/pages/user/account.tsx
Normal file
6
frontend/src/pages/user/account.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
// TODO: Add user account
|
||||
const Account = () => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default Account;
|
||||
36
frontend/src/services/api.service.ts
Normal file
36
frontend/src/services/api.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { getCookie } from "cookies-next";
|
||||
import toast from "../utils/toast.util";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: "/api",
|
||||
});
|
||||
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const accessToken = getCookie("access_token");
|
||||
if (accessToken) {
|
||||
config!.headers!.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
const status = error.response?.status;
|
||||
if (status == 400) {
|
||||
toast.error(error.response?.data?.message ?? "An unkown error occured");
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
41
frontend/src/services/auth.service.ts
Normal file
41
frontend/src/services/auth.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getCookie, setCookies } from "cookies-next";
|
||||
import * as jose from "jose";
|
||||
import api from "./api.service";
|
||||
|
||||
const signIn = async (email: string, password: string) => {
|
||||
const response = await api.post("auth/signIn", { email, password });
|
||||
setCookies("access_token", response.data.accessToken);
|
||||
setCookies("refresh_token", response.data.refreshToken);
|
||||
return response;
|
||||
};
|
||||
|
||||
const signUp = async (email: string, password: string) => {
|
||||
return await api.post("auth/signUp", { email, password });
|
||||
};
|
||||
|
||||
const signOut = () => {
|
||||
setCookies("access_token", null);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const refreshAccessToken = async () => {
|
||||
const currentAccessToken = getCookie("access_token") as string;
|
||||
|
||||
if (
|
||||
currentAccessToken &&
|
||||
(jose.decodeJwt(currentAccessToken).exp ?? 0) * 1000 <
|
||||
Date.now() + 2 * 60 * 1000
|
||||
) {
|
||||
const refreshToken = getCookie("refresh_token");
|
||||
|
||||
const response = await api.post("auth/token", { refreshToken });
|
||||
setCookies("access_token", response.data.accessToken);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
refreshAccessToken,
|
||||
};
|
||||
88
frontend/src/services/share.service.ts
Normal file
88
frontend/src/services/share.service.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
MyShare,
|
||||
Share,
|
||||
ShareMetaData,
|
||||
ShareSecurity,
|
||||
} from "../types/share.type";
|
||||
import api from "./api.service";
|
||||
|
||||
const create = async (
|
||||
id: string,
|
||||
expiration: string,
|
||||
security?: ShareSecurity
|
||||
) => {
|
||||
return (await api.post("shares", { id, expiration, security })).data;
|
||||
};
|
||||
|
||||
const completeShare = async (id: string) => {
|
||||
return (await api.post(`shares/${id}/complete`)).data;
|
||||
};
|
||||
|
||||
const get = async (id: string): Promise<Share> => {
|
||||
const shareToken = localStorage.getItem(`share_${id}_token`);
|
||||
return (
|
||||
await api.get(`shares/${id}`, {
|
||||
headers: { "X-Share-Token": shareToken ?? "" },
|
||||
})
|
||||
).data;
|
||||
};
|
||||
|
||||
const getMetaData = async (id: string): Promise<ShareMetaData> => {
|
||||
const shareToken = localStorage.getItem(`share_${id}_token`);
|
||||
return (
|
||||
await api.get(`shares/${id}/metaData`, {
|
||||
headers: { "X-Share-Token": shareToken ?? "" },
|
||||
})
|
||||
).data;
|
||||
};
|
||||
|
||||
const remove = async (id: string) => {
|
||||
await api.delete(`shares/${id}`);
|
||||
};
|
||||
|
||||
const getMyShares = async (): Promise<MyShare[]> => {
|
||||
return (await api.get("shares")).data;
|
||||
};
|
||||
|
||||
const exchangeSharePasswordWithToken = async (id: string, password: string) => {
|
||||
const { token } = (await api.post(`/shares/${id}/password`, { password }))
|
||||
.data;
|
||||
|
||||
localStorage.setItem(`share_${id}_token`, token);
|
||||
};
|
||||
|
||||
const isShareIdAvailable = async (id: string): Promise<boolean> => {
|
||||
return (await api.get(`shares/isShareIdAvailable/${id}`)).data.isAvailable;
|
||||
};
|
||||
|
||||
const getFileDownloadUrl = async (shareId: string, fileId: string) => {
|
||||
const shareToken = localStorage.getItem(`share_${shareId}_token`);
|
||||
return (
|
||||
await api.get(`shares/${shareId}/files/${fileId}/download`, {
|
||||
headers: { "X-Share-Token": shareToken ?? "" },
|
||||
})
|
||||
).data.url;
|
||||
};
|
||||
|
||||
const downloadFile = async (shareId: string, fileId: string) => {
|
||||
window.location.href = await getFileDownloadUrl(shareId, fileId);
|
||||
};
|
||||
|
||||
const uploadFile = async (shareId: string, file: File) => {
|
||||
var formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return (await api.post(`shares/${shareId}/files`, formData)).data;
|
||||
};
|
||||
|
||||
export default {
|
||||
create,
|
||||
completeShare,
|
||||
exchangeSharePasswordWithToken,
|
||||
get,
|
||||
remove,
|
||||
getMetaData,
|
||||
getMyShares,
|
||||
isShareIdAvailable,
|
||||
downloadFile,
|
||||
uploadFile,
|
||||
};
|
||||
16
frontend/src/services/user.service.ts
Normal file
16
frontend/src/services/user.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CurrentUser } from "../types/user.type";
|
||||
import api from "./api.service";
|
||||
import authService from "./auth.service";
|
||||
|
||||
const getCurrentUser = async (): Promise<CurrentUser | null> => {
|
||||
try {
|
||||
await authService.refreshAccessToken();
|
||||
return (await api.get("users/me")).data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
getCurrentUser,
|
||||
};
|
||||
15
frontend/src/styles/global.style.tsx
Normal file
15
frontend/src/styles/global.style.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Global } from "@mantine/core";
|
||||
|
||||
const GlobalStyle = () => {
|
||||
return (
|
||||
<Global
|
||||
styles={(theme) => ({
|
||||
a: {
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default GlobalStyle;
|
||||
80
frontend/src/styles/header.style.ts
Normal file
80
frontend/src/styles/header.style.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { createStyles } from "@mantine/core";
|
||||
|
||||
export default createStyles((theme) => ({
|
||||
root: {
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
dropdown: {
|
||||
position: "absolute",
|
||||
top: 60,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopWidth: 0,
|
||||
overflow: "hidden",
|
||||
|
||||
[theme.fn.largerThan("sm")]: {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
|
||||
header: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
},
|
||||
|
||||
links: {
|
||||
[theme.fn.smallerThan("sm")]: {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
|
||||
burger: {
|
||||
[theme.fn.largerThan("sm")]: {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
|
||||
link: {
|
||||
display: "block",
|
||||
lineHeight: 1,
|
||||
padding: "8px 12px",
|
||||
borderRadius: theme.radius.sm,
|
||||
textDecoration: "none",
|
||||
color:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.dark[0]
|
||||
: theme.colors.gray[7],
|
||||
fontSize: theme.fontSizes.sm,
|
||||
fontWeight: 500,
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.dark[6]
|
||||
: theme.colors.gray[0],
|
||||
},
|
||||
|
||||
[theme.fn.smallerThan("sm")]: {
|
||||
borderRadius: 0,
|
||||
padding: theme.spacing.md,
|
||||
},
|
||||
},
|
||||
|
||||
linkActive: {
|
||||
"&, &:hover": {
|
||||
backgroundColor:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.25)
|
||||
: theme.colors[theme.primaryColor][0],
|
||||
color:
|
||||
theme.colors[theme.primaryColor][theme.colorScheme === "dark" ? 3 : 7],
|
||||
},
|
||||
},
|
||||
}));
|
||||
19
frontend/src/styles/mantine.style.ts
Normal file
19
frontend/src/styles/mantine.style.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { MantineThemeOverride } from "@mantine/core";
|
||||
|
||||
export default <MantineThemeOverride>{
|
||||
colors: {
|
||||
victoria: [
|
||||
"#E2E1F1",
|
||||
"#C2C0E7",
|
||||
"#A19DE4",
|
||||
"#7D76E8",
|
||||
"#544AF4",
|
||||
"#4940DE",
|
||||
"#4239C8",
|
||||
"#463FA8",
|
||||
"#47428E",
|
||||
"#464379",
|
||||
],
|
||||
},
|
||||
primaryColor: "victoria",
|
||||
};
|
||||
2
frontend/src/types/File.type.ts
Normal file
2
frontend/src/types/File.type.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type FileUpload = File & { uploadingState?: UploadState };
|
||||
export type UploadState = "finished" | "inProgress" | undefined;
|
||||
23
frontend/src/types/share.type.ts
Normal file
23
frontend/src/types/share.type.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import User from "./user.type";
|
||||
|
||||
export type Share = {
|
||||
id: string;
|
||||
files: any;
|
||||
creator: User;
|
||||
expiration: Date;
|
||||
};
|
||||
|
||||
export type ShareMetaData = {
|
||||
id: string;
|
||||
isZipReady: boolean;
|
||||
};
|
||||
|
||||
export type MyShare = Share & {
|
||||
views: number;
|
||||
cratedAt: Date;
|
||||
};
|
||||
|
||||
export type ShareSecurity = {
|
||||
maxViews?: number;
|
||||
password?: string;
|
||||
};
|
||||
8
frontend/src/types/user.type.ts
Normal file
8
frontend/src/types/user.type.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default interface User {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface CurrentUser extends User {}
|
||||
6
frontend/src/utils/loading.util.ts
Normal file
6
frontend/src/utils/loading.util.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createContext, Dispatch, SetStateAction } from "react";
|
||||
|
||||
export const GlobalLoadingContext = createContext<{
|
||||
isLoading: boolean;
|
||||
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||
}>({ isLoading: false, setIsLoading: () => {} });
|
||||
11
frontend/src/utils/math/byteStringToHumanSizeString.util.ts
Normal file
11
frontend/src/utils/math/byteStringToHumanSizeString.util.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function byteStringToHumanSizeString(bytes: string) {
|
||||
const bytesNumber = parseInt(bytes);
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
if (bytesNumber == 0) return "0 Byte";
|
||||
const i = parseInt(
|
||||
Math.floor(Math.log(bytesNumber) / Math.log(1024)).toString()
|
||||
);
|
||||
return (
|
||||
(bytesNumber / Math.pow(1024, i)).toFixed(1).toString() + " " + sizes[i]
|
||||
);
|
||||
}
|
||||
27
frontend/src/utils/toast.util.tsx
Normal file
27
frontend/src/utils/toast.util.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { Check, X } from "tabler-icons-react";
|
||||
|
||||
const error = (message: string) =>
|
||||
showNotification({
|
||||
icon: <X />,
|
||||
color: "red",
|
||||
radius: "md",
|
||||
title: "Error",
|
||||
|
||||
message: message,
|
||||
});
|
||||
|
||||
const success = (message: string) =>
|
||||
showNotification({
|
||||
icon: <Check />,
|
||||
color: "green",
|
||||
radius: "md",
|
||||
title: "Success",
|
||||
message: message,
|
||||
});
|
||||
|
||||
const toast = {
|
||||
error,
|
||||
success,
|
||||
};
|
||||
export default toast;
|
||||
Reference in New Issue
Block a user