client: lint and minor scroll button/file explorer adjustments
This commit is contained in:
parent
887ecfabbc
commit
0815d43ee8
10 changed files with 454 additions and 427 deletions
|
@ -23,7 +23,7 @@
|
||||||
transition: var(--transition);
|
transition: var(--transition);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 2px var(--gap);
|
padding: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content li:hover,
|
.content li:hover,
|
||||||
|
@ -32,9 +32,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content li a {
|
.content li a {
|
||||||
display: block;
|
display: flex;
|
||||||
height: 100%;
|
align-items: center;
|
||||||
width: min-content;
|
text-align: center;
|
||||||
|
padding: var(--gap-half) var(--gap);
|
||||||
|
color: var(--dark-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Button, Link, Text, Popover } from '@geist-ui/core'
|
import { Button, Link, Text, Popover } from '@geist-ui/core'
|
||||||
import FileIcon from '@geist-ui/icons/fileText'
|
import FileIcon from '@geist-ui/icons/fileText'
|
||||||
import CodeIcon from '@geist-ui/icons/fileLambda'
|
import CodeIcon from '@geist-ui/icons/fileFunction'
|
||||||
import styles from './dropdown.module.css'
|
import styles from './dropdown.module.css'
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { codeFileExtensions } from "@lib/constants"
|
import { codeFileExtensions } from "@lib/constants"
|
||||||
import ChevronDown from '@geist-ui/icons/chevronDown'
|
import ChevronDown from '@geist-ui/icons/chevronDown'
|
||||||
import ShiftBy from "@components/shift-by"
|
import ShiftBy from "@components/shift-by"
|
||||||
|
@ -20,6 +20,16 @@ const FileDropdown = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [items, setItems] = useState<Item[]>([])
|
const [items, setItems] = useState<Item[]>([])
|
||||||
|
|
||||||
|
const onOpen = useCallback(() => {
|
||||||
|
setExpanded(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setExpanded(false)
|
||||||
|
// contentRef.current?.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newItems = files.map(file => {
|
const newItems = files.map(file => {
|
||||||
const extension = file.title.split('.').pop()
|
const extension = file.title.split('.').pop()
|
||||||
|
@ -40,24 +50,24 @@ const FileDropdown = ({
|
||||||
|
|
||||||
const content = useCallback(() => (<ul className={styles.content}>
|
const content = useCallback(() => (<ul className={styles.content}>
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<li key={item.id} onClick={() => setExpanded(false)}>
|
<li key={item.id} onClick={onClose}>
|
||||||
<Link color={false} href={`#${item.title}`}>
|
<a href={`#${item.title}`}>
|
||||||
<ShiftBy y={5}><span className={styles.fileIcon}>
|
<ShiftBy y={5}><span className={styles.fileIcon}>
|
||||||
{item.icon}</span></ShiftBy>
|
{item.icon}</span></ShiftBy>
|
||||||
<span className={styles.fileTitle}>{item.title ? item.title : 'Untitled'}</span>
|
<span className={styles.fileTitle}>{item.title ? item.title : 'Untitled'}</span>
|
||||||
</Link>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
), [items])
|
), [items, onClose])
|
||||||
|
|
||||||
// a list of files with an icon and a title
|
// a list of files with an icon and a title
|
||||||
return (
|
return (
|
||||||
<Button auto onClick={() => setExpanded(!expanded)} className={styles.button} iconRight={<ChevronDown />}>
|
<Button auto onClick={onOpen} className={styles.button} iconRight={<ChevronDown />}>
|
||||||
<Popover content={content} visible={expanded} trigger="click" hideArrow={true}>
|
<Popover tabIndex={0} content={content} visible={expanded} trigger="click" hideArrow={true}>
|
||||||
{files.length} {files.length === 1 ? 'file' : 'files'}
|
Jump to {files.length} {files.length === 1 ? 'file' : 'files'}
|
||||||
</Popover>
|
</Popover>
|
||||||
</Button>
|
</Button >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ const PostPage = ({ post }: Props) => {
|
||||||
{/* {!isLoading && <PostFileExplorer files={post.files} />} */}
|
{/* {!isLoading && <PostFileExplorer files={post.files} />} */}
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<span className={styles.title}>
|
<span className={styles.title}>
|
||||||
<Text h2>{post.title}</Text>
|
<Text h3>{post.title}</Text>
|
||||||
<div className={styles.badges}>
|
<div className={styles.badges}>
|
||||||
<VisibilityBadge visibility={post.visibility} />
|
<VisibilityBadge visibility={post.visibility} />
|
||||||
<Badge type="secondary"><Tooltip text={formattedTime}>{time}</Tooltip></Badge>
|
<Badge type="secondary"><Tooltip text={formattedTime}>{time}</Tooltip></Badge>
|
||||||
|
|
|
@ -11,9 +11,13 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header .title h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.header .title .badges > * {
|
.header .title .badges > * {
|
||||||
margin-left: var(--gap);
|
margin-left: var(--gap);
|
||||||
margin-bottom: var(--gap-quarter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .buttons {
|
.header .buttons {
|
||||||
|
@ -42,6 +46,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 80%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,15 @@ const ScrollToTop = () => {
|
||||||
return () => window.removeEventListener('scroll', handleScroll)
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
// blur the button
|
||||||
|
e.currentTarget.blur()
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', width: '100%', height: 24, justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', flexDirection: 'row', width: '100%', height: 24, justifyContent: 'flex-end' }}>
|
||||||
<Tooltip text="Scroll to Top" className={`${styles['scroll-up']} ${shouldShow ? styles['scroll-up-shown'] : ''}`}>
|
<Tooltip hideArrow text="Scroll to Top" className={`${styles['scroll-up']} ${shouldShow ? styles['scroll-up-shown'] : ''}`}>
|
||||||
<Button aria-label='Scroll to Top' onClick={onClick} style={{ background: 'var(--light-gray)' }} auto >
|
<Button aria-label='Scroll to Top' onClick={onClick} style={{ background: 'var(--light-gray)' }} auto >
|
||||||
<Spacer height={2 / 3} inline width={0} />
|
<Spacer height={2 / 3} inline width={0} />
|
||||||
<ChevronUp />
|
<ChevronUp />
|
||||||
|
|
|
@ -1,141 +1,141 @@
|
||||||
export const allowedFileTypes = [
|
export const allowedFileTypes = [
|
||||||
'application/json',
|
"application/json",
|
||||||
'application/x-javascript',
|
"application/x-javascript",
|
||||||
'application/xhtml+xml',
|
"application/xhtml+xml",
|
||||||
'application/xml',
|
"application/xml",
|
||||||
'text/xml',
|
"text/xml",
|
||||||
'text/plain',
|
"text/plain",
|
||||||
'text/html',
|
"text/html",
|
||||||
'text/csv',
|
"text/csv",
|
||||||
'text/tab-separated-values',
|
"text/tab-separated-values",
|
||||||
'text/x-c',
|
"text/x-c",
|
||||||
'text/x-c++',
|
"text/x-c++",
|
||||||
'text/x-csharp',
|
"text/x-csharp",
|
||||||
'text/x-java',
|
"text/x-java",
|
||||||
'text/x-javascript',
|
"text/x-javascript",
|
||||||
'text/x-php',
|
"text/x-php",
|
||||||
'text/x-python',
|
"text/x-python",
|
||||||
'text/x-ruby',
|
"text/x-ruby",
|
||||||
'text/x-scala',
|
"text/x-scala",
|
||||||
'text/x-swift',
|
"text/x-swift",
|
||||||
'text/x-typescript',
|
"text/x-typescript",
|
||||||
'text/x-vb',
|
"text/x-vb",
|
||||||
'text/x-vbscript',
|
"text/x-vbscript",
|
||||||
'text/x-yaml',
|
"text/x-yaml",
|
||||||
'text/x-c++',
|
"text/x-c++",
|
||||||
'text/x-c#',
|
"text/x-c#",
|
||||||
'text/mathml',
|
"text/mathml",
|
||||||
'text/x-markdown',
|
"text/x-markdown",
|
||||||
'text/markdown',
|
"text/markdown"
|
||||||
]
|
]
|
||||||
|
|
||||||
export const allowedFileNames = [
|
export const allowedFileNames = [
|
||||||
'Makefile',
|
"Makefile",
|
||||||
'README',
|
"README",
|
||||||
'Dockerfile',
|
"Dockerfile",
|
||||||
'Jenkinsfile',
|
"Jenkinsfile",
|
||||||
'LICENSE',
|
"LICENSE",
|
||||||
'.env',
|
".env",
|
||||||
'.gitignore',
|
".gitignore",
|
||||||
'.gitattributes',
|
".gitattributes",
|
||||||
'.env.example',
|
".env.example",
|
||||||
'.env.development',
|
".env.development",
|
||||||
'.env.production',
|
".env.production",
|
||||||
'.env.test',
|
".env.test",
|
||||||
'.env.staging',
|
".env.staging",
|
||||||
'.env.development.local',
|
".env.development.local",
|
||||||
'yarn.lock',
|
"yarn.lock",
|
||||||
'.bash',
|
".bash",
|
||||||
'.bashrc',
|
".bashrc",
|
||||||
'.bash_profile',
|
".bash_profile",
|
||||||
'.bash_logout',
|
".bash_logout",
|
||||||
'.profile',
|
".profile",
|
||||||
'.fish_prompt',
|
".fish_prompt",
|
||||||
'.zshrc',
|
".zshrc",
|
||||||
'.zsh',
|
".zsh",
|
||||||
'.zprofile',
|
".zprofile",
|
||||||
'go',
|
"go"
|
||||||
]
|
]
|
||||||
|
|
||||||
export const allowedFileExtensions = [
|
export const allowedFileExtensions = [
|
||||||
'json',
|
"json",
|
||||||
'js',
|
"js",
|
||||||
'jsx',
|
"jsx",
|
||||||
'ts',
|
"ts",
|
||||||
'tsx',
|
"tsx",
|
||||||
'c',
|
"c",
|
||||||
'cpp',
|
"cpp",
|
||||||
'c++',
|
"c++",
|
||||||
'c#',
|
"c#",
|
||||||
'java',
|
"java",
|
||||||
'php',
|
"php",
|
||||||
'py',
|
"py",
|
||||||
'rb',
|
"rb",
|
||||||
'scala',
|
"scala",
|
||||||
'swift',
|
"swift",
|
||||||
'vb',
|
"vb",
|
||||||
'vbscript',
|
"vbscript",
|
||||||
'yaml',
|
"yaml",
|
||||||
'less',
|
"less",
|
||||||
'stylus',
|
"stylus",
|
||||||
'styl',
|
"styl",
|
||||||
'sass',
|
"sass",
|
||||||
'scss',
|
"scss",
|
||||||
'lock',
|
"lock",
|
||||||
'md',
|
"md",
|
||||||
'markdown',
|
"markdown",
|
||||||
'txt',
|
"txt",
|
||||||
'html',
|
"html",
|
||||||
'htm',
|
"htm",
|
||||||
'css',
|
"css",
|
||||||
'csv',
|
"csv",
|
||||||
'log',
|
"log",
|
||||||
'sql',
|
"sql",
|
||||||
'xml',
|
"xml",
|
||||||
'webmanifest',
|
"webmanifest",
|
||||||
'vue',
|
"vue",
|
||||||
'vuex',
|
"vuex",
|
||||||
'rs',
|
"rs",
|
||||||
'zig',
|
"zig"
|
||||||
]
|
]
|
||||||
|
|
||||||
export const codeFileExtensions = [
|
export const codeFileExtensions = [
|
||||||
'json',
|
"json",
|
||||||
'js',
|
"js",
|
||||||
'jsx',
|
"jsx",
|
||||||
'ts',
|
"ts",
|
||||||
'tsx',
|
"tsx",
|
||||||
'c',
|
"c",
|
||||||
'cpp',
|
"cpp",
|
||||||
'c++',
|
"c++",
|
||||||
'c#',
|
"c#",
|
||||||
'java',
|
"java",
|
||||||
'php',
|
"php",
|
||||||
'py',
|
"py",
|
||||||
'rb',
|
"rb",
|
||||||
'scala',
|
"scala",
|
||||||
'swift',
|
"swift",
|
||||||
'vb',
|
"vb",
|
||||||
'vbscript',
|
"vbscript",
|
||||||
'yaml',
|
"yaml",
|
||||||
'less',
|
"less",
|
||||||
'stylus',
|
"stylus",
|
||||||
'styl',
|
"styl",
|
||||||
'sass',
|
"sass",
|
||||||
'scss',
|
"scss",
|
||||||
'html',
|
"html",
|
||||||
'htm',
|
"htm",
|
||||||
'css',
|
"css",
|
||||||
'asm',
|
"asm",
|
||||||
's',
|
"s",
|
||||||
'sh',
|
"sh",
|
||||||
'bat',
|
"bat",
|
||||||
'cmd',
|
"cmd",
|
||||||
'sql',
|
"sql",
|
||||||
'xml',
|
"xml",
|
||||||
'rust',
|
"rust",
|
||||||
'h',
|
"h",
|
||||||
'zig',
|
"zig",
|
||||||
'rs',
|
"rs",
|
||||||
'go'
|
"go"
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
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 } = req.query
|
const { id } = req.query
|
||||||
const file = await fetch(`${process.env.API_URL}/files/html/${id}`, {
|
const file = await fetch(`${process.env.API_URL}/files/html/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
"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"]}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (file.ok) {
|
if (file.ok) {
|
||||||
const json = await file.text()
|
const json = await file.text()
|
||||||
const data = json
|
const data = json
|
||||||
// serve the file raw as plain text
|
// serve the file raw as plain text
|
||||||
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")
|
||||||
res.status(200).write(data, "utf-8")
|
res.status(200).write(data, "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
|
||||||
|
|
|
@ -11,8 +11,7 @@ const renderMarkdown: NextApiHandler = async (req, res) => {
|
||||||
Authorization: `Bearer ${req.cookies["drift-token"]}`
|
Authorization: `Bearer ${req.cookies["drift-token"]}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (file.status
|
if (file.status !== 200) {
|
||||||
!== 200) {
|
|
||||||
return res.status(404).json({ error: "File not found" })
|
return res.status(404).json({ error: "File not found" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,26 +16,26 @@ app.use("/posts", posts)
|
||||||
app.use("/users", users)
|
app.use("/users", users)
|
||||||
app.use("/files", files)
|
app.use("/files", files)
|
||||||
|
|
||||||
app.get('/welcome', secretKey, (req, res) => {
|
app.get("/welcome", secretKey, (req, res) => {
|
||||||
const introContent = process.env.WELCOME_CONTENT;
|
const introContent = process.env.WELCOME_CONTENT
|
||||||
const introTitle = process.env.WELCOME_TITLE;
|
const introTitle = process.env.WELCOME_TITLE
|
||||||
|
|
||||||
if (!introContent || !introTitle) {
|
if (!introContent || !introTitle) {
|
||||||
return res.status(500).json({ error: 'Missing welcome content' });
|
return res.status(500).json({ error: "Missing welcome content" })
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
title: introTitle,
|
title: introTitle,
|
||||||
content: introContent,
|
content: introContent,
|
||||||
rendered: markdown(introContent)
|
rendered: markdown(introContent)
|
||||||
});
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(errors())
|
app.use(errors())
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
errorhandler({
|
errorhandler({
|
||||||
debug: process.env.ENV !== "production",
|
debug: process.env.ENV !== "production",
|
||||||
log: true
|
log: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,275 +12,285 @@ import { Op } from "sequelize"
|
||||||
export const posts = Router()
|
export const posts = Router()
|
||||||
|
|
||||||
const postVisibilitySchema = (value: string) => {
|
const postVisibilitySchema = (value: string) => {
|
||||||
if (value === "public" || value === "private" ||
|
if (
|
||||||
value === "unlisted" || value === "protected") {
|
value === "public" ||
|
||||||
return value
|
value === "private" ||
|
||||||
} else {
|
value === "unlisted" ||
|
||||||
throw new Error("Invalid post visibility")
|
value === "protected"
|
||||||
}
|
) {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid post visibility")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
posts.post(
|
posts.post(
|
||||||
"/create",
|
"/create",
|
||||||
jwt,
|
jwt,
|
||||||
celebrate({
|
celebrate({
|
||||||
body: {
|
body: {
|
||||||
title: Joi.string().required().allow("", null),
|
title: Joi.string().required().allow("", null),
|
||||||
files: Joi.any().required(),
|
files: Joi.any().required(),
|
||||||
visibility: Joi.string()
|
visibility: Joi.string()
|
||||||
.custom(postVisibilitySchema, "valid visibility")
|
.custom(postVisibilitySchema, "valid visibility")
|
||||||
.required(),
|
.required(),
|
||||||
userId: Joi.string().required(),
|
userId: Joi.string().required(),
|
||||||
password: Joi.string().optional()
|
password: Joi.string().optional()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
async (req, res, next) => {
|
async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
let hashedPassword: string = ""
|
let hashedPassword: string = ""
|
||||||
if (req.body.visibility === "protected") {
|
if (req.body.visibility === "protected") {
|
||||||
hashedPassword = crypto
|
hashedPassword = crypto
|
||||||
.createHash("sha256")
|
.createHash("sha256")
|
||||||
.update(req.body.password)
|
.update(req.body.password)
|
||||||
.digest("hex")
|
.digest("hex")
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPost = new Post({
|
const newPost = new Post({
|
||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
visibility: req.body.visibility,
|
visibility: req.body.visibility,
|
||||||
password: hashedPassword
|
password: hashedPassword
|
||||||
})
|
})
|
||||||
|
|
||||||
await newPost.save()
|
await newPost.save()
|
||||||
await newPost.$add("users", req.body.userId)
|
await newPost.$add("users", req.body.userId)
|
||||||
const newFiles = await Promise.all(
|
const newFiles = await Promise.all(
|
||||||
req.body.files.map(async (file) => {
|
req.body.files.map(async (file) => {
|
||||||
const html = getHtmlFromFile(file)
|
const html = getHtmlFromFile(file)
|
||||||
const newFile = new File({
|
const newFile = new File({
|
||||||
title: file.title || "",
|
title: file.title || "",
|
||||||
content: file.content,
|
content: file.content,
|
||||||
sha: crypto
|
sha: crypto
|
||||||
.createHash("sha256")
|
.createHash("sha256")
|
||||||
.update(file.content)
|
.update(file.content)
|
||||||
.digest("hex")
|
.digest("hex")
|
||||||
.toString(),
|
.toString(),
|
||||||
html
|
html
|
||||||
})
|
})
|
||||||
|
|
||||||
await newFile.$set("user", req.body.userId)
|
await newFile.$set("user", req.body.userId)
|
||||||
await newFile.$set("post", newPost.id)
|
await newFile.$set("post", newPost.id)
|
||||||
await newFile.save()
|
await newFile.save()
|
||||||
return newFile
|
return newFile
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
newFiles.map((file) => {
|
newFiles.map((file) => {
|
||||||
newPost.$add("files", file.id)
|
newPost.$add("files", file.id)
|
||||||
newPost.save()
|
newPost.save()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
res.json(newPost)
|
res.json(newPost)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e)
|
next(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
posts.get("/", secretKey, async (req, res, next) => {
|
posts.get("/", secretKey, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const posts = await Post.findAll({
|
const posts = await Post.findAll({
|
||||||
attributes: ["id", "title", "visibility", "createdAt"]
|
attributes: ["id", "title", "visibility", "createdAt"]
|
||||||
})
|
})
|
||||||
|
|
||||||
res.json(posts)
|
res.json(posts)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e)
|
next(e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
posts.get("/mine", jwt,
|
posts.get(
|
||||||
celebrate({
|
"/mine",
|
||||||
query: {
|
jwt,
|
||||||
page: Joi.number().integer().min(1).default(1).optional()
|
celebrate({
|
||||||
}
|
query: {
|
||||||
}),
|
page: Joi.number().integer().min(1).default(1).optional()
|
||||||
async (req: UserJwtRequest, res, next) => {
|
}
|
||||||
if (!req.user) {
|
}),
|
||||||
return res.status(401).json({ error: "Unauthorized" })
|
async (req: UserJwtRequest, res, next) => {
|
||||||
}
|
if (!req.user) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized" })
|
||||||
|
}
|
||||||
|
|
||||||
const { page } = req.query
|
const { page } = req.query
|
||||||
const parsedPage = page ? parseInt(page as string) : 1
|
const parsedPage = page ? parseInt(page as string) : 1
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await User.findByPk(req.user.id, {
|
const user = await User.findByPk(req.user.id, {
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Post,
|
model: Post,
|
||||||
as: "posts",
|
as: "posts",
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: File,
|
model: File,
|
||||||
as: "files",
|
as: "files",
|
||||||
attributes: ["id", "title", "createdAt"]
|
attributes: ["id", "title", "createdAt"]
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
attributes: ["id", "title", "visibility", "createdAt"]
|
attributes: ["id", "title", "visibility", "createdAt"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({ error: "User not found" })
|
return res.status(404).json({ error: "User not found" })
|
||||||
}
|
}
|
||||||
return res.json(
|
return res.json(
|
||||||
user.posts?.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).slice((parsedPage - 1) * 10, parsedPage * 10)
|
user.posts
|
||||||
)
|
?.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||||
} catch (error) {
|
.slice((parsedPage - 1) * 10, parsedPage * 10)
|
||||||
next(error)
|
)
|
||||||
}
|
} catch (error) {
|
||||||
})
|
next(error)
|
||||||
|
}
|
||||||
posts.get("/search",
|
}
|
||||||
jwt,
|
|
||||||
celebrate({
|
|
||||||
query: {
|
|
||||||
q: Joi.string().required()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
async (req, res, next) => {
|
|
||||||
const { q } = req.query
|
|
||||||
if (typeof q !== "string") {
|
|
||||||
return res.status(400).json({ error: "Invalid query" })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const posts = await Post.findAll({
|
|
||||||
where: {
|
|
||||||
[Op.or]: [
|
|
||||||
{ title: { [Op.like]: `%${q}%` } },
|
|
||||||
{ "$files.title$": { [Op.like]: `%${q}%` } },
|
|
||||||
{ "$files.content$": { [Op.like]: `%${q}%` } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: File,
|
|
||||||
as: "files",
|
|
||||||
attributes: ["id", "title"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
attributes: ["id", "title", "visibility", "createdAt"],
|
|
||||||
order: [["createdAt", "DESC"]]
|
|
||||||
})
|
|
||||||
|
|
||||||
res.json(posts)
|
|
||||||
} catch (e) {
|
|
||||||
next(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
posts.get(
|
posts.get(
|
||||||
"/:id",
|
"/search",
|
||||||
celebrate({
|
jwt,
|
||||||
params: {
|
celebrate({
|
||||||
id: Joi.string().required()
|
query: {
|
||||||
}
|
q: Joi.string().required()
|
||||||
}),
|
}
|
||||||
async (req: UserJwtRequest, res, next) => {
|
}),
|
||||||
try {
|
async (req, res, next) => {
|
||||||
const post = await Post.findByPk(req.params.id, {
|
const { q } = req.query
|
||||||
include: [
|
if (typeof q !== "string") {
|
||||||
{
|
return res.status(400).json({ error: "Invalid query" })
|
||||||
model: File,
|
}
|
||||||
as: "files",
|
|
||||||
attributes: [
|
|
||||||
"id",
|
|
||||||
"title",
|
|
||||||
"content",
|
|
||||||
"sha",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "users",
|
|
||||||
attributes: ["id", "username"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!post) {
|
try {
|
||||||
return res.status(404).json({ error: "Post not found" })
|
const posts = await Post.findAll({
|
||||||
}
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ title: { [Op.like]: `%${q}%` } },
|
||||||
|
{ "$files.title$": { [Op.like]: `%${q}%` } },
|
||||||
|
{ "$files.content$": { [Op.like]: `%${q}%` } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: File,
|
||||||
|
as: "files",
|
||||||
|
attributes: ["id", "title"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attributes: ["id", "title", "visibility", "createdAt"],
|
||||||
|
order: [["createdAt", "DESC"]]
|
||||||
|
})
|
||||||
|
|
||||||
// if public or unlisted, cache
|
res.json(posts)
|
||||||
if (post.visibility === "public" || post.visibility === "unlisted") {
|
} catch (e) {
|
||||||
res.set("Cache-Control", "public, max-age=4800")
|
next(e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (post.visibility === "public" || post?.visibility === "unlisted") {
|
posts.get(
|
||||||
secretKey(req, res, () => {
|
"/:id",
|
||||||
res.json(post)
|
celebrate({
|
||||||
})
|
params: {
|
||||||
} else if (post.visibility === "private") {
|
id: Joi.string().required()
|
||||||
jwt(req as UserJwtRequest, res, () => {
|
}
|
||||||
res.json(post)
|
}),
|
||||||
})
|
async (req: UserJwtRequest, res, next) => {
|
||||||
} else if (post.visibility === "protected") {
|
try {
|
||||||
const { password } = req.query
|
const post = await Post.findByPk(req.params.id, {
|
||||||
if (!password || typeof password !== "string") {
|
include: [
|
||||||
return jwt(req as UserJwtRequest, res, () => {
|
{
|
||||||
res.json(post)
|
model: File,
|
||||||
})
|
as: "files",
|
||||||
}
|
attributes: [
|
||||||
const hash = crypto
|
"id",
|
||||||
.createHash("sha256")
|
"title",
|
||||||
.update(password)
|
"content",
|
||||||
.digest("hex")
|
"sha",
|
||||||
.toString()
|
"createdAt",
|
||||||
if (hash !== post.password) {
|
"updatedAt"
|
||||||
return res.status(400).json({ error: "Incorrect password." })
|
]
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
attributes: ["id", "username"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
res.json(post)
|
if (!post) {
|
||||||
}
|
return res.status(404).json({ error: "Post not found" })
|
||||||
} catch (e) {
|
}
|
||||||
next(e)
|
|
||||||
}
|
// if public or unlisted, cache
|
||||||
}
|
if (post.visibility === "public" || post.visibility === "unlisted") {
|
||||||
|
res.set("Cache-Control", "public, max-age=4800")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.visibility === "public" || post?.visibility === "unlisted") {
|
||||||
|
secretKey(req, res, () => {
|
||||||
|
res.json(post)
|
||||||
|
})
|
||||||
|
} else if (post.visibility === "private") {
|
||||||
|
jwt(req as UserJwtRequest, res, () => {
|
||||||
|
res.json(post)
|
||||||
|
})
|
||||||
|
} else if (post.visibility === "protected") {
|
||||||
|
const { password } = req.query
|
||||||
|
if (!password || typeof password !== "string") {
|
||||||
|
return jwt(req as UserJwtRequest, res, () => {
|
||||||
|
res.json(post)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const hash = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(password)
|
||||||
|
.digest("hex")
|
||||||
|
.toString()
|
||||||
|
if (hash !== post.password) {
|
||||||
|
return res.status(400).json({ error: "Incorrect password." })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(post)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
next(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
function getHtmlFromFile(file: any) {
|
function getHtmlFromFile(file: any) {
|
||||||
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 = file.title.split(".")
|
const pathParts = file.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 = file.content || ""
|
let contentToRender: string = file.content || ""
|
||||||
|
|
||||||
if (!renderAsMarkdown.includes(type)) {
|
if (!renderAsMarkdown.includes(type)) {
|
||||||
contentToRender = `~~~${type}
|
contentToRender = `~~~${type}
|
||||||
${file.content}
|
${file.content}
|
||||||
~~~`
|
~~~`
|
||||||
} else {
|
} else {
|
||||||
contentToRender = "\n" + file.content
|
contentToRender = "\n" + file.content
|
||||||
}
|
}
|
||||||
const html = markdown(contentToRender)
|
const html = markdown(contentToRender)
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue