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
No known key found for this signature in database
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 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<boolean>(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<Tab[]>([])
const { changeTheme, theme } = useTheme()
useEffect(() => {
@ -50,49 +49,7 @@ const Header = () => {
}, [isMobile])
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"
},
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: <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])
// 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) => {
if (typeof window === 'undefined') return

View file

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

View file

@ -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<HTMLTextAreaElement>(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
</div>
</Tabs.Item>
<Tabs.Item label="Preview" value="preview">
<HtmlPreview height={height} html={html} />
<HtmlPreview height={height} fileId={id} />
</Tabs.Item>
</Tabs>

View file

@ -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;

View file

@ -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")
}

View file

@ -7,5 +7,5 @@
.main {
max-width: var(--main-content) !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 { 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,

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 { 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,

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 { 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,

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 { 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,

View file

@ -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)
}
})

View file

@ -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,

View file

@ -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,