Update to next 13, switch to pnpm (#127)
* switch to pnpm * dep improvements, style fixes, next/link codemod * server: upgrade sqlite
This commit is contained in:
parent
9771e64f93
commit
55c5ecfe6c
31 changed files with 9005 additions and 3401 deletions
4
client/.vscode/settings.json
vendored
Normal file
4
client/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
import type { LinkProps } from "@geist-ui/core"
|
|
||||||
import { Link as GeistLink } from "@geist-ui/core"
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
|
|
||||||
const Link = (props: LinkProps) => {
|
|
||||||
const { basePath } = useRouter()
|
|
||||||
const propHrefWithoutLeadingSlash =
|
|
||||||
props.href && props.href.startsWith("/")
|
|
||||||
? props.href.substring(1)
|
|
||||||
: props.href
|
|
||||||
const href = basePath
|
|
||||||
? `${basePath}/${propHrefWithoutLeadingSlash}`
|
|
||||||
: props.href
|
|
||||||
return <GeistLink {...props} href={href} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Link
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Text, Spacer } from "@geist-ui/core"
|
|
||||||
import Cookies from "js-cookie"
|
import Cookies from "js-cookie"
|
||||||
import styles from "./admin.module.css"
|
import styles from "./admin.module.css"
|
||||||
import PostTable from "./post-table"
|
import PostTable from "./post-table"
|
||||||
|
@ -23,10 +22,18 @@ export const adminFetcher = async (
|
||||||
const Admin = () => {
|
const Admin = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.adminWrapper}>
|
<div className={styles.adminWrapper}>
|
||||||
<Text h2>Administration</Text>
|
<h2>Administration</h2>
|
||||||
<UserTable />
|
<div
|
||||||
<Spacer height={1} />
|
style={{
|
||||||
<PostTable />
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserTable />
|
||||||
|
<PostTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { FormEvent, useEffect, useState } from "react"
|
import { FormEvent, useEffect, useState } from "react"
|
||||||
import { Button, Input, Text, Note } from "@geist-ui/core"
|
|
||||||
import styles from "./auth.module.css"
|
import styles from "./auth.module.css"
|
||||||
import { useRouter } from "next/router"
|
import { useRouter } from "next/router"
|
||||||
import Link from "../Link"
|
import Link from "../link"
|
||||||
import Cookies from "js-cookie"
|
import Cookies from "js-cookie"
|
||||||
import useSignedIn from "@lib/hooks/use-signed-in"
|
import useSignedIn from "@lib/hooks/use-signed-in"
|
||||||
|
import Input from "@components/input"
|
||||||
|
import Button from "@components/button"
|
||||||
|
import Note from "@components/note"
|
||||||
|
|
||||||
const NO_EMPTY_SPACE_REGEX = /^\S*$/
|
const NO_EMPTY_SPACE_REGEX = /^\S*$/
|
||||||
const ERROR_MESSAGE =
|
const ERROR_MESSAGE =
|
||||||
|
@ -90,58 +92,57 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<Input
|
<Input
|
||||||
htmlType="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(event) => setUsername(event.target.value)}
|
onChange={(event) => setUsername(event.currentTarget.value)}
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
required
|
required
|
||||||
scale={4 / 3}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
htmlType="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(event) => setPassword(event.target.value)}
|
onChange={(event) => setPassword(event.currentTarget.value)}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
required
|
required
|
||||||
scale={4 / 3}
|
|
||||||
/>
|
/>
|
||||||
{requiresServerPassword && (
|
{requiresServerPassword && (
|
||||||
<Input
|
<Input
|
||||||
htmlType="password"
|
type="password"
|
||||||
id="server-password"
|
id="server-password"
|
||||||
value={serverPassword}
|
value={serverPassword}
|
||||||
onChange={(event) => setServerPassword(event.target.value)}
|
onChange={(event) =>
|
||||||
|
setServerPassword(event.currentTarget.value)
|
||||||
|
}
|
||||||
placeholder="Server Password"
|
placeholder="Server Password"
|
||||||
required
|
required
|
||||||
scale={4 / 3}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="success" htmlType="submit">
|
<Button buttonType="primary" type="submit">
|
||||||
{signingIn ? "Sign In" : "Sign Up"}
|
{signingIn ? "Sign In" : "Sign Up"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formContentSpace}>
|
<div className={styles.formContentSpace}>
|
||||||
{signingIn ? (
|
{signingIn ? (
|
||||||
<Text>
|
<p>
|
||||||
Don't have an account?{" "}
|
Don't have an account?{" "}
|
||||||
<Link color href="/signup">
|
<Link colored href="/signup">
|
||||||
Sign up
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Text>
|
<p>
|
||||||
Already have an account?{" "}
|
Already have an account?{" "}
|
||||||
<Link color href="/signin">
|
<Link colored href="/signin">
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{errorMsg && (
|
{errorMsg && (
|
||||||
<Note scale={0.75} type="error">
|
<Note type="error">
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</Note>
|
</Note>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
|
.button:root {
|
||||||
|
--hover: var(--bg);
|
||||||
|
--hover-bg: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
color: var(--input-fg);
|
border: 1px solid var(--border);
|
||||||
font-weight: 400;
|
padding: var(--gap-half) var(--gap);
|
||||||
font-size: 1.1rem;
|
|
||||||
background: var(--input-bg);
|
|
||||||
border: var(--input-border);
|
|
||||||
height: 2rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--gap-quarter) var(--gap-half);
|
|
||||||
transition: background-color var(--transition), color var(--transition);
|
|
||||||
width: 100%;
|
|
||||||
height: var(--input-height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover,
|
.button:hover,
|
||||||
.button:focus {
|
.button:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
background: var(--input-bg-hover);
|
color: var(--hover);
|
||||||
border: var(--input-border-focus);
|
background: var(--hover-bg);
|
||||||
|
border: var(--);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button[disabled] {
|
.button[disabled] {
|
||||||
|
@ -38,3 +34,20 @@
|
||||||
background: var(--fg);
|
background: var(--fg);
|
||||||
color: var(--bg);
|
color: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconRight {
|
||||||
|
margin-left: var(--gap-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ type Props = React.HTMLProps<HTMLButtonElement> & {
|
||||||
buttonType?: "primary" | "secondary"
|
buttonType?: "primary" | "secondary"
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
|
iconRight?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
|
@ -18,6 +19,7 @@ const Button = forwardRef<HTMLButtonElement, Props>(
|
||||||
buttonType = "primary",
|
buttonType = "primary",
|
||||||
type = "button",
|
type = "button",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
iconRight,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
|
@ -31,6 +33,11 @@ const Button = forwardRef<HTMLButtonElement, Props>(
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
{iconRight && (
|
||||||
|
<span className={`${styles.icon} ${styles.iconRight}`}>
|
||||||
|
{iconRight}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,29 @@
|
||||||
import { File } from "@lib/types"
|
import { File } from "@lib/types"
|
||||||
import { Card, Link, Text } from "@geist-ui/core"
|
|
||||||
import FileIcon from "@geist-ui/icons/fileText"
|
import FileIcon from "@geist-ui/icons/fileText"
|
||||||
import CodeIcon from "@geist-ui/icons/fileLambda"
|
import CodeIcon from "@geist-ui/icons/fileLambda"
|
||||||
import styles from "./file-tree.module.css"
|
import styles from "./file-tree.module.css"
|
||||||
import ShiftBy from "@components/shift-by"
|
import ShiftBy from "@components/shift-by"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { codeFileExtensions } from "@lib/constants"
|
import { codeFileExtensions } from "@lib/constants"
|
||||||
|
import Link from "@components/link"
|
||||||
|
|
||||||
type Item = File & {
|
type Item = File & {
|
||||||
icon: JSX.Element
|
icon: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Card = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
} & React.ComponentProps<"div">) => (
|
||||||
|
<div className={styles.card} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const FileTree = ({ files }: { files: File[] }) => {
|
const FileTree = ({ files }: { files: File[] }) => {
|
||||||
const [items, setItems] = useState<Item[]>([])
|
const [items, setItems] = useState<Item[]>([])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -34,13 +47,13 @@ const FileTree = ({ files }: { files: File[] }) => {
|
||||||
// a list of files with an icon and a title
|
// a list of files with an icon and a title
|
||||||
return (
|
return (
|
||||||
<div className={styles.fileTreeWrapper}>
|
<div className={styles.fileTreeWrapper}>
|
||||||
<Card height={"100%"} className={styles.card}>
|
<Card className={styles.card}>
|
||||||
<div className={styles.cardContent}>
|
<div className={styles.cardContent}>
|
||||||
<Text h4>Files</Text>
|
<h4>Files</h4>
|
||||||
<ul className={styles.fileTree}>
|
<ul className={styles.fileTree}>
|
||||||
{items.map(({ id, title, icon }) => (
|
{items.map(({ id, title, icon }) => (
|
||||||
<li key={id}>
|
<li key={id}>
|
||||||
<Link color={false} href={`#${title}`}>
|
<Link href={`#${title}`}>
|
||||||
<ShiftBy y={5}>
|
<ShiftBy y={5}>
|
||||||
<span className={styles.fileTreeIcon}>{icon}</span>
|
<span className={styles.fileTreeIcon}>{icon}</span>
|
||||||
</ShiftBy>
|
</ShiftBy>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Button,
|
Button,
|
||||||
|
@ -168,19 +170,18 @@ const Header = () => {
|
||||||
)
|
)
|
||||||
} else if (tab.href) {
|
} else if (tab.href) {
|
||||||
return (
|
return (
|
||||||
<Link key={tab.value} href={tab.href}>
|
(<Link key={tab.value} href={tab.href} className={styles.tab}>
|
||||||
<a className={styles.tab}>
|
<Button
|
||||||
<Button
|
className={activeStyle}
|
||||||
className={activeStyle}
|
auto={isMobile ? false : true}
|
||||||
auto={isMobile ? false : true}
|
icon={tab.icon}
|
||||||
icon={tab.icon}
|
shadow={false}
|
||||||
shadow={false}
|
>
|
||||||
>
|
{tab.name ? tab.name : undefined}
|
||||||
{tab.name ? tab.name : undefined}
|
</Button>
|
||||||
</Button>
|
|
||||||
</a>
|
</Link>)
|
||||||
</Link>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isMobile, onTabChange, router.pathname]
|
[isMobile, onTabChange, router.pathname]
|
||||||
|
|
|
@ -20,8 +20,8 @@ const Home = ({
|
||||||
<ShiftBy y={-2}>
|
<ShiftBy y={-2}>
|
||||||
<Image
|
<Image
|
||||||
src={"/assets/logo-optimized.svg"}
|
src={"/assets/logo-optimized.svg"}
|
||||||
width={"48px"}
|
width={48}
|
||||||
height={"48px"}
|
height={48}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</ShiftBy>
|
</ShiftBy>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.input::placeholder {
|
.input::placeholder {
|
||||||
font-size: 1.5rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
.input:focus {
|
||||||
|
|
26
client/components/link/index.tsx
Normal file
26
client/components/link/index.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import NextLink from "next/link"
|
||||||
|
import styles from "./link.module.css"
|
||||||
|
|
||||||
|
type LinkProps = {
|
||||||
|
href: string,
|
||||||
|
colored?: boolean,
|
||||||
|
children: React.ReactNode
|
||||||
|
} & React.ComponentProps<typeof NextLink>
|
||||||
|
|
||||||
|
const Link = ({ href, colored, children, ...props }: LinkProps) => {
|
||||||
|
const { basePath } = useRouter()
|
||||||
|
const propHrefWithoutLeadingSlash =
|
||||||
|
href && href.startsWith("/") ? href.substring(1) : href
|
||||||
|
|
||||||
|
const url = basePath ? `${basePath}/${propHrefWithoutLeadingSlash}` : href
|
||||||
|
|
||||||
|
const className = colored ? `${styles.link} ${styles.color}` : styles.link
|
||||||
|
return (
|
||||||
|
<NextLink {...props} href={url} className={className}>
|
||||||
|
{children}
|
||||||
|
</NextLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Link
|
12
client/components/link/link.module.css
Normal file
12
client/components/link/link.module.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color {
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
|
@ -169,10 +169,7 @@ const Post = ({
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitPassword = useCallback(
|
const submitPassword = (password: string) => onSubmit("protected", password)
|
||||||
(password: string) => onSubmit("protected", password),
|
|
||||||
[onSubmit]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onChangeExpiration = useCallback((date: Date) => setExpiresAt(date), [])
|
const onChangeExpiration = useCallback((date: Date) => setExpiresAt(date), [])
|
||||||
|
|
||||||
|
@ -199,41 +196,32 @@ const Post = ({
|
||||||
[setDocs]
|
[setDocs]
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateDocContent = useCallback(
|
const updateDocContent = (i: number) => (content: string) => {
|
||||||
(i: number) => (content: string) => {
|
setDocs((docs) =>
|
||||||
setDocs((docs) =>
|
docs.map((doc, index) => (i === index ? { ...doc, content } : doc))
|
||||||
docs.map((doc, index) => (i === index ? { ...doc, content } : doc))
|
)
|
||||||
)
|
}
|
||||||
},
|
|
||||||
[setDocs]
|
|
||||||
)
|
|
||||||
|
|
||||||
const removeDoc = useCallback(
|
const removeDoc = (i: number) => () => {
|
||||||
(i: number) => () => {
|
setDocs((docs) => docs.filter((_, index) => i !== index))
|
||||||
setDocs((docs) => docs.filter((_, index) => i !== index))
|
}
|
||||||
},
|
|
||||||
[setDocs]
|
|
||||||
)
|
|
||||||
|
|
||||||
const uploadDocs = useCallback(
|
const uploadDocs = (files: DocumentType[]) => {
|
||||||
(files: DocumentType[]) => {
|
// if no title is set and the only document is empty,
|
||||||
// if no title is set and the only document is empty,
|
const isFirstDocEmpty =
|
||||||
const isFirstDocEmpty =
|
docs.length <= 1 && (docs.length ? docs[0].title === "" : true)
|
||||||
docs.length <= 1 && (docs.length ? docs[0].title === "" : true)
|
const shouldSetTitle = !title && isFirstDocEmpty
|
||||||
const shouldSetTitle = !title && isFirstDocEmpty
|
if (shouldSetTitle) {
|
||||||
if (shouldSetTitle) {
|
if (files.length === 1) {
|
||||||
if (files.length === 1) {
|
setTitle(files[0].title)
|
||||||
setTitle(files[0].title)
|
} else if (files.length > 1) {
|
||||||
} else if (files.length > 1) {
|
setTitle("Uploaded files")
|
||||||
setTitle("Uploaded files")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isFirstDocEmpty) setDocs(files)
|
if (isFirstDocEmpty) setDocs(files)
|
||||||
else setDocs((docs) => [...docs, ...files])
|
else setDocs((docs) => [...docs, ...files])
|
||||||
},
|
}
|
||||||
[docs, title]
|
|
||||||
)
|
|
||||||
|
|
||||||
// pasted files
|
// pasted files
|
||||||
// const files = e.clipboardData.files as File[]
|
// const files = e.clipboardData.files as File[]
|
||||||
|
@ -340,15 +328,15 @@ const Post = ({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<ButtonDropdown loading={isSubmitting} type="success">
|
<ButtonDropdown loading={isSubmitting} type="success">
|
||||||
|
<ButtonDropdown.Item onClick={() => onSubmit("unlisted")}>
|
||||||
|
Create Unlisted
|
||||||
|
</ButtonDropdown.Item>
|
||||||
<ButtonDropdown.Item main onClick={() => onSubmit("private")}>
|
<ButtonDropdown.Item main onClick={() => onSubmit("private")}>
|
||||||
Create Private
|
Create Private
|
||||||
</ButtonDropdown.Item>
|
</ButtonDropdown.Item>
|
||||||
<ButtonDropdown.Item onClick={() => onSubmit("public")}>
|
<ButtonDropdown.Item onClick={() => onSubmit("public")}>
|
||||||
Create Public
|
Create Public
|
||||||
</ButtonDropdown.Item>
|
</ButtonDropdown.Item>
|
||||||
<ButtonDropdown.Item onClick={() => onSubmit("unlisted")}>
|
|
||||||
Create Unlisted
|
|
||||||
</ButtonDropdown.Item>
|
|
||||||
<ButtonDropdown.Item onClick={() => onSubmit("protected")}>
|
<ButtonDropdown.Item onClick={() => onSubmit("protected")}>
|
||||||
Create with Password
|
Create with Password
|
||||||
</ButtonDropdown.Item>
|
</ButtonDropdown.Item>
|
||||||
|
|
17
client/components/note/index.tsx
Normal file
17
client/components/note/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import styles from "./note.module.css"
|
||||||
|
|
||||||
|
const Note = ({
|
||||||
|
type = "info",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
type: "info" | "warning" | "error"
|
||||||
|
children: React.ReactNode
|
||||||
|
} & React.ComponentProps<"div">) => (
|
||||||
|
<div className={`${styles.note} ${styles[type]}`} {...props}>
|
||||||
|
<strong className={styles.type}>{type}:</strong>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Note
|
27
client/components/note/note.module.css
Normal file
27
client/components/note/note.module.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.note {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--fg);
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--gap);
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
background: #f33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
color: var(--fg);
|
||||||
|
margin-right: 0.5em;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
import { Button, Input, Select, Text } from "@geist-ui/core"
|
import { Button, Input, Text } from "@geist-ui/core"
|
||||||
import NextLink from "next/link"
|
|
||||||
import Link from "../Link"
|
|
||||||
|
|
||||||
import styles from "./post-list.module.css"
|
import styles from "./post-list.module.css"
|
||||||
import ListItemSkeleton from "./list-item-skeleton"
|
import ListItemSkeleton from "./list-item-skeleton"
|
||||||
|
@ -9,6 +7,7 @@ import { Post } from "@lib/types"
|
||||||
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react"
|
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react"
|
||||||
import Cookies from "js-cookie"
|
import Cookies from "js-cookie"
|
||||||
import useDebounce from "@lib/hooks/use-debounce"
|
import useDebounce from "@lib/hooks/use-debounce"
|
||||||
|
import Link from "@components/link"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialPosts: Post[]
|
initialPosts: Post[]
|
||||||
|
@ -136,9 +135,9 @@ const PostList = ({ morePosts, initialPosts, error }: Props) => {
|
||||||
{posts?.length === 0 && !error && (
|
{posts?.length === 0 && !error && (
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
No posts found. Create one{" "}
|
No posts found. Create one{" "}
|
||||||
<NextLink passHref={true} href="/new">
|
<Link colored href="/new">
|
||||||
<Link color>here</Link>
|
here
|
||||||
</NextLink>
|
</Link>
|
||||||
.
|
.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import NextLink from "next/link"
|
import NextLink from "next/link"
|
||||||
import VisibilityBadge from "../badges/visibility-badge"
|
import VisibilityBadge from "../badges/visibility-badge"
|
||||||
import {
|
import { Text, Card, Tooltip, Divider, Badge, Button } from "@geist-ui/core"
|
||||||
Link,
|
|
||||||
Text,
|
|
||||||
Card,
|
|
||||||
Tooltip,
|
|
||||||
Divider,
|
|
||||||
Badge,
|
|
||||||
Button
|
|
||||||
} from "@geist-ui/core"
|
|
||||||
import { File, Post } from "@lib/types"
|
import { File, Post } from "@lib/types"
|
||||||
import FadeIn from "@components/fade-in"
|
import FadeIn from "@components/fade-in"
|
||||||
import Trash from "@geist-ui/icons/trash"
|
import Trash from "@geist-ui/icons/trash"
|
||||||
|
@ -18,6 +10,7 @@ import Edit from "@geist-ui/icons/edit"
|
||||||
import { useRouter } from "next/router"
|
import { useRouter } from "next/router"
|
||||||
import Parent from "@geist-ui/icons/arrowUpCircle"
|
import Parent from "@geist-ui/icons/arrowUpCircle"
|
||||||
import styles from "./list-item.module.css"
|
import styles from "./list-item.module.css"
|
||||||
|
import Link from "@components/link"
|
||||||
|
|
||||||
// TODO: isOwner should default to false so this can be used generically
|
// TODO: isOwner should default to false so this can be used generically
|
||||||
const ListItem = ({
|
const ListItem = ({
|
||||||
|
@ -45,15 +38,14 @@ const ListItem = ({
|
||||||
<Card style={{ overflowY: "scroll" }}>
|
<Card style={{ overflowY: "scroll" }}>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<Text h3 className={styles.title}>
|
<Text h3 className={styles.title}>
|
||||||
<NextLink
|
<Link
|
||||||
passHref={true}
|
colored
|
||||||
|
style={{ marginRight: "var(--gap)" }}
|
||||||
href={`/post/[id]`}
|
href={`/post/[id]`}
|
||||||
as={`/post/${post.id}`}
|
as={`/post/${post.id}`}
|
||||||
>
|
>
|
||||||
<Link color marginRight={"var(--gap)"}>
|
{post.title}
|
||||||
{post.title}
|
</Link>
|
||||||
</Link>
|
|
||||||
</NextLink>
|
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<span className={styles.buttons}>
|
<span className={styles.buttons}>
|
||||||
{post.parent && (
|
{post.parent && (
|
||||||
|
@ -97,7 +89,7 @@ const ListItem = ({
|
||||||
{post.files?.map((file: File) => {
|
{post.files?.map((file: File) => {
|
||||||
return (
|
return (
|
||||||
<div key={file.id}>
|
<div key={file.id}>
|
||||||
<Link color href={`/post/${post.id}#${file.title}`}>
|
<Link colored href={`/post/${post.id}#${file.title}`}>
|
||||||
{file.title || "Untitled file"}
|
{file.title || "Untitled file"}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,16 +3,15 @@ import styles from "./document.module.css"
|
||||||
import Download from "@geist-ui/icons/download"
|
import Download from "@geist-ui/icons/download"
|
||||||
import ExternalLink from "@geist-ui/icons/externalLink"
|
import ExternalLink from "@geist-ui/icons/externalLink"
|
||||||
import Skeleton from "react-loading-skeleton"
|
import Skeleton from "react-loading-skeleton"
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Text,
|
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Spacer,
|
Spacer,
|
||||||
Tabs,
|
Tabs,
|
||||||
Textarea,
|
Textarea,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Link,
|
|
||||||
Tag
|
Tag
|
||||||
} from "@geist-ui/core"
|
} from "@geist-ui/core"
|
||||||
import HtmlPreview from "@components/preview"
|
import HtmlPreview from "@components/preview"
|
||||||
|
@ -32,7 +31,7 @@ const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
||||||
<div className={styles.actionWrapper}>
|
<div className={styles.actionWrapper}>
|
||||||
<ButtonGroup className={styles.actions}>
|
<ButtonGroup className={styles.actions}>
|
||||||
<Tooltip hideArrow text="Download">
|
<Tooltip hideArrow text="Download">
|
||||||
<a
|
<Link
|
||||||
href={`${rawLink}?download=true`}
|
href={`${rawLink}?download=true`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
@ -44,10 +43,10 @@ const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
||||||
auto
|
auto
|
||||||
aria-label="Download"
|
aria-label="Download"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip hideArrow text="Open raw in new tab">
|
<Tooltip hideArrow text="Open raw in new tab">
|
||||||
<a href={rawLink} target="_blank" rel="noopener noreferrer">
|
<Link href={rawLink || ""} target="_blank" rel="noopener noreferrer">
|
||||||
<Button
|
<Button
|
||||||
scale={2 / 3}
|
scale={2 / 3}
|
||||||
px={0.6}
|
px={0.6}
|
||||||
|
@ -55,7 +54,7 @@ const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
||||||
auto
|
auto
|
||||||
aria-label="Open raw file in new tab"
|
aria-label="Open raw file in new tab"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function generateUUID() {
|
||||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
|
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
|
||||||
).toString(16)
|
).toString(16)
|
||||||
}
|
}
|
||||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, callback)
|
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let timestamp = new Date().getTime()
|
let timestamp = new Date().getTime()
|
||||||
|
@ -35,5 +35,5 @@ export default function generateUUID() {
|
||||||
perforNow = Math.floor(perforNow / 16)
|
perforNow = Math.floor(perforNow / 16)
|
||||||
}
|
}
|
||||||
return (c === "x" ? random : (random & 0x3) | 0x8).toString(16)
|
return (c === "x" ? random : (random & 0x3) | 0x8).toString(16)
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { NextFetchEvent, NextRequest, NextResponse } from "next/server"
|
import { NextFetchEvent, NextResponse } from "next/server"
|
||||||
|
import type { NextRequest } from "next/server"
|
||||||
|
|
||||||
const PUBLIC_FILE = /\.(.*)$/
|
const PUBLIC_FILE = /\.(.*)$/
|
||||||
|
|
||||||
export function middleware(req: NextRequest, event: NextFetchEvent) {
|
export function middleware(req: NextRequest, event: NextFetchEvent) {
|
||||||
const pathname = req.nextUrl.pathname
|
const pathname = req.nextUrl.pathname
|
||||||
const signedIn = req.cookies["drift-token"]
|
const signedIn = req.cookies.get("drift-token")
|
||||||
const getURL = (pageName: string) => new URL(`/${pageName}`, req.url).href
|
const getURL = (pageName: string) => new URL(`/${pageName}`, req.url).href
|
||||||
const isPageRequest =
|
const isPageRequest =
|
||||||
!PUBLIC_FILE.test(pathname) &&
|
!PUBLIC_FILE.test(pathname) &&
|
||||||
!pathname.startsWith("/api") &&
|
|
||||||
// header added when next/link pre-fetches a route
|
// header added when next/link pre-fetches a route
|
||||||
!req.headers.get("x-middleware-preflight")
|
!req.headers.get("x-middleware-preflight")
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ export function middleware(req: NextRequest, event: NextFetchEvent) {
|
||||||
// If you're not signed in we redirect to the home page
|
// If you're not signed in we redirect to the home page
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
const resp = NextResponse.redirect(getURL(""))
|
const resp = NextResponse.redirect(getURL(""))
|
||||||
resp.clearCookie("drift-token")
|
resp.cookies.delete("drift-token")
|
||||||
resp.clearCookie("drift-userid")
|
resp.cookies.delete("drift-userid")
|
||||||
const signoutPromise = new Promise((resolve) => {
|
const signoutPromise = new Promise((resolve) => {
|
||||||
fetch(`${process.env.API_URL}/auth/signout`, {
|
fetch(`${process.env.API_URL}/auth/signout`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -61,3 +61,17 @@ export function middleware(req: NextRequest, event: NextFetchEvent) {
|
||||||
|
|
||||||
return NextResponse.next()
|
return NextResponse.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
match: [
|
||||||
|
"/signout",
|
||||||
|
"/",
|
||||||
|
"/signin",
|
||||||
|
"/signup",
|
||||||
|
"/new",
|
||||||
|
"/protected/:path*",
|
||||||
|
"/private/:path*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,9 @@ dotenv.config()
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
experimental: {
|
experimental: {
|
||||||
outputStandalone: true,
|
// outputStandalone: true,
|
||||||
esmExternals: true
|
esmExternals: true,
|
||||||
|
// appDir: true
|
||||||
},
|
},
|
||||||
webpack: (config, { dev, isServer }) => {
|
webpack: (config, { dev, isServer }) => {
|
||||||
if (!dev && !isServer) {
|
if (!dev && !isServer) {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"find:unused": "next-unused"
|
"find:unused": "next-unused"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@geist-ui/core": "2.3.8",
|
"@geist-ui/core": "^2.3.8",
|
||||||
"@geist-ui/icons": "1.0.2",
|
"@geist-ui/icons": "1.0.2",
|
||||||
"@types/cookie": "0.5.1",
|
"@types/cookie": "0.5.1",
|
||||||
"@types/js-cookie": "3.0.2",
|
"@types/js-cookie": "3.0.2",
|
||||||
|
@ -19,13 +19,13 @@
|
||||||
"cookie": "0.5.0",
|
"cookie": "0.5.0",
|
||||||
"dotenv": "16.0.0",
|
"dotenv": "16.0.0",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"next": "12.1.6",
|
"next": "13.0.2",
|
||||||
"next-themes": "0.2.0",
|
"next-themes": "0.2.1",
|
||||||
"rc-table": "7.24.1",
|
"rc-table": "7.24.1",
|
||||||
"react": "18.1.0",
|
"react": "18.2.0",
|
||||||
"react-datepicker": "4.7.0",
|
"react-datepicker": "4.8.0",
|
||||||
"react-dom": "18.1.0",
|
"react-dom": "18.2.0",
|
||||||
"react-dropzone": "12.1.0",
|
"react-dropzone": "14.2.3",
|
||||||
"react-loading-skeleton": "3.1.0",
|
"react-loading-skeleton": "3.1.0",
|
||||||
"swr": "1.3.0",
|
"swr": "1.3.0",
|
||||||
"textarea-markdown-editor": "0.1.13"
|
"textarea-markdown-editor": "0.1.13"
|
||||||
|
@ -37,13 +37,16 @@
|
||||||
"@types/react-datepicker": "4.4.1",
|
"@types/react-datepicker": "4.4.1",
|
||||||
"@types/react-dom": "18.0.3",
|
"@types/react-dom": "18.0.3",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.15.0",
|
"eslint": "8.27.0",
|
||||||
"eslint-config-next": "12.1.6",
|
"eslint-config-next": "13.0.2",
|
||||||
"next-unused": "0.0.6",
|
"next-unused": "0.0.6",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"typescript": "4.6.4",
|
"typescript": "4.6.4",
|
||||||
"typescript-plugin-css-modules": "3.4.0"
|
"typescript-plugin-css-modules": "3.4.0"
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"sharp": "^0.31.2"
|
||||||
|
},
|
||||||
"next-unused": {
|
"next-unused": {
|
||||||
"alias": {
|
"alias": {
|
||||||
"@components": "components/",
|
"@components": "components/",
|
||||||
|
@ -54,5 +57,8 @@
|
||||||
"components",
|
"components",
|
||||||
"lib"
|
"lib"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"next": "13.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,11 +49,9 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<title>Drift</title>
|
<title>Drift</title>
|
||||||
</Head>
|
</Head>
|
||||||
<React.StrictMode>
|
<ThemeProvider defaultTheme="system" disableTransitionOnChange>
|
||||||
<ThemeProvider defaultTheme="system" disableTransitionOnChange>
|
<App Component={Component} pageProps={pageProps} />
|
||||||
<App Component={Component} pageProps={pageProps} />
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import styles from "@styles/Home.module.css"
|
import styles from "@styles/Home.module.css"
|
||||||
|
|
||||||
import Header from "@components/header"
|
|
||||||
import { Page } from "@geist-ui/core"
|
import { Page } from "@geist-ui/core"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import Admin from "@components/admin"
|
import Admin from "@components/admin"
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import Header from "@components/header"
|
|
||||||
import { Note, Page, Text } from "@geist-ui/core"
|
import { Note, Page, Text } from "@geist-ui/core"
|
||||||
import styles from "@styles/Home.module.css"
|
import styles from "@styles/Home.module.css"
|
||||||
|
|
||||||
|
|
3740
client/pnpm-lock.yaml
Normal file
3740
client/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -48,8 +48,9 @@
|
||||||
--header-bg: rgba(19, 20, 21, 0.45);
|
--header-bg: rgba(19, 20, 21, 0.45);
|
||||||
--gray-alpha: rgba(255, 255, 255, 0.5);
|
--gray-alpha: rgba(255, 255, 255, 0.5);
|
||||||
--selection: rgba(255, 255, 255, 0.99);
|
--selection: rgba(255, 255, 255, 0.99);
|
||||||
|
--border: var(--lighter-gray);
|
||||||
--warning: rgb(27, 134, 23);
|
--warning: rgb(27, 134, 23);
|
||||||
|
--link: #3291ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"] {
|
[data-theme="light"] {
|
||||||
|
|
|
@ -1,37 +1,61 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"plugins": [{ "name": "typescript-plugin-css-modules" }],
|
"plugins": [
|
||||||
"target": "es2020",
|
{
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"name": "typescript-plugin-css-modules"
|
||||||
"allowJs": true,
|
},
|
||||||
"skipLibCheck": true,
|
{
|
||||||
"strict": true,
|
"name": "next"
|
||||||
"forceConsistentCasingInFileNames": true,
|
}
|
||||||
"noImplicitAny": true,
|
],
|
||||||
"strictNullChecks": true,
|
"target": "es2020",
|
||||||
"strictFunctionTypes": true,
|
"lib": [
|
||||||
"strictBindCallApply": true,
|
"dom",
|
||||||
"strictPropertyInitialization": true,
|
"dom.iterable",
|
||||||
"noImplicitThis": true,
|
"esnext"
|
||||||
"alwaysStrict": true,
|
],
|
||||||
"noUnusedLocals": false,
|
"allowJs": true,
|
||||||
"noUnusedParameters": true,
|
"skipLibCheck": true,
|
||||||
"noEmit": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"module": "esnext",
|
"noImplicitAny": true,
|
||||||
"moduleResolution": "node",
|
"strictNullChecks": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"strictFunctionTypes": true,
|
||||||
"resolveJsonModule": true,
|
"strictBindCallApply": true,
|
||||||
"isolatedModules": true,
|
"strictPropertyInitialization": true,
|
||||||
"jsx": "preserve",
|
"noImplicitThis": true,
|
||||||
"incremental": true,
|
"alwaysStrict": true,
|
||||||
"baseUrl": ".",
|
"noUnusedLocals": false,
|
||||||
"paths": {
|
"noUnusedParameters": true,
|
||||||
"@components/*": ["components/*"],
|
"noEmit": true,
|
||||||
"@lib/*": ["lib/*"],
|
"esModuleInterop": true,
|
||||||
"@styles/*": ["styles/*"]
|
"module": "esnext",
|
||||||
}
|
"moduleResolution": "node",
|
||||||
},
|
"allowSyntheticDefaultImports": true,
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"resolveJsonModule": true,
|
||||||
"exclude": ["node_modules"]
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@components/*": [
|
||||||
|
"components/*"
|
||||||
|
],
|
||||||
|
"@lib/*": [
|
||||||
|
"lib/*"
|
||||||
|
],
|
||||||
|
"@styles/*": [
|
||||||
|
"styles/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
3198
client/yarn.lock
3198
client/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -34,7 +34,7 @@
|
||||||
"reflect-metadata": "^0.1.10",
|
"reflect-metadata": "^0.1.10",
|
||||||
"sequelize": "^6.17.0",
|
"sequelize": "^6.17.0",
|
||||||
"sequelize-typescript": "^2.1.3",
|
"sequelize-typescript": "^2.1.3",
|
||||||
"sqlite3": "https://github.com/mapbox/node-sqlite3#918052b538b0effe6c4a44c74a16b2749c08a0d2",
|
"sqlite3": "^5.1.2",
|
||||||
"strong-error-handler": "^4.0.0",
|
"strong-error-handler": "^4.0.0",
|
||||||
"umzug": "^3.1.0"
|
"umzug": "^3.1.0"
|
||||||
},
|
},
|
||||||
|
@ -50,6 +50,7 @@
|
||||||
"@types/node-fetch": "2.6.1",
|
"@types/node-fetch": "2.6.1",
|
||||||
"@types/react-dom": "17.0.16",
|
"@types/react-dom": "17.0.16",
|
||||||
"@types/supertest": "2.0.12",
|
"@types/supertest": "2.0.12",
|
||||||
|
"@types/validator": "^13.7.10",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"jest": "27.5.1",
|
"jest": "27.5.1",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
|
|
4930
server/pnpm-lock.yaml
Normal file
4930
server/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue