client: lint with useTabs
This commit is contained in:
parent
48a8e9f6a9
commit
60f2ab99b3
13 changed files with 241 additions and 240 deletions
|
@ -2,5 +2,6 @@
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"printWidth": 80
|
"printWidth": 80,
|
||||||
|
"useTabs": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ChangeEvent, memo, useCallback, useEffect, useState } from 'react'
|
import { ChangeEvent, memo, useEffect, useState } from 'react'
|
||||||
import { Text } from '@geist-ui/core'
|
import { Text } from '@geist-ui/core'
|
||||||
|
|
||||||
import ShiftBy from '@components/shift-by'
|
import ShiftBy from '@components/shift-by'
|
||||||
|
|
|
@ -1,39 +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 (
|
if (
|
||||||
typeof crypto.getRandomValues === "function" &&
|
typeof crypto.getRandomValues === "function" &&
|
||||||
typeof Uint8Array === "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 (
|
return (
|
||||||
num ^
|
num ^
|
||||||
(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()
|
||||||
let perforNow =
|
let perforNow =
|
||||||
(typeof performance !== "undefined" &&
|
(typeof performance !== "undefined" &&
|
||||||
performance.now &&
|
performance.now &&
|
||||||
performance.now() * 1000) ||
|
performance.now() * 1000) ||
|
||||||
0
|
0
|
||||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||||
let random = Math.random() * 16
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import type { PostVisibility } from "./types"
|
import type { PostVisibility } from "./types"
|
||||||
|
|
||||||
export default function getPostPath(visibility: PostVisibility, id: string) {
|
export default function getPostPath(visibility: PostVisibility, id: string) {
|
||||||
switch (visibility) {
|
switch (visibility) {
|
||||||
case "private":
|
case "private":
|
||||||
return `/post/private/${id}`
|
return `/post/private/${id}`
|
||||||
case "protected":
|
case "protected":
|
||||||
return `/post/protected/${id}`
|
return `/post/protected/${id}`
|
||||||
case "unlisted":
|
case "unlisted":
|
||||||
case "public":
|
case "public":
|
||||||
return `/post/${id}`
|
return `/post/${id}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ import useSWR from "swr"
|
||||||
|
|
||||||
// https://2020.paco.me/blog/shared-hook-state-with-swr
|
// https://2020.paco.me/blog/shared-hook-state-with-swr
|
||||||
const useSharedState = <T>(key: string, initial?: T) => {
|
const useSharedState = <T>(key: string, initial?: T) => {
|
||||||
const { data: state, mutate: setState } = useSWR(key, {
|
const { data: state, mutate: setState } = useSWR(key, {
|
||||||
fallbackData: initial
|
fallbackData: initial
|
||||||
})
|
})
|
||||||
return [state, setState] as const
|
return [state, setState] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useSharedState
|
export default useSharedState
|
||||||
|
|
|
@ -4,32 +4,32 @@ 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(
|
const [signedIn, setSignedIn] = useSharedState(
|
||||||
"signedIn",
|
"signedIn",
|
||||||
typeof window === "undefined" ? false : !!Cookies.get("drift-token")
|
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
|
||||||
|
|
|
@ -2,26 +2,26 @@ import { useCallback, useEffect } from "react"
|
||||||
import useSharedState from "./use-shared-state"
|
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])
|
||||||
|
|
||||||
return { theme: themeType, changeTheme }
|
return { theme: themeType, changeTheme }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useTheme
|
export default useTheme
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
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)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
|
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
|
||||||
if (prev.current[k] !== v) {
|
if (prev.current[k] !== v) {
|
||||||
ps[k] = [prev.current[k], v]
|
ps[k] = [prev.current[k], v]
|
||||||
}
|
}
|
||||||
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
|
||||||
|
|
|
@ -2,42 +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(
|
const timeAgoInSeconds = Math.floor(
|
||||||
(new Date().getTime() - new Date(date).getTime()) / 1000
|
(new Date().getTime() - new Date(date).getTime()) / 1000
|
||||||
)
|
)
|
||||||
const { interval, epoch } = getDuration(timeAgoInSeconds)
|
const { interval, epoch } = getDuration(timeAgoInSeconds)
|
||||||
const suffix = interval === 1 ? "" : "s"
|
const suffix = interval === 1 ? "" : "s"
|
||||||
|
|
||||||
return `${interval} ${epoch}${suffix} ago`
|
return `${interval} ${epoch}${suffix} ago`
|
||||||
}
|
}
|
||||||
|
|
||||||
export default timeAgo
|
export default timeAgo
|
||||||
|
|
28
client/lib/types.d.ts
vendored
28
client/lib/types.d.ts
vendored
|
@ -1,29 +1,29 @@
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Document = {
|
export type Document = {
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type File = {
|
export type File = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
html: string
|
html: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Files = File[]
|
type Files = File[]
|
||||||
|
|
||||||
export type Post = {
|
export type Post = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
visibility: PostVisibility
|
visibility: PostVisibility
|
||||||
files: Files
|
files: Files
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,52 +3,52 @@ 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 = [
|
const renderAsMarkdown = [
|
||||||
"markdown",
|
"markdown",
|
||||||
"md",
|
"md",
|
||||||
"mdown",
|
"mdown",
|
||||||
"mkdn",
|
"mkdn",
|
||||||
"mkd",
|
"mkd",
|
||||||
"mdwn",
|
"mdwn",
|
||||||
"mdtxt",
|
"mdtxt",
|
||||||
"mdtext",
|
"mdtext",
|
||||||
"text",
|
"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}
|
||||||
${content}
|
${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()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default renderMarkdown
|
export default renderMarkdown
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
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
|
||||||
// serve the file raw as plain text
|
// serve the file raw as plain text
|
||||||
|
|
||||||
if (download) {
|
if (download) {
|
||||||
res.setHeader("Content-Disposition", `attachment; filename="${title}"`)
|
res.setHeader("Content-Disposition", `attachment; filename="${title}"`)
|
||||||
} else {
|
} else {
|
||||||
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getRawFile
|
export default getRawFile
|
||||||
|
|
|
@ -3,41 +3,41 @@ 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 = [
|
const renderAsMarkdown = [
|
||||||
"markdown",
|
"markdown",
|
||||||
"md",
|
"md",
|
||||||
"mdown",
|
"mdown",
|
||||||
"mkdn",
|
"mkdn",
|
||||||
"mkd",
|
"mkd",
|
||||||
"mdwn",
|
"mdwn",
|
||||||
"mdtxt",
|
"mdtxt",
|
||||||
"mdtext",
|
"mdtext",
|
||||||
"text",
|
"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))
|
||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default renderMarkdown
|
export default renderMarkdown
|
||||||
|
|
Loading…
Reference in a new issue