dep improvements, style fixes, next/link codemod

This commit is contained in:
Max Leiter 2022-11-08 00:23:28 -08:00
parent 0405f821c4
commit 0a5a2adb26
29 changed files with 818 additions and 3616 deletions

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@
}
.input::placeholder {
font-size: 1.5rem;
font-size: 1rem;
}
.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)
}
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>

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

View file

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

View file

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

View file

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

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 = /\.(.*)$/
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*"
]
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import Header from "@components/header"
import { Note, Page, Text } from "@geist-ui/core"
import styles from "@styles/Home.module.css"

File diff suppressed because it is too large Load diff

View file

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

View file

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

File diff suppressed because it is too large Load diff