Merge pull request #56 from MaxLeiter/expiringPosts
client/server: add support for expiring posts
This commit is contained in:
commit
47cd9cc094
27 changed files with 719 additions and 92 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.vercel
|
||||
drift.sqlite
|
|
@ -1,6 +1,6 @@
|
|||
.container {
|
||||
padding: 2rem 2rem;
|
||||
border-radius: var(--border-radius);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
|
22
client/components/badges/created-ago-badge/index.tsx
Normal file
22
client/components/badges/created-ago-badge/index.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Badge, Tooltip } from "@geist-ui/core";
|
||||
import { timeAgo } from "@lib/time-ago";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
|
||||
const CreatedAgoBadge = ({ createdAt }: {
|
||||
createdAt: string | Date;
|
||||
}) => {
|
||||
const createdDate = useMemo(() => new Date(createdAt), [createdAt])
|
||||
const [time, setTimeAgo] = useState(timeAgo(createdDate))
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTimeAgo(timeAgo(createdDate))
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [createdDate])
|
||||
|
||||
const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}`
|
||||
return (<Badge type="secondary"> <Tooltip text={formattedTime}>Created {time}</Tooltip></Badge>)
|
||||
}
|
||||
|
||||
export default CreatedAgoBadge
|
58
client/components/badges/expiration-badge/index.tsx
Normal file
58
client/components/badges/expiration-badge/index.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { Badge, Tooltip } from "@geist-ui/core";
|
||||
import { timeUntil } from "@lib/time-ago";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
const ExpirationBadge = ({
|
||||
postExpirationDate,
|
||||
onExpires
|
||||
}: {
|
||||
postExpirationDate: Date | string | null
|
||||
onExpires?: () => void
|
||||
}) => {
|
||||
const expirationDate = useMemo(() => postExpirationDate ? new Date(postExpirationDate) : null, [postExpirationDate])
|
||||
const [timeUntilString, setTimeUntil] = useState<string | null>(expirationDate ? timeUntil(expirationDate) : null);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timer | null = null;
|
||||
if (expirationDate) {
|
||||
interval = setInterval(() => {
|
||||
if (expirationDate) {
|
||||
setTimeUntil(timeUntil(expirationDate))
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}
|
||||
}, [expirationDate])
|
||||
|
||||
const isExpired = useMemo(() => {
|
||||
return expirationDate && new Date(expirationDate) < new Date()
|
||||
}, [expirationDate])
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpired) {
|
||||
if (onExpires) {
|
||||
onExpires();
|
||||
}
|
||||
}
|
||||
}, [isExpired, onExpires])
|
||||
|
||||
if (!expirationDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge type={isExpired ? "error" : "warning"}>
|
||||
<Tooltip
|
||||
text={`${expirationDate.toLocaleDateString()} ${expirationDate.toLocaleTimeString()}`}>
|
||||
{isExpired ? "Expired" : `Expires ${timeUntilString}`}
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExpirationBadge
|
|
@ -34,19 +34,6 @@
|
|||
color: var(--fg);
|
||||
}
|
||||
|
||||
/*
|
||||
--bg: #131415;
|
||||
--fg: #fafbfc;
|
||||
--gray: #666;
|
||||
--light-gray: #444;
|
||||
--lighter-gray: #222;
|
||||
--lightest-gray: #1a1a1a;
|
||||
--article-color: #eaeaea;
|
||||
--header-bg: rgba(19, 20, 21, 0.45);
|
||||
--gray-alpha: rgba(255, 255, 255, 0.5);
|
||||
--selection: rgba(255, 255, 255, 0.99);
|
||||
*/
|
||||
|
||||
.primary {
|
||||
background: var(--fg);
|
||||
color: var(--bg);
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import { Button, useToasts, ButtonDropdown } from '@geist-ui/core'
|
||||
import { Button, useToasts, ButtonDropdown, Toggle, Input, useClickAway } from '@geist-ui/core'
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import generateUUID from '@lib/generate-uuid';
|
||||
import FileDropzone from './drag-and-drop';
|
||||
import styles from './post.module.css'
|
||||
import Title from './title';
|
||||
import Cookies from 'js-cookie'
|
||||
import type { PostVisibility, Document as DocumentType } from '@lib/types';
|
||||
import PasswordModal from './password';
|
||||
import PasswordModal from './password-modal';
|
||||
import getPostPath from '@lib/get-post-path';
|
||||
import EditDocumentList from '@components/edit-document-list';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
import DatePicker from 'react-datepicker';
|
||||
const Post = () => {
|
||||
const { setToast } = useToasts()
|
||||
const router = useRouter();
|
||||
const [title, setTitle] = useState<string>()
|
||||
const [expiresAt, setExpiresAt] = useState<Date | null>(null)
|
||||
|
||||
const [docs, setDocs] = useState<DocumentType[]>([{
|
||||
title: '',
|
||||
|
@ -24,7 +25,8 @@ const Post = () => {
|
|||
}])
|
||||
|
||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
||||
const sendRequest = useCallback(async (url: string, data: { visibility?: PostVisibility, title?: string, files?: DocumentType[], password?: string, userId: string }) => {
|
||||
|
||||
const sendRequest = useCallback(async (url: string, data: { expiresAt: Date | null, visibility?: PostVisibility, title?: string, files?: DocumentType[], password?: string, userId: string }) => {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
@ -55,11 +57,14 @@ const Post = () => {
|
|||
|
||||
const [isSubmitting, setSubmitting] = useState(false)
|
||||
|
||||
const onSubmit = async (visibility: PostVisibility, password?: string) => {
|
||||
const onSubmit = useCallback(async (visibility: PostVisibility, password?: string) => {
|
||||
if (visibility === 'protected' && !password) {
|
||||
setPasswordModalVisible(true)
|
||||
return
|
||||
}
|
||||
|
||||
setPasswordModalVisible(false)
|
||||
|
||||
setSubmitting(true)
|
||||
|
||||
let hasErrored = false
|
||||
|
@ -91,15 +96,20 @@ const Post = () => {
|
|||
files: docs,
|
||||
visibility,
|
||||
password,
|
||||
userId: Cookies.get('drift-userid') || ''
|
||||
userId: Cookies.get('drift-userid') || '',
|
||||
expiresAt
|
||||
})
|
||||
}
|
||||
}, [docs, expiresAt, sendRequest, setToast, title])
|
||||
|
||||
const onClosePasswordModal = () => {
|
||||
setPasswordModalVisible(false)
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
const submitPassword = useCallback((password) => onSubmit('protected', password), [onSubmit])
|
||||
|
||||
const onChangeExpiration = useCallback((date) => setExpiresAt(date), [])
|
||||
|
||||
const onChangeTitle = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.target.value)
|
||||
}, [setTitle])
|
||||
|
@ -117,7 +127,6 @@ const Post = () => {
|
|||
setDocs((docs) => docs.filter((_, index) => i !== index))
|
||||
}, [setDocs])
|
||||
|
||||
|
||||
const uploadDocs = useCallback((files: DocumentType[]) => {
|
||||
// if no title is set and the only document is empty,
|
||||
const isFirstDocEmpty = docs.length <= 1 && (docs.length ? docs[0].title === '' : true)
|
||||
|
@ -174,16 +183,36 @@ const Post = () => {
|
|||
>
|
||||
Add a File
|
||||
</Button>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 'var(--gap)',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
{<DatePicker
|
||||
onChange={onChangeExpiration}
|
||||
customInput={<Input label="Expires at" clearable width="300px" height="40px" />}
|
||||
placeholderText="Won't expire"
|
||||
selected={expiresAt}
|
||||
showTimeInput={true}
|
||||
// customTimeInput={<Input htmlType="time" />}
|
||||
timeInputLabel="Time:"
|
||||
dateFormat="MM/dd/yyyy h:mm aa"
|
||||
className={styles.datePicker}
|
||||
clearButtonTitle={"Clear"}
|
||||
// TODO: investigate why this causes margin shift if true
|
||||
enableTabLoop={false}
|
||||
minDate={new Date()}
|
||||
/>}
|
||||
<ButtonDropdown loading={isSubmitting} type="success">
|
||||
<ButtonDropdown.Item main onClick={() => onSubmit('private')}>Create Private</ButtonDropdown.Item>
|
||||
<ButtonDropdown.Item onClick={() => onSubmit('public')} >Create Public</ButtonDropdown.Item>
|
||||
<ButtonDropdown.Item onClick={() => onSubmit('unlisted')} >Create Unlisted</ButtonDropdown.Item>
|
||||
<ButtonDropdown.Item onClick={() => onSubmit('protected')} >Create with Password</ButtonDropdown.Item>
|
||||
</ButtonDropdown>
|
||||
<PasswordModal isOpen={passwordModalVisible} onClose={onClosePasswordModal} onSubmit={(password) => onSubmit('protected', password)} />
|
||||
</div>
|
||||
</div>
|
||||
<PasswordModal isOpen={passwordModalVisible} onClose={onClosePasswordModal} onSubmit={submitPassword} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,9 @@ const PasswordModal = ({ isOpen, onClose, onSubmit: onSubmitAfterVerify, creatin
|
|||
}
|
||||
|
||||
return (<>
|
||||
{<Modal visible={isOpen} >
|
||||
{/* TODO: investigate disableBackdropClick not updating state? */}
|
||||
|
||||
{<Modal visible={isOpen} disableBackdropClick={true} >
|
||||
<Modal.Title>Enter a password</Modal.Title>
|
||||
<Modal.Content>
|
||||
{!error && creating && <Note type="warning" label='Warning'>
|
|
@ -6,6 +6,10 @@
|
|||
margin-top: var(--gap-double);
|
||||
}
|
||||
|
||||
.datePicker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -21,7 +21,6 @@ const PostList = ({ morePosts, initialPosts, error }: Props) => {
|
|||
const [posts, setPosts] = useState<Post[]>(initialPosts)
|
||||
const [searching, setSearching] = useState(false)
|
||||
const [hasMorePosts, setHasMorePosts] = useState(morePosts)
|
||||
|
||||
const loadMoreClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
if (hasMorePosts) {
|
||||
|
|
|
@ -1,30 +1,21 @@
|
|||
|
||||
import NextLink from "next/link"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import timeAgo from "@lib/time-ago"
|
||||
import VisibilityBadge from "../visibility-badge"
|
||||
import { timeAgo } from "@lib/time-ago"
|
||||
import VisibilityBadge from "../badges/visibility-badge"
|
||||
import getPostPath from "@lib/get-post-path"
|
||||
import { Link, Text, Card, Tooltip, Divider, Badge, Button } from "@geist-ui/core"
|
||||
import { File, Post } from "@lib/types"
|
||||
import FadeIn from "@components/fade-in"
|
||||
import Trash from "@geist-ui/icons/trash"
|
||||
import Cookies from "js-cookie"
|
||||
import ExpirationBadge from "@components/badges/expiration-badge"
|
||||
import CreatedAgoBadge from "@components/badges/created-ago-badge"
|
||||
|
||||
// TODO: isOwner should default to false so this can be used generically
|
||||
const ListItem = ({ post, isOwner = true, deletePost }: { post: Post, isOwner?: boolean, deletePost: () => void }) => {
|
||||
const createdDate = useMemo(() => new Date(post.createdAt), [post.createdAt])
|
||||
const [time, setTimeAgo] = useState(timeAgo(createdDate))
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTimeAgo(timeAgo(createdDate))
|
||||
}, 10000)
|
||||
return () => clearInterval(interval)
|
||||
}, [createdDate])
|
||||
|
||||
const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}`
|
||||
return (<FadeIn><li key={post.id}>
|
||||
|
||||
<Card style={{ overflowY: 'scroll' }}>
|
||||
<Card.Body>
|
||||
<Text h3>
|
||||
|
@ -38,11 +29,14 @@ const ListItem = ({ post, isOwner = true, deletePost }: { post: Post, isOwner?:
|
|||
<VisibilityBadge visibility={post.visibility} />
|
||||
</span>
|
||||
<span style={{ marginLeft: 'var(--gap)' }}>
|
||||
<Badge type="secondary"><Tooltip text={formattedTime}>{time}</Tooltip></Badge>
|
||||
<CreatedAgoBadge createdAt={post.createdAt} />
|
||||
</span>
|
||||
<span style={{ marginLeft: 'var(--gap)' }}>
|
||||
<Badge type="secondary">{post.files.length === 1 ? "1 file" : `${post.files.length} files`}</Badge>
|
||||
</span>
|
||||
<span style={{ marginLeft: 'var(--gap)' }}>
|
||||
<ExpirationBadge postExpirationDate={post.expiresAt} />
|
||||
</span>
|
||||
</div>
|
||||
{isOwner && <span style={{ float: 'right' }}>
|
||||
<Button iconRight={<Trash />} onClick={deletePost} auto />
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
import Header from "@components/header/header"
|
||||
import PageSeo from "@components/page-seo"
|
||||
import VisibilityBadge from "@components/visibility-badge"
|
||||
import VisibilityBadge from "@components/badges/visibility-badge"
|
||||
import DocumentComponent from '@components/view-document'
|
||||
import styles from './post-page.module.css'
|
||||
import homeStyles from '@styles/Home.module.css'
|
||||
|
||||
import type { File, Post } from "@lib/types"
|
||||
import { Page, Button, Text, Badge, Tooltip, Spacer, ButtonDropdown, ButtonGroup, useMediaQuery } from "@geist-ui/core"
|
||||
import { useMemo, useState } from "react"
|
||||
import timeAgo from "@lib/time-ago"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { timeAgo, timeUntil } from "@lib/time-ago"
|
||||
import Archive from '@geist-ui/icons/archive'
|
||||
import FileDropdown from "@components/file-dropdown"
|
||||
import ScrollToTop from "@components/scroll-to-top"
|
||||
import { useRouter } from "next/router"
|
||||
import ExpirationBadge from "@components/badges/expiration-badge"
|
||||
import CreatedAgoBadge from "@components/badges/created-ago-badge"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
type Props = {
|
||||
post: Post
|
||||
}
|
||||
|
||||
const PostPage = ({ post }: Props) => {
|
||||
const router = useRouter()
|
||||
const download = async () => {
|
||||
const downloadZip = (await import("client-zip")).downloadZip
|
||||
const blob = await downloadZip(post.files.map((file: any) => {
|
||||
|
@ -33,11 +38,23 @@ const PostPage = ({ post }: Props) => {
|
|||
link.click()
|
||||
link.remove()
|
||||
}
|
||||
const createdDate = useMemo(() => new Date(post.createdAt), [post.createdAt])
|
||||
const [time, setTimeAgo] = useState(timeAgo(createdDate))
|
||||
|
||||
const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}`
|
||||
|
||||
const isMobile = useMediaQuery("mobile")
|
||||
|
||||
const isExpired = useMemo(() => {
|
||||
return post.expiresAt && new Date(post.expiresAt) < new Date()
|
||||
}, [post.expiresAt])
|
||||
|
||||
const onExpires = useCallback(() => {
|
||||
const isOwner = post.users ? post.users[0].id === Cookies.get("drift-userid") : false
|
||||
|
||||
if (isExpired && !isOwner) {
|
||||
router.push("/expired")
|
||||
return <></>
|
||||
}
|
||||
}, [isExpired, post.users, router])
|
||||
|
||||
return (
|
||||
<Page width={"100%"}>
|
||||
<PageSeo
|
||||
|
@ -50,17 +67,24 @@ const PostPage = ({ post }: Props) => {
|
|||
<Header />
|
||||
</Page.Header>
|
||||
<Page.Content className={homeStyles.main}>
|
||||
{/* {!isLoading && <PostFileExplorer files={post.files} />} */}
|
||||
<div className={styles.header}>
|
||||
<span className={styles.title}>
|
||||
<Text h3>{post.title}</Text>
|
||||
<div className={styles.badges}>
|
||||
<ButtonGroup
|
||||
vertical={isMobile}
|
||||
style={{
|
||||
border: "none",
|
||||
gap: 'var(--gap-half)',
|
||||
marginLeft: isMobile ? "0" : "var(--gap-half)",
|
||||
}}>
|
||||
<VisibilityBadge visibility={post.visibility} />
|
||||
<Badge type="secondary"><Tooltip text={formattedTime}>{time}</Tooltip></Badge>
|
||||
</div>
|
||||
<CreatedAgoBadge createdAt={post.createdAt} />
|
||||
<ExpirationBadge onExpires={onExpires} postExpirationDate={post.expiresAt} />
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
<span className={styles.buttons}>
|
||||
<ButtonGroup vertical={isMobile}>
|
||||
{/* If it hasn't expired, the badge can be too long */}
|
||||
<ButtonGroup vertical={isMobile || (post.expiresAt && !isExpired) ? true : false}>
|
||||
<Button auto onClick={download} icon={<Archive />}>
|
||||
Download as ZIP archive
|
||||
</Button>
|
||||
|
|
|
@ -9,5 +9,8 @@ export default function getPostPath(visibility: PostVisibility, id: string) {
|
|||
case "unlisted":
|
||||
case "public":
|
||||
return `/post/${id}`
|
||||
default:
|
||||
console.error(`Unknown visibility: ${visibility}`)
|
||||
return `/post/${id}`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ const getDuration = (timeAgoInSeconds: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Calculate
|
||||
const timeAgo = (date: Date) => {
|
||||
const timeAgoInSeconds = Math.floor(
|
||||
(new Date().getTime() - new Date(date).getTime()) / 1000
|
||||
|
@ -40,4 +39,14 @@ const timeAgo = (date: Date) => {
|
|||
return `${interval} ${epoch}${suffix} ago`
|
||||
}
|
||||
|
||||
export default timeAgo
|
||||
const timeUntil = (date: Date) => {
|
||||
const timeUntilInSeconds = Math.floor(
|
||||
(new Date(date).getTime() - new Date().getTime()) / 1000
|
||||
)
|
||||
const { interval, epoch } = getDuration(timeUntilInSeconds)
|
||||
const suffix = interval === 1 ? "" : "s"
|
||||
|
||||
return `in ${interval} ${epoch}${suffix}`
|
||||
}
|
||||
|
||||
export { timeAgo, timeUntil }
|
||||
|
|
1
client/lib/types.d.ts
vendored
1
client/lib/types.d.ts
vendored
|
@ -24,6 +24,7 @@ export type Post = {
|
|||
files: Files
|
||||
createdAt: string
|
||||
users?: User[]
|
||||
expiresAt: Date | string | null
|
||||
}
|
||||
|
||||
type User = {
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"preact": "^10.6.6",
|
||||
"prism-react-renderer": "^1.3.1",
|
||||
"react": "17.0.2",
|
||||
"react-datepicker": "^4.7.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "^12.0.4",
|
||||
"react-loading-skeleton": "^3.0.3",
|
||||
|
@ -48,6 +49,7 @@
|
|||
"@types/node": "17.0.21",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/react-datepicker": "^4.3.4",
|
||||
"@types/react-dom": "^17.0.14",
|
||||
"@types/react-syntax-highlighter": "^13.5.2",
|
||||
"eslint": "8.10.0",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const PUBLIC_FILE = /.(.*)$/
|
||||
// const PUBLIC_FILE = /.(.*)$/
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname
|
||||
|
|
19
client/pages/expired.tsx
Normal file
19
client/pages/expired.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Header from "@components/header"
|
||||
import { Note, Page, Text } from "@geist-ui/core"
|
||||
import styles from '@styles/Home.module.css'
|
||||
|
||||
const Expired = () => {
|
||||
return (
|
||||
<Page>
|
||||
<Header />
|
||||
<Page.Content className={styles.main}>
|
||||
<Note type="error" label={false}>
|
||||
<Text h4>Error: The drift you're trying to view has expired.</Text>
|
||||
</Note>
|
||||
|
||||
</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default Expired
|
|
@ -3,12 +3,17 @@ import NewPost from '@components/new-post'
|
|||
import Header from '@components/header'
|
||||
import PageSeo from '@components/page-seo'
|
||||
import { Page } from '@geist-ui/core'
|
||||
import Head from 'next/head'
|
||||
|
||||
const New = () => {
|
||||
return (
|
||||
<Page className={styles.wrapper}>
|
||||
<PageSeo title="Drift - New" />
|
||||
|
||||
<Head>
|
||||
{/* TODO: solve this. */}
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/css/react-datepicker.css" />
|
||||
</Head>
|
||||
<Page.Header>
|
||||
<Header />
|
||||
</Page.Header>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Page, useToasts } from '@geist-ui/core';
|
||||
|
||||
import type { Post } from "@lib/types";
|
||||
import PasswordModal from "@components/new-post/password";
|
||||
import PasswordModal from "@components/new-post/password-modal";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Cookies from "js-cookie";
|
||||
|
@ -70,7 +70,9 @@ const Post = () => {
|
|||
}
|
||||
|
||||
if (!post) {
|
||||
return <Page><PasswordModal creating={false} onClose={onClose} onSubmit={onSubmit} isOpen={isPasswordModalOpen} /></Page>
|
||||
return <Page>
|
||||
<PasswordModal creating={false} onClose={onClose} onSubmit={onSubmit} isOpen={isPasswordModalOpen} />
|
||||
</Page>
|
||||
}
|
||||
|
||||
return (<PostPage post={post} />)
|
||||
|
|
372
client/public/css/react-datepicker.css
Normal file
372
client/public/css/react-datepicker.css
Normal file
|
@ -0,0 +1,372 @@
|
|||
.react-datepicker__year-read-view--down-arrow,
|
||||
.react-datepicker__month-read-view--down-arrow,
|
||||
.react-datepicker__month-year-read-view--down-arrow,
|
||||
.react-datepicker__navigation-icon::before {
|
||||
border-color: var(--light-gray);
|
||||
border-style: solid;
|
||||
border-width: 3px 3px 0 0;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 9px;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
width: 9px;
|
||||
}
|
||||
.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
|
||||
.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle {
|
||||
margin-left: -4px;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.react-datepicker-wrapper {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.react-datepicker {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.8rem;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--gray);
|
||||
border-radius: var(--radius);
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.react-datepicker--time-only .react-datepicker__triangle {
|
||||
left: 35px;
|
||||
}
|
||||
.react-datepicker--time-only .react-datepicker__time-container {
|
||||
border-left: 0;
|
||||
}
|
||||
.react-datepicker--time-only .react-datepicker__time,
|
||||
.react-datepicker--time-only .react-datepicker__time-box {
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.react-datepicker__triangle {
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.react-datepicker-popper {
|
||||
z-index: 1;
|
||||
}
|
||||
.react-datepicker-popper[data-placement^="bottom"] {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.react-datepicker-popper[data-placement="bottom-end"]
|
||||
.react-datepicker__triangle,
|
||||
.react-datepicker-popper[data-placement="top-end"] .react-datepicker__triangle {
|
||||
left: auto;
|
||||
right: 50px;
|
||||
}
|
||||
.react-datepicker-popper[data-placement^="top"] {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.react-datepicker-popper[data-placement^="right"] {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.react-datepicker-popper[data-placement^="right"] .react-datepicker__triangle {
|
||||
left: auto;
|
||||
right: 42px;
|
||||
}
|
||||
.react-datepicker-popper[data-placement^="left"] {
|
||||
padding-right: 8px;
|
||||
}
|
||||
.react-datepicker-popper[data-placement^="left"] .react-datepicker__triangle {
|
||||
left: 42px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.react-datepicker__header {
|
||||
text-align: center;
|
||||
background-color: var(--bg);
|
||||
border-bottom: 1px solid var(--gray);
|
||||
border-top-left-radius: var(--radius);
|
||||
border-top-right-radius: var(--radius);
|
||||
padding: 8px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.react-datepicker__header--time {
|
||||
padding-bottom: 8px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.react-datepicker__year-dropdown-container--select,
|
||||
.react-datepicker__month-dropdown-container--select,
|
||||
.react-datepicker__month-year-dropdown-container--select,
|
||||
.react-datepicker__year-dropdown-container--scroll,
|
||||
.react-datepicker__month-dropdown-container--scroll,
|
||||
.react-datepicker__month-year-dropdown-container--scroll {
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.react-datepicker__current-month,
|
||||
.react-datepicker-time__header,
|
||||
.react-datepicker-year-header {
|
||||
margin-top: 0;
|
||||
font-weight: bold;
|
||||
font-size: 0.944rem;
|
||||
}
|
||||
|
||||
.react-datepicker-time__header {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.react-datepicker__navigation {
|
||||
align-items: center;
|
||||
background: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
z-index: 1;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
text-indent: -999em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.react-datepicker__navigation--previous {
|
||||
left: 2px;
|
||||
}
|
||||
.react-datepicker__navigation--next {
|
||||
right: 2px;
|
||||
}
|
||||
.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button) {
|
||||
right: 85px;
|
||||
}
|
||||
.react-datepicker__navigation--years {
|
||||
position: relative;
|
||||
top: 0;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.react-datepicker__navigation--years-previous {
|
||||
top: 4px;
|
||||
}
|
||||
.react-datepicker__navigation--years-upcoming {
|
||||
top: -4px;
|
||||
}
|
||||
.react-datepicker__navigation:hover *::before {
|
||||
border-color: var(--lighter-gray);
|
||||
}
|
||||
|
||||
.react-datepicker__navigation-icon {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
font-size: 20px;
|
||||
width: 0;
|
||||
}
|
||||
.react-datepicker__navigation-icon--next {
|
||||
left: -2px;
|
||||
}
|
||||
.react-datepicker__navigation-icon--next::before {
|
||||
transform: rotate(45deg);
|
||||
left: -7px;
|
||||
}
|
||||
.react-datepicker__navigation-icon--previous {
|
||||
right: -2px;
|
||||
}
|
||||
.react-datepicker__navigation-icon--previous::before {
|
||||
transform: rotate(225deg);
|
||||
right: -7px;
|
||||
}
|
||||
|
||||
.react-datepicker__month-container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.react-datepicker__year {
|
||||
margin: 0.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
.react-datepicker__year-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
max-width: 180px;
|
||||
}
|
||||
.react-datepicker__year .react-datepicker__year-text {
|
||||
display: inline-block;
|
||||
width: 4rem;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.react-datepicker__month {
|
||||
margin: 0.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
.react-datepicker__month .react-datepicker__month-text,
|
||||
.react-datepicker__month .react-datepicker__quarter-text {
|
||||
display: inline-block;
|
||||
width: 4rem;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.react-datepicker__input-time-container {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
float: left;
|
||||
margin: 5px 0 10px 15px;
|
||||
text-align: left;
|
||||
}
|
||||
.react-datepicker__input-time-container .react-datepicker-time__caption {
|
||||
display: inline-block;
|
||||
}
|
||||
.react-datepicker__input-time-container
|
||||
.react-datepicker-time__input-container {
|
||||
display: inline-block;
|
||||
}
|
||||
.react-datepicker__input-time-container
|
||||
.react-datepicker-time__input-container
|
||||
.react-datepicker-time__input {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.react-datepicker__input-time-container
|
||||
.react-datepicker-time__input-container
|
||||
.react-datepicker-time__input
|
||||
input {
|
||||
width: auto;
|
||||
}
|
||||
.react-datepicker__input-time-container
|
||||
.react-datepicker-time__input-container
|
||||
.react-datepicker-time__input
|
||||
input[type="time"]::-webkit-inner-spin-button,
|
||||
.react-datepicker__input-time-container
|
||||
.react-datepicker-time__input-container
|
||||
.react-datepicker-time__input
|
||||
input[type="time"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
.react-datepicker__input-time-container
|
||||
.react-datepicker-time__input-container
|
||||
.react-datepicker-time__input
|
||||
input[type="time"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
.react-datepicker__input-time-container
|
||||
.react-datepicker-time__input-container
|
||||
.react-datepicker-time__delimiter {
|
||||
margin-left: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.react-datepicker__day-names,
|
||||
.react-datepicker__week {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.react-datepicker__day-names {
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
|
||||
.react-datepicker__day-name,
|
||||
.react-datepicker__day,
|
||||
.react-datepicker__time-name {
|
||||
color: var(--fg);
|
||||
display: inline-block;
|
||||
width: 1.7rem;
|
||||
line-height: 1.7rem;
|
||||
text-align: center;
|
||||
margin: 0.166rem;
|
||||
}
|
||||
.react-datepicker__day,
|
||||
.react-datepicker__month-text,
|
||||
.react-datepicker__quarter-text,
|
||||
.react-datepicker__year-text {
|
||||
cursor: pointer;
|
||||
}
|
||||
.react-datepicker__day:hover,
|
||||
.react-datepicker__month-text:hover,
|
||||
.react-datepicker__quarter-text:hover,
|
||||
.react-datepicker__year-text:hover {
|
||||
border-radius: 0.3rem;
|
||||
background-color: var(--light-gray);
|
||||
}
|
||||
.react-datepicker__day--today,
|
||||
.react-datepicker__month-text--today,
|
||||
.react-datepicker__quarter-text--today,
|
||||
.react-datepicker__year-text--today {
|
||||
font-weight: bold;
|
||||
}
|
||||
.react-datepicker__day--highlighted,
|
||||
.react-datepicker__month-text--highlighted,
|
||||
.react-datepicker__quarter-text--highlighted,
|
||||
.react-datepicker__year-text--highlighted {
|
||||
border-radius: 0.3rem;
|
||||
background-color: #3dcc4a;
|
||||
color: var(--fg);
|
||||
}
|
||||
.react-datepicker__day--highlighted:hover,
|
||||
.react-datepicker__month-text--highlighted:hover,
|
||||
.react-datepicker__quarter-text--highlighted:hover,
|
||||
.react-datepicker__year-text--highlighted:hover {
|
||||
background-color: #32be3f;
|
||||
}
|
||||
|
||||
.react-datepicker__day--selected,
|
||||
.react-datepicker__day--in-selecting-range,
|
||||
.react-datepicker__day--in-range,
|
||||
.react-datepicker__month-text--selected,
|
||||
.react-datepicker__month-text--in-selecting-range,
|
||||
.react-datepicker__month-text--in-range,
|
||||
.react-datepicker__quarter-text--selected,
|
||||
.react-datepicker__quarter-text--in-selecting-range,
|
||||
.react-datepicker__quarter-text--in-range,
|
||||
.react-datepicker__year-text--selected,
|
||||
.react-datepicker__year-text--in-selecting-range,
|
||||
.react-datepicker__year-text--in-range {
|
||||
border-radius: 0.3rem;
|
||||
background-color: var(--light-gray);
|
||||
color: var(--fg);
|
||||
}
|
||||
.react-datepicker__day--selected:hover {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.react-datepicker__day--keyboard-selected,
|
||||
.react-datepicker__month-text--keyboard-selected,
|
||||
.react-datepicker__quarter-text--keyboard-selected,
|
||||
.react-datepicker__year-text--keyboard-selected {
|
||||
border-radius: 0.3rem;
|
||||
background-color: var(--light-gray);
|
||||
color: var(--fg);
|
||||
}
|
||||
.react-datepicker__day--keyboard-selected:hover {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.react-datepicker__month--selecting-range
|
||||
.react-datepicker__day--in-range:not(.react-datepicker__day--in-selecting-range, .react-datepicker__month-text--in-selecting-range, .react-datepicker__quarter-text--in-selecting-range, .react-datepicker__year-text--in-selecting-range) {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.react-datepicker {
|
||||
transform: scale(1.15) translateY(-12px);
|
||||
}
|
||||
|
||||
.react-datepicker__day--disabled {
|
||||
color: var(--darker-gray);
|
||||
}
|
||||
|
||||
.react-datepicker__day--disabled:hover {
|
||||
background-color: transparent;
|
||||
cursor: not-allowed;
|
||||
}
|
|
@ -227,6 +227,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
|
||||
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
|
||||
|
||||
"@popperjs/core@^2.9.2":
|
||||
version "2.11.4"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.4.tgz#d8c7b8db9226d2d7664553a0741ad7d0397ee503"
|
||||
integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==
|
||||
|
||||
"@rushstack/eslint-patch@1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.8.tgz#be3e914e84eacf16dbebd311c0d0b44aa1174c64"
|
||||
|
@ -315,6 +320,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
||||
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
||||
|
||||
"@types/react-datepicker@^4.3.4":
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.3.4.tgz#1cccf5acfb8672fce08940d1cf69e664500ea63d"
|
||||
integrity sha512-5nTTz37KdTUgMZ1AAxztMWNtEnIMVRo8oCAEhIv0a6uUqDjvSKaMyPRpBV+8chi6f/A8wlTKJIpojpXca2dx3A==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
"@types/react" "*"
|
||||
date-fns "^2.0.1"
|
||||
react-popper "^2.2.5"
|
||||
|
||||
"@types/react-dom@^17.0.14":
|
||||
version "17.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.14.tgz#c8f917156b652ddf807711f5becbd2ab018dea9f"
|
||||
|
@ -722,6 +737,11 @@ character-reference-invalid@^1.0.0:
|
|||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
classnames@^2.2.6:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||
|
||||
cli-cursor@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
|
||||
|
@ -893,6 +913,11 @@ damerau-levenshtein@^1.0.7:
|
|||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
|
||||
|
||||
date-fns@^2.0.1, date-fns@^2.24.0:
|
||||
version "2.28.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
||||
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
|
||||
|
||||
debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
|
@ -2306,7 +2331,7 @@ longest-streak@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.0.1.tgz#c97315b7afa0e7d9525db9a5a2953651432bdc5d"
|
||||
integrity sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==
|
||||
|
||||
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
|
@ -3577,7 +3602,7 @@ process-nextick-args@~2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
||||
|
||||
prop-types@^15.0.0, prop-types@^15.8.1:
|
||||
prop-types@^15.0.0, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
|
@ -3623,6 +3648,18 @@ rc@^1.2.7:
|
|||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-datepicker@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.7.0.tgz#75e03b0a6718b97b84287933307faf2ed5f03cf4"
|
||||
integrity sha512-FS8KgbwqpxmJBv/bUdA42MYqYZa+fEYcpc746DZiHvVE2nhjrW/dg7c5B5fIUuI8gZET6FOzuDgezNcj568Czw==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
classnames "^2.2.6"
|
||||
date-fns "^2.24.0"
|
||||
prop-types "^15.7.2"
|
||||
react-onclickoutside "^6.12.0"
|
||||
react-popper "^2.2.5"
|
||||
|
||||
react-dom@17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
|
@ -3641,6 +3678,11 @@ react-dropzone@^12.0.4:
|
|||
file-selector "^0.4.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-fast-compare@^3.0.1:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
@ -3676,6 +3718,19 @@ react-markdown@^8.0.0:
|
|||
unist-util-visit "^4.0.0"
|
||||
vfile "^5.0.0"
|
||||
|
||||
react-onclickoutside@^6.12.0:
|
||||
version "6.12.1"
|
||||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz#92dddd28f55e483a1838c5c2930e051168c1e96b"
|
||||
integrity sha512-a5Q7CkWznBRUWPmocCvE8b6lEYw1s6+opp/60dCunhO+G6E4tDTO2Sd2jKE+leEnnrLAE2Wj5DlDHNqj5wPv1Q==
|
||||
|
||||
react-popper@^2.2.5:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96"
|
||||
integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==
|
||||
dependencies:
|
||||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-syntax-highlighter@^15.4.5:
|
||||
version "15.4.5"
|
||||
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.4.5.tgz#db900d411d32a65c8e90c39cd64555bf463e712e"
|
||||
|
@ -4468,6 +4523,13 @@ walkdir@^0.4.1:
|
|||
resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39"
|
||||
integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==
|
||||
|
||||
warning@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
wcwidth@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
|
||||
|
|
|
@ -73,4 +73,12 @@ export class Post extends Model {
|
|||
@UpdatedAt
|
||||
@Column
|
||||
updatedAt!: Date
|
||||
|
||||
@Column
|
||||
deletedAt?: Date
|
||||
|
||||
@Column
|
||||
expiresAt?: Date
|
||||
|
||||
// TODO: deletedBy
|
||||
}
|
||||
|
|
12
server/src/migrations/05_expiring_posts.ts
Normal file
12
server/src/migrations/05_expiring_posts.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.addColumn("posts", "expiresAt", {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
await queryInterface.removeColumn("posts", "expiresAt")
|
|
@ -82,7 +82,7 @@ auth.post(
|
|||
} catch (e) {
|
||||
res.status(401).json({
|
||||
error: {
|
||||
message: e.message,
|
||||
message: e.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ auth.post(
|
|||
} catch (e) {
|
||||
res.status(401).json({
|
||||
error: {
|
||||
message: error,
|
||||
message: error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -36,19 +36,12 @@ posts.post(
|
|||
.custom(postVisibilitySchema, "valid visibility")
|
||||
.required(),
|
||||
userId: Joi.string().required(),
|
||||
password: Joi.string().optional()
|
||||
password: Joi.string().optional(),
|
||||
expiresAt: Joi.date().optional()
|
||||
}
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
let hashedPassword: string = ""
|
||||
if (req.body.visibility === "protected") {
|
||||
hashedPassword = crypto
|
||||
.createHash("sha256")
|
||||
.update(req.body.password)
|
||||
.digest("hex")
|
||||
}
|
||||
|
||||
// check if all files have titles
|
||||
const files = req.body.files as File[]
|
||||
const fileTitles = files.map((file) => file.title)
|
||||
|
@ -61,10 +54,19 @@ posts.post(
|
|||
throw new Error("You must submit at least one file")
|
||||
}
|
||||
|
||||
let hashedPassword: string = ""
|
||||
if (req.body.visibility === "protected") {
|
||||
hashedPassword = crypto
|
||||
.createHash("sha256")
|
||||
.update(req.body.password)
|
||||
.digest("hex")
|
||||
}
|
||||
|
||||
const newPost = new Post({
|
||||
title: req.body.title,
|
||||
visibility: req.body.visibility,
|
||||
password: hashedPassword
|
||||
password: hashedPassword,
|
||||
expiresAt: req.body.expiresAt
|
||||
})
|
||||
|
||||
await newPost.save()
|
||||
|
@ -134,7 +136,7 @@ posts.get("/mine", jwt, async (req: UserJwtRequest, res, next) => {
|
|||
attributes: ["id", "title", "createdAt"]
|
||||
}
|
||||
],
|
||||
attributes: ["id", "title", "visibility", "createdAt"]
|
||||
attributes: ["id", "title", "visibility", "createdAt", "expiresAt"]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -235,6 +237,15 @@ posts.get(
|
|||
as: "users",
|
||||
attributes: ["id", "username"]
|
||||
}
|
||||
],
|
||||
attributes: [
|
||||
"id",
|
||||
"title",
|
||||
"visibility",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"deletedAt",
|
||||
"expiresAt"
|
||||
]
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue