Skip to content

Commit

Permalink
feat: ✨ implement paste to post/upload
Browse files Browse the repository at this point in the history
- Implement `usePasteFiles()` hook
- Implement `useCreatePost()` hook
- Add `acceptTypes` to `useUploadFiles()` hook for validation
- Use `usePasteFiles()` hook to paste files in post or create new post
  • Loading branch information
Dan6erbond committed May 7, 2023
1 parent 5fe24e8 commit 2cb990d
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 34 deletions.
41 changes: 41 additions & 0 deletions src/hooks/useCreatePost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { usePocketBase } from "@/pocketbase";
import { NewFile, useUploadFiles } from "./useUploadFiles";

interface NewPost {
author: string;
title: string;
files: NewFile[];
public?: boolean;
nsfw?: boolean;
}

interface UseCreatePostOptions {
acceptTypes: string[];
}

export const useCreatePost = ({ acceptTypes }: UseCreatePostOptions) => {
const pb = usePocketBase();

const { uploadFiles, uploading } = useUploadFiles({
acceptTypes,
});

const createPost = (newPost: NewPost) =>
uploadFiles(newPost.files).then(async (records) => {
if (records.length === 0) {
return;
}

const post = await pb.collection("posts").create({
title: newPost.title,
author: newPost.author,
files: records.map((f) => f.id),
public: newPost.public,
nsfw: newPost.nsfw,
});

return post;
});

return { uploading, createPost };
};
45 changes: 45 additions & 0 deletions src/hooks/usePasteFiles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { notifications } from "@mantine/notifications";
import { IconAlertCircle } from "@tabler/icons-react";
import { useEffect, useRef } from "react";

interface UsePasteFilesOptions {
acceptTypes: string[];
onPaste: (files: File[]) => void;
}

export const usePasteFiles = ({
acceptTypes,
onPaste,
}: UsePasteFilesOptions) => {
const pasteListenerRef = useRef<(ev: ClipboardEvent) => void>();

useEffect(() => {
if (typeof document !== "undefined") {
pasteListenerRef.current &&
document.removeEventListener("paste", pasteListenerRef.current);

pasteListenerRef.current = (ev) => {
if (!ev.clipboardData?.files.length) {
return;
}
const files = Array.from(ev.clipboardData.files);
if (files.filter((f) => !acceptTypes.includes(f.type)).length !== 0) {
notifications.show({
color: "red",
title: "An error occured",
message: "Please contact the developers",
icon: <IconAlertCircle />,
});
return;
}
onPaste(files);
};

document.addEventListener("paste", pasteListenerRef.current);

return () =>
pasteListenerRef.current &&
document.removeEventListener("paste", pasteListenerRef.current);
}
}, [onPaste, acceptTypes]);
};
10 changes: 8 additions & 2 deletions src/hooks/useUploadFiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ import { IconAlertCircle } from "@tabler/icons-react";
import { useState } from "react";
import { usePocketBase } from "@/pocketbase";

interface NewFile {
export interface NewFile {
file: File;
name: string;
author: string;
description: string;
}

export const useUploadFiles = () => {
interface UseUploadFilesOptions {
acceptTypes: string[];
}

export const useUploadFiles = ({ acceptTypes }: UseUploadFilesOptions) => {
const pb = usePocketBase();
const [uploading, setUploading] = useState(false);

const uploadFiles = async (files: NewFile[]) => {
setUploading(true);

const promises = files.map(async (file) => {
if (!acceptTypes.includes(file.file.type)) return;

try {
const createdRecord = await uploadFile(pb, {
file: file.file,
Expand Down
153 changes: 130 additions & 23 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,163 @@
import Dropzone from "@/components/dropzone";
import Head from "@/components/head";
import Layout from "@/components/layout";
import { useUploadFiles } from "@/hooks/useUploadFiles";
import { useCreatePost } from "@/hooks/useCreatePost";
import { pocketBaseUrl, usePocketBase } from "@/pocketbase";
import { useAuth } from "@/pocketbase/auth";
import { Group } from "@mantine/core";
import { FileWithPath } from "@mantine/dropzone";
import { MEDIA_MIME_TYPE } from "@/utils/mediaTypes";
import {
Anchor,
Badge,
Box,
Card,
Center,
Container,
Flex,
Group,
Image,
ScrollArea,
Text,
Title,
} from "@mantine/core";
import { IMAGE_MIME_TYPE } from "@mantine/dropzone";
import { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { Record } from "pocketbase";
import { useEffect, useState } from "react";
import { usePasteFiles } from "../hooks/usePasteFiles";
import { Post } from "../pocketbase/models";
import Link from "next/link";

export default function Home() {
const router = useRouter();
const pb = usePocketBase();
const { user } = useAuth();

const { uploading, uploadFiles: _uploadFiles } = useUploadFiles();
const [posts, setPosts] = useState<Post[]>([]);

useEffect(() => {
if (!user) router.push("/login");
}, [user, router]);
pb.collection("posts")
.getList<Post>(1, 10, {
expand: "files,author",
sort: "-created",
$autoCancel: false,
})
.then((records) => setPosts(records.items));
}, [pb, setPosts]);

const uploadFiles = async (files: FileWithPath[]) =>
_uploadFiles(
files.map((file) => ({
const { uploading, createPost: _createPost } = useCreatePost({
acceptTypes: MEDIA_MIME_TYPE,
});

const createPost = (files: File[]) =>
_createPost({
title: "",
author: user?.id!,
files: files.map((file) => ({
file: file,
name: file.name,
author: user?.id!,
description: "",
}))
).then(async (records) => {
if (records.length === 0) {
})),
}).then(async (post) => {
if (!post) {
return;
}

const post = await pb.collection("posts").create({
title: "",
author: user?.id!,
files: records.map((f) => f.id),
public: false,
});

router.push("/posts/" + post.id);
});

usePasteFiles({
acceptTypes: MEDIA_MIME_TYPE,
onPaste: createPost,
});

return (
<>
<Head pageTitle="Upload" />
<Layout>
<Group sx={{ justifyContent: "center" }} align="start">
<Dropzone onDrop={uploadFiles} loading={uploading} />
</Group>
<Container>
<Flex sx={{ justifyContent: "space-between" }}>
<Title order={2}>Latest Posts</Title>
<Anchor component={Link} href="/posts">
More
</Anchor>
</Flex>
<ScrollArea mb="lg" py="md">
<Group sx={{ flexWrap: "nowrap" }}>
{posts.map((post) => (
<Card key={post.id} p={0}>
<Card.Section p="md" pt="lg" m={0}>
<Title order={4}>
{post.title ||
`Post by ${(post.expand.author as Record).username}`}
</Title>
{post.nsfw && <Badge color="red">NSFW</Badge>}
</Card.Section>
<Card.Section>
{Array.isArray(post.expand.files) && (
<Box pos="relative">
{IMAGE_MIME_TYPE.includes(post.expand.files[0].type) ? (
<Image
src={pb.files.getUrl(
post.expand.files[0],
post.expand.files[0].file
)}
sx={{ flex: 1 }}
alt={
post.title ||
`Post by ${
(post.expand.author as Record).username
}`
}
/>
) : (
<video
src={pb.files.getUrl(
post.expand.files[0],
post.expand.files[0].file
)}
muted
autoPlay
controls={false}
/>
)}
{post.expand.files.length > 1 && (
<Box
pos="absolute"
sx={(theme) => ({
background: theme.fn.rgba(
theme.colors.dark[8],
0.7
),
})}
top={0}
right={0}
bottom={0}
left={0}
>
<Center h="100%" w="100%">
<Box>
<Text size="xl">
{post.expand.files.length - 1} +
</Text>
</Box>
</Center>
</Box>
)}
</Box>
)}
</Card.Section>
</Card>
))}
</Group>
</ScrollArea>
{user && (
<Group sx={{ justifyContent: "center" }} align="start">
<Dropzone onDrop={createPost} loading={uploading} />
</Group>
)}
</Container>
</Layout>
</>
);
Expand Down
23 changes: 14 additions & 9 deletions src/pages/posts/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import Dropzone from "@/components/dropzone";
import Head from "@/components/head";
import Layout from "@/components/layout";
import { usePasteFiles } from "@/hooks/usePasteFiles";
import { useUploadFiles } from "@/hooks/useUploadFiles";
import {
initPocketBaseServer,
pocketBaseUrl,
usePocketBase,
} from "@/pocketbase";
import { useAuth } from "@/pocketbase/auth";
import { File, Post } from "@/pocketbase/models";
import { Post, File as ShareMeFile } from "@/pocketbase/models";
import { MEDIA_MIME_TYPE } from "@/utils/mediaTypes";
import {
ActionIcon,
Box,
Expand All @@ -32,7 +34,6 @@ import {
IconClipboardCopy,
IconTrash,
} from "@tabler/icons-react";
import { FileWithPath } from "file-selector";
import { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import { Record } from "pocketbase";
Expand All @@ -56,7 +57,7 @@ export default function Post(props: PostProps) {

const [post, setPost] = useState<Post | null>();
const [userIsAuthor, setUserIsAuthor] = useState(props.userIsAuthor);
const [files, setFiles] = useState<File[]>([]);
const [files, setFiles] = useState<ShareMeFile[]>([]);
const [title, setTitle] = useState(props.title);
const [isPublic, setIsPublic] = useState(props.isPublic);
const [nsfw, setNsfw] = useState(props.nsfw);
Expand All @@ -65,9 +66,11 @@ export default function Post(props: PostProps) {

const [debouncedTitle] = useDebouncedValue(title, 200, { leading: true });

const { uploading, uploadFiles: _uploadFiles } = useUploadFiles();
const { uploading, uploadFiles: _uploadFiles } = useUploadFiles({
acceptTypes: MEDIA_MIME_TYPE,
});

const uploadFiles = (f: FileWithPath[]) =>
const uploadFiles = (f: File[]) =>
_uploadFiles(
f.map((file) => ({
file: file,
Expand All @@ -88,14 +91,16 @@ export default function Post(props: PostProps) {
setValues(record);
});

usePasteFiles({ acceptTypes: MEDIA_MIME_TYPE, onPaste: uploadFiles });

useEffect(() => {
setBlurred(files.map(() => nsfw));
}, [nsfw, setBlurred, files]);

const setValues = useCallback(
(record: Post) => {
setPost(record);
setFiles((files) => (record.expand.files as File[]) || files);
setFiles((files) => (record.expand.files as ShareMeFile[]) || files);
setTitle(record.title);
setIsPublic(record.public);
setNsfw(record.nsfw);
Expand Down Expand Up @@ -133,7 +138,7 @@ export default function Post(props: PostProps) {
values: { name?: string; description?: string }
) => {
try {
const record = await pb.collection("files").update<File>(id, {
const record = await pb.collection("files").update<ShareMeFile>(id, {
...values,
});
setFiles((files) => files.map((f) => (f.id === id ? record : f)));
Expand Down Expand Up @@ -396,10 +401,10 @@ export const getServerSideProps: GetServerSideProps<PostProps> = async ({
return { notFound: true };
}

const images = ((record.expand.files as File[]) ?? []).filter((f) =>
const images = ((record.expand.files as ShareMeFile[]) ?? []).filter((f) =>
IMAGE_MIME_TYPE.includes(f.type as any)
);
const videos = ((record.expand.files as File[]) ?? []).filter(
const videos = ((record.expand.files as ShareMeFile[]) ?? []).filter(
(f) => !IMAGE_MIME_TYPE.includes(f.type as any)
);

Expand Down

0 comments on commit 2cb990d

Please sign in to comment.