From c1449a0cb248464e289ec760b0d4a828ca109ef1 Mon Sep 17 00:00:00 2001 From: eyasfar Date: Fri, 31 Dec 2021 18:29:54 +0100 Subject: [PATCH] Art Gallery, complete. --- arts-gallery/.gitignore | 24 +++ arts-gallery/README.md | 143 ++++++++---------- arts-gallery/client/.gitignore | 23 +++ arts-gallery/client/package.json | 42 +++++ arts-gallery/client/public/index.html | 47 ++++++ arts-gallery/client/public/robots.txt | 3 + arts-gallery/client/src/App.css | 38 +++++ arts-gallery/client/src/App.js | 25 +++ arts-gallery/client/src/App.test.js | 8 + .../src/components/addPainting/AddPainting.js | 84 ++++++++++ .../components/addPainting/addPainting.css | 0 .../components/editPainting/editPainting.css | 30 ++++ .../components/editPainting/editPainting.js | 91 +++++++++++ .../client/src/components/gallery/Gallery.css | 109 +++++++++++++ .../client/src/components/gallery/Gallery.js | 65 ++++++++ .../client/src/components/navbar/Navbar.css | 21 +++ .../client/src/components/navbar/Navbar.js | 20 +++ arts-gallery/client/src/index.css | 13 ++ arts-gallery/client/src/index.js | 18 +++ arts-gallery/client/src/logo.svg | 1 + arts-gallery/client/src/reportWebVitals.js | 13 ++ arts-gallery/client/src/setupTests.js | 5 + arts-gallery/dummy_data.js | 30 ---- arts-gallery/package.json | 24 +++ arts-gallery/server/.gitignore | 21 +++ arts-gallery/server/LICENSE | 21 +++ arts-gallery/server/README.md | 12 ++ arts-gallery/server/models/painting.js | 11 ++ arts-gallery/server/package.json | 25 +++ arts-gallery/server/routes/painting.routes.js | 93 ++++++++++++ arts-gallery/server/server.js | 55 +++++++ arts-gallery/server/utils/cloudinary.js | 9 ++ arts-gallery/server/utils/multer.js | 15 ++ 33 files changed, 1031 insertions(+), 108 deletions(-) create mode 100644 arts-gallery/.gitignore create mode 100644 arts-gallery/client/.gitignore create mode 100644 arts-gallery/client/package.json create mode 100644 arts-gallery/client/public/index.html create mode 100644 arts-gallery/client/public/robots.txt create mode 100644 arts-gallery/client/src/App.css create mode 100644 arts-gallery/client/src/App.js create mode 100644 arts-gallery/client/src/App.test.js create mode 100644 arts-gallery/client/src/components/addPainting/AddPainting.js create mode 100644 arts-gallery/client/src/components/addPainting/addPainting.css create mode 100644 arts-gallery/client/src/components/editPainting/editPainting.css create mode 100644 arts-gallery/client/src/components/editPainting/editPainting.js create mode 100644 arts-gallery/client/src/components/gallery/Gallery.css create mode 100644 arts-gallery/client/src/components/gallery/Gallery.js create mode 100644 arts-gallery/client/src/components/navbar/Navbar.css create mode 100644 arts-gallery/client/src/components/navbar/Navbar.js create mode 100644 arts-gallery/client/src/index.css create mode 100644 arts-gallery/client/src/index.js create mode 100644 arts-gallery/client/src/logo.svg create mode 100644 arts-gallery/client/src/reportWebVitals.js create mode 100644 arts-gallery/client/src/setupTests.js delete mode 100644 arts-gallery/dummy_data.js create mode 100644 arts-gallery/package.json create mode 100644 arts-gallery/server/.gitignore create mode 100644 arts-gallery/server/LICENSE create mode 100644 arts-gallery/server/README.md create mode 100644 arts-gallery/server/models/painting.js create mode 100644 arts-gallery/server/package.json create mode 100644 arts-gallery/server/routes/painting.routes.js create mode 100644 arts-gallery/server/server.js create mode 100644 arts-gallery/server/utils/cloudinary.js create mode 100644 arts-gallery/server/utils/multer.js diff --git a/arts-gallery/.gitignore b/arts-gallery/.gitignore new file mode 100644 index 0000000..bb3fcdf --- /dev/null +++ b/arts-gallery/.gitignore @@ -0,0 +1,24 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +/.env +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/arts-gallery/README.md b/arts-gallery/README.md index 9cc366c..4e5c245 100644 --- a/arts-gallery/README.md +++ b/arts-gallery/README.md @@ -1,104 +1,91 @@ -# ArtsGallery +# Art Gallery -You will be creating a full-stack application to save paintings into a gallery by uploading them and saving them into a database. In order to do this you will be using MongoDB with the [Mongoose ODM](http://mongoosejs.com/). Your front end will display views created from data in the database. You will use [ReactJS](https://facebook.github.io/react/) for that, and will serve your application with a [NodeJS](https://nodejs.org/) web server, using [ExpressJS](https://expressjs.com/). +Full-stack application to save paintings into a gallery by uploading them and saving them into a database. -Please work on the following features **in order**, moving on to the next feature only after the one you are working on is complete. **Please commit WORKING code early and often**. In addition, after each step, please follow the guidelines for a commit message. +## Tech Stack -### Part 1 - Paintings Gallery +**Client:** React -1. **As a user**, I want to be able to view the paintings I have in my gallery. If no paintings are present in the database, I will have to see a message indicating that `No paintings in Gallery` and a button to upload new ones. +**Server:** Node, Express, MongoDB, Cloudinary, Multer -To implement this user story, you should: +## API Reference -- Write an ExpressJS web server that listens to request on port `8000`. -- Run this command a brand new React App in a folder named `client`. Then navigate to it. - ``` - npx create-react-app client - cd client/ - ``` -- You may want to use [React Router](https://reactrouter.com/) or [Conditional Rendering](https://www.reactjs.org/docs/conditional-rendering.html) to navigate between components. -- Write a script that would add the dummy data to your database when `npm run seed-database` is run from the command line. Add this command to the `package.json` to be able to run it with `npm`. When you have this working, run the command so that your database is populated. - \_Note: Create a Painting Schema under `server/models/Painting.js`. It should have these following attributes: - - `id`: Number - - `artist`: String _(for the author field)_ - - `name`: String - - `year`: Number -- Complete the route `/api/paintings` in `server/routes/paintings.routes.js` so that requests to this route are responded to with the data for all the paintings, retrieved from the database. -- You can use the `dummy_data.js` for your front end views. Then, you can refactor your front end to retrieve seed data from the server rather than using the dummy data. -- Render each painting in a `Card` containing the image, the name, the artist, and the date. -- **WHEN COMPLETE AND WORKING, make a commit that says `Part 1 Complete`** +#### Get all paintings -### Part 2 - Create new Paintings +```http + GET /api/v1/paintings +``` -1. **As a user**, I want to be able to create new Paintings and save them in the database. First, make this feature work with a simple form where the user can manually input: +#### Get ONE painting -- Name -- Artist -- Year -- Painting url - For consistency, use real data from the internet when you test your application. +```http + GET /api/v1/paintings/${id} +``` -2. **As a user**, I want to be able create new Paintings by uploading images from my local machine. - For this, you should: +| Parameter | Type | Description | +| :-------- | :------- | :-------------------------------- | +| `_id` | `ObjectID` | **Required**. Id of painting to fetch | -- Add an input of type `file` to your form where user can upload images -- Use `FormData` to send a request including data and files -- Use [multer](https://www.npmjs.com/package/multer) to handle requests including files -- Use [Cloudinary](https://cloudinary.com/) to store images in the cloud and generate urls -- Save the Painting with the data from the inputs and the url generated by Cloudinary -- **WHEN COMPLETE AND WORKING, make a commit that says `Part 2 Complete`** -### Part 3 - Edit Existing Paintings +#### Create painting -1. **As a user**, I want to update existing paintings in the database. +```http + POST /api/v1/paintings +``` -- With every Painting Card, there should be an `Edit` button. -- When the user clicks on `Edit`, a new `Modal` should be rendered -- The `Modal` will contain a **prefilled** form with the data of the selected painting -- The user can click on `Cancel` to close the Modal -- The user can update the data and click on `save` -- The modal will be closed and the data of the painting will be updated in the `PaitningList` component +#### Update painting -- **WHEN COMPLETE AND WORKING, make a commit that says `Part 3 Complete`** +```http + GET /api/v1/paintings/${id} +``` -### Part 4 - Delete Painting +| Parameter | Type | Description | +| :-------- | :------- | :-------------------------------- | +| `_id` | `ObjectID` | **Required**. Id of painting to update | -1. **As a user**, I want to be able to delete existing paintings from the database +#### Delete painting -- Each painting card will contain a `Delete` button -- When the user clicks on the `Delete` button, a `Modal` will be rendered with 2 options: `Confirm` and `Cancel` -- Clicking on `Confirm` will delete the painting and close the modal -- The painting will no longuer exist in the `PaintingList` component +```http + GET /api/paintings/${id} +``` -- **WHEN COMPLETE AND WORKING, make a commit that says `Part 4 Complete`** +| Parameter | Type | Description | +| :-------- | :------- | :-------------------------------- | +| `_id` | `ObjectID` | **Required**. Id of painting to delete | -### API Structure +## Installation -> **Pro tip:** Install and use [Postman](https://www.getpostman.com/) to test your API routes for this section +Clone the project into your local machine -Using the existing code provided in `server/`, follow the steps below to build out a Paintings API: +```bash + git clone https://github.com/WaelChettaoui/art-gallery.git + cd art-gallery +``` +⚠ BEFORE RUNNING THE PROJECT, YOU MUST CONFIGURE YOUR .env FILE ⚠ -| URL | HTTP Verb | Request Body | Result | -| :----------------: | :-------: | :----------: | :--------------------------------------------------------------------: | -| /api/paintings | GET | empty | Return JSON of all Paintings | -| /api/paintings | POST | JSON | Create new Painting and return JSON of created Painting | -| /api/paintings/:id | DELETE | empty | Return JSON of single Painting with matching `number` | -| /api/paintings/:id | PUT | FormData | Update Painting with matching `id` and return JSON of updated Painting | +Switch into the server folder and install the dependencies -## Available Resources +```bash + cd server + npm i +``` -- [Stack Overflow](http://stackoverflow.com/) -- [MDN](https://developer.mozilla.org/) -- [ExpressJS Docs](https://expressjs.com/) -- [Body Parser Middleware Docs](https://github.com/expressjs/body-parser) -- [Mongo Docs](https://www.mongodb.com/) -- [Mongoose ODM Docs](http://mongoosejs.com/) -- [Cloudinary API](https://cloudinary.com/documentation/node_integration) -- [ReactJS Docs](https://facebook.github.io/react/) -- [React Router Docs](https://github.com/ReactTraining/react-router/tree/master/docs) -- [NodeJS Docs](https://nodejs.org/) -- [Academind Node-Multer](https://www.youtube.com/watch?v=srPXMt1Q0nY&ab_channel=Academind) to learn how to handle uploaded images in Node -- [Academind React Image Upload](https://www.youtube.com/watch?v=XeiOnkEI7XI&ab_channel=Academind) to learn how to upload images in React -- [Postman](https://www.getpostman.com/) -- Docs for any npm packages you might use +Switch into the client folder and install the dependencies + +```bash + cd ../client + npm i +``` + +Run the project +```bash + cd .. + npm run dev +``` + +## Authors + +- [@mohamedazizkallel](https://github.com/mohamedazizkallel) +- [@sfareya](https://github.com/sfareya) +- [@WaelChettaoui](https://github.com/WaelChettaoui) diff --git a/arts-gallery/client/.gitignore b/arts-gallery/client/.gitignore new file mode 100644 index 0000000..4d29575 --- /dev/null +++ b/arts-gallery/client/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/arts-gallery/client/package.json b/arts-gallery/client/package.json new file mode 100644 index 0000000..ac9a97a --- /dev/null +++ b/arts-gallery/client/package.json @@ -0,0 +1,42 @@ +{ + "name": "gallery", + "version": "0.1.0", + "private": true, + "dependencies": { + "@material-ui/core": "^4.12.3", + "@material-ui/icons": "^4.11.2", + "@testing-library/jest-dom": "^5.16.1", + "@testing-library/react": "^12.1.2", + "@testing-library/user-event": "^13.5.0", + "axios": "^0.24.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-router-dom": "^5.3.0", + "react-scripts": "5.0.0", + "web-vitals": "^2.1.2" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/arts-gallery/client/public/index.html b/arts-gallery/client/public/index.html new file mode 100644 index 0000000..e78fef3 --- /dev/null +++ b/arts-gallery/client/public/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + Art Gallery + + + +
+ + + diff --git a/arts-gallery/client/public/robots.txt b/arts-gallery/client/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/arts-gallery/client/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/arts-gallery/client/src/App.css b/arts-gallery/client/src/App.css new file mode 100644 index 0000000..74b5e05 --- /dev/null +++ b/arts-gallery/client/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/arts-gallery/client/src/App.js b/arts-gallery/client/src/App.js new file mode 100644 index 0000000..57dc7e1 --- /dev/null +++ b/arts-gallery/client/src/App.js @@ -0,0 +1,25 @@ +import './App.css' +import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' + +// Importing components +import Gallery from './components/gallery/Gallery' +import AddPainting from './components/addPainting/AddPainting' +import EditPainting from './components/editPainting/editPainting' +import Navbar from './components/navbar/Navbar' + +function App() { + return ( + +
+ + + + + + +
+
+ ) +} + +export default App diff --git a/arts-gallery/client/src/App.test.js b/arts-gallery/client/src/App.test.js new file mode 100644 index 0000000..4741580 --- /dev/null +++ b/arts-gallery/client/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react' +import App from './App' + +test('renders learn react link', () => { + render() + const linkElement = screen.getByText(/learn react/i) + expect(linkElement).toBeInTheDocument() +}) diff --git a/arts-gallery/client/src/components/addPainting/AddPainting.js b/arts-gallery/client/src/components/addPainting/AddPainting.js new file mode 100644 index 0000000..cd4a242 --- /dev/null +++ b/arts-gallery/client/src/components/addPainting/AddPainting.js @@ -0,0 +1,84 @@ +import { useState } from 'react' +import { useHistory } from 'react-router-dom' + +const AddPainting = () => { + const history = useHistory() + const [data, setData] = useState({ + name: '', + artist: '', + year: '', + paintingImg: '', + }) + const handleChange = (name, artist, year) => (e) => { + const value = name === 'paintingImg' ? e.target.files[0] : e.target.value + setData({ ...data, [name]: value, [artist]: value, [year]: value }) + } + const handleSubmit = async () => { + try { + let formData = new FormData() + formData.append('paintingImg', data.paintingImg) + formData.append('name', data.name) + formData.append('artist', data.artist) + formData.append('year', data.year) + + const res = await fetch(`http://localhost:8000/api/v1/painting/`, { + method: 'POST', + body: formData, + }) + if (res.ok) { + setData({ name: '', paintingImg: '', artist: '', year: '' }) + history.replace('/') + } + } catch (error) { + console.log(error) + } + } + + return ( +
+

Add New Painting

+ + + + + + + + + + + +
+ ) +} + +export default AddPainting diff --git a/arts-gallery/client/src/components/addPainting/addPainting.css b/arts-gallery/client/src/components/addPainting/addPainting.css new file mode 100644 index 0000000..e69de29 diff --git a/arts-gallery/client/src/components/editPainting/editPainting.css b/arts-gallery/client/src/components/editPainting/editPainting.css new file mode 100644 index 0000000..9b6752a --- /dev/null +++ b/arts-gallery/client/src/components/editPainting/editPainting.css @@ -0,0 +1,30 @@ +.create { + max-width: 400px; + margin: 0 auto; + text-align: center; +} +.create label { + text-align: left; + display: block; +} +.create h2 { + font-size: 20px; + color: #f1356d; + margin-bottom: 30px; +} +.create input { + width: 100%; + padding: 6px 10px; + margin: 10px 0; + border: 1px solid #ddd; + box-sizing: border-box; + display: block; +} +.create button { + background: #f1356d; + color: #fff; + border: 0; + padding: 8px; + border-radius: 8px; + cursor: pointer; +} diff --git a/arts-gallery/client/src/components/editPainting/editPainting.js b/arts-gallery/client/src/components/editPainting/editPainting.js new file mode 100644 index 0000000..385bc92 --- /dev/null +++ b/arts-gallery/client/src/components/editPainting/editPainting.js @@ -0,0 +1,91 @@ +import { useEffect, useState } from 'react' +import { useHistory, useParams } from 'react-router-dom' +import './editPainting.css' +const EditPainting = () => { + const { id } = useParams() + console.log(id) + const history = useHistory() + const [data, setData] = useState({ + name: '', + artist: '', + year: '', + paintingImg: '', + }) + useEffect(() => { + fetch(`http://localhost:8000/api/v1/painting/${id}`) + .then((res) => res.json()) + .then((data) => setData(data)) + }, []) + + const handleChange = (name, artist, year) => (e) => { + const value = name === 'paintingImg' ? e.target.files[0] : e.target.value + setData({ ...data, [name]: value, [artist]: value, [year]: value }) + } + + const handleSubmit = async () => { + try { + let formData = new FormData() + formData.append('paintingImg', data.paintingImg) + formData.append('name', data.name) + formData.append('artist', data.artist) + formData.append('year', data.year) + + const res = await fetch(`http://localhost:8000/api/v1/painting/${id}`, { + method: 'PUT', + body: formData, + }) + if (res.ok) { + setData({ name: '', paintingImg: '', artist: '', year: '' }) + history.replace('/') + } + } catch (error) { + console.log(error) + } + } + + return ( +
+

Edit a Painting

+ + + + + + + + + +
+ ) +} + +export default EditPainting diff --git a/arts-gallery/client/src/components/gallery/Gallery.css b/arts-gallery/client/src/components/gallery/Gallery.css new file mode 100644 index 0000000..b376693 --- /dev/null +++ b/arts-gallery/client/src/components/gallery/Gallery.css @@ -0,0 +1,109 @@ +.gallery { + /* -webkit-column-count: 3; + -moz-column-count: 3; */ + /* column-count: 3; */ + /* -webkit-column-width: 33%; + -moz-column-width: 33%; + column-width: 33%; */ + padding: 12px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: space-evenly; +} + +.title { + text-align: center; +} +.gallery .image-wrapper { + width: 300px; + overflow: hidden; + box-shadow: 0px 0px 15px -5px; + /* padding: 12px; */ + margin: 20px; + text-align: center; +} + +.gallery .pics img { + -webkit-transition: all 350ms ease; + transition: all 350ms ease; + cursor: pointer; + margin-bottom: 12px; + overflow: hidden; + height: 100%; +} + +.gallery .pics:hover { + filter: opacity(0.8); +} + +/* @media (max-width: 991px) { + .gallery { + -webkit-column-count: 2; + -moz-column-count: 2; + column-count: 2; + } +} + +@media (max-width: 480px) { + .gallery { + -webkit-column-count: 1; + -moz-column-count: 1; + column-count: 1; + -webkit-column-width: 100%; + -moz-column-width: 100%; + column-width: 100%; + } +} */ + +/*model */ + +.model { + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: #000000; + transition: opacity 0.4s ease, visibility 0.4s ease, + transform 0.5s ease-in-out; + visibility: hidden; + opacity: 0; + transform: scale(0); + overflow: hidden; + z-index: 999; +} + +.model.open { + visibility: visible; + opacity: 1; + transform: scale(1); +} + +.model img { + width: auto; + max-width: 100%; + max-height: 100%; + height: auto; + display: block; + line-height: 0; + box-sizing: border-box; + padding: 20px 0 20px; + margin: 0 auto; +} + +.model.open svg { + position: fixed; + top: 10px; + right: 10px; + width: 2rem; + height: 2rem; + padding: 5px; + background-color: rgba(0, 0, 0, 0.4); + color: #ffffff; + cursor: pointer; +} diff --git a/arts-gallery/client/src/components/gallery/Gallery.js b/arts-gallery/client/src/components/gallery/Gallery.js new file mode 100644 index 0000000..47e442a --- /dev/null +++ b/arts-gallery/client/src/components/gallery/Gallery.js @@ -0,0 +1,65 @@ +import { Link } from 'react-router-dom' +import { useEffect, useState } from 'react' +import './Gallery.css' + +const Gallery = () => { + const [paintings, setPaintings] = useState() + + useEffect(() => { + const fetchPaintings = async () => { + const res = await fetch(`http://localhost:8000/api/v1/painting/`) + const data = await res.json() + setPaintings(data) + } + fetchPaintings() + }, []) + + const handleDelete = async (id) => { + try { + const res = await fetch(`http://localhost:8000/api/v1/painting/${id}`, { + method: 'DELETE', + }) + if (res.ok) { + const updatedPaintings = paintings.filter( + (painting) => painting._id !== id + ) + setPaintings(updatedPaintings) + } + } catch (error) { + console.log(error) + } + } + return ( +
+
+ {paintings?.map((painting) => ( +
+ +

{painting.name}

+

{painting.artist}

+

{painting.year}

+
+ + Edit + + +
+
+ ))} +
+
+ ) +} + +export default Gallery diff --git a/arts-gallery/client/src/components/navbar/Navbar.css b/arts-gallery/client/src/components/navbar/Navbar.css new file mode 100644 index 0000000..2dc719c --- /dev/null +++ b/arts-gallery/client/src/components/navbar/Navbar.css @@ -0,0 +1,21 @@ +.navbar { + margin: 20px 50px; + height: 20px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + font-family: 'Abril Fatface', cursive; +} + +.homePage { + text-decoration: none; + color: black; + font-size: 36px; +} + +.nav-link { + text-decoration: none; + color: black; + font-size: 20px; +} \ No newline at end of file diff --git a/arts-gallery/client/src/components/navbar/Navbar.js b/arts-gallery/client/src/components/navbar/Navbar.js new file mode 100644 index 0000000..82de82d --- /dev/null +++ b/arts-gallery/client/src/components/navbar/Navbar.js @@ -0,0 +1,20 @@ +import { Link } from 'react-router-dom' +import './Navbar.css' + +const Navbar = () => { + return ( + + ) +} + +export default Navbar diff --git a/arts-gallery/client/src/index.css b/arts-gallery/client/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/arts-gallery/client/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/arts-gallery/client/src/index.js b/arts-gallery/client/src/index.js new file mode 100644 index 0000000..29964c9 --- /dev/null +++ b/arts-gallery/client/src/index.js @@ -0,0 +1,18 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import './index.css' +import App from './App' +import reportWebVitals from './reportWebVitals' + +ReactDOM.render( + + + + , + document.getElementById('root') +) + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals() diff --git a/arts-gallery/client/src/logo.svg b/arts-gallery/client/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/arts-gallery/client/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/arts-gallery/client/src/reportWebVitals.js b/arts-gallery/client/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/arts-gallery/client/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/arts-gallery/client/src/setupTests.js b/arts-gallery/client/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/arts-gallery/client/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/arts-gallery/dummy_data.js b/arts-gallery/dummy_data.js deleted file mode 100644 index 39ce787..0000000 --- a/arts-gallery/dummy_data.js +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = [ - { - 'id': 1234, - 'name': 'Guernica', - 'artist': 'Picasso', - 'image': 'https://www.worldatlas.com/r/w960-q80/upload/ca/27/70/shutterstock-337184468.jpg', - 'year': 1937 - }, - { - 'id': 5265, - 'name': 'The Girl With A Pearl Earring', - 'artist': 'Johannes Vermeer', - 'image': 'https://www.worldatlas.com/r/w960-q80/upload/9d/d2/c4/meisje-met-de-parel.jpg', - 'year': 1665 - }, - { - 'id': 4487, - 'name': 'The Scream', - 'artist': 'Edvard Munch', - 'image': 'https://www.worldatlas.com/r/w960-q80/upload/5f/96/29/edvard-munch-1893-the-scream-oil-tempera-and-pastel-on-cardboard-91-x-73-cm-national-gallery-of-norway.jpg', - 'year': 1893 - }, - { - 'id': 8357, - 'name': 'The Starry Night', - 'artist': 'Vincent van Gogh', - 'image': 'https://www.worldatlas.com/r/w960-q80/upload/1f/e7/fd/1280px-van-gogh-starry-night-google-art-project.jpg', - 'year': 1889 - } - ]; \ No newline at end of file diff --git a/arts-gallery/package.json b/arts-gallery/package.json new file mode 100644 index 0000000..c6904f0 --- /dev/null +++ b/arts-gallery/package.json @@ -0,0 +1,24 @@ +{ + "name": "art-gallery", + "version": "1.0.0", + "description": "Full-stack application to save paintings into a gallery by uploading them and saving them into a database.", + "main": "index.js", + "scripts": { + "client": "cd client && npm start", + "backend": "cd server && nodemon server.js", + "dev": "concurrently \"npm run backend\" \"npm run client\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sfareya/art-gallery.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/sfareya/art-gallery/issues" + }, + "homepage": "https://github.com/sfareya/art-gallery#readme", + "dependencies": { + "concurrently": "^6.5.1" + } +} diff --git a/arts-gallery/server/.gitignore b/arts-gallery/server/.gitignore new file mode 100644 index 0000000..d30f40e --- /dev/null +++ b/arts-gallery/server/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/arts-gallery/server/LICENSE b/arts-gallery/server/LICENSE new file mode 100644 index 0000000..f7272cf --- /dev/null +++ b/arts-gallery/server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Didin Jamaludin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/arts-gallery/server/README.md b/arts-gallery/server/README.md new file mode 100644 index 0000000..ce67ae5 --- /dev/null +++ b/arts-gallery/server/README.md @@ -0,0 +1,12 @@ +# Building CRUD Web Application using MERN Stack + +This source code is part of [Building CRUD Web Application using MERN Stack](https://www.djamware.com/post/59faec0a80aca7739224ee1f/building-crud-web-application-using-mern-stack) tutorial. + +If you think this source code is useful, it will be great if you just give it star or just buy me a cup of cofee [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Q5WK24UVWUGBN) + +To run this app: + +* Clone this repo +* Run `npm install` +* Run `npm run build` +* Run `npm start` diff --git a/arts-gallery/server/models/painting.js b/arts-gallery/server/models/painting.js new file mode 100644 index 0000000..defd7ab --- /dev/null +++ b/arts-gallery/server/models/painting.js @@ -0,0 +1,11 @@ +var mongoose = require('mongoose'); + +var PaintingSchema = new mongoose.Schema({ + name: String, + artist: String, + year: Number, + paintingImg: String, + cloudinary_id: String +}); + +module.exports = mongoose.model('Painting', PaintingSchema); diff --git a/arts-gallery/server/package.json b/arts-gallery/server/package.json new file mode 100644 index 0000000..4049e05 --- /dev/null +++ b/arts-gallery/server/package.json @@ -0,0 +1,25 @@ +{ + "name": "mern-crud", + "version": "0.1.0", + "private": true, + "dependencies": { + "axios": ">=0.21.1", + "body-parser": "^1.18.2", + "cloudinary": "^1.27.1", + "cloudinary-core": "^2.12.0", + "cors": "^2.8.5", + "dotenv": "^10.0.0", + "express": "^4.16.2", + "mongoose": ">=5.7.5", + "morgan": "^1.9.0", + "multer": "^1.4.4", + "nodemon": "^2.0.15" + }, + "scripts": { + "start": "node server.js", + "server": "nodemon server.js" + }, + "devDependencies": { + "bootstrap": "^3.3.7" + } +} diff --git a/arts-gallery/server/routes/painting.routes.js b/arts-gallery/server/routes/painting.routes.js new file mode 100644 index 0000000..664152a --- /dev/null +++ b/arts-gallery/server/routes/painting.routes.js @@ -0,0 +1,93 @@ +const router = require("express").Router(); +const cloudinary = require("../utils/cloudinary"); +const upload = require("../utils/multer"); +const Painting = require("../models/painting"); + + +// CREATE A PAINTING +router.post("/", upload.single("paintingImg"), async (req, res) => { + try { + // Upload image to cloudinary + const result = await cloudinary.uploader.upload(req.file.path); + + // Create new painting + let painting = new Painting({ + name: req.body.name, + artist: req.body.artist, + year: req.body.year, + paintingImg: result.secure_url, + cloudinary_id: result.public_id, + }); + // Save painting + await painting.save(); + res.json(painting); + } catch (err) { + console.log(err); + } +}); + + +// GET ALL PAINTINGS +router.get("/", async (req, res) => { + try { + let painting = await Painting.find(); + res.json(painting); + } catch (err) { + console.log(err); + } +}); + + +// DELETE PAINTING +router.delete("/:id", async (req, res) => { + try { + // Find painting by id + let painting = await Painting.findById(req.params.id); + // Delete image from cloudinary + await cloudinary.uploader.destroy(painting.cloudinary_id); + // Delete painting from db + await painting.remove(); + res.json(painting); + } catch (err) { + console.log(err); + } +}); + + +// UPDATE PAINTING +router.put("/:id", upload.single("paintingImg"), async (req, res) => { + try { + let painting = await Painting.findById(req.params.id); + // Delete image from cloudinary + await cloudinary.uploader.destroy(painting.cloudinary_id); + // Upload image to cloudinary + let result; + if (req.file) { + result = await cloudinary.uploader.upload(req.file.path); + } + const data = { + name: req.body.name || painting.name, + artist: req.body.artist || painting.artist, + year: req.body.year || painting.year, + paintingImg: result?.secure_url || painting.paintingImg, + cloudinary_id: result?.public_id || painting.cloudinary_id, + }; + painting = await Painting.findByIdAndUpdate(req.params.id, data, { new: true }); + res.json(painting); + } catch (err) { + console.log(err); + } +}); + +// GET ONE PAINTING BY ID +router.get("/:id", async (req, res) => { + try { + // Find painting by id + let painting = await Painting.findById(req.params.id); + res.json(painting); + } catch (err) { + console.log(err); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/arts-gallery/server/server.js b/arts-gallery/server/server.js new file mode 100644 index 0000000..7274199 --- /dev/null +++ b/arts-gallery/server/server.js @@ -0,0 +1,55 @@ +// Setting up Express & Middlewares +var express = require('express'); +var path = require('path'); +var logger = require('morgan'); // HTTP Logging Middleware +var bodyParser = require('body-parser'); +var mongoose = require('mongoose'); +const cors = require('cors'); +require('dotenv').config() + +// Connecting to the DB +const uri = process.env.MONGO_URI + +mongoose.connect(uri) + .then(() => console.log('Connected to the DB')) + .catch((err) => console.error(err)); + +// Creating the app +var app = express(); + +// Middlewares +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({'extended':'false'})); +app.use(express.static(path.join(__dirname, 'build'))); +app.use(cors()); + +// Routes +var paintingRoute = require('./routes/painting.routes'); +app.use('/api/v1/painting', paintingRoute); + +// Catching 404 error and forwarding to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// Error handler +app.use(function(err, req, res, next) { + // Set locals, only providing the error in dev + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // Render the error page + res.status(err.status || 500); + res.send(err); +}); + +const PORT = process.env.port; + +app.listen( PORT || 4000, function(){ + console.log(`Now listening on port ${PORT}`); +}) + +module.exports = app; diff --git a/arts-gallery/server/utils/cloudinary.js b/arts-gallery/server/utils/cloudinary.js new file mode 100644 index 0000000..c6fa931 --- /dev/null +++ b/arts-gallery/server/utils/cloudinary.js @@ -0,0 +1,9 @@ +const cloudinary = require("cloudinary").v2; + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, + }); + +module.exports = cloudinary; \ No newline at end of file diff --git a/arts-gallery/server/utils/multer.js b/arts-gallery/server/utils/multer.js new file mode 100644 index 0000000..ede23dc --- /dev/null +++ b/arts-gallery/server/utils/multer.js @@ -0,0 +1,15 @@ +const multer = require("multer"); +const path = require("path"); + +// Multer config +module.exports = multer({ + storage: multer.diskStorage({}), + fileFilter: (req, file, cb) => { + let ext = path.extname(file.originalname); + if (ext !== ".jpg" && ext !== ".jpeg" && ext !== ".png") { + cb(new Error("File type is not supported"), false); + return; + } + cb(null, true); + }, +}); \ No newline at end of file