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