Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature_image_upload #23

Merged
merged 1 commit into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 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 @@ -17,6 +17,7 @@
"react-bootstrap-icons": "^1.11.4",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-images-uploading": "^3.1.7",
"react-scripts": "5.0.1",
"typescript": "^4.3.0",
"web-vitals": "^2.1.4"
Expand Down
26 changes: 17 additions & 9 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { AxiosInstance } from "axios";
import { Collection } from "types/model";

export interface Image {
filename: string;
s3_url: string;
}
import { Collection, Image } from "types/model";

export interface CollectionCreateFragment {
title: string;
description: string;
}
export class api {
export class Api {
public api: AxiosInstance;

constructor(api: AxiosInstance) {
Expand All @@ -20,7 +15,7 @@ export class api {
const response = await this.api.get(`/`);
return response.data.message;
}
public async handleUpload(formData: FormData): Promise<Image> {
public async handleUpload(formData: FormData): Promise<Image | null> {
try {
const response = await this.api.post("/upload/", formData, {
headers: {
Expand All @@ -30,7 +25,7 @@ export class api {
return response.data;
} catch (error) {
console.error("Error uploading the file", error);
return { s3_url: "", filename: "" };
return null;
}
}
public async createCollection(
Expand All @@ -51,4 +46,17 @@ export class api {
const response = await this.api.get(`/get_collections`);
return response.data;
}
public async uploadImage(file: File, collection_id: string): Promise<Image> {
const response = await this.api.post("/upload", {
file,
id: collection_id,
});
return response.data;
}
public async getImages(collection_id: string): Promise<Array<Image>> {
const response = await this.api.get(
`/get_images?collection_id=${collection_id}`,
);
return response.data;
}
}
18 changes: 11 additions & 7 deletions frontend/src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ export const signup = async (data: SignupData): Promise<Response> => {
const response = await axios.post(`${API_URL}/signup`, data);
return response.data;
};
export const read_me = async (token: string): Promise<Response> => {
const response = await axios.get(`${API_URL}/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
export const read_me = async (token: string): Promise<Response | null> => {
try {
const response = await axios.get(`${API_URL}/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch {
return null;
}
};
export const signin = async (data: SigninData): Promise<Response> => {
const response = await axios.post(`${API_URL}/signin`, data);
Expand Down
124 changes: 124 additions & 0 deletions frontend/src/components/UploadContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { FC, useState } from "react";
import { XCircleFill } from "react-bootstrap-icons";
import ImageUploading, { ImageListType } from "react-images-uploading";

interface UploadContentProps {
onUpload: (file: File) => Promise<void>; // Updated to handle a single file
}

const UploadContent: FC<UploadContentProps> = ({ onUpload }) => {
const [images, setImages] = useState<ImageListType>([]);
const [uploading, setUploading] = useState<boolean>(false);
// const [uploadIndex, setUploadIndex] = useState<number>(0);

const maxNumber = 10; // Set the maximum number of files allowed

const handleUpload = async () => {
if (images.length === 0) return;

setUploading(true);
for (let i = 0; i < images.length; i++) {
const file = images[i].file as File;
try {
await onUpload(file); // Upload each file one by one
// Optionally, handle success feedback here
} catch (error) {
console.error(`Failed to upload file ${file.name}:`, error);
// Optionally, handle failure feedback here
}
}
setImages([]); // Clear images after uploading
setUploading(false);
};

const onChange = (imageList: ImageListType) => {
setImages(imageList);
};

return (
<div className="p-6 bg-white dark:bg-gray-800 rounded-lg">
<ImageUploading
multiple
value={images}
onChange={onChange}
maxNumber={maxNumber}
dataURLKey="data_url"
>
{({
imageList,
onImageUpload,
onImageRemoveAll,
onImageRemove,
isDragging,
dragProps,
}) => (
<div className="upload__image-wrapper">
{/* Dropzone Area */}
<div
className={`border-2 border-dashed p-5 rounded-lg flex flex-col items-center justify-center h-64 transition-colors duration-300 ${
isDragging
? "border-green-500 bg-green-100 dark:border-green-600 dark:bg-green-900"
: "border-gray-300 bg-gray-50 dark:border-gray-700 dark:bg-gray-800"
}`}
onClick={onImageUpload} // Click to upload images
{...dragProps} // Drag-n-Drop props
>
<p className="text-gray-500 dark:text-gray-400">
Drag & drop images here, or click to select files
</p>
</div>

{/* Display uploaded images below the dropzone */}
<div className="mt-5 grid grid-cols-3 gap-4 w-full">
{imageList.length > 0 ? (
imageList.map((image, index) => (
<div key={index} className="relative text-center">
<img
src={image["data_url"]}
alt=""
className="w-full h-32 object-cover rounded-lg border border-gray-300 dark:border-gray-700"
/>
<button
className="absolute top-2 right-2 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300 transition-colors duration-200"
onClick={() => onImageRemove(index)}
>
<XCircleFill size={24} />
</button>
</div>
))
) : (
<p className="text-gray-500 dark:text-gray-400">
No images uploaded yet.
</p>
)}
</div>

{/* Optional: Remove all button */}
{imageList.length > 0 && (
<div className="flex gap-4">
{/* Upload Button */}
<button
className={`mt-3 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 transition-colors duration-200 ${
uploading ? "cursor-not-allowed opacity-50" : ""
}`}
onClick={handleUpload}
disabled={uploading || images.length === 0}
>
{uploading ? "Uploading..." : "Upload Images"}
</button>
<button
className="mt-3 bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-500 transition-colors duration-200"
onClick={onImageRemoveAll}
>
Remove All Images
</button>
</div>
)}
</div>
)}
</ImageUploading>
</div>
);
};

export default UploadContent;
33 changes: 33 additions & 0 deletions frontend/src/components/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FC, ReactNode } from "react";
import { X } from "react-bootstrap-icons"; // Using react-bootstrap-icons for the close button

interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
}

const Modal: FC<ModalProps> = ({ isOpen, onClose, children }) => {
if (!isOpen) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-70 dark:bg-gray-900 dark:bg-opacity-80">
{/* Modal container */}
<div className="bg-white p-8 rounded-2xl shadow-xl max-w-3xl w-full relative border border-gray-200 dark:bg-gray-800 dark:border-gray-700 dark:text-white">
{/* Close button in the top right corner */}
<button
className="absolute top-4 right-4 text-gray-600 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 transition-colors duration-200"
onClick={onClose}
aria-label="Close Modal"
>
<X size={28} /> {/* Close icon from react-bootstrap-icons */}
</button>

{/* Modal content */}
<div className="p-6">{children}</div>
</div>
</div>
);
};

export default Modal;
9 changes: 6 additions & 3 deletions frontend/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
const token = localStorage.getItem("token");
if (token) {
const fetch_data = async (token: string) => {
const response = await read_me(token);
console.log(response);
if (response) setAuth(response);
try {
const response = await read_me(token);
if (response) setAuth(response);
} catch {
return;
}
};
fetch_data(token);
} else signout();
Expand Down
Loading
Loading