client: add prettier, switch to preact
This commit is contained in:
parent
9bdff8f28f
commit
d4120e6f41
19 changed files with 1482 additions and 299 deletions
6
client/.prettierrc
Normal file
6
client/.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 80
|
||||
}
|
|
@ -1,30 +1,39 @@
|
|||
export default function generateUUID() {
|
||||
if (typeof crypto === 'object') {
|
||||
if (typeof crypto.randomUUID === 'function') {
|
||||
if (typeof crypto === "object") {
|
||||
if (typeof crypto.randomUUID === "function") {
|
||||
// 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
|
||||
const callback = (c: string) => {
|
||||
const num = Number(c);
|
||||
return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(16);
|
||||
};
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback);
|
||||
const num = Number(c)
|
||||
return (
|
||||
num ^
|
||||
(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 perforNow = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0;
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
let random = Math.random() * 16;
|
||||
let timestamp = new Date().getTime()
|
||||
let perforNow =
|
||||
(typeof performance !== "undefined" &&
|
||||
performance.now &&
|
||||
performance.now() * 1000) ||
|
||||
0
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
let random = Math.random() * 16
|
||||
if (timestamp > 0) {
|
||||
random = (timestamp + random) % 16 | 0;
|
||||
timestamp = Math.floor(timestamp / 16);
|
||||
random = (timestamp + random) % 16 | 0
|
||||
timestamp = Math.floor(timestamp / 16)
|
||||
} else {
|
||||
random = (perforNow + random) % 16 | 0;
|
||||
perforNow = Math.floor(perforNow / 16);
|
||||
random = (perforNow + random) % 16 | 0
|
||||
perforNow = Math.floor(perforNow / 16)
|
||||
}
|
||||
return (c === "x" ? random : (random & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
}
|
||||
return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,32 +1,35 @@
|
|||
import Cookies from "js-cookie";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import useSharedState from "./use-shared-state";
|
||||
import Cookies from "js-cookie"
|
||||
import { useRouter } from "next/router"
|
||||
import { useEffect, useState } from "react"
|
||||
import useSharedState from "./use-shared-state"
|
||||
|
||||
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 router = useRouter();
|
||||
const router = useRouter()
|
||||
const signin = (token: string) => {
|
||||
setSignedIn(true);
|
||||
Cookies.set("drift-token", token);
|
||||
setSignedIn(true)
|
||||
Cookies.set("drift-token", token)
|
||||
}
|
||||
|
||||
const signout = () => {
|
||||
setSignedIn(false);
|
||||
Cookies.remove("drift-token");
|
||||
router.push("/");
|
||||
setSignedIn(false)
|
||||
Cookies.remove("drift-token")
|
||||
router.push("/")
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
setSignedIn(true);
|
||||
setSignedIn(true)
|
||||
} 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
|
||||
|
|
|
@ -3,20 +3,20 @@ import useSharedState from "./use-shared-state"
|
|||
|
||||
const useTheme = () => {
|
||||
const isClient = typeof window === "object"
|
||||
const [themeType, setThemeType] = useSharedState<string>('theme', 'light')
|
||||
const [themeType, setThemeType] = useSharedState<string>("theme", "light")
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClient) return
|
||||
const storedTheme = localStorage.getItem('drift-theme')
|
||||
const storedTheme = localStorage.getItem("drift-theme")
|
||||
if (storedTheme) {
|
||||
setThemeType(storedTheme)
|
||||
}
|
||||
}, [isClient, setThemeType])
|
||||
|
||||
const changeTheme = useCallback(() => {
|
||||
setThemeType(last => {
|
||||
const newTheme = last === 'dark' ? 'light' : 'dark'
|
||||
localStorage.setItem('drift-theme', newTheme)
|
||||
setThemeType((last) => {
|
||||
const newTheme = last === "dark" ? "light" : "dark"
|
||||
localStorage.setItem("drift-theme", newTheme)
|
||||
return newTheme
|
||||
})
|
||||
}, [setThemeType])
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useRef, useEffect } from "react";
|
||||
import { useRef, useEffect } from "react"
|
||||
|
||||
function useTraceUpdate(props: { [key: string]: any }) {
|
||||
const prev = useRef(props)
|
||||
|
@ -10,10 +10,10 @@ function useTraceUpdate(props: { [key: string]: any }) {
|
|||
return ps
|
||||
}, {} as { [key: string]: any })
|
||||
if (Object.keys(changedProps).length > 0) {
|
||||
console.log('Changed props:', changedProps)
|
||||
console.log("Changed props:", changedProps)
|
||||
}
|
||||
prev.current = props
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export default useTraceUpdate
|
|
@ -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
|
||||
|
||||
const epochs = [
|
||||
['year', 31536000],
|
||||
['month', 2592000],
|
||||
['day', 86400],
|
||||
['hour', 3600],
|
||||
['minute', 60],
|
||||
['second', 1]
|
||||
] as const;
|
||||
["year", 31536000],
|
||||
["month", 2592000],
|
||||
["day", 86400],
|
||||
["hour", 3600],
|
||||
["minute", 60],
|
||||
["second", 1]
|
||||
] as const
|
||||
|
||||
// Get duration
|
||||
const getDuration = (timeAgoInSeconds: number) => {
|
||||
for (let [name, seconds] of epochs) {
|
||||
const interval = Math.floor(timeAgoInSeconds / seconds);
|
||||
const interval = Math.floor(timeAgoInSeconds / seconds)
|
||||
|
||||
if (interval >= 1) {
|
||||
return {
|
||||
interval: interval,
|
||||
epoch: name
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
interval: 0,
|
||||
epoch: 'second'
|
||||
epoch: "second"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate
|
||||
const timeAgo = (date: Date) => {
|
||||
const timeAgoInSeconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000);
|
||||
const { interval, epoch } = getDuration(timeAgoInSeconds);
|
||||
const suffix = interval === 1 ? '' : 's';
|
||||
const timeAgoInSeconds = Math.floor(
|
||||
(new Date().getTime() - new Date(date).getTime()) / 1000
|
||||
)
|
||||
const { interval, epoch } = getDuration(timeAgoInSeconds)
|
||||
const suffix = interval === 1 ? "" : "s"
|
||||
|
||||
return `${interval} ${epoch}${suffix} ago`;
|
||||
};
|
||||
return `${interval} ${epoch}${suffix} ago`
|
||||
}
|
||||
|
||||
export default timeAgo
|
||||
|
|
2
client/lib/types.d.ts
vendored
2
client/lib/types.d.ts
vendored
|
@ -1,7 +1,7 @@
|
|||
export type PostVisibility = "unlisted" | "private" | "public" | "protected"
|
||||
|
||||
export type ThemeProps = {
|
||||
theme: "light" | "dark" | string,
|
||||
theme: "light" | "dark" | string
|
||||
changeTheme: () => void
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +1,39 @@
|
|||
import dotenv from "dotenv";
|
||||
import bundleAnalyzer from "@next/bundle-analyzer";
|
||||
import dotenv from "dotenv"
|
||||
import bundleAnalyzer from "@next/bundle-analyzer"
|
||||
|
||||
dotenv.config();
|
||||
dotenv.config()
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
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() {
|
||||
return [
|
||||
{
|
||||
source: "/server-api/:path*",
|
||||
destination: `${process.env.API_URL}/:path*`,
|
||||
destination: `${process.env.API_URL}/:path*`
|
||||
},
|
||||
{
|
||||
source: "/file/raw/:id",
|
||||
destination: `/api/raw/:id`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
destination: `/api/raw/:id`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(
|
||||
nextConfig
|
||||
);
|
||||
)
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
"dev": "next dev --port 3001",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"analyze": "ANALYZE=true next build"
|
||||
"lint": "next lint && prettier --config .prettierrc '{components,lib,pages}/**/*.ts' --write",
|
||||
"analyze": "ANALYZE=true next build",
|
||||
"find:unused": "next-unused"
|
||||
},
|
||||
"dependencies": {
|
||||
"@geist-ui/core": "^2.3.5",
|
||||
|
@ -18,10 +19,13 @@
|
|||
"cookie": "^0.4.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"marked": "^4.0.12",
|
||||
"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",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
|
@ -46,7 +50,20 @@
|
|||
"@types/react-syntax-highlighter": "^13.5.2",
|
||||
"eslint": "8.10.0",
|
||||
"eslint-config-next": "^12.1.1-canary.16",
|
||||
"next-unused": "^0.0.6",
|
||||
"prettier": "^2.6.0",
|
||||
"typescript": "4.6.2",
|
||||
"typescript-plugin-css-modules": "^3.4.0"
|
||||
},
|
||||
"next-unused": {
|
||||
"alias": {
|
||||
"@components": "components/",
|
||||
"@lib": "lib/",
|
||||
"@styles": "styles/"
|
||||
},
|
||||
"include": [
|
||||
"components",
|
||||
"lib"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,23 +7,6 @@ import Head from 'next/head';
|
|||
import useTheme from '@lib/hooks/use-theme';
|
||||
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> = {
|
||||
pageProps: P;
|
||||
} & Omit<NextAppProps<P>, "pageProps">;
|
||||
|
|
|
@ -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 { id } = req.query
|
||||
const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, {
|
||||
headers: {
|
||||
'Accept': 'text/plain',
|
||||
'x-secret-key': process.env.SECRET_KEY || '',
|
||||
'Authorization': `Bearer ${req.cookies['drift-token']}`,
|
||||
Accept: "text/plain",
|
||||
"x-secret-key": process.env.SECRET_KEY || "",
|
||||
Authorization: `Bearer ${req.cookies["drift-token"]}`
|
||||
}
|
||||
})
|
||||
|
||||
const json = await file.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 pathParts = title.split(".")
|
||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
||||
return language
|
||||
}
|
||||
const type = fileType()
|
||||
let contentToRender: string = '\n' + content;
|
||||
let contentToRender: string = "\n" + content
|
||||
|
||||
if (!renderAsMarkdown.includes(type)) {
|
||||
contentToRender = `~~~${type}
|
||||
|
@ -29,13 +40,13 @@ ${content}
|
|||
~~~`
|
||||
}
|
||||
|
||||
if (typeof contentToRender !== 'string') {
|
||||
res.status(400).send('content must be a string')
|
||||
if (typeof contentToRender !== "string") {
|
||||
res.status(400).send("content must be a string")
|
||||
return
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
res.setHeader('Cache-Control', 'public, max-age=4800')
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
res.setHeader("Cache-Control", "public, max-age=4800")
|
||||
res.status(200).write(markdown(contentToRender))
|
||||
res.end()
|
||||
}
|
||||
|
|
|
@ -4,14 +4,14 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
const { id, download } = req.query
|
||||
const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, {
|
||||
headers: {
|
||||
'Accept': 'text/plain',
|
||||
'x-secret-key': process.env.SECRET_KEY || '',
|
||||
'Authorization': `Bearer ${req.cookies['drift-token']}`,
|
||||
Accept: "text/plain",
|
||||
"x-secret-key": process.env.SECRET_KEY || "",
|
||||
Authorization: `Bearer ${req.cookies["drift-token"]}`
|
||||
}
|
||||
})
|
||||
const json = await file.json()
|
||||
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) {
|
||||
const data = json
|
||||
const { title, content } = data
|
||||
|
@ -23,7 +23,7 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
res.setHeader("Content-Disposition", `inline; filename="${title}"`)
|
||||
}
|
||||
|
||||
res.status(200).write(content, 'utf-8')
|
||||
res.status(200).write(content, "utf-8")
|
||||
res.end()
|
||||
} else {
|
||||
res.status(404).send("File not found")
|
||||
|
|
|
@ -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 { 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 pathParts = title.split(".")
|
||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
||||
return language
|
||||
}
|
||||
const type = fileType()
|
||||
let contentToRender: string = (content || '');
|
||||
let contentToRender: string = content || ""
|
||||
|
||||
if (!renderAsMarkdown.includes(type)) {
|
||||
contentToRender = `~~~${type}
|
||||
${content}
|
||||
~~~`
|
||||
} else {
|
||||
contentToRender = '\n' + content;
|
||||
contentToRender = "\n" + content
|
||||
}
|
||||
|
||||
if (typeof contentToRender !== 'string') {
|
||||
res.status(400).send('content must be a string')
|
||||
if (typeof contentToRender !== "string") {
|
||||
res.status(400).send("content must be a string")
|
||||
return
|
||||
}
|
||||
res.status(200).write(markdown(contentToRender))
|
||||
|
|
18
client/postcss.config.json
Normal file
18
client/postcss.config.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
@import "./syntax.css";
|
||||
@import "./markdown.css";
|
||||
@import "./nprogress.css";
|
||||
@import "./inter.css";
|
||||
|
||||
:root {
|
||||
|
|
|
@ -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;
|
||||
}
|
1187
client/yarn.lock
1187
client/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue