Skip to content

Commit

Permalink
Merge pull request #97 from JimTheCat/CU-8697bp6d2_Add-possibility-to…
Browse files Browse the repository at this point in the history
…-add-pictures-to-posts_Patryk-Kosiski

feature: Add pictures to post
  • Loading branch information
JimTheCat authored Jan 14, 2025
2 parents 69acec5 + 60423cd commit fad3e51
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 61 deletions.
31 changes: 31 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@mantine/carousel": "^7.15.2",
"@mantine/core": "^7.15.2",
"@mantine/dates": "^7.15.2",
"@mantine/dropzone": "^7.15.2",
"@mantine/form": "^7.15.2",
"@mantine/hooks": "^7.15.2",
"@mantine/modals": "^7.15.2",
Expand Down
93 changes: 61 additions & 32 deletions frontend/src/Features/CreatePost/CreatePost.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,89 @@
import {IconPencil} from "@tabler/icons-react";
import {Button} from "@mantine/core";
import {ModificationModal} from "../shared/components/ModificationModal";
import {PostForm} from "./components/PostForm";
import {useRef, useState} from "react";
import api from "../shared/services/api.ts";
import {useAlert} from "../../Providers/AlertProvider.tsx";
import {ModificationModal} from "../shared/components/ModificationModal";
import {Button} from "@mantine/core";
import {ModalRichContent} from "../shared/consts";
import api from "../shared/services/api.ts";

export const CreatePost = () => {
const [content, setContent] = useState('');
const [content, setContent] = useState("");
const contentRef = useRef(content);
const [images, setImages] = useState<File[]>([]);
const imagesRef = useRef(images);
const alert = useAlert();

const handleContentChange = (html: string) => {
setContent(html);
contentRef.current = html;
}
};

const handleCreatePost = () => {
const handleImagesChange = (files: File[]) => {
setImages(files);
imagesRef.current = files;
};

const handleCreatePost = async () => {
const contentToSave = contentRef.current;
console.log('Create post: ', contentToSave);
if (!contentToSave) {
const images = imagesRef.current;

// Walidacja: musi być treść lub zdjęcia
if (!contentToSave && images.length === 0) {
alert.showError({
title: 'Error',
message: 'Content is empty',
level: 'WARNING',
timestamp: new Date().toISOString()
title: "Error",
message: "Post cannot be empty. Please add content or images.",
level: "WARNING",
timestamp: new Date().toISOString(),
});
return;
}

api.post('/api/posts', null, {params: {content: contentToSave}}).then((response) => {
if (response.status === 200) {
close();
}
// Prepare form data
const formData = new FormData();
if (contentToSave) formData.append("content", contentToSave);
images.forEach((image) => {
formData.append(`pictures`, image);
});
}

const response = await api.post("/api/posts", formData);

if (response.status === 200) {
alert.showError({
title: "Success",
message: "Post created successfully!",
level: "INFO",
timestamp: new Date().toISOString(),
});
close();
}
};

return (
<Button
variant={"subtle"}
size={"md"}
variant="subtle"
size="md"
leftSection={<IconPencil/>}
autoContrast
fullWidth
justify={"flex-start"}
color={"gray"}
onClick={() => ModificationModal({
handleAction: handleCreatePost,
title: 'Create post',
buttonConfirmText: 'Create',
buttonConfirmColor: 'blue',
childrenContent: <ModalRichContent handleContentChange={handleContentChange}/>
})}
justify="flex-start"
color="gray"
onClick={() =>
ModificationModal({
handleAction: () => {
handleCreatePost().catch();
},
title: "Create post",
buttonConfirmText: "Create",
buttonConfirmColor: "blue",
childrenContent: (
<PostForm
handleContentChange={handleContentChange}
setImages={handleImagesChange}
/>
),
})
}
>
Create post
</Button>

);
}
};
110 changes: 110 additions & 0 deletions frontend/src/Features/CreatePost/components/PostForm/PostForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {useState} from "react";
import {Dropzone, MIME_TYPES} from "@mantine/dropzone";
import {Button, Group, Image, Paper, rem, SimpleGrid, Text} from "@mantine/core";
import {ModalRichContent} from "../../../shared/consts";
import {useAlert} from "../../../../Providers/AlertProvider.tsx";
import {IconPhoto, IconUpload, IconX} from "@tabler/icons-react";

type PostFormProps = {
handleContentChange: (html: string) => void;
setImages: (files: File[]) => void;
};

export const PostForm = ({handleContentChange, setImages}: PostFormProps) => {
const [localImages, setLocalImages] = useState<File[]>([]);
const [previews, setPreviews] = useState<{ src: string; name: string }[]>([]);
const alert = useAlert();

// Function to handle dropping images
const handleDrop = (files: File[]) => {
if (localImages.length + files.length > 5) {
alert.showError({
title: "Too many images",
message: "You can upload up to 5 images",
level: "WARNING",
timestamp: new Date().toISOString(),
});
return;
}

// Add new images to the existing ones
const updatedImages = [...localImages, ...files];
setLocalImages(updatedImages);
setImages(updatedImages); // Passing to CreatePost

setPreviews((prevPreviews) => [
...prevPreviews,
...files.map((file) => ({src: URL.createObjectURL(file), name: file.name})),
]);
};

// Function to remove an image
const handleRemoveImage = (index: number) => {
const updatedImages = localImages.filter((_, i) => i !== index);
setLocalImages(updatedImages);
setImages(updatedImages); // Passing to CreatePost

URL.revokeObjectURL(previews[index].src);
setPreviews((prevPreviews) => prevPreviews.filter((_, i) => i !== index));
};

return (
<>
<ModalRichContent handleContentChange={handleContentChange}/>

<Dropzone
onDrop={handleDrop}
accept={[MIME_TYPES.jpeg, MIME_TYPES.png, MIME_TYPES.gif]}
maxSize={3 * 1024 ** 2}
multiple
>
<Group justify="center" gap="xl" mih={120} style={{pointerEvents: "none"}}>
<Dropzone.Accept>
<IconUpload
style={{width: rem(52), height: rem(52), color: "var(--mantine-color-blue-6)"}}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX
style={{width: rem(52), height: rem(52), color: "var(--mantine-color-red-6)"}}
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto
style={{width: rem(52), height: rem(52), color: "var(--mantine-color-dimmed)"}}
stroke={1.5}
/>
</Dropzone.Idle>

<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach up to 5 files, each file should not exceed 3MB
</Text>
</div>
</Group>
</Dropzone>

{previews.length > 0 && (
<SimpleGrid cols={2} mt="md">
{previews.map((preview, index) => (
<Paper maw={"25vw"} withBorder p="sm" radius="md" key={index + 1}>
<Image src={preview.src} alt={`preview-${index}`} width={100} height={100} radius="sm"/>

<Text size="sm" fw={500} my={"sm"} truncate={"end"}>
{preview.name}
</Text>
<Button fullWidth color="red" size="xs" onClick={() => handleRemoveImage(index)}>
Remove
</Button>
</Paper>
))}
</SimpleGrid>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {PostForm} from './PostForm';
9 changes: 2 additions & 7 deletions frontend/src/Features/MainPage/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useEffect, useState} from "react";
import {PostDTO} from "./types/Post.tsx";
import {PostDTO} from "../shared/types";
import api from "../shared/services/api.ts";
import {Box, Stack} from "@mantine/core";
import {InfiniteScroll} from "./components/InfiniteScroll";
Expand Down Expand Up @@ -49,12 +49,7 @@ export const MainPage = () => {
content={post.content}
createdAt={post.createdAt}
numberOfComments={post.numberOfComments}
photosUrls={
// generate 100 random photos
Array.from({length: 100}, () => {
return "https://picsum.photos/seed/" + Math.random() + "/800/2200";
})
}
pictures={post.pictures}
/>
))}
</Stack>
Expand Down
Loading

0 comments on commit fad3e51

Please sign in to comment.