diff --git a/prisma/migrations/20220728183855_expiring_images/migration.sql b/prisma/migrations/20220728183855_expiring_images/migration.sql new file mode 100644 index 0000000..b30e87c --- /dev/null +++ b/prisma/migrations/20220728183855_expiring_images/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Image" ADD COLUMN "expires_at" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index adacefd..abd4950 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,6 +36,7 @@ model Image { file String mimetype String @default("image/png") created_at DateTime @default(now()) + expires_at DateTime? views Int @default(0) favorite Boolean @default(false) embed Boolean @default(false) diff --git a/src/components/File.tsx b/src/components/File.tsx index 8712fdd..f49dcda 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -4,8 +4,9 @@ import { showNotification } from '@mantine/notifications'; import useFetch from 'hooks/useFetch'; import { useState } from 'react'; import Type from './Type'; -import { CalendarIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons'; +import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons'; import MutedText from './MutedText'; +import { relativeTime } from 'lib/clientUtils'; export function FileMeta({ Icon, title, subtitle }) { return ( @@ -22,7 +23,6 @@ export function FileMeta({ Icon, title, subtitle }) { export default function File({ image, updateImages }) { const [open, setOpen] = useState(false); const clipboard = useClipboard(); - const theme = useMantineTheme(); const handleDelete = async () => { const res = await useFetch('/api/user/files', 'DELETE', { id: image.id }); @@ -88,6 +88,7 @@ export default function File({ image, updateImages }) { + {image.expires_at && } diff --git a/src/components/icons/ClockIcon.tsx b/src/components/icons/ClockIcon.tsx new file mode 100644 index 0000000..e55f7a2 --- /dev/null +++ b/src/components/icons/ClockIcon.tsx @@ -0,0 +1,5 @@ +import { Clock } from 'react-feather'; + +export default function ClockIcon({ ...props }) { + return ; +} \ No newline at end of file diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx index d9530ad..45d17a4 100644 --- a/src/components/icons/index.tsx +++ b/src/components/icons/index.tsx @@ -22,6 +22,7 @@ import PlayIcon from './PlayIcon'; import CalendarIcon from './CalendarIcon'; import HashIcon from './HashIcon'; import TagIcon from './TagIcon'; +import ClockIcon from './ClockIcon'; export { ActivityIcon, @@ -48,4 +49,5 @@ export { CalendarIcon, HashIcon, TagIcon, + ClockIcon, }; \ No newline at end of file diff --git a/src/components/pages/Upload.tsx b/src/components/pages/Upload.tsx index 0fef926..0422a1a 100644 --- a/src/components/pages/Upload.tsx +++ b/src/components/pages/Upload.tsx @@ -1,13 +1,44 @@ -import { Button, Collapse, Group, Progress, Title } from '@mantine/core'; +import { Button, Collapse, Group, Progress, Select, Title } from '@mantine/core'; import { randomId, useClipboard } from '@mantine/hooks'; import { showNotification, updateNotification } from '@mantine/notifications'; import Dropzone from 'components/dropzone/Dropzone'; import FileDropzone from 'components/dropzone/DropzoneFile'; -import { CrossIcon, UploadIcon } from 'components/icons'; +import { ClockIcon, CrossIcon, UploadIcon } from 'components/icons'; import Link from 'components/Link'; import { useStoreSelector } from 'lib/redux/store'; import { useEffect, useState } from 'react'; +const expires = [ + '5min', + '10min', + '15min', + '30min', + '1h', + '2h', + '3h', + '4h', + '5h', + '6h', + '8h', + '12h', + '1d', + '3d', + '5d', + '7d', + '1w', + '1.5w', + '2w', + '3w', + '1m', + '1.5m', + '2m', + '3m', + '6m', + '8m', + '1y', + 'never', +]; + export default function Upload() { const clipboard = useClipboard(); const user = useStoreSelector(state => state.user); @@ -15,6 +46,7 @@ export default function Upload() { const [files, setFiles] = useState([]); const [progress, setProgress] = useState(0); const [loading, setLoading] = useState(false); + const [expires, setExpires] = useState('never'); useEffect(() => { window.addEventListener('paste', (e: ClipboardEvent) => { @@ -29,6 +61,36 @@ export default function Upload() { }); const handleUpload = async () => { + const expires_at = expires === 'never' ? null : new Date({ + '5min': Date.now() + 5 * 60 * 1000, + '10min': Date.now() + 10 * 60 * 1000, + '15min': Date.now() + 15 * 60 * 1000, + '30min': Date.now() + 30 * 60 * 1000, + '1h': Date.now() + 60 * 60 * 1000, + '2h': Date.now() + 2 * 60 * 60 * 1000, + '3h': Date.now() + 3 * 60 * 60 * 1000, + '4h': Date.now() + 4 * 60 * 60 * 1000, + '5h': Date.now() + 5 * 60 * 60 * 1000, + '6h': Date.now() + 6 * 60 * 60 * 1000, + '8h': Date.now() + 8 * 60 * 60 * 1000, + '12h': Date.now() + 12 * 60 * 60 * 1000, + '1d': Date.now() + 24 * 60 * 60 * 1000, + '3d': Date.now() + 3 * 24 * 60 * 60 * 1000, + '5d': Date.now() + 5 * 24 * 60 * 60 * 1000, + '7d': Date.now() + 7 * 24 * 60 * 60 * 1000, + '1w': Date.now() + 7 * 24 * 60 * 60 * 1000, + '1.5w': Date.now() + 1.5 * 7 * 24 * 60 * 60 * 1000, + '2w': Date.now() + 2 * 7 * 24 * 60 * 60 * 1000, + '3w': Date.now() + 3 * 7 * 24 * 60 * 60 * 1000, + '1m': Date.now() + 30 * 24 * 60 * 60 * 1000, + '1.5m': Date.now() + 1.5 * 30 * 24 * 60 * 60 * 1000, + '2m': Date.now() + 2 * 30 * 24 * 60 * 60 * 1000, + '3m': Date.now() + 3 * 30 * 24 * 60 * 60 * 1000, + '6m': Date.now() + 6 * 30 * 24 * 60 * 60 * 1000, + '8m': Date.now() + 8 * 30 * 24 * 60 * 60 * 1000, + '1y': Date.now() + 365 * 24 * 60 * 60 * 1000, + }[expires]); + setProgress(0); setLoading(true); const body = new FormData(); @@ -78,6 +140,7 @@ export default function Upload() { req.open('POST', '/api/upload'); req.setRequestHeader('Authorization', user.token); + req.setRequestHeader('Expires-At', expires_at.toISOString()); req.send(body); }; @@ -95,8 +158,43 @@ export default function Upload() { {progress !== 0 && } - - + +