Compare commits

...

4 commits

Author SHA1 Message Date
renovate[bot]
25a2e0c863
chore(deps): update dependency @next/bundle-analyzer to v13 2022-11-23 23:58:43 +00:00
dependabot[bot]
53059e14ad
build(deps): bump sqlite3 from 5.0.2 to 5.0.3 in /server (#128)
Bumps [sqlite3](https://github.com/TryGhost/node-sqlite3) from 5.0.2 to 5.0.3.
- [Release notes](https://github.com/TryGhost/node-sqlite3/releases)
- [Commits](https://github.com/TryGhost/node-sqlite3/compare/v5.0.2...v5.0.3)

---
updated-dependencies:
- dependency-name: sqlite3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-08 00:31:23 -08:00
Max Leiter
55c5ecfe6c
Update to next 13, switch to pnpm (#127)
* switch to pnpm

* dep improvements, style fixes, next/link codemod

* server: upgrade sqlite
2022-11-08 00:28:19 -08:00
Max Leiter
9771e64f93
Update README.md 2022-11-07 23:06:27 -08:00
33 changed files with 9335 additions and 3800 deletions

View file

@ -2,7 +2,7 @@
Drift is a self-hostable Gist clone. It's also a major work-in-progress, but is completely functional. Drift is a self-hostable Gist clone. It's also a major work-in-progress, but is completely functional.
You can try a demo at https://drift.maxleiter.com. The demo is built on master but has no database, so files and accounts can be wiped at any time. You can try a demo at https://drift.lol. The demo is built on master but has no database, so files and accounts can be wiped at any time.
If you want to contribute, need support, or want to stay updated, you can join the IRC channel at #drift on irc.libera.chat or [reach me on twitter](https://twitter.com/Max_Leiter). If you don't have an IRC client yet, you can use a webclient [here](https://demo.thelounge.chat/#/connect?join=%23drift&nick=drift-user&realname=Drift%20User). If you want to contribute, need support, or want to stay updated, you can join the IRC channel at #drift on irc.libera.chat or [reach me on twitter](https://twitter.com/Max_Leiter). If you don't have an IRC client yet, you can use a webclient [here](https://demo.thelounge.chat/#/connect?join=%23drift&nick=drift-user&realname=Drift%20User).
<hr /> <hr />
@ -92,3 +92,5 @@ Drift is a major work in progress. Below is a (rough) list of completed and envi
- [ ] works enough with JavaScript disabled - [ ] works enough with JavaScript disabled
- [x] documentation - [x] documentation
- [x] customizable homepage, so the demo can exist as-is but other instances can be built from the same source. Environment variable for the file contents? - [x] customizable homepage, so the demo can exist as-is but other instances can be built from the same source. Environment variable for the file contents?
- [ ] Next.js 13 + app directory / server components
- [ ] Migrate away from `geist-ui`

4
client/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

View file

@ -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

View file

@ -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,11 +22,19 @@ 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>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 4
}}
>
<UserTable /> <UserTable />
<Spacer height={1} />
<PostTable /> <PostTable />
</div> </div>
</div>
) )
} }

View file

@ -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&apos;t have an account?{" "} Don&apos;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>
)} )}

View file

@ -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%;
}

View file

@ -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>
) )
} }

View file

@ -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>

View file

@ -1,3 +1,5 @@
'use client';
import { import {
ButtonGroup, ButtonGroup,
Button, Button,
@ -168,8 +170,7 @@ 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}
@ -178,9 +179,9 @@ const Header = () => {
> >
{tab.name ? tab.name : undefined} {tab.name ? tab.name : undefined}
</Button> </Button>
</a>
</Link> </Link>)
) );
} }
}, },
[isMobile, onTabChange, router.pathname] [isMobile, onTabChange, router.pathname]

View file

@ -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>

View file

@ -23,7 +23,7 @@
} }
.input::placeholder { .input::placeholder {
font-size: 1.5rem; font-size: 1rem;
} }
.input:focus { .input:focus {

View 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

View file

@ -0,0 +1,12 @@
.link {
text-decoration: none;
color: var(--fg);
}
.color {
color: var(--link);
}
.color:hover {
text-decoration: underline;
}

View file

@ -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,24 +196,17 @@ 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)
@ -231,9 +221,7 @@ const Post = ({
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>

View 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

View 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;
}

View file

@ -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>
)} )}

View file

@ -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>

View file

@ -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>

View file

@ -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)
}) });
} }

View file

@ -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*"
]
}

View file

@ -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) {

View file

@ -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,31 +19,34 @@
"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"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "12.1.6", "@next/bundle-analyzer": "13.0.5",
"@types/node": "17.0.23", "@types/node": "17.0.23",
"@types/react": "18.0.9", "@types/react": "18.0.9",
"@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"
} }
} }

View file

@ -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>
) )
} }

View file

@ -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"

View file

@ -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"

3745
client/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -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"] {

View file

@ -1,8 +1,19 @@
{ {
"compilerOptions": { "compilerOptions": {
"plugins": [{ "name": "typescript-plugin-css-modules" }], "plugins": [
{
"name": "typescript-plugin-css-modules"
},
{
"name": "next"
}
],
"target": "es2020", "target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@ -27,11 +38,24 @@
"incremental": true, "incremental": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@components/*": ["components/*"], "@components/*": [
"@lib/*": ["lib/*"], "components/*"
"@styles/*": ["styles/*"] ],
"@lib/*": [
"lib/*"
],
"@styles/*": [
"styles/*"
]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": [
"exclude": ["node_modules"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }

File diff suppressed because it is too large Load diff

View file

@ -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.0.3",
"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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff