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 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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -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(() => {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
.main {
|
||||
max-width: var(--main-content) !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 { 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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue