Skip to content

Commit

Permalink
Image pulled (#130)
Browse files Browse the repository at this point in the history
* upload images

* upload images

* fixed image upload

* added smaller versions of files

* image/ui fixes

* put images on s3 bucket, set up s3

---------

Co-authored-by: Isaac Light <[email protected]>
  • Loading branch information
chennisden and is2ac2 authored Jun 14, 2024
1 parent 0d8d179 commit e04b704
Show file tree
Hide file tree
Showing 23 changed files with 423 additions and 72 deletions.
21 changes: 14 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ To get started developing:
## Database

### DynamoDB
### DynamoDB/S3

When developing locally, use the `amazon/dynamodb-local` Docker image to run a local instance of DynamoDB:
When developing locally, install the `aws` CLI and use the `localstack/localstack` Docker image to run a local instance of AWS:

```bash
docker pull amazon/dynamodb-local # If you haven't already
docker run --name store-db -d -p 8000:8000 amazon/dynamodb-local # Start the container in the background
docker pull localstack/localstack # If you haven't already
docker run --name store-db -d -p 4566:4566 localstack/localstack # Start the container in the background
```

Then, if you need to kill the database, you can run:
Expand All @@ -41,6 +41,12 @@ Initialize the test databases by running the creation script:
python -m store.app.db create
```

And initialize the image bucket:

```
aws s3api create-bucket --bucket images
```

#### Admin Panel

DynamoDB Admin is a GUI that allows you to visually see your tables and their entries. To install, run
Expand All @@ -52,7 +58,7 @@ npm i -g dynamodb-admin
To run, **source the same environment variables that you use for FastAPI** and then run

```bash
dynamodb-admin
DYNAMO_ENDPOINT=http://127.0.0.1:4566 dynamodb-admin
```

### Redis
Expand Down Expand Up @@ -105,7 +111,8 @@ export ROBOLIST_ENVIRONMENT=local
export AWS_DEFAULT_REGION='us-east-1'
export AWS_ACCESS_KEY_ID=idk
export AWS_SECRET_ACCESS_KEY=idk
export AWS_ENDPOINT_URL_DYNAMODB=http://127.0.0.1:8000
export AWS_ENDPOINT_URL_DYNAMODB=http://127.0.0.1:4566
export AWS_ENDPOINT_URL_S3=http://127.0.0.1:4566
export REACT_APP_BACKEND_URL=http://127.0.0.1:8080
export ROBOLIST_SMTP_HOST=smtp.gmail.com
export ROBOLIST_SMTP_SENDER_EMAIL=
Expand Down Expand Up @@ -145,7 +152,7 @@ To run code formatting:
npm run format
```

### Environment Variables
### Google Client ID

You will need to set `REACT_APP_GOOGLE_CLIENT_ID`. To do this, first create a Google client id (see [this LogRocket post](https://blog.logrocket.com/guide-adding-google-login-react-app/)). Then create a `.env.local` file in the `frontend` directory and add the following line:

Expand Down
41 changes: 37 additions & 4 deletions frontend/package-lock.json

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

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@types/node": "^16.18.97",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"browser-image-compression": "^2.0.2",
"holderjs": "^2.9.9",
"nth-check": ">=2.0.1",
"postcss": ">=8.4.31",
Expand All @@ -19,6 +20,7 @@
"react-scripts": "5.0.1",
"react-spring": "^9.7.3",
"typescript": "^4.9.5",
"uuid": "^10.0.0",
"web-vitals": "^2.1.4"
},
"type": "module",
Expand Down Expand Up @@ -56,6 +58,7 @@
"@fortawesome/react-fontawesome": "github:fortawesome/react-fontawesome",
"@react-oauth/google": "^0.12.1",
"@types/jest": "^29.5.12",
"@types/uuid": "^9.0.8",
"axios": "^1.7.2",
"babel-eslint": "*",
"babel-jest": "^29.7.0",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Register from "pages/Register";
import ResetPassword from "pages/ResetPassword";
import RobotDetails from "pages/RobotDetails";
import Robots from "pages/Robots";
import TestImages from "pages/TestImages";
import VerifyEmail from "pages/VerifyEmail";
import YourParts from "pages/YourParts";
import YourRobots from "pages/YourRobots";
Expand Down Expand Up @@ -62,6 +63,7 @@ const App = () => {
<Route path="/parts/add" element={<NewPart />} />
<Route path="/robot/:id" element={<RobotDetails />} />
<Route path="/edit-robot/:id" element={<EditRobotForm />} />
<Route path="/test-images" element={<TestImages />} />
<Route path="/parts/" element={<Parts />} />
<Route path="/part/:id" element={<PartDetails />} />
<Route path="robots/your" element={<YourRobots />} />
Expand Down
18 changes: 9 additions & 9 deletions frontend/src/components/RobotForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Bom, Image, Part } from "hooks/api";
import { ChangeEvent, Dispatch, FormEvent, SetStateAction } from "react";
import { Button, Col, Form, Row } from "react-bootstrap";
import ImageUploadComponent from "./files/UploadImage";

interface RobotFormProps {
title: string;
Expand Down Expand Up @@ -57,6 +58,12 @@ const RobotForm: React.FC<RobotFormProps> = ({
setImages([...robot_images, { url: "", caption: "" }]);
};

const handleImageUploadSuccess = (url: string, index: number) => {
const newImages = [...robot_images];
newImages[index].url = url;
setImages(newImages);
};

const handleRemoveImage = (index: number) => {
const newImages = robot_images.filter((_, i) => i !== index);
setImages(newImages);
Expand Down Expand Up @@ -150,15 +157,8 @@ const RobotForm: React.FC<RobotFormProps> = ({
{robot_images.map((image, index) => (
<Row key={index} className="mb-3">
<Col md={12}>
<label htmlFor={"url-" + index}>URL</label>
<Form.Control
id={"url-" + index}
className="mb-1"
type="text"
name="url"
value={image.url}
onChange={(e) => handleImageChange(index, e)}
required
<ImageUploadComponent
onUploadSuccess={(url) => handleImageUploadSuccess(url, index)}
/>
<label htmlFor={"caption-" + index}>Caption</label>
<Form.Control
Expand Down
103 changes: 103 additions & 0 deletions frontend/src/components/files/UploadImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import imageCompression from "browser-image-compression";
import { api } from "hooks/api";
import { useAuthentication } from "hooks/auth";
import React, { useState } from "react";
import { Alert, Button, Col, Form } from "react-bootstrap";

interface ImageUploadProps {
onUploadSuccess: (url: string) => void;
}

const ImageUploadComponent: React.FC<ImageUploadProps> = ({
onUploadSuccess,
}) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [compressedFile, setCompressedFile] = useState<File | null>(null);
const [uploadStatus, setUploadStatus] = useState<string | null>(null);
const [fileError, setFileError] = useState<string | null>(null);
const auth = useAuthentication();
const auth_api = new api(auth.api);
const MAX_FILE_SIZE = 2 * 1024 * 1024;
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
if (event.target.files) {
const file = event.target.files[0];
if (file) {
setUploadStatus(null);
if (file.size > MAX_FILE_SIZE) {
setFileError(
`File size should not exceed ${MAX_FILE_SIZE / 1024 / 1024} MB`,
);
} else {
const options = {
maxSizeMB: 0.2, // Maximum size in MB
maxWidthOrHeight: 800, // Maximum width or height in pixels
useWebWorker: true, // Use multi-threading for compression
};
try {
const thecompressedFile = await imageCompression(file, options);
setCompressedFile(thecompressedFile);
} catch (error) {
console.error("Error compressing the image:", error);
setFileError("Error compressing the image");
}
setSelectedFile(file);
setFileError(null);
}
} else {
setFileError("No file selected");
}
}
};

const handleUpload = async () => {
if (fileError) {
setUploadStatus("Failed to upload file");
return;
}
if (!selectedFile || !compressedFile) {
setUploadStatus("No file selected");
return;
}
const formData = new FormData();
formData.append("file", selectedFile);
const compressedFormData = new FormData();
compressedFormData.append("file", compressedFile);
try {
const image_id = await auth_api.uploadImage(formData);
onUploadSuccess(image_id);
setUploadStatus("File uploaded successfully");
} catch (error) {
setUploadStatus("Failed to upload file");
console.error("Error uploading file:", error);
}
};

return (
<Col md="6">
<div>
<Form.Group controlId="formFile" className="mb-3">
<Form.Label>Select Image</Form.Label>
<Form.Control type="file" onChange={handleFileChange} accept=".png" />
</Form.Group>
{fileError && <Alert variant="danger">{fileError}</Alert>}
<Button onClick={handleUpload} disabled={!selectedFile}>
Upload
</Button>
{uploadStatus && (
<Alert
variant={
uploadStatus.includes("successfully") ? "success" : "danger"
}
className="mt-3"
>
{uploadStatus}
</Alert>
)}
</div>
</Col>
);
};

export default ImageUploadComponent;
56 changes: 56 additions & 0 deletions frontend/src/components/files/ViewImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { api } from "hooks/api";
import { useAuthentication } from "hooks/auth";
import React, { useEffect, useState } from "react";

interface ImageProps {
imageId: string;
}

const ImageComponent: React.FC<ImageProps> = ({ imageId }) => {
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const auth = useAuthentication();
const auth_api = new api(auth.api);

useEffect(() => {
const fetchImage = async () => {
try {
const response = await auth_api.getImage(imageId);
const url = URL.createObjectURL(response);
setImageSrc(url);
setLoading(false);
} catch (err) {
setError("Failed to fetch image " + imageId);
setLoading(false);
}
};

fetchImage();
}, [imageId]);

if (loading) return <p>Loading...</p>;
if (error) return <p>{error}</p>;

return (
<div style={{ width: "100%", paddingTop: "100%", position: "relative" }}>
{imageSrc && (
<img
src={imageSrc}
alt="Robot"
className="d-block rounded-lg"
style={{
position: "absolute",
top: "0",
left: "0",
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
)}
</div>
);
};

export default ImageComponent;
Loading

0 comments on commit e04b704

Please sign in to comment.