client/server: load rendered html after page load, fix raw exporting

This commit is contained in:
Max Leiter 2022-03-23 12:36:29 -07:00
parent 534cd87dc9
commit 9bdff8f28f
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
15 changed files with 154 additions and 87 deletions

View file

@ -16,7 +16,6 @@ import NewIcon from '@geist-ui/icons/plusCircle';
import YourIcon from '@geist-ui/icons/list' import YourIcon from '@geist-ui/icons/list'
import MoonIcon from '@geist-ui/icons/moon'; import MoonIcon from '@geist-ui/icons/moon';
import SunIcon from '@geist-ui/icons/sun'; import SunIcon from '@geist-ui/icons/sun';
import type { ThemeProps } from "@lib/types";
import useTheme from "@lib/hooks/use-theme"; import useTheme from "@lib/hooks/use-theme";
import { Button } from "@geist-ui/core"; import { Button } from "@geist-ui/core";
@ -36,7 +35,7 @@ const Header = () => {
const [expanded, setExpanded] = useState<boolean>(false) const [expanded, setExpanded] = useState<boolean>(false)
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true }) const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
const isMobile = useMediaQuery('xs', { match: 'down' }) const isMobile = useMediaQuery('xs', { match: 'down' })
const { signedIn: isSignedIn } = useSignedIn() const { signedIn: isSignedIn, signout } = useSignedIn()
const [pages, setPages] = useState<Tab[]>([]) const [pages, setPages] = useState<Tab[]>([])
const { changeTheme, theme } = useTheme() const { changeTheme, theme } = useTheme()
useEffect(() => { useEffect(() => {
@ -50,49 +49,7 @@ const Header = () => {
}, [isMobile]) }, [isMobile])
useEffect(() => { useEffect(() => {
const pageList: Tab[] = [ const defaultPages: Tab[] = [
{
name: "Home",
href: "/",
icon: <HomeIcon />,
condition: !isSignedIn,
value: "home"
},
{
name: "New",
href: "/new",
icon: <NewIcon />,
condition: isSignedIn,
value: "new"
},
{
name: "Yours",
href: "/mine",
icon: <YourIcon />,
condition: isSignedIn,
value: "mine"
},
{
name: "Sign out",
href: "/signout",
icon: <SignOutIcon />,
condition: isSignedIn,
value: "signout"
},
{
name: "Sign in",
href: "/signin",
icon: <SignInIcon />,
condition: !isSignedIn,
value: "signin"
},
{
name: "Sign up",
href: "/signup",
icon: <SignUpIcon />,
condition: !isSignedIn,
value: "signup"
},
{ {
name: isMobile ? "GitHub" : "", name: isMobile ? "GitHub" : "",
href: "https://github.com/maxleiter/drift", href: "https://github.com/maxleiter/drift",
@ -113,9 +70,104 @@ const Header = () => {
} }
] ]
setPages(pageList.filter(page => page.condition)) if (isSignedIn)
setPages([
{
name: 'home',
icon: <HomeIcon />,
value: 'home',
href: '/'
},
{
name: 'yours',
icon: <YourIcon />,
value: 'yours',
href: '/mine'
},
{
name: 'sign out',
icon: <SignOutIcon />,
value: 'signout',
onClick: signout
},
...defaultPages
])
else
setPages([
{
name: 'home',
icon: <HomeIcon />,
value: 'home',
href: '/'
},
{
name: 'Sign in',
icon: <SignInIcon />,
value: 'signin',
href: '/signin'
},
{
name: 'Sign up',
icon: <SignUpIcon />,
value: 'signup',
href: '/signup'
},
...defaultPages
])
// TODO: investigate deps causing infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [changeTheme, isMobile, isSignedIn, theme]) }, [changeTheme, isMobile, isSignedIn, theme])
// useEffect(() => {
// const pageList: Tab[] = [
// {
// name: "Home",
// href: "/",
// icon: <HomeIcon />,
// condition: !isSignedIn,
// value: "home"
// },
// {
// name: "New",
// href: "/new",
// icon: <NewIcon />,
// condition: isSignedIn,
// value: "new"
// },
// {
// name: "Yours",
// href: "/mine",
// icon: <YourIcon />,
// condition: isSignedIn,
// value: "mine"
// },
// {
// name: "Sign out",
// href: "/signout",
// icon: <SignOutIcon />,
// condition: isSignedIn,
// value: "signout"
// },
// {
// name: "Sign in",
// href: "/signin",
// icon: <SignInIcon />,
// condition: !isSignedIn,
// value: "signin"
// },
// {
// name: "Sign up",
// href: "/signup",
// icon: <SignUpIcon />,
// condition: !isSignedIn,
// value: "signup"
// },
// ]
// setPages(pageList.filter(page => page.condition))
// }, [changeTheme, isMobile, isSignedIn, theme])
const onTabChange = useCallback((tab: string) => { const onTabChange = useCallback((tab: string) => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return

View file

@ -51,13 +51,12 @@ const PostPage = ({ post }: Props) => {
Download as ZIP archive Download as ZIP archive
</Button> </Button>
</div> </div>
{post.files.map(({ id, content, html, title }: File) => ( {post.files.map(({ id, content, title }: File) => (
<DocumentComponent <DocumentComponent
key={id} key={id}
title={title} title={title}
initialTab={'preview'} initialTab={'preview'}
id={id} id={id}
html={html}
content={content} content={content}
/> />
))} ))}

View file

@ -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 (<article
data-theme={theme}
className={styles.markdownPreview}
dangerouslySetInnerHTML={{ __html: html }}
style={{ height }} />)
}
export default HtmlPreview

View file

@ -17,11 +17,13 @@ const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
useEffect(() => { useEffect(() => {
async function fetchPost() { async function fetchPost() {
if (fileId) { if (fileId) {
const resp = await fetch(`/api/markdown/${fileId}`, { const resp = await fetch(`/server-api/files/html/${fileId}`, {
method: "GET", method: "GET",
}) })
console.log(resp)
if (resp.ok) { if (resp.ok) {
const res = await resp.text() const res = await resp.text()
console.log(res)
setPreview(res) setPreview(res)
setIsLoading(false) setIsLoading(false)
} }

View file

@ -7,13 +7,11 @@ import ExternalLink from '@geist-ui/icons/externalLink'
import Skeleton from "react-loading-skeleton" import Skeleton from "react-loading-skeleton"
import { Button, ButtonGroup, Card, Input, Spacer, Tabs, Textarea, Tooltip } from "@geist-ui/core" import { Button, ButtonGroup, Card, Input, Spacer, Tabs, Textarea, Tooltip } from "@geist-ui/core"
import Preview from "@components/preview" import HtmlPreview from "@components/preview"
import HtmlPreview from "@components/preview/html"
// import Link from "next/link" // import Link from "next/link"
type Props = { type Props = {
title: string title: string
html: string
initialTab?: "edit" | "preview" initialTab?: "edit" | "preview"
skeleton?: boolean skeleton?: boolean
id: string 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<HTMLTextAreaElement>(null) const codeEditorRef = useRef<HTMLTextAreaElement>(null)
const [tab, setTab] = useState(initialTab) const [tab, setTab] = useState(initialTab)
// const height = editable ? "500px" : '100%' // const height = editable ? "500px" : '100%'
@ -118,7 +116,7 @@ const Document = ({ content, title, html, initialTab = 'edit', skeleton, id }: P
</div> </div>
</Tabs.Item> </Tabs.Item>
<Tabs.Item label="Preview" value="preview"> <Tabs.Item label="Preview" value="preview">
<HtmlPreview height={height} html={html} /> <HtmlPreview height={height} fileId={id} />
</Tabs.Item> </Tabs.Item>
</Tabs> </Tabs>

View file

@ -1,15 +1,23 @@
import Cookies from "js-cookie"; import Cookies from "js-cookie";
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 signin = (token: string) => { const signin = (token: string) => {
setSignedIn(true); setSignedIn(true);
Cookies.set("drift-token", token); Cookies.set("drift-token", token);
} }
const signout = () => {
setSignedIn(false);
Cookies.remove("drift-token");
router.push("/");
}
useEffect(() => { useEffect(() => {
if (token) { if (token) {
setSignedIn(true); setSignedIn(true);
@ -18,7 +26,7 @@ const useSignedIn = () => {
} }
}, [setSignedIn, token]); }, [setSignedIn, token]);
return { signedIn, signin, token }; return { signedIn, signin, token, signout };
} }
export default useSignedIn; export default useSignedIn;

View file

@ -9,12 +9,11 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
'Authorization': `Bearer ${req.cookies['drift-token']}`, 'Authorization': `Bearer ${req.cookies['drift-token']}`,
} }
}) })
const json = await file.json()
res.setHeader("Content-Type", "text/plain") 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 = await file.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
@ -24,7 +23,8 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Disposition", `inline; filename="${title}"`) res.setHeader("Content-Disposition", `inline; filename="${title}"`)
} }
res.status(200).send(content) res.status(200).write(content, 'utf-8')
res.end()
} else { } else {
res.status(404).send("File not found") res.status(404).send("File not found")
} }

View file

@ -7,5 +7,5 @@
.main { .main {
max-width: var(--main-content) !important; max-width: var(--main-content) !important;
margin: 0 auto !important; margin: 0 auto !important;
padding: 0 var(--gap) !important; padding: 0 0 !important;
} }

View file

@ -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 { Post } from './Post';
import { User } from './User'; import { User } from './User';
@ -20,6 +20,7 @@ import { User } from './User';
export class File extends Model { export class File extends Model {
@IsUUID(4) @IsUUID(4)
@PrimaryKey @PrimaryKey
@Unique
@Column({ @Column({
type: DataType.UUID, type: DataType.UUID,
defaultValue: DataType.UUIDV4, defaultValue: DataType.UUIDV4,

View file

@ -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 { PostAuthor } from './PostAuthor';
import { User } from './User'; import { User } from './User';
import { File } from './File'; import { File } from './File';
@ -26,6 +26,7 @@ import { File } from './File';
export class Post extends Model { export class Post extends Model {
@IsUUID(4) @IsUUID(4)
@PrimaryKey @PrimaryKey
@Unique
@Column({ @Column({
type: DataType.UUID, type: DataType.UUID,
defaultValue: DataType.UUIDV4, defaultValue: DataType.UUIDV4,

View file

@ -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 { Post } from "./Post";
import { User } from "./User"; import { User } from "./User";
@ -6,6 +6,7 @@ import { User } from "./User";
export class PostAuthor extends Model { export class PostAuthor extends Model {
@IsUUID(4) @IsUUID(4)
@PrimaryKey @PrimaryKey
@Unique
@Column({ @Column({
type: DataType.UUID, type: DataType.UUID,
defaultValue: DataType.UUIDV4, defaultValue: DataType.UUIDV4,

View file

@ -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 { Post } from "./Post";
import { PostAuthor } from "./PostAuthor"; import { PostAuthor } from "./PostAuthor";
@ -22,6 +22,7 @@ import { PostAuthor } from "./PostAuthor";
export class User extends Model { export class User extends Model {
@IsUUID(4) @IsUUID(4)
@PrimaryKey @PrimaryKey
@Unique
@Column({ @Column({
type: DataType.UUID, type: DataType.UUID,
defaultValue: DataType.UUIDV4, defaultValue: DataType.UUIDV4,

View file

@ -27,3 +27,27 @@ files.get("/raw/:id", secretKey, async (req, res, next) => {
next(e); 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)
}
})

View file

@ -138,7 +138,7 @@ posts.get("/:id", async (req, res, next) => {
{ {
model: File, model: File,
as: "files", as: "files",
attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt", "html"], attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt"],
}, },
{ {
model: User, model: User,

View file

@ -4,7 +4,7 @@ import config from './lib/config';
import { sequelize } from './lib/sequelize'; import { sequelize } from './lib/sequelize';
(async () => { (async () => {
await sequelize.sync({ alter: true }); await sequelize.sync({});
createServer(app) createServer(app)
.listen( .listen(
config.port, config.port,