onSubmit('protected', password)} />
+
+ {}
+ placeholderText="Won't expire"
+ selected={expiresAt}
+ showTimeInput={true}
+ // customTimeInput={}
+ 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()}
+ />}
+
+ onSubmit('private')}>Create Private
+ onSubmit('public')} >Create Public
+ onSubmit('unlisted')} >Create Unlisted
+ onSubmit('protected')} >Create with Password
+
+
+
+ {/* */}
)
}
diff --git a/client/components/new-post/password/index.tsx b/client/components/new-post/password-modal/index.tsx
similarity index 92%
rename from client/components/new-post/password/index.tsx
rename to client/components/new-post/password-modal/index.tsx
index 9ab7a8f..040f6af 100644
--- a/client/components/new-post/password/index.tsx
+++ b/client/components/new-post/password-modal/index.tsx
@@ -29,7 +29,9 @@ const PasswordModal = ({ isOpen, onClose, onSubmit: onSubmitAfterVerify, creatin
}
return (<>
- {
+ {/* TODO: investigate disableBackdropClick not updating state? */}
+
+ {
Enter a password
{!error && creating &&
diff --git a/client/components/new-post/post.module.css b/client/components/new-post/post.module.css
index 1a57424..77e4b07 100644
--- a/client/components/new-post/post.module.css
+++ b/client/components/new-post/post.module.css
@@ -6,6 +6,10 @@
margin-top: var(--gap-double);
}
+.datePicker {
+ flex: 1;
+}
+
.title {
display: flex;
flex-direction: row;
diff --git a/client/components/post-list/index.tsx b/client/components/post-list/index.tsx
index 6918580..ecd6c81 100644
--- a/client/components/post-list/index.tsx
+++ b/client/components/post-list/index.tsx
@@ -21,7 +21,6 @@ const PostList = ({ morePosts, initialPosts, error }: Props) => {
const [posts, setPosts] = useState(initialPosts)
const [searching, setSearching] = useState(false)
const [hasMorePosts, setHasMorePosts] = useState(morePosts)
-
const loadMoreClick = useCallback((e: React.MouseEvent) => {
e.preventDefault()
if (hasMorePosts) {
diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx
index 809fae8..a67baa0 100644
--- a/client/components/post-list/list-item.tsx
+++ b/client/components/post-list/list-item.tsx
@@ -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 (
-
@@ -38,11 +29,14 @@ const ListItem = ({ post, isOwner = true, deletePost }: { post: Post, isOwner?:
- {time}
+
{post.files.length === 1 ? "1 file" : `${post.files.length} files`}
+
+
+
{isOwner &&
} onClick={deletePost} auto />
diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx
index 9d94792..442c616 100644
--- a/client/components/post-page/index.tsx
+++ b/client/components/post-page/index.tsx
@@ -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 (
{
{post.title}
-
+
- {time}
-
+
+
+
-
+ {/* If it hasn't expired, the badge can be too long */}
+
}>
Download as ZIP archive
diff --git a/client/lib/get-post-path.ts b/client/lib/get-post-path.ts
index 5235cdf..76cac64 100644
--- a/client/lib/get-post-path.ts
+++ b/client/lib/get-post-path.ts
@@ -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}`
}
}
diff --git a/client/lib/time-ago.ts b/client/lib/time-ago.ts
index 46a14e8..66e452c 100644
--- a/client/lib/time-ago.ts
+++ b/client/lib/time-ago.ts
@@ -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 }
diff --git a/client/lib/types.d.ts b/client/lib/types.d.ts
index cf35bb3..ef9e2f8 100644
--- a/client/lib/types.d.ts
+++ b/client/lib/types.d.ts
@@ -24,6 +24,7 @@ export type Post = {
files: Files
createdAt: string
users?: User[]
+ expiresAt: Date | string | null
}
type User = {
diff --git a/client/package.json b/client/package.json
index cd567a2..653753e 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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,8 @@
"@types/node": "17.0.21",
"@types/nprogress": "^0.2.0",
"@types/react": "17.0.39",
+ "@types/react-datepicker": "^4.3.4",
+ "@types/react-datetime-picker": "^3.4.1",
"@types/react-dom": "^17.0.14",
"@types/react-syntax-highlighter": "^13.5.2",
"eslint": "8.10.0",
diff --git a/client/pages/_document.tsx b/client/pages/_document.tsx
index 4f43e76..4acbd87 100644
--- a/client/pages/_document.tsx
+++ b/client/pages/_document.tsx
@@ -28,4 +28,4 @@ class MyDocument extends Document {
}
}
-export default MyDocument
\ No newline at end of file
+export default MyDocument
diff --git a/client/pages/_middleware.tsx b/client/pages/_middleware.tsx
index b1a856e..dbcbe8e 100644
--- a/client/pages/_middleware.tsx
+++ b/client/pages/_middleware.tsx
@@ -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
diff --git a/client/pages/expired.tsx b/client/pages/expired.tsx
new file mode 100644
index 0000000..a1adcff
--- /dev/null
+++ b/client/pages/expired.tsx
@@ -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 (
+
+
+
+
+ Error: The drift you're trying to view has expired.
+
+
+
+
+ )
+}
+
+export default Expired
diff --git a/client/pages/new.tsx b/client/pages/new.tsx
index e8cd5d3..2a8c3f3 100644
--- a/client/pages/new.tsx
+++ b/client/pages/new.tsx
@@ -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 (
-
+
+ {/* */}
+ {/* eslint-disable-next-line @next/next/no-css-tags */}
+
+
diff --git a/client/pages/post/protected/[id].tsx b/client/pages/post/protected/[id].tsx
index d34e970..5dcbdc2 100644
--- a/client/pages/post/protected/[id].tsx
+++ b/client/pages/post/protected/[id].tsx
@@ -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
+ return
+
+
}
return ()
diff --git a/client/public/css/react-datepicker.css b/client/public/css/react-datepicker.css
new file mode 100644
index 0000000..97949a9
--- /dev/null
+++ b/client/public/css/react-datepicker.css
@@ -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;
+}
diff --git a/client/yarn.lock b/client/yarn.lock
index 71a90be..a222c35 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -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,23 @@
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-datetime-picker@^3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@types/react-datetime-picker/-/react-datetime-picker-3.4.1.tgz#8acbc3e6f4e69fac0f91be4e920c3efdc28f3ed7"
+ integrity sha512-JHqB74+8Zq6cY0PTJ6Wi5Pm6qkNUmooyFfW5SiknSY2xJG1UG8+ljyWTZAvgHvj0XpqcWCHqqYUPiAVagnf9Sg==
+ dependencies:
+ "@types/react" "*"
+
"@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 +744,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 +920,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 +2338,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 +3609,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 +3655,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 +3685,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 +3725,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 +4530,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"
diff --git a/server/src/lib/models/Post.ts b/server/src/lib/models/Post.ts
index 07f94c7..e0fc59d 100644
--- a/server/src/lib/models/Post.ts
+++ b/server/src/lib/models/Post.ts
@@ -73,4 +73,12 @@ export class Post extends Model {
@UpdatedAt
@Column
updatedAt!: Date
+
+ @Column
+ deletedAt?: Date
+
+ @Column
+ expiresAt?: Date
+
+ // TODO: deletedBy
}
diff --git a/server/src/migrations/05_expiring_posts.ts b/server/src/migrations/05_expiring_posts.ts
new file mode 100644
index 0000000..0ff1963
--- /dev/null
+++ b/server/src/migrations/05_expiring_posts.ts
@@ -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")
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
index de1aae4..2c9f02f 100644
--- a/server/src/routes/auth.ts
+++ b/server/src/routes/auth.ts
@@ -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
}
})
}
diff --git a/server/src/routes/posts.ts b/server/src/routes/posts.ts
index c55f97d..4b148e1 100644
--- a/server/src/routes/posts.ts
+++ b/server/src/routes/posts.ts
@@ -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"
]
})