Merge pull request #56 from MaxLeiter/expiringPosts

client/server: add support for expiring posts
This commit is contained in:
Max Leiter 2022-03-30 20:07:59 -07:00 committed by GitHub
commit 47cd9cc094
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 719 additions and 92 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.vercel
drift.sqlite

View file

@ -1,22 +1,22 @@
.container {
padding: 2rem 2rem;
border-radius: var(--border-radius);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
padding: 2rem 2rem;
border-radius: var(--radius);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
}
.form {
display: grid;
place-items: center;
display: grid;
place-items: center;
}
.formGroup {
display: flex;
flex-direction: column;
place-items: center;
gap: 10px;
display: flex;
flex-direction: column;
place-items: center;
gap: 10px;
}
.formContentSpace {
margin-bottom: 1rem;
text-align: center;
}
margin-bottom: 1rem;
text-align: center;
}

View 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

View 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

View file

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

View file

@ -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,15 +183,35 @@ const Post = () => {
>
Add a File
</Button>
<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 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>
</div>
</div>
<PasswordModal isOpen={passwordModalVisible} onClose={onClosePasswordModal} onSubmit={submitPassword} />
</div>
)
}

View file

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

View file

@ -6,6 +6,10 @@
margin-top: var(--gap-double);
}
.datePicker {
flex: 1;
}
.title {
display: flex;
flex-direction: row;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ export type Post = {
files: Files
createdAt: string
users?: User[]
expiresAt: Date | string | null
}
type User = {

View file

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

View file

@ -28,4 +28,4 @@ class MyDocument extends Document {
}
}
export default MyDocument
export default MyDocument

View file

@ -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
View 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&apos;re trying to view has expired.</Text>
</Note>
</Page.Content>
</Page>
)
}
export default Expired

View file

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

View file

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

View 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;
}

View file

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

View file

@ -73,4 +73,12 @@ export class Post extends Model {
@UpdatedAt
@Column
updatedAt!: Date
@Column
deletedAt?: Date
@Column
expiresAt?: Date
// TODO: deletedBy
}

View 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")

View file

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

View file

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