client: add prettier, switch to preact

This commit is contained in:
Max Leiter 2022-03-23 15:34:23 -07:00
parent 9bdff8f28f
commit d4120e6f41
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG key ID: A3512F2F2F17EBDA
19 changed files with 1482 additions and 299 deletions

6
client/.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"semi": false,
"trailingComma": "none",
"singleQuote": false,
"printWidth": 80
}

View file

@ -1,30 +1,39 @@
export default function generateUUID() { export default function generateUUID() {
if (typeof crypto === 'object') { if (typeof crypto === "object") {
if (typeof crypto.randomUUID === 'function') { if (typeof crypto.randomUUID === "function") {
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
return crypto.randomUUID(); return crypto.randomUUID()
} }
if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') { if (
typeof crypto.getRandomValues === "function" &&
typeof Uint8Array === "function"
) {
// https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid // https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid
const callback = (c: string) => { const callback = (c: string) => {
const num = Number(c); const num = Number(c)
return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(16); return (
}; num ^
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback); (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
).toString(16)
}
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, callback)
} }
} }
let timestamp = new Date().getTime(); let timestamp = new Date().getTime()
let perforNow = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0; let perforNow =
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { (typeof performance !== "undefined" &&
let random = Math.random() * 16; performance.now &&
performance.now() * 1000) ||
0
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
let random = Math.random() * 16
if (timestamp > 0) { if (timestamp > 0) {
random = (timestamp + random) % 16 | 0; random = (timestamp + random) % 16 | 0
timestamp = Math.floor(timestamp / 16); timestamp = Math.floor(timestamp / 16)
} else { } else {
random = (perforNow + random) % 16 | 0; random = (perforNow + random) % 16 | 0
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,32 +1,35 @@
import Cookies from "js-cookie"; import Cookies from "js-cookie"
import { useRouter } from "next/router"; import { useRouter } from "next/router"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react"
import useSharedState from "./use-shared-state"; import useSharedState from "./use-shared-state"
const useSignedIn = () => { const useSignedIn = () => {
const [signedIn, setSignedIn] = useSharedState('signedIn', typeof window === 'undefined' ? false : !!Cookies.get("drift-token")); const [signedIn, setSignedIn] = useSharedState(
"signedIn",
typeof window === "undefined" ? false : !!Cookies.get("drift-token")
)
const token = Cookies.get("drift-token") const token = Cookies.get("drift-token")
const router = useRouter(); const router = useRouter()
const signin = (token: string) => { const signin = (token: string) => {
setSignedIn(true); setSignedIn(true)
Cookies.set("drift-token", token); Cookies.set("drift-token", token)
} }
const signout = () => { const signout = () => {
setSignedIn(false); setSignedIn(false)
Cookies.remove("drift-token"); Cookies.remove("drift-token")
router.push("/"); router.push("/")
} }
useEffect(() => { useEffect(() => {
if (token) { if (token) {
setSignedIn(true); setSignedIn(true)
} else { } else {
setSignedIn(false); setSignedIn(false)
} }
}, [setSignedIn, token]); }, [setSignedIn, token])
return { signedIn, signin, token, signout }; return { signedIn, signin, token, signout }
} }
export default useSignedIn; export default useSignedIn

View file

@ -3,20 +3,20 @@ import useSharedState from "./use-shared-state"
const useTheme = () => { const useTheme = () => {
const isClient = typeof window === "object" const isClient = typeof window === "object"
const [themeType, setThemeType] = useSharedState<string>('theme', 'light') const [themeType, setThemeType] = useSharedState<string>("theme", "light")
useEffect(() => { useEffect(() => {
if (!isClient) return if (!isClient) return
const storedTheme = localStorage.getItem('drift-theme') const storedTheme = localStorage.getItem("drift-theme")
if (storedTheme) { if (storedTheme) {
setThemeType(storedTheme) setThemeType(storedTheme)
} }
}, [isClient, setThemeType]) }, [isClient, setThemeType])
const changeTheme = useCallback(() => { const changeTheme = useCallback(() => {
setThemeType(last => { setThemeType((last) => {
const newTheme = last === 'dark' ? 'light' : 'dark' const newTheme = last === "dark" ? "light" : "dark"
localStorage.setItem('drift-theme', newTheme) localStorage.setItem("drift-theme", newTheme)
return newTheme return newTheme
}) })
}, [setThemeType]) }, [setThemeType])

View file

@ -1,4 +1,4 @@
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react"
function useTraceUpdate(props: { [key: string]: any }) { function useTraceUpdate(props: { [key: string]: any }) {
const prev = useRef(props) const prev = useRef(props)
@ -10,10 +10,10 @@ function useTraceUpdate(props: { [key: string]: any }) {
return ps return ps
}, {} as { [key: string]: any }) }, {} as { [key: string]: any })
if (Object.keys(changedProps).length > 0) { if (Object.keys(changedProps).length > 0) {
console.log('Changed props:', changedProps) console.log("Changed props:", changedProps)
} }
prev.current = props prev.current = props
}); })
} }
export default useTraceUpdate export default useTraceUpdate

View file

@ -2,40 +2,42 @@
// which is based on https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site // which is based on https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site
const epochs = [ const epochs = [
['year', 31536000], ["year", 31536000],
['month', 2592000], ["month", 2592000],
['day', 86400], ["day", 86400],
['hour', 3600], ["hour", 3600],
['minute', 60], ["minute", 60],
['second', 1] ["second", 1]
] as const; ] as const
// Get duration // Get duration
const getDuration = (timeAgoInSeconds: number) => { const getDuration = (timeAgoInSeconds: number) => {
for (let [name, seconds] of epochs) { for (let [name, seconds] of epochs) {
const interval = Math.floor(timeAgoInSeconds / seconds); const interval = Math.floor(timeAgoInSeconds / seconds)
if (interval >= 1) { if (interval >= 1) {
return { return {
interval: interval, interval: interval,
epoch: name epoch: name
}; }
} }
} }
return { return {
interval: 0, interval: 0,
epoch: 'second' epoch: "second"
}
} }
};
// Calculate // Calculate
const timeAgo = (date: Date) => { const timeAgo = (date: Date) => {
const timeAgoInSeconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000); const timeAgoInSeconds = Math.floor(
const { interval, epoch } = getDuration(timeAgoInSeconds); (new Date().getTime() - new Date(date).getTime()) / 1000
const suffix = interval === 1 ? '' : 's'; )
const { interval, epoch } = getDuration(timeAgoInSeconds)
const suffix = interval === 1 ? "" : "s"
return `${interval} ${epoch}${suffix} ago`; return `${interval} ${epoch}${suffix} ago`
}; }
export default timeAgo export default timeAgo

View file

@ -1,7 +1,7 @@
export type PostVisibility = "unlisted" | "private" | "public" | "protected" export type PostVisibility = "unlisted" | "private" | "public" | "protected"
export type ThemeProps = { export type ThemeProps = {
theme: "light" | "dark" | string, theme: "light" | "dark" | string
changeTheme: () => void changeTheme: () => void
} }

View file

@ -1,29 +1,39 @@
import dotenv from "dotenv"; import dotenv from "dotenv"
import bundleAnalyzer from "@next/bundle-analyzer"; import bundleAnalyzer from "@next/bundle-analyzer"
dotenv.config(); dotenv.config()
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
experimental: { experimental: {
outputStandalone: true, outputStandalone: true,
esmExternals: true, esmExternals: true
},
webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
Object.assign(config.resolve.alias, {
react: "preact/compat",
"react-dom/test-utils": "preact/test-utils",
"react-dom": "preact/compat"
})
}
return config
}, },
async rewrites() { async rewrites() {
return [ return [
{ {
source: "/server-api/:path*", source: "/server-api/:path*",
destination: `${process.env.API_URL}/:path*`, destination: `${process.env.API_URL}/:path*`
}, },
{ {
source: "/file/raw/:id", source: "/file/raw/:id",
destination: `/api/raw/:id`, destination: `/api/raw/:id`
}, }
]; ]
}, }
}; }
export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })( export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(
nextConfig nextConfig
); )

View file

@ -6,8 +6,9 @@
"dev": "next dev --port 3001", "dev": "next dev --port 3001",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint && prettier --config .prettierrc '{components,lib,pages}/**/*.ts' --write",
"analyze": "ANALYZE=true next build" "analyze": "ANALYZE=true next build",
"find:unused": "next-unused"
}, },
"dependencies": { "dependencies": {
"@geist-ui/core": "^2.3.5", "@geist-ui/core": "^2.3.5",
@ -18,10 +19,13 @@
"cookie": "^0.4.2", "cookie": "^0.4.2",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8",
"marked": "^4.0.12", "marked": "^4.0.12",
"next": "^12.1.1-canary.15", "next": "^12.1.1-canary.15",
"nprogress": "^0.2.0", "postcss": "^8.4.12",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-hover-media-feature": "^1.0.2",
"postcss-preset-env": "^7.4.3",
"preact": "^10.6.6",
"prism-react-renderer": "^1.3.1", "prism-react-renderer": "^1.3.1",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
@ -46,7 +50,20 @@
"@types/react-syntax-highlighter": "^13.5.2", "@types/react-syntax-highlighter": "^13.5.2",
"eslint": "8.10.0", "eslint": "8.10.0",
"eslint-config-next": "^12.1.1-canary.16", "eslint-config-next": "^12.1.1-canary.16",
"next-unused": "^0.0.6",
"prettier": "^2.6.0",
"typescript": "4.6.2", "typescript": "4.6.2",
"typescript-plugin-css-modules": "^3.4.0" "typescript-plugin-css-modules": "^3.4.0"
},
"next-unused": {
"alias": {
"@components": "components/",
"@lib": "lib/",
"@styles": "styles/"
},
"include": [
"components",
"lib"
]
} }
} }

View file

@ -7,23 +7,6 @@ import Head from 'next/head';
import useTheme from '@lib/hooks/use-theme'; import useTheme from '@lib/hooks/use-theme';
import { CssBaseline, GeistProvider } from '@geist-ui/core'; import { CssBaseline, GeistProvider } from '@geist-ui/core';
import nprogress from 'nprogress'
import debounce from 'lodash.debounce'
import Router from 'next/router';
// Only show nprogress after 500ms (slow loading)
const start = debounce(nprogress.start, 500)
Router.events.on('routeChangeStart', start)
Router.events.on('routeChangeComplete', () => {
start.cancel()
nprogress.done()
window.scrollTo(0, 0)
})
Router.events.on('routeChangeError', () => {
start.cancel()
nprogress.done()
})
type AppProps<P = any> = { type AppProps<P = any> = {
pageProps: P; pageProps: P;
} & Omit<NextAppProps<P>, "pageProps">; } & Omit<NextAppProps<P>, "pageProps">;

View file

@ -1,27 +1,38 @@
import type { NextApiHandler } from "next"; import type { NextApiHandler } from "next"
import markdown from "@lib/render-markdown"; import markdown from "@lib/render-markdown"
const renderMarkdown: NextApiHandler = async (req, res) => { const renderMarkdown: NextApiHandler = async (req, res) => {
const { id } = req.query const { id } = req.query
const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, { const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, {
headers: { headers: {
'Accept': 'text/plain', Accept: "text/plain",
'x-secret-key': process.env.SECRET_KEY || '', "x-secret-key": process.env.SECRET_KEY || "",
'Authorization': `Bearer ${req.cookies['drift-token']}`, Authorization: `Bearer ${req.cookies["drift-token"]}`
} }
}) })
const json = await file.json() const json = await file.json()
const { content, title } = json const { content, title } = json
const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', ''] const renderAsMarkdown = [
"markdown",
"md",
"mdown",
"mkdn",
"mkd",
"mdwn",
"mdtxt",
"mdtext",
"text",
""
]
const fileType = () => { const fileType = () => {
const pathParts = title.split(".") const pathParts = title.split(".")
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : "" const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
return language return language
} }
const type = fileType() const type = fileType()
let contentToRender: string = '\n' + content; let contentToRender: string = "\n" + content
if (!renderAsMarkdown.includes(type)) { if (!renderAsMarkdown.includes(type)) {
contentToRender = `~~~${type} contentToRender = `~~~${type}
@ -29,13 +40,13 @@ ${content}
~~~` ~~~`
} }
if (typeof contentToRender !== 'string') { if (typeof contentToRender !== "string") {
res.status(400).send('content must be a string') res.status(400).send("content must be a string")
return return
} }
res.setHeader('Content-Type', 'text/plain') res.setHeader("Content-Type", "text/plain")
res.setHeader('Cache-Control', 'public, max-age=4800') res.setHeader("Cache-Control", "public, max-age=4800")
res.status(200).write(markdown(contentToRender)) res.status(200).write(markdown(contentToRender))
res.end() res.end()
} }

View file

@ -4,14 +4,14 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
const { id, download } = req.query const { id, download } = req.query
const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, { const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, {
headers: { headers: {
'Accept': 'text/plain', Accept: "text/plain",
'x-secret-key': process.env.SECRET_KEY || '', "x-secret-key": process.env.SECRET_KEY || "",
'Authorization': `Bearer ${req.cookies['drift-token']}`, Authorization: `Bearer ${req.cookies["drift-token"]}`
} }
}) })
const json = await file.json() const json = await file.json()
res.setHeader("Content-Type", "text/plain; charset=utf-8") res.setHeader("Content-Type", "text/plain; charset=utf-8")
res.setHeader('Cache-Control', 's-maxage=86400'); res.setHeader("Cache-Control", "s-maxage=86400")
if (file.ok) { if (file.ok) {
const data = json const data = json
const { title, content } = data const { title, content } = data
@ -23,7 +23,7 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Disposition", `inline; filename="${title}"`) res.setHeader("Content-Disposition", `inline; filename="${title}"`)
} }
res.status(200).write(content, 'utf-8') res.status(200).write(content, "utf-8")
res.end() res.end()
} else { } else {
res.status(404).send("File not found") res.status(404).send("File not found")

View file

@ -1,28 +1,39 @@
import type { NextApiHandler } from "next"; import type { NextApiHandler } from "next"
import markdown from "@lib/render-markdown"; import markdown from "@lib/render-markdown"
const renderMarkdown: NextApiHandler = async (req, res) => { const renderMarkdown: NextApiHandler = async (req, res) => {
const { content, title } = req.body const { content, title } = req.body
const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', ''] const renderAsMarkdown = [
"markdown",
"md",
"mdown",
"mkdn",
"mkd",
"mdwn",
"mdtxt",
"mdtext",
"text",
""
]
const fileType = () => { const fileType = () => {
const pathParts = title.split(".") const pathParts = title.split(".")
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : "" const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
return language return language
} }
const type = fileType() const type = fileType()
let contentToRender: string = (content || ''); let contentToRender: string = content || ""
if (!renderAsMarkdown.includes(type)) { if (!renderAsMarkdown.includes(type)) {
contentToRender = `~~~${type} contentToRender = `~~~${type}
${content} ${content}
~~~` ~~~`
} else { } else {
contentToRender = '\n' + content; contentToRender = "\n" + content
} }
if (typeof contentToRender !== 'string') { if (typeof contentToRender !== "string") {
res.status(400).send('content must be a string') res.status(400).send("content must be a string")
return return
} }
res.status(200).write(markdown(contentToRender)) res.status(200).write(markdown(contentToRender))

View file

@ -0,0 +1,18 @@
{
"plugins": [
"postcss-flexbugs-fixes",
"postcss-hover-media-feature",
[
"postcss-preset-env",
{
"autoprefixer": {
"flexbox": "no-2009"
},
"stage": 3,
"features": {
"custom-properties": false
}
}
]
]
}

View file

@ -1,6 +1,5 @@
@import "./syntax.css"; @import "./syntax.css";
@import "./markdown.css"; @import "./markdown.css";
@import "./nprogress.css";
@import "./inter.css"; @import "./inter.css";
:root { :root {

View file

@ -1,23 +0,0 @@
#nprogress {
pointer-events: none;
}
#nprogress .bar {
position: fixed;
z-index: 2000;
top: 0;
left: 0;
width: 100%;
height: 5px;
background: var(--fg);
}
#nprogress::after {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 5px;
background: transparent;
}

File diff suppressed because it is too large Load diff