From 9bdff8f28f712061c3955e394c73fe10493e241f Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Wed, 23 Mar 2022 12:36:29 -0700 Subject: [PATCH] client/server: load rendered html after page load, fix raw exporting --- client/components/header/header.tsx | 144 +++++++++++++++------- client/components/post-page/index.tsx | 3 +- client/components/preview/html.tsx | 20 --- client/components/preview/index.tsx | 4 +- client/components/view-document/index.tsx | 8 +- client/lib/hooks/use-signed-in.ts | 10 +- client/pages/api/raw/[id].ts | 10 +- client/styles/Home.module.css | 2 +- server/src/lib/models/File.ts | 3 +- server/src/lib/models/Post.ts | 3 +- server/src/lib/models/PostAuthor.ts | 3 +- server/src/lib/models/User.ts | 3 +- server/src/routes/files.ts | 24 ++++ server/src/routes/posts.ts | 2 +- server/src/server.ts | 2 +- 15 files changed, 154 insertions(+), 87 deletions(-) delete mode 100644 client/components/preview/html.tsx diff --git a/client/components/header/header.tsx b/client/components/header/header.tsx index 4f07b548..0e89b226 100644 --- a/client/components/header/header.tsx +++ b/client/components/header/header.tsx @@ -16,7 +16,6 @@ import NewIcon from '@geist-ui/icons/plusCircle'; import YourIcon from '@geist-ui/icons/list' import MoonIcon from '@geist-ui/icons/moon'; import SunIcon from '@geist-ui/icons/sun'; -import type { ThemeProps } from "@lib/types"; import useTheme from "@lib/hooks/use-theme"; import { Button } from "@geist-ui/core"; @@ -36,7 +35,7 @@ const Header = () => { const [expanded, setExpanded] = useState(false) const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true }) const isMobile = useMediaQuery('xs', { match: 'down' }) - const { signedIn: isSignedIn } = useSignedIn() + const { signedIn: isSignedIn, signout } = useSignedIn() const [pages, setPages] = useState([]) const { changeTheme, theme } = useTheme() useEffect(() => { @@ -50,49 +49,7 @@ const Header = () => { }, [isMobile]) useEffect(() => { - const pageList: Tab[] = [ - { - name: "Home", - href: "/", - icon: , - condition: !isSignedIn, - value: "home" - }, - { - name: "New", - href: "/new", - icon: , - condition: isSignedIn, - value: "new" - }, - { - name: "Yours", - href: "/mine", - icon: , - condition: isSignedIn, - value: "mine" - }, - { - name: "Sign out", - href: "/signout", - icon: , - condition: isSignedIn, - value: "signout" - }, - { - name: "Sign in", - href: "/signin", - icon: , - condition: !isSignedIn, - value: "signin" - }, - { - name: "Sign up", - href: "/signup", - icon: , - condition: !isSignedIn, - value: "signup" - }, + const defaultPages: Tab[] = [ { name: isMobile ? "GitHub" : "", href: "https://github.com/maxleiter/drift", @@ -113,9 +70,104 @@ const Header = () => { } ] - setPages(pageList.filter(page => page.condition)) + if (isSignedIn) + setPages([ + { + name: 'home', + icon: , + value: 'home', + href: '/' + }, + { + name: 'yours', + icon: , + value: 'yours', + href: '/mine' + }, + { + name: 'sign out', + icon: , + value: 'signout', + onClick: signout + }, + ...defaultPages + ]) + else + setPages([ + { + name: 'home', + icon: , + value: 'home', + href: '/' + }, + { + name: 'Sign in', + icon: , + value: 'signin', + href: '/signin' + }, + { + name: 'Sign up', + icon: , + value: 'signup', + href: '/signup' + }, + ...defaultPages + ]) + // TODO: investigate deps causing infinite loop + // eslint-disable-next-line react-hooks/exhaustive-deps }, [changeTheme, isMobile, isSignedIn, theme]) + // useEffect(() => { + // const pageList: Tab[] = [ + // { + // name: "Home", + // href: "/", + // icon: , + // condition: !isSignedIn, + // value: "home" + // }, + // { + // name: "New", + // href: "/new", + // icon: , + // condition: isSignedIn, + // value: "new" + // }, + // { + // name: "Yours", + // href: "/mine", + // icon: , + // condition: isSignedIn, + // value: "mine" + // }, + // { + // name: "Sign out", + // href: "/signout", + // icon: , + // condition: isSignedIn, + // value: "signout" + // }, + // { + // name: "Sign in", + // href: "/signin", + // icon: , + // condition: !isSignedIn, + // value: "signin" + // }, + // { + // name: "Sign up", + // href: "/signup", + // icon: , + // condition: !isSignedIn, + // value: "signup" + // }, + + // ] + + // setPages(pageList.filter(page => page.condition)) + // }, [changeTheme, isMobile, isSignedIn, theme]) + const onTabChange = useCallback((tab: string) => { if (typeof window === 'undefined') return diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx index 3fdbc6e4..8484beb3 100644 --- a/client/components/post-page/index.tsx +++ b/client/components/post-page/index.tsx @@ -51,13 +51,12 @@ const PostPage = ({ post }: Props) => { Download as ZIP archive - {post.files.map(({ id, content, html, title }: File) => ( + {post.files.map(({ id, content, title }: File) => ( ))} diff --git a/client/components/preview/html.tsx b/client/components/preview/html.tsx deleted file mode 100644 index f681eecb..00000000 --- a/client/components/preview/html.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import useTheme from "@lib/hooks/use-theme" -import { memo, useEffect, useState } from "react" -import styles from './preview.module.css' - -type Props = { - height?: number | string - html: string - // file extensions we can highlight -} - -const HtmlPreview = ({ height = 500, html }: Props) => { - const { theme } = useTheme() - return (
) -} - -export default HtmlPreview diff --git a/client/components/preview/index.tsx b/client/components/preview/index.tsx index ac72f4fa..c179ad08 100644 --- a/client/components/preview/index.tsx +++ b/client/components/preview/index.tsx @@ -17,11 +17,13 @@ const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => { useEffect(() => { async function fetchPost() { if (fileId) { - const resp = await fetch(`/api/markdown/${fileId}`, { + const resp = await fetch(`/server-api/files/html/${fileId}`, { method: "GET", }) + console.log(resp) if (resp.ok) { const res = await resp.text() + console.log(res) setPreview(res) setIsLoading(false) } diff --git a/client/components/view-document/index.tsx b/client/components/view-document/index.tsx index 8b2503ce..213f51ea 100644 --- a/client/components/view-document/index.tsx +++ b/client/components/view-document/index.tsx @@ -7,13 +7,11 @@ import ExternalLink from '@geist-ui/icons/externalLink' import Skeleton from "react-loading-skeleton" import { Button, ButtonGroup, Card, Input, Spacer, Tabs, Textarea, Tooltip } from "@geist-ui/core" -import Preview from "@components/preview" -import HtmlPreview from "@components/preview/html" +import HtmlPreview from "@components/preview" // import Link from "next/link" type Props = { title: string - html: string initialTab?: "edit" | "preview" skeleton?: boolean id: string @@ -48,7 +46,7 @@ const DownloadButton = ({ rawLink }: { rawLink?: string }) => { } -const Document = ({ content, title, html, initialTab = 'edit', skeleton, id }: Props) => { +const Document = ({ content, title, initialTab = 'edit', skeleton, id }: Props) => { const codeEditorRef = useRef(null) const [tab, setTab] = useState(initialTab) // const height = editable ? "500px" : '100%' @@ -118,7 +116,7 @@ const Document = ({ content, title, html, initialTab = 'edit', skeleton, id }: P - + diff --git a/client/lib/hooks/use-signed-in.ts b/client/lib/hooks/use-signed-in.ts index e6da5bd3..6da97c50 100644 --- a/client/lib/hooks/use-signed-in.ts +++ b/client/lib/hooks/use-signed-in.ts @@ -1,15 +1,23 @@ 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 token = Cookies.get("drift-token") + const router = useRouter(); const signin = (token: string) => { setSignedIn(true); Cookies.set("drift-token", token); } + const signout = () => { + setSignedIn(false); + Cookies.remove("drift-token"); + router.push("/"); + } + useEffect(() => { if (token) { setSignedIn(true); @@ -18,7 +26,7 @@ const useSignedIn = () => { } }, [setSignedIn, token]); - return { signedIn, signin, token }; + return { signedIn, signin, token, signout }; } export default useSignedIn; diff --git a/client/pages/api/raw/[id].ts b/client/pages/api/raw/[id].ts index 4cfe4e52..c671c584 100644 --- a/client/pages/api/raw/[id].ts +++ b/client/pages/api/raw/[id].ts @@ -9,12 +9,11 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => { 'Authorization': `Bearer ${req.cookies['drift-token']}`, } }) - - res.setHeader("Content-Type", "text/plain") + const json = await file.json() + res.setHeader("Content-Type", "text/plain; charset=utf-8") res.setHeader('Cache-Control', 's-maxage=86400'); - if (file.ok) { - const data = await file.json() + const data = json const { title, content } = data // serve the file raw as plain text @@ -24,7 +23,8 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => { res.setHeader("Content-Disposition", `inline; filename="${title}"`) } - res.status(200).send(content) + res.status(200).write(content, 'utf-8') + res.end() } else { res.status(404).send("File not found") } diff --git a/client/styles/Home.module.css b/client/styles/Home.module.css index 3c3959ed..1baae48f 100644 --- a/client/styles/Home.module.css +++ b/client/styles/Home.module.css @@ -7,5 +7,5 @@ .main { max-width: var(--main-content) !important; margin: 0 auto !important; - padding: 0 var(--gap) !important; + padding: 0 0 !important; } diff --git a/server/src/lib/models/File.ts b/server/src/lib/models/File.ts index f52b4ad3..85f19e18 100644 --- a/server/src/lib/models/File.ts +++ b/server/src/lib/models/File.ts @@ -1,4 +1,4 @@ -import { BelongsTo, Column, CreatedAt, DataType, ForeignKey, IsUUID, Model, PrimaryKey, Scopes, Table } from 'sequelize-typescript'; +import { BelongsTo, Column, CreatedAt, DataType, ForeignKey, IsUUID, Model, PrimaryKey, Scopes, Table, Unique } from 'sequelize-typescript'; import { Post } from './Post'; import { User } from './User'; @@ -20,6 +20,7 @@ import { User } from './User'; export class File extends Model { @IsUUID(4) @PrimaryKey + @Unique @Column({ type: DataType.UUID, defaultValue: DataType.UUIDV4, diff --git a/server/src/lib/models/Post.ts b/server/src/lib/models/Post.ts index 2be1b024..0992d078 100644 --- a/server/src/lib/models/Post.ts +++ b/server/src/lib/models/Post.ts @@ -1,4 +1,4 @@ -import { BelongsToMany, Column, CreatedAt, DataType, HasMany, IsUUID, Model, PrimaryKey, Scopes, Table, UpdatedAt } from 'sequelize-typescript'; +import { BelongsToMany, Column, CreatedAt, DataType, HasMany, IsUUID, Model, PrimaryKey, Scopes, Table, Unique, UpdatedAt } from 'sequelize-typescript'; import { PostAuthor } from './PostAuthor'; import { User } from './User'; import { File } from './File'; @@ -26,6 +26,7 @@ import { File } from './File'; export class Post extends Model { @IsUUID(4) @PrimaryKey + @Unique @Column({ type: DataType.UUID, defaultValue: DataType.UUIDV4, diff --git a/server/src/lib/models/PostAuthor.ts b/server/src/lib/models/PostAuthor.ts index 1c718cce..76af896f 100644 --- a/server/src/lib/models/PostAuthor.ts +++ b/server/src/lib/models/PostAuthor.ts @@ -1,4 +1,4 @@ -import { Model, Column, Table, ForeignKey, IsUUID, PrimaryKey, DataType } from "sequelize-typescript"; +import { Model, Column, Table, ForeignKey, IsUUID, PrimaryKey, DataType, Unique } from "sequelize-typescript"; import { Post } from "./Post"; import { User } from "./User"; @@ -6,6 +6,7 @@ import { User } from "./User"; export class PostAuthor extends Model { @IsUUID(4) @PrimaryKey + @Unique @Column({ type: DataType.UUID, defaultValue: DataType.UUIDV4, diff --git a/server/src/lib/models/User.ts b/server/src/lib/models/User.ts index daa7de70..d050e956 100644 --- a/server/src/lib/models/User.ts +++ b/server/src/lib/models/User.ts @@ -1,4 +1,4 @@ -import { Model, Column, Table, BelongsToMany, Scopes, CreatedAt, UpdatedAt, IsUUID, PrimaryKey, DataType } from "sequelize-typescript"; +import { Model, Column, Table, BelongsToMany, Scopes, CreatedAt, UpdatedAt, IsUUID, PrimaryKey, DataType, Unique } from "sequelize-typescript"; import { Post } from "./Post"; import { PostAuthor } from "./PostAuthor"; @@ -22,6 +22,7 @@ import { PostAuthor } from "./PostAuthor"; export class User extends Model { @IsUUID(4) @PrimaryKey + @Unique @Column({ type: DataType.UUID, defaultValue: DataType.UUIDV4, diff --git a/server/src/routes/files.ts b/server/src/routes/files.ts index 70a43cde..acaaf9f2 100644 --- a/server/src/routes/files.ts +++ b/server/src/routes/files.ts @@ -27,3 +27,27 @@ files.get("/raw/:id", secretKey, async (req, res, next) => { next(e); } }); + + +files.get("/html/:id", async (req, res, next) => { + try { + const file = await File.findOne({ + where: { + id: req.params.id + }, + attributes: ["html"], + }) + + if (!file) { + return res.status(404).json({ error: "File not found" }) + } + + console.log(file.html) + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Cache-Control', 'public, max-age=4800') + res.status(200).write(file.html) + res.end() + } catch (error) { + next(error) + } +}) \ No newline at end of file diff --git a/server/src/routes/posts.ts b/server/src/routes/posts.ts index 66c39ce4..343ed435 100644 --- a/server/src/routes/posts.ts +++ b/server/src/routes/posts.ts @@ -138,7 +138,7 @@ posts.get("/:id", async (req, res, next) => { { model: File, as: "files", - attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt", "html"], + attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt"], }, { model: User, diff --git a/server/src/server.ts b/server/src/server.ts index 5cf5bf97..dd741cd0 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -4,7 +4,7 @@ import config from './lib/config'; import { sequelize } from './lib/sequelize'; (async () => { - await sequelize.sync({ alter: true }); + await sequelize.sync({}); createServer(app) .listen( config.port,