feat(web): use pandora for ui

This commit is contained in:
sylv 2022-07-26 05:59:19 +08:00
parent 97087bf87c
commit 531aeb6a26
36 changed files with 121 additions and 457 deletions

View File

@ -17,15 +17,19 @@
"dependencies": {
"@apollo/client": "^3.6.9",
"@headlessui/react": "^1.6.1",
"@ryanke/pandora": "^0.0.9",
"@tailwindcss/typography": "^0.5.2",
"autoprefixer": "^10.4.7",
"classnames": "^2.3.1",
"concurrently": "^7.2.2",
"copy-to-clipboard": "^3.3.1",
"dayjs": "^1.11.1",
"deepmerge": "^4.2.2",
"formik": "^2.2.9",
"generate-avatar": "1.4.10",
"graphql": "^16.5.0",
"http-status-codes": "^2.2.0",
"lodash": "^4.17.21",
"nanoid": "^3.3.4",
"next": "12.2.0",
"postcss": "^8.4.13",
@ -33,14 +37,12 @@
"react": "18.2.0",
"react-dom": "^18.1.0",
"react-feather": "^2.0.9",
"react-icons": "^4.4.0",
"react-markdown": "^8.0.3",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"swr": "^1.3.0",
"tailwindcss": "^3.0.24",
"deepmerge": "^4.2.2",
"concurrently": "^7.2.2",
"lodash": "^4.17.21",
"yup": "^0.32.11"
},
"devDependencies": {

View File

@ -1,13 +0,0 @@
import classNames from 'classnames';
import type { FC } from 'react';
import { ArrowLeft } from 'react-feather';
import { Link } from './link';
export const Breadcrumbs: FC<{ to: string; children: string; className?: string }> = ({ to, children, className }) => {
const classes = classNames('text-sm text-gray-500 flex items-center gap-1 hover:underline', className);
return (
<Link href={to} className={classes}>
<ArrowLeft className="h-4 w-4" /> {children}
</Link>
);
};

View File

@ -1,64 +0,0 @@
/* eslint-disable react/button-has-type */
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { Link } from './link';
import { Spinner } from './spinner';
export interface ButtonProps extends Omit<HTMLAttributes<HTMLButtonElement | HTMLAnchorElement>, 'prefix' | 'style'> {
href?: string;
disabled?: boolean;
style?: ButtonStyle;
loading?: boolean;
type?: 'submit' | 'reset' | 'button';
}
export enum ButtonStyle {
Primary = 'bg-purple-500 hover:bg-purple-400',
Secondary = 'bg-dark-600 hover:bg-dark-900',
Disabled = 'bg-dark-300 hover:bg-dark-400',
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{ href, disabled, className, type, children, loading, style = ButtonStyle.Primary, onClick, onKeyDown, ...rest },
ref
) => {
if (disabled) style = ButtonStyle.Disabled;
const onClickWrap = disabled ? undefined : onClick;
const onKeyDownWrap = disabled ? undefined : onKeyDown;
const classes = classNames(
'flex items-center justify-center gap-2 w-full px-3 py-2 text-sm font-medium transition rounded truncate max-h-[2.65em]',
disabled && 'cursor-not-allowed',
className,
style
);
if (href) {
if (ref) {
throw new Error('Button cannot have ref and href');
}
return (
<Link href={href} className={classes} onClick={onClickWrap} onKeyDown={onKeyDownWrap} {...rest}>
{children} {loading && <Spinner size="small" />}
</Link>
);
}
return (
<button
type={type}
className={classes}
disabled={disabled}
onClick={onClickWrap}
onKeyDown={onKeyDownWrap}
style={{ height: '2.5rem' }}
{...rest}
ref={ref}
>
{children} {loading && <Spinner size="small" />}
</button>
);
}
);

View File

@ -1,11 +0,0 @@
import classNames from 'classnames';
import type { FC, HTMLAttributes } from 'react';
export const Card: FC<HTMLAttributes<HTMLDivElement>> = ({ className, children, ...rest }) => {
const classes = classNames(className, 'p-4 bg-dark-200 rounded');
return (
<div className={classes} {...rest}>
{children}
</div>
);
};

View File

@ -1,30 +0,0 @@
import classNames from 'classnames';
import type { FC, ReactNode } from 'react';
export interface ContainerProps {
centerX?: boolean;
centerY?: boolean;
center?: boolean;
small?: boolean;
className?: string;
children: ReactNode;
}
export const Container: FC<ContainerProps> = ({
center,
centerX = center,
centerY = center,
className,
small,
children,
}) => {
const classes = classNames(className, 'px-4 mx-auto', {
'sm:max-w-screen-sm md:max-w-screen-md lg:max-w-screen-lg xl:max-w-screen-xl': !small,
'flex justify-center flex-col': centerX || centerY,
'absolute top-16 bottom-0 right-0 left-0': centerY,
'items-center': centerX,
'max-w-xs': small,
});
return <div className={classes}>{children}</div>;
};

View File

@ -1,14 +1,11 @@
import { Button, ButtonStyle, Container, useAsync, useOnClickOutside } from '@ryanke/pandora';
import classNames from 'classnames';
import { Fragment, memo, useRef, useState } from 'react';
import { Crop } from 'react-feather';
import { useResendVerificationEmailMutation } from '../../generated/graphql';
import { useAsync } from '../../hooks/useAsync';
import { useConfig } from '../../hooks/useConfig';
import { useOnClickOutside } from '../../hooks/useOnClickOutside';
import { usePaths } from '../../hooks/usePaths';
import { useUser } from '../../hooks/useUser';
import { Button, ButtonStyle } from '../button';
import { Container } from '../container';
import { Input } from '../input/input';
import { Link } from '../link';
import { HeaderUser } from './header-user';
@ -92,7 +89,7 @@ export const Header = memo(() => {
<nav className={classes}>
<div className="flex items-center">
<Link href={paths.home} className="flex">
<Crop className="mr-2 text-brand" /> micro
<Crop className="mr-2 text-primary" /> micro
</Link>
</div>
<div className="flex items-center">

View File

@ -14,11 +14,11 @@ export const Markdown = memo<{ children: string; className?: string }>(({ childr
// remove "" quotes from blockquotes
'prose-p:before:content-none prose-p:after:content-none',
// make links purple
'prose-a:text-brand hover:prose-a:underline prose-a:no-underline',
'prose-a:text-primary hover:prose-a:underline prose-a:no-underline',
// remove italics from blockquotes
'prose-blockquote:font-normal prose-blockquote:not-italic',
// make inline `code` blocks purple
'prose-code:text-brand',
'prose-code:text-primary',
className
);

View File

@ -1,7 +1,6 @@
import { Container, Spinner } from '@ryanke/pandora';
import type { FC } from 'react';
import { useEffect } from 'react';
import { Container } from './container';
import { Spinner } from './spinner';
import { Title } from '../components/title';
export const PageLoader: FC<{ title?: string }> = ({ title }) => {

View File

@ -1,25 +0,0 @@
import classNames from 'classnames';
import type { FC, HTMLAttributes } from 'react';
export interface SpinnerProps extends HTMLAttributes<SVGElement> {
size?: 'small' | 'medium' | 'large';
}
export const Spinner: FC<SpinnerProps> = ({ size, className, ...rest }) => {
const classes = classNames('animate-spin', className, {
'w-4': size === 'small',
'w-6': !size || size === 'medium',
'w-9': size === 'large',
});
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" {...rest} className={classes}>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
};

View File

@ -3,7 +3,7 @@ import type { Language } from 'prism-react-renderer';
import { memo } from 'react';
import { ChevronDown } from 'react-feather';
import languages from '../../data/languages.json';
import { useToasts } from '../../hooks/useToasts';
import { useToasts } from '@ryanke/pandora';
export interface SyntaxHighlighterControls {
onLanguageChange: (language: Language) => void;

View File

@ -1,59 +0,0 @@
import { nanoid } from 'nanoid';
import type { FC, ReactNode } from 'react';
import React, { useCallback, useState } from 'react';
import type { ToastProps } from './toast';
import { Toast, TRANSITION_DURATION } from './toast';
export type ToastContextData = null | ((toast: ToastProps) => void);
export const ToastContext = React.createContext<ToastContextData>(null);
export const ToastWrapper: FC<{ children: ReactNode }> = (props) => {
// spread operators on arrays are to fix this
// https://stackoverflow.com/questions/56266575/why-is-usestate-not-triggering-re-render
const [toasts, setToasts] = useState<(ToastProps & { id: string; timer: NodeJS.Timeout })[]>([]);
const createToast = useCallback(
(toast: ToastProps) => {
if (toasts.some((existing) => existing.text === toast.text)) {
// skip duplicate cards
return;
}
const timeout = toast.timeout ?? 5000;
const id = nanoid(10);
const timer = setTimeout(() => {
// set removing on toast
setToasts((toasts) => {
for (const toast of toasts) {
if (toast.id !== id) continue;
toast.removing = true;
}
return [...toasts];
});
setTimeout(() => {
// remove toast once transition is complete
setToasts((toasts) => toasts.filter((toast) => toast.id !== id));
}, TRANSITION_DURATION);
}, timeout - TRANSITION_DURATION);
// create toast
setToasts((toasts) => {
const data = Object.assign(toast, { id, timer });
return [...toasts, data];
});
},
[toasts, setToasts]
);
return (
<ToastContext.Provider value={createToast}>
{props.children}
<div className="fixed flex justify-end bottom-5 right-5 left-5">
{toasts.map((toast) => (
<Toast key={toast.id} removing={toast.removing} {...toast} />
))}
</div>
</ToastContext.Provider>
);
};

View File

@ -1,35 +0,0 @@
import classNames from 'classnames';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
export interface ToastProps {
text: string;
error?: boolean;
timeout?: number;
removing?: boolean;
}
export const TRANSITION_DURATION = 300;
export const Toast: FC<ToastProps> = (props) => {
const initialClasses = 'opacity-0 scale-90';
const animateClasses = 'opacity-100 translate-x-0';
const [transition, setTransition] = useState(initialClasses);
const classes = classNames('p-4 transition duration-300 rounded shadow-xl select-none w-96', transition, {
'bg-violet-500': !props.error,
'bg-red-600': props.error,
});
useEffect(() => {
if (props.removing) setTransition(initialClasses);
else {
// breaks the browser trying to optimise the transition by skipping it because we add it so fast
requestAnimationFrame(() => {
setTimeout(() => {
setTransition(animateClasses);
});
});
}
}, [props.removing]);
return <div className={classes}>{props.text}</div>;
};

View File

@ -1,9 +1,8 @@
import { Container, Spinner } from '@ryanke/pandora';
import classNames from 'classnames';
import { Fragment, useState } from 'react';
import { Download } from 'react-feather';
import { Container } from '../../components/container';
import { Section } from '../../components/section';
import { Spinner } from '../../components/spinner';
import { Toggle } from '../../components/toggle';
import { downloadFile } from '../../helpers/download.helper';
import { generateConfig } from '../../helpers/generate-config.helper';
@ -34,7 +33,7 @@ export const ConfigGenerator = () => {
return (
<Section>
<Container className="flex flex-col justify-between dots selection:bg-blue-500 py-8">
<Container className="flex flex-col justify-between dots selection:bg-purple-600 py-8">
<div className="w-full flex-grow">
{!config.data && (
<div className="flex items-center justify-center w-full h-full py-10">
@ -55,7 +54,7 @@ export const ConfigGenerator = () => {
>
<Toggle
selected={embedded}
backgroundColour="bg-blue-500"
backgroundColour="bg-purple-600"
onChange={({ value }) => setEmbedded(value)}
options={[
{
@ -75,7 +74,7 @@ export const ConfigGenerator = () => {
>
<Toggle
selected={pasteShortcut}
backgroundColour="bg-blue-500"
backgroundColour="bg-purple-600"
onChange={({ value }) => setPasteShortcut(value)}
options={[
{
@ -96,8 +95,8 @@ export const ConfigGenerator = () => {
const isSelected = selectedHosts.includes(host.normalised);
const classes = classNames(
'rounded px-2 py-1 truncate transition border border-transparent',
isSelected && 'bg-blue-500 text-white',
!isSelected && 'text-gray-400 bg-dark-100 hover:bg-gray-800 hover:text-white'
isSelected && 'bg-purple-600 text-white',
!isSelected && 'text-gray-400 bg-dark-100 hover:bg-dark-200 hover:text-white'
);
return (
@ -127,7 +126,7 @@ export const ConfigGenerator = () => {
onClick={download}
className={classNames(
'mt-8 ml-auto flex items-center gap-1',
downloadable ? 'text-blue-400 hover:underline' : 'text-gray-700 cursor-not-allowed'
downloadable ? 'text-purple-400 hover:underline' : 'text-gray-700 cursor-not-allowed'
)}
>
download config <Download className="h-3.5 w-3.5" />

View File

@ -1,7 +1,6 @@
import { Breadcrumbs, Card } from '@ryanke/pandora';
import type { FC } from 'react';
import { Fragment } from 'react';
import { Breadcrumbs } from '../../components/breadcrumbs';
import { Card } from '../../components/card';
import { PageLoader } from '../../components/page-loader';
import { Toggle } from '../../components/toggle';
import { useGetFilesQuery, useGetPastesQuery } from '../../generated/graphql';
@ -29,7 +28,7 @@ export const FileList: FC = () => {
<Fragment>
<div className="flex justify-between gap-2 mb-6">
<div>
<Breadcrumbs to="/">Home</Breadcrumbs>
<Breadcrumbs href="/">Home</Breadcrumbs>
<h2 className="capitalize font-bold text-2xl">My {filter}</h2>
</div>
<div>

View File

@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
import type { FC } from 'react';
import { Fragment, useCallback, useEffect } from 'react';
import * as Yup from 'yup';
import { Button } from '../components/button';
import { Button } from '@ryanke/pandora';
import { Input } from '../components/input/input';
import { useUser } from '../hooks/useUser';

View File

@ -2,7 +2,7 @@ import { Form, Formik } from 'formik';
import type { FC } from 'react';
import { Fragment, useMemo } from 'react';
import * as Yup from 'yup';
import { Button } from '../components/button';
import { Button } from '@ryanke/pandora';
import { Input } from '../components/input/input';
import { useConfig } from '../hooks/useConfig';

View File

@ -1,39 +0,0 @@
import { useState } from 'react';
import { getErrorMessage } from '../helpers/get-error-message.helper';
import { useToasts } from './useToasts';
export function useAsync<T, X extends any[]>(handler: (...params: X) => Promise<T>) {
const [promise, setPromise] = useState<Promise<T> | null>(null);
const [error, setError] = useState<Error | null>(null);
const [result, setResult] = useState<T | null>(null);
const createToast = useToasts();
const running = !!promise;
const run = async (...params: X) => {
if (promise) {
return promise;
}
try {
const promise = handler(...params);
setPromise(promise);
setError(null);
const result = await promise;
setResult(result);
} catch (error: any) {
const message = getErrorMessage(error);
if (message) {
createToast({
error: true,
text: message,
});
}
setError(error);
throw error;
} finally {
setPromise(null);
}
};
return [run, running, error, result] as const;
}

View File

@ -1,24 +0,0 @@
import { useEffect } from 'react';
export function useOnClickOutside(ref: React.MutableRefObject<any>, handler: () => void) {
useEffect(() => {
const onClick = (event: Event) => {
if (!ref.current || ref.current.contains(event.target)) return;
handler();
};
const onKeyPress = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
handler();
};
document.addEventListener('mousedown', onClick);
document.addEventListener('touchstart', onClick);
document.addEventListener('keydown', onKeyPress);
return () => {
document.removeEventListener('mousedown', onClick);
document.removeEventListener('touchstart', onClick);
document.removeEventListener('keydown', onKeyPress);
};
}, [ref, handler]);
}

View File

@ -1,13 +0,0 @@
import { useContext } from 'react';
import { ToastContext } from '../components/toast/toast-wrapper';
export const useToasts = () => {
const createToast = useContext(ToastContext);
if (!createToast) {
// todo: this should be an error, but it seems like it can be undefined.
// maybe due to concurrent rendering? idk shit about fuck.
return () => undefined;
}
return createToast;
};

View File

@ -1,9 +1,9 @@
import { useAsync } from '@ryanke/pandora';
import Router, { useRouter } from 'next/router';
import { useEffect } from 'react';
import { resetClient } from '../apollo';
import { useGetUserQuery } from '../generated/graphql';
import { http } from '../helpers/http.helper';
import { useAsync } from './useAsync';
interface LoginData {
username: string;

View File

@ -4,7 +4,7 @@ import Head from 'next/head';
import { useApollo } from '../apollo';
import { Header } from '../components/header/header';
import { Title } from '../components/title';
import { ToastWrapper } from '../components/toast/toast-wrapper';
import { ToastProvider } from '@ryanke/pandora';
import '../styles/globals.css';
export default function App({ Component, pageProps }: AppProps) {
@ -17,12 +17,12 @@ export default function App({ Component, pageProps }: AppProps) {
<meta property="og:site_name" content="micro" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</Head>
<ToastWrapper>
<ToastProvider>
<Header />
<div className="py-4 md:py-16">
<Component {...pageProps} />
</div>
</ToastWrapper>
</ToastProvider>
</ApolloProvider>
);
}

View File

@ -1,4 +1,4 @@
import { Container } from '../components/container';
import { Container } from '@ryanke/pandora';
import { Link } from '../components/link';
import { Title } from '../components/title';
import { getErrorMessage } from '../helpers/get-error-message.helper';
@ -26,7 +26,7 @@ export default function Error(props: ErrorProps) {
<Title>{message}</Title>
<h1 className="mb-4 text-4xl font-fold">{lenny}</h1>
<p className="text-lg">{message}</p>
<Link className="text-brand" href={paths.home}>
<Link className="text-primary" href={paths.home}>
Go Home
</Link>
</Container>

View File

@ -1,4 +1,4 @@
import { Container } from '../../components/container';
import { Container } from '@ryanke/pandora';
import { FileList } from '../../containers/file-list/file-list';
import { useUser } from '../../hooks/useUser';

View File

@ -1,12 +1,9 @@
import { Container } from '../../components/container';
import { PageLoader } from '../../components/page-loader';
import { Breadcrumbs } from '../../components/breadcrumbs';
import { Breadcrumbs, Container, useAsync } from '@ryanke/pandora';
import { Input } from '../../components/input/input';
import { PageLoader } from '../../components/page-loader';
import { Title } from '../../components/title';
import { ConfigGenerator } from '../../containers/config-generator/config-generator';
import { useRefreshTokenMutation } from '../../generated/graphql';
import { useAsync } from '../../hooks/useAsync';
import { useConfig } from '../../hooks/useConfig';
import { useUser } from '../../hooks/useUser';
@ -29,7 +26,7 @@ export default function Preferences() {
return (
<Container>
<Title>Preferences</Title>
<Breadcrumbs to="/dashboard" className="mb-4">
<Breadcrumbs href="/dashboard" className="mb-4">
Dashboard
</Breadcrumbs>
<div className="grid grid-cols-2 gap-4">

View File

@ -1,3 +1,4 @@
import { Container, Spinner, useAsync, useToasts } from '@ryanke/pandora';
import classNames from 'classnames';
import copyToClipboard from 'copy-to-clipboard';
import type { GetServerSidePropsContext } from 'next';
@ -6,16 +7,12 @@ import type { FC, ReactNode } from 'react';
import { useState } from 'react';
import { Download, Share, Trash } from 'react-feather';
import { addStateToPageProps, initializeApollo } from '../../apollo';
import { Container } from '../../components/container';
import { Embed } from '../../components/embed/embed';
import { PageLoader } from '../../components/page-loader';
import { Spinner } from '../../components/spinner';
import { Title } from '../../components/title';
import { ConfigDocument, GetFileDocument, useDeleteFileMutation, useGetFileQuery } from '../../generated/graphql';
import { downloadUrl } from '../../helpers/download.helper';
import { useAsync } from '../../hooks/useAsync';
import { useQueryState } from '../../hooks/useQueryState';
import { useToasts } from '../../hooks/useToasts';
import ErrorPage from '../_error';
const FileOption: FC<{ children: ReactNode; className?: string; onClick: () => void }> = ({

View File

@ -1,5 +1,4 @@
import { Container } from '../components/container';
import { Spinner } from '../components/spinner';
import { Spinner, Container } from '@ryanke/pandora';
import { useConfig } from '../hooks/useConfig';
export default function Home() {
@ -14,7 +13,7 @@ export default function Home() {
<p className="mb-2 text-gray-400">
An invite-only file sharing and paste service with vanity domains and a ShareX compatible endpoint. Sign in to
download a generated ShareX configuration. You can view the source code{' '}
<a className="text-brand" href="https://github.com/sylv/micro" target="_blank" rel="noreferrer">
<a className="text-primary" href="https://github.com/sylv/micro" target="_blank" rel="noreferrer">
here.
</a>
</p>
@ -27,7 +26,7 @@ export default function Home() {
{config.data && (
<p className="text-gray-400">
To get an account or get a file taken down, email{' '}
<a href={`mailto:${config.data.inquiriesEmail}`} className="text-brand">
<a href={`mailto:${config.data.inquiriesEmail}`} className="text-primary">
{config.data.inquiriesEmail}
</a>
.

View File

@ -1,6 +1,6 @@
import { Container, useAsync, useToasts } from '@ryanke/pandora';
import Router, { useRouter } from 'next/router';
import { useEffect } from 'react';
import { Container } from '../../components/container';
import { PageLoader } from '../../components/page-loader';
import { Time } from '../../components/time';
import { Title } from '../../components/title';
@ -8,9 +8,7 @@ import type { SignupData } from '../../containers/signup-form';
import { SignupForm } from '../../containers/signup-form';
import { useCreateUserMutation, useGetInviteQuery } from '../../generated/graphql';
import { getErrorMessage } from '../../helpers/get-error-message.helper';
import { useAsync } from '../../hooks/useAsync';
import { useConfig } from '../../hooks/useConfig';
import { useToasts } from '../../hooks/useToasts';
import ErrorPage from '../_error';
export default function Invite() {

View File

@ -1,8 +1,7 @@
import { Container, useToasts } from '@ryanke/pandora';
import { useEffect } from 'react';
import { Container } from '../components/container';
import { Title } from '../components/title';
import { LoginForm } from '../containers/login-form';
import { useToasts } from '../hooks/useToasts';
export default function Login() {
const createToast = useToasts();
@ -17,7 +16,7 @@ export default function Login() {
text: 'Your account has been verified.',
});
}
}, []);
}, [createToast]);
return (
<Container center small>

View File

@ -1,10 +1,9 @@
import { Button, Container } from '@ryanke/pandora';
import type { GetServerSidePropsContext } from 'next';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { BookOpen, Clock, Trash } from 'react-feather';
import { addStateToPageProps, initializeApollo } from '../../apollo';
import { Button } from '../../components/button';
import { Container } from '../../components/container';
import { Embed } from '../../components/embed/embed';
import { PageLoader } from '../../components/page-loader';
import { Time } from '../../components/time';

View File

@ -1,7 +1,6 @@
import { Button, Container } from '@ryanke/pandora';
import { Form, Formik } from 'formik';
import * as Yup from 'yup';
import { Button } from '../../components/button';
import { Container } from '../../components/container';
import { Checkbox } from '../../components/input/checkbox';
import { Input } from '../../components/input/input';
import { Select } from '../../components/input/select';

View File

@ -1,8 +1,7 @@
import { Button, Container } from '@ryanke/pandora';
import { Form, Formik } from 'formik';
import { useState } from 'react';
import * as Yup from 'yup';
import { Button } from '../components/button';
import { Container } from '../components/container';
import { Input } from '../components/input/input';
import { Select } from '../components/input/select';
import { useShortenMutation } from '../generated/graphql';

View File

@ -1,19 +1,15 @@
import { Button, Card, Container, Spinner, useToasts } from '@ryanke/pandora';
import { useRouter } from 'next/router';
import type { ChangeEventHandler, DragEventHandler } from 'react';
import { useRef, useState } from 'react';
import { Upload as UploadIcon } from 'react-feather';
import { Button } from '../components/button';
import { Card } from '../components/card';
import { Container } from '../components/container';
import { Select } from '../components/input/select';
import { PageLoader } from '../components/page-loader';
import { Spinner } from '../components/spinner';
import { Title } from '../components/title';
import { getErrorMessage } from '../helpers/get-error-message.helper';
import { http } from '../helpers/http.helper';
import { replaceUsername } from '../helpers/replace-username.helper';
import { useConfig } from '../hooks/useConfig';
import { useToasts } from '../hooks/useToasts';
import { useUser } from '../hooks/useUser';
interface CreateFileResponse {
@ -138,7 +134,7 @@ export default function Upload() {
<Button onClick={handleUpload}>Upload</Button>
</div>
<span
className="mt-4 cursor-pointer text-brand"
className="mt-4 cursor-pointer text-primary"
onClick={() => {
setFile(null);
}}
@ -172,7 +168,7 @@ export default function Upload() {
)}
</h1>
<p className="text-gray-400 select-none">
Or <span className="text-brand">click here</span> to select a file.
Or <span className="text-primary">click here</span> to select a file.
</p>
</Card>
</Container>

View File

@ -2,24 +2,6 @@
@tailwind components;
@tailwind utilities;
:root {
--brand: rgb(167, 139, 250);
}
#__next {
@apply h-screen;
}
body {
@apply text-white;
background-color: #141414;
}
::selection {
@apply bg-violet-600;
@apply text-white;
}
::-webkit-scrollbar {
background: inherit;
height: 14px;
@ -48,15 +30,6 @@ body {
border-color: transparent;
}
body {
overflow-y: overlay;
}
.dots {
background-image: radial-gradient(black 1px, transparent 0);
background-size: 40px 40px;
}
code[class*='language-'],
pre[class*='language-'] {
white-space: normal !important;

View File

@ -1,47 +1,6 @@
const pandora = require('@ryanke/pandora/plugin');
module.exports = {
content: ['./src/**/*.tsx'],
theme: {
extend: {
colors: {
brand: 'var(--brand)',
gray: {
100: '#f5f5f5',
200: '#eeeeee',
300: '#e0e0e0',
400: '#bdbdbd',
500: '#9e9e9e',
600: '#757575',
700: '#616161',
800: '#424242',
900: '#212121',
},
dark: {
100: '#121212',
200: '#1d1d1d',
300: '#212121',
400: '#242424',
500: '#272727',
600: '#2c2c2c',
700: '#2d2d2d',
800: '#333333',
900: '#353535',
999: '#373737',
},
blue: {
DEFAULT: '#0067F1',
50: '#AACEFF',
100: '#95C2FF',
200: '#6CABFF',
300: '#4494FF',
400: '#1B7CFF',
500: '#0067F1',
600: '#004FB9',
700: '#003781',
800: '#001F49',
900: '#000711',
},
},
},
},
plugins: [require('@tailwindcss/typography')],
content: ['./src/**/*.tsx', pandora.content],
plugins: [require('@tailwindcss/typography'), pandora.plugin],
};

View File

@ -180,6 +180,7 @@ importers:
'@graphql-codegen/typescript-operations': 2.4.3
'@graphql-codegen/typescript-react-apollo': 3.2.17
'@headlessui/react': ^1.6.1
'@ryanke/pandora': ^0.0.9
'@sylo-digital/scripts': ^1.0.2
'@tailwindcss/typography': ^0.5.2
'@types/lodash': ^4.14.182
@ -203,6 +204,7 @@ importers:
react: 18.2.0
react-dom: ^18.1.0
react-feather: ^2.0.9
react-icons: ^4.4.0
react-markdown: ^8.0.3
rehype-raw: ^6.1.1
remark-gfm: ^3.0.1
@ -213,6 +215,7 @@ importers:
dependencies:
'@apollo/client': 3.6.9_aez2jvt6lsvokp3l4ousdbdxf4
'@headlessui/react': 1.6.5_biqbaboplfbrettd7655fr4n2y
'@ryanke/pandora': 0.0.9_d3a4q72qanlbr76d5qpg5hrygu
'@tailwindcss/typography': 0.5.2_tailwindcss@3.1.4
autoprefixer: 10.4.7_postcss@8.4.14
classnames: 2.3.1
@ -232,6 +235,7 @@ importers:
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-feather: 2.0.10_react@18.2.0
react-icons: 4.4.0_react@18.2.0
react-markdown: 8.0.3_luyos4mouogwq6z3wafb3re4ce
rehype-raw: 6.1.1
remark-gfm: 3.0.1
@ -1594,6 +1598,7 @@ packages:
engines: {node: '>=12'}
dependencies:
'@jridgewell/trace-mapping': 0.3.9
dev: true
/@endemolshinegroup/cosmiconfig-typescript-loader/3.0.2_zmjss6mecb4soo3dpdlecld3xa:
resolution: {integrity: sha512-QRVtqJuS1mcT56oHpVegkKBlgtWjXw/gHNWO3eL9oyB5Sc7HBoc2OLG/nYpVfT/Jejvo3NUrD0Udk7XgoyDKkA==}
@ -2349,7 +2354,6 @@ packages:
transitivePeerDependencies:
- supports-color
- ts-node
dev: true
/@jest/core/28.1.2_ts-node@10.8.2:
resolution: {integrity: sha512-Xo4E+Sb/nZODMGOPt2G3cMmCBqL4/W2Ijwr7/mrXlq4jdJwcFQ/9KrrJZT2adQRk2otVBXXOz1GRQ4Z5iOgvRQ==}
@ -2392,6 +2396,7 @@ packages:
transitivePeerDependencies:
- supports-color
- ts-node
dev: true
/@jest/create-cache-key-function/27.5.1:
resolution: {integrity: sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==}
@ -2657,6 +2662,7 @@ packages:
dependencies:
'@jridgewell/resolve-uri': 3.0.8
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@mikro-orm/cli/5.2.2_4k2cb7nrrwahnwnvazylxfgb44:
resolution: {integrity: sha512-IRMcfVFF6edBX2lQsKswMAjw6ILOvwnLT0cMJziknVMuPop/ckaWxlA5r4Uu1BorNtoHf52qB9i3r5YwEe9qAQ==}
@ -3451,6 +3457,19 @@ packages:
colors: 1.2.5
string-argv: 0.3.1
/@ryanke/pandora/0.0.9_d3a4q72qanlbr76d5qpg5hrygu:
resolution: {integrity: sha512-MV5LDSS52OkDqmeXqlEF1uRBq51q3kTXBbX9ggF61UIJn83jLGleuy8sLj/K8zWvBmF8K+/yVU/VlvsUnSV/jQ==}
peerDependencies:
react: ^18.2.0
tailwindcss: ^3.1.6
dependencies:
clsx: 1.2.1
nanoid: 4.0.0
react: 18.2.0
react-feather: 2.0.10_react@18.2.0
tailwindcss: 3.1.4
dev: false
/@ryanke/venera/0.0.2_33pkbfq2kwsj5qeeqstpf4i2q4:
resolution: {integrity: sha512-EfT55TubMsI3SDLH9S8nDGPsuZlWom0DB9jn+AUdBYQWqZJVKRs5xXtePGo8NDPJ6Jk31q+AM3uNXFkKZXFTHw==}
peerDependencies:
@ -3529,6 +3548,7 @@ packages:
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@swc/core-android-arm64/1.2.204:
@ -3545,6 +3565,7 @@ packages:
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@swc/core-darwin-arm64/1.2.204:
@ -3561,6 +3582,7 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@swc/core-darwin-x64/1.2.204:
@ -3577,6 +3599,7 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@swc/core-freebsd-x64/1.2.204:
@ -3593,6 +3616,7 @@ packages:
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-arm-gnueabihf/1.2.204:
@ -3609,6 +3633,7 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-arm64-gnu/1.2.204:
@ -3625,6 +3650,7 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-arm64-musl/1.2.204:
@ -3641,6 +3667,7 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-x64-gnu/1.2.204:
@ -3657,6 +3684,7 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-x64-musl/1.2.204:
@ -3673,6 +3701,7 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-win32-arm64-msvc/1.2.204:
@ -3689,6 +3718,7 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@swc/core-win32-ia32-msvc/1.2.204:
@ -3705,6 +3735,7 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@swc/core-win32-x64-msvc/1.2.204:
@ -3721,6 +3752,7 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@swc/core/1.2.204:
@ -3760,6 +3792,7 @@ packages:
'@swc/core-win32-arm64-msvc': 1.2.208
'@swc/core-win32-ia32-msvc': 1.2.208
'@swc/core-win32-x64-msvc': 1.2.208
dev: true
/@swc/helpers/0.4.2:
resolution: {integrity: sha512-556Az0VX7WR6UdoTn4htt/l3zPQ7bsQWK+HqdG4swV7beUCxo/BqmvbOpUkTIm/9ih86LIf1qsUnywNL3obGHw==}
@ -3802,7 +3835,7 @@ packages:
eslint: 8.19.0
eslint-config-galex: 3.6.5_eslint@8.19.0+jest@28.1.2
eslint-plugin-es: 4.1.0_eslint@8.19.0
jest: 28.1.2_qv5kk3vgcyi3feqo7l5zvvzluy
jest: 28.1.2_@types+node@16.11.43
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@ -3866,15 +3899,19 @@ packages:
/@tsconfig/node10/1.0.9:
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
dev: true
/@tsconfig/node12/1.0.11:
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
dev: true
/@tsconfig/node14/1.0.3:
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
dev: true
/@tsconfig/node16/1.0.3:
resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==}
dev: true
/@types/accepts/1.3.5:
resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==}
@ -4520,6 +4557,7 @@ packages:
/acorn-walk/8.2.0:
resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==}
engines: {node: '>=0.4.0'}
dev: true
/acorn/7.4.1:
resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
@ -4983,6 +5021,7 @@ packages:
/arg/4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
dev: true
/arg/5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@ -5830,6 +5869,11 @@ packages:
engines: {node: '>=0.8'}
dev: true
/clsx/1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
dev: false
/co/4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@ -6035,6 +6079,7 @@ packages:
/create-require/1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
/cron/1.8.2:
resolution: {integrity: sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==}
@ -6336,6 +6381,7 @@ packages:
/diff/4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
dev: true
/diff/5.1.0:
resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
@ -6976,7 +7022,7 @@ packages:
'@typescript-eslint/eslint-plugin': 5.12.0_byxz3mnn3ms3gbtbci7fo4cm6q
'@typescript-eslint/utils': 5.29.0_m32fwjepeyylyephxtubzxm4ui
eslint: 8.19.0
jest: 28.1.2_qv5kk3vgcyi3feqo7l5zvvzluy
jest: 28.1.2_@types+node@16.11.43
transitivePeerDependencies:
- supports-color
- typescript
@ -8946,7 +8992,6 @@ packages:
- '@types/node'
- supports-color
- ts-node
dev: true
/jest-cli/28.1.2_qv5kk3vgcyi3feqo7l5zvvzluy:
resolution: {integrity: sha512-l6eoi5Do/IJUXAFL9qRmDiFpBeEJAnjJb1dcd9i/VWfVWbp3mJhuH50dNtX67Ali4Ecvt4eBkWb4hXhPHkAZTw==}
@ -8974,6 +9019,7 @@ packages:
- '@types/node'
- supports-color
- ts-node
dev: true
/jest-config/28.1.2_4maxphccb5fztufhofwcslq6fm:
resolution: {integrity: sha512-g6EfeRqddVbjPVBVY4JWpUY4IvQoFRIZcv4V36QkqzE0IGhEC/VkugFeBMAeUE7PRgC8KJF0yvJNDeQRbamEVA==}
@ -9013,6 +9059,7 @@ packages:
ts-node: 10.8.2_pbcylixk5f7tclmtradmulh4qa
transitivePeerDependencies:
- supports-color
dev: true
/jest-config/28.1.2_@types+node@16.11.43:
resolution: {integrity: sha512-g6EfeRqddVbjPVBVY4JWpUY4IvQoFRIZcv4V36QkqzE0IGhEC/VkugFeBMAeUE7PRgC8KJF0yvJNDeQRbamEVA==}
@ -9051,7 +9098,6 @@ packages:
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
dev: true
/jest-config/28.1.2_qv5kk3vgcyi3feqo7l5zvvzluy:
resolution: {integrity: sha512-g6EfeRqddVbjPVBVY4JWpUY4IvQoFRIZcv4V36QkqzE0IGhEC/VkugFeBMAeUE7PRgC8KJF0yvJNDeQRbamEVA==}
@ -9091,6 +9137,7 @@ packages:
ts-node: 10.8.2_pbcylixk5f7tclmtradmulh4qa
transitivePeerDependencies:
- supports-color
dev: true
/jest-diff/28.1.1:
resolution: {integrity: sha512-/MUUxeR2fHbqHoMMiffe/Afm+U8U4olFRJ0hiVG2lZatPJcnGxx292ustVu7bULhjV65IYMxRdploAKLbcrsyg==}
@ -9430,7 +9477,6 @@ packages:
- '@types/node'
- supports-color
- ts-node
dev: true
/jest/28.1.2_qv5kk3vgcyi3feqo7l5zvvzluy:
resolution: {integrity: sha512-Tuf05DwLeCh2cfWCQbcz9UxldoDyiR1E9Igaei5khjonKncYdc6LDfynKCEWozK0oLE3GD+xKAo2u8x/0s6GOg==}
@ -9450,6 +9496,7 @@ packages:
- '@types/node'
- supports-color
- ts-node
dev: true
/joycon/3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
@ -9979,6 +10026,7 @@ packages:
/make-error/1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
dev: true
/make-fetch-happen/8.0.14:
resolution: {integrity: sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ==}
@ -10682,6 +10730,12 @@ packages:
hasBin: true
dev: false
/nanoid/4.0.0:
resolution: {integrity: sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==}
engines: {node: ^14 || ^16 || >=18}
hasBin: true
dev: false
/nanomatch/1.2.13:
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
engines: {node: '>=0.10.0'}
@ -11686,6 +11740,14 @@ packages:
react: 18.2.0
dev: false
/react-icons/4.4.0_react@18.2.0:
resolution: {integrity: sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==}
peerDependencies:
react: '*'
dependencies:
react: 18.2.0
dev: false
/react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@ -13074,6 +13136,7 @@ packages:
typescript: 4.7.4
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/ts-node/9.1.1_typescript@4.7.4:
resolution: {integrity: sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==}
@ -13390,6 +13453,7 @@ packages:
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
/ua-parser-js/0.7.31:
resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==}
@ -13622,6 +13686,7 @@ packages:
/v8-compile-cache-lib/3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
dev: true
/v8-compile-cache/2.3.0:
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
@ -13969,6 +14034,7 @@ packages:
/yn/3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
dev: true
/yocto-queue/0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}