client/server: load rendered html after page load, fix raw exporting
This commit is contained in:
parent
534cd87dc9
commit
9bdff8f28f
15 changed files with 154 additions and 87 deletions
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue