diff --git a/README.md b/README.md index 4c8fd7876..de78f573c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # Project Movies -Replace this readme with your own information about your project. +This is a multi-page React application designed to showcase the use of APIs in React. The project utilizes useState and useEffect hooks to display information about upcoming movie releases. It was developed through pair programming. -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. ## The problem -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +- working with an API and having it in an external folder 'utils' and then exporting it for the use of the project +- first time working with React Router +- using new React hooks ## View it live -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. +https://michelle-maja-movies.netlify.app/ + diff --git a/code/package-lock.json b/code/package-lock.json index bb51e893e..964acdc50 100644 --- a/code/package-lock.json +++ b/code/package-lock.json @@ -15,8 +15,11 @@ "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", + "moment": "^2.29.4", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-moment": "^1.1.3", + "react-router-dom": "^6.9.0" }, "devDependencies": { "react-scripts": "5.0.1" @@ -3124,6 +3127,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.4.0.tgz", + "integrity": "sha512-BJ9SxXux8zAg991UmT8slpwpsd31K1dHHbD3Ba4VzD+liLQ4WAMSxQp2d2ZPRPfN0jN2NPRowcSSoM7lCaF08Q==", + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -12008,6 +12019,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -14375,6 +14394,16 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-moment": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.3.tgz", + "integrity": "sha512-8EPvlUL8u6EknPp1ISF5MQ3wx2OHJVXIP/iZc4wRh3iV3XozftZERDv9ANZeAtMlhNNQHdFoqcZHFUkBSTONfA==", + "peerDependencies": { + "moment": "^2.29.0", + "prop-types": "^15.7.0", + "react": "^16.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14384,6 +14413,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.9.0.tgz", + "integrity": "sha512-51lKevGNUHrt6kLuX3e/ihrXoXCa9ixY/nVWRLlob4r/l0f45x3SzBvYJe3ctleLUQQ5fVa4RGgJOTH7D9Umhw==", + "dependencies": { + "@remix-run/router": "1.4.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.9.0.tgz", + "integrity": "sha512-/seUAPY01VAuwkGyVBPCn1OXfVbaWGGu4QN9uj0kCPcTyNYgL1ldZpxZUpRU7BLheKQI4Twtl/OW2nHRF1u26Q==", + "dependencies": { + "@remix-run/router": "1.4.0", + "react-router": "6.9.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -19509,6 +19568,11 @@ "source-map": "^0.7.3" } }, + "@remix-run/router": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.4.0.tgz", + "integrity": "sha512-BJ9SxXux8zAg991UmT8slpwpsd31K1dHHbD3Ba4VzD+liLQ4WAMSxQp2d2ZPRPfN0jN2NPRowcSSoM7lCaF08Q==" + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -26117,6 +26181,11 @@ "minimist": "^1.2.6" } }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -27678,12 +27747,35 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-moment": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.3.tgz", + "integrity": "sha512-8EPvlUL8u6EknPp1ISF5MQ3wx2OHJVXIP/iZc4wRh3iV3XozftZERDv9ANZeAtMlhNNQHdFoqcZHFUkBSTONfA==", + "requires": {} + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "dev": true }, + "react-router": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.9.0.tgz", + "integrity": "sha512-51lKevGNUHrt6kLuX3e/ihrXoXCa9ixY/nVWRLlob4r/l0f45x3SzBvYJe3ctleLUQQ5fVa4RGgJOTH7D9Umhw==", + "requires": { + "@remix-run/router": "1.4.0" + } + }, + "react-router-dom": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.9.0.tgz", + "integrity": "sha512-/seUAPY01VAuwkGyVBPCn1OXfVbaWGGu4QN9uj0kCPcTyNYgL1ldZpxZUpRU7BLheKQI4Twtl/OW2nHRF1u26Q==", + "requires": { + "@remix-run/router": "1.4.0", + "react-router": "6.9.0" + } + }, "react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/code/package.json b/code/package.json index 7aad26ebc..db73fce7e 100644 --- a/code/package.json +++ b/code/package.json @@ -10,8 +10,11 @@ "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", + "moment": "^2.29.4", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-moment": "^1.1.3", + "react-router-dom": "^6.9.0" }, "scripts": { "start": "react-scripts start", diff --git a/code/public/index.html b/code/public/index.html index e6730aa66..e974b1c67 100644 --- a/code/public/index.html +++ b/code/public/index.html @@ -13,7 +13,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - Technigo React App + Movies Page by Michelle and Maja diff --git a/code/src/App.js b/code/src/App.js index f2007d229..bbe9a860f 100644 --- a/code/src/App.js +++ b/code/src/App.js @@ -1,9 +1,52 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import NotFound from 'components/NotFound'; +import Details from 'components/Details'; +import ListMovies from 'components/ListMovies'; +import { LIST_URL } from 'utils/urls.js'; export const App = () => { + const [listMovies, setListMovies] = useState([]); + const [loading, setLoading] = useState(false); + + // this is borrowed from the example project + useEffect(() => { + setLoading(true); + fetch(LIST_URL) + .then((response) => response.json()) + .then((data) => { + setListMovies(data.results) + }) + .catch((e) => { + console.error(e) + }) + .finally(() => { + setLoading(false); + }) + }, []); + + if (loading) { + return ( +

Loading...

+ ); + } + console.log(listMovies) + return ( -
- Find me in src/app.js! -
+ // this is the main wrapper for the whole app + +
+
+ + {/* path to a single component */} + } /> + } /> + } /> + } /> + +
+ {/* wrapper for every component that need to be linked to */} +
+
); -} +} \ No newline at end of file diff --git a/code/src/assets/github.png b/code/src/assets/github.png new file mode 100644 index 000000000..671a8a66a Binary files /dev/null and b/code/src/assets/github.png differ diff --git a/code/src/assets/movie.png b/code/src/assets/movie.png new file mode 100644 index 000000000..3a7360d6e Binary files /dev/null and b/code/src/assets/movie.png differ diff --git a/code/src/components/Details.js b/code/src/components/Details.js new file mode 100644 index 000000000..da332a825 --- /dev/null +++ b/code/src/components/Details.js @@ -0,0 +1,45 @@ +/* eslint-disable react/jsx-max-props-per-line */ +/* eslint-disable react/jsx-indent-props */ +/* eslint-disable react/jsx-first-prop-new-line */ +/* eslint-disable react/jsx-closing-tag-location */ +/* eslint-disable max-len */ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import '../details.css'; + +// Define the Details component +const Details = () => { + // Get the id parameter from the URL using the useParams hook + const { id } = useParams(); + // Define state for the movie details, initialized as an empty object + const [movieDetail, setMovieDetail] = useState({}); + + useEffect(() => { + fetch(`https://api.themoviedb.org/3/movie/${id}?api_key=39168ef639d8a2d49b6d7a9893ad1b8c&language=en-US`) + .then((data) => data.json()) + .then((configuredData) => setMovieDetail(configuredData)) + }, [id]); + + return ( +
+ {/* The ternary operator checks if the image paths property of the movieDetail object is truthy (i.e., not undefined, null, 0, false, etc.). If it is truthy, it + sets the src attribute of the imgtag to the URL of the image file. If it is falsy, it sets the src` attribute to an empty string. */} + {`Movie + Movies +
+
+ {`Movie +
+

+ {movieDetail.title} + ⭐{Math.round(movieDetail.vote_average * 10) / 10} +

+

{movieDetail.overview}

+
+
+
+
+ ); +}; + +export default Details; \ No newline at end of file diff --git a/code/src/components/Header.js b/code/src/components/Header.js new file mode 100644 index 000000000..d5fed5fff --- /dev/null +++ b/code/src/components/Header.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import GitIcon from '../assets/github.png'; +import MovieIcon from '../assets/movie.png'; +import '../header.css'; + +const Header = () => { + return ( +
+ +
) +} + +export default Header; \ No newline at end of file diff --git a/code/src/components/ListMovies.js b/code/src/components/ListMovies.js new file mode 100644 index 000000000..a93d66249 --- /dev/null +++ b/code/src/components/ListMovies.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import moment from 'moment'; +import '../listmovies.css'; +import Header from './Header'; + +const ListMovies = ({ listMovies }) => { + return ( + <> +
+
+ {listMovies.map((movie) => { + return ( + + poster +
+

{movie.original_title}

+

Released: {moment(movie.release_date).format('D MMMM YYYY')}

+
+ + ) + })} +
+ + ) +} + +export default ListMovies; \ No newline at end of file diff --git a/code/src/components/NotFound.js b/code/src/components/NotFound.js new file mode 100644 index 000000000..08d4113a3 --- /dev/null +++ b/code/src/components/NotFound.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +const NotFound = () => { + const navigate = useNavigate(); + const onGoToHomeButtonClick = () => { + navigate('/'); + } + + const onGoBackButtonClick = () => { + navigate(-1); + } + return ( +
+

Sorry, nothing here :(

+ + +
) +} + +export default NotFound; \ No newline at end of file diff --git a/code/src/details.css b/code/src/details.css new file mode 100644 index 000000000..bc818f593 --- /dev/null +++ b/code/src/details.css @@ -0,0 +1,139 @@ +.movie-details-container { + height: auto; + width: 100vw; + position: relative; + display: flex; + align-items: flex-end; +} + +.poster-rating-container { + margin: 80px 20px 50px 20px; +} + +.rating { + font-size: 20px; + align-self: center; + margin-left: 10px; + background: #fff; + color:#000; + padding: 0 5px; +} + +/* Background image */ +.backdrop { + top: 0; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + position: fixed; + height: 100%; + width: auto; + z-index: -1; +} + +/* Poster image */ +.poster { + height: 300px; + width: auto; + border: 5px solid #fff; +} + +/* Go back button */ +.goBackBtn { + display: flex; + align-items: center; + height: 25px; + position: absolute; + top: 20px; + left: 20px; + font-size: 18px; + font-weight: 700; + text-decoration: none; + z-index: 2; +} + +.backBtnName { + margin-left: 5px; +} + +.backBtnName:hover { + margin-left: 10px; +} + +.goback-icon { + width: 25px; +} + +.title-rating-text { + display: flex; + flex-direction: column; + gap: 5px; + padding: 2px 3px 2px 8px; + background-color:rgba(40,39,39,.5); + border-radius: 4px; + margin-top: 5px; +} + +.title-rating-container { + font-size: 1.5em; + margin-bottom: 0; +} + +.title-details { + text-shadow: 1px 1px #4d4d4d; +} + +.title-details-margin { + margin: 16px 0 0 0; +} + +.goBackBtn, .title-details, +.movie-description { + color: #fff; +} + +/* ---- TABLET ----- */ +@media only screen and (min-width: 744px) { + .title-rating-text { + max-width: 400px; + } +} + +/* ---- DESKTOP ----- */ +@media only screen and (min-width: 1280px) { +.goBackBtn { + margin-left: 30px; + margin-top: 30px; + } + +.backdrop { + top: 0; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + position: fixed; + height: 100%; + width: 100%; + z-index: -1; +} + +.title-rating-text +{ + bottom: 50px; + display: flex; + flex-direction: column; + margin-left: 400px; + position: fixed; + max-width: 400px; +} + +.poster { + width: 342px; + height: auto; + position: fixed; + bottom: 50px; + margin-left: 30px; + margin-right: 20px; +} + +} \ No newline at end of file diff --git a/code/src/header.css b/code/src/header.css new file mode 100644 index 000000000..ba2e8c3e2 --- /dev/null +++ b/code/src/header.css @@ -0,0 +1,83 @@ +.header { + display: block; + } + .header-items-container { + display: flex; + flex-direction: row; + justify-content: space-between; + margin: 0 15px; + height: 100px; + } + + .authors { + display: flex; + } + ul { + display: flex; + gap: 20px; + list-style-type: none; + padding: 10px; + } + + li { + padding: 10px; + } + + .text-welcome { + padding: 10px; + color: aliceblue; + } + + .welcome { + margin-top: 3px; + display: flex; + flex-direction: row; + } + + .icon-popcorn { + max-width: 100px; + transition: 1s ease; + } + + .icon-popcorn:hover { + -webkit-transform: scale(1.2); + -ms-transform: scale(1.2); + transform: scale(1.2); + transition: 1s ease; + } + + .icon-contact { + max-width: 100px; + opacity: 1; + } + + .icon-contact:hover { + opacity: 0.5; + } + + a:link { + text-decoration: none; + color: aliceblue; + } + + a:visited { + text-decoration: none; + color: aliceblue; + } + + a:hover { + text-decoration:dashed; + color: aliceblue; + } + + a:active { + text-decoration: none; + color: aliceblue; + } + + @media (min-width: 0px) and (max-width: 570px) { + .text-welcome { + display: none; + } + + } \ No newline at end of file diff --git a/code/src/index.css b/code/src/index.css index 4a1df4db7..a126e9f83 100644 --- a/code/src/index.css +++ b/code/src/index.css @@ -5,9 +5,15 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: black; + color: aliceblue; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } + +.outer-container { + +} diff --git a/code/src/listmovies.css b/code/src/listmovies.css new file mode 100644 index 000000000..921415600 --- /dev/null +++ b/code/src/listmovies.css @@ -0,0 +1,67 @@ +.movies-list { + display: flex; + flex-wrap: wrap; + } + + .single-movie { + width: 25%; + size: fit-content; + position: relative; + display: flex; + flex-wrap: wrap; + } + + .hover-container { + color: rgb(241, 242, 246); + text-transform: uppercase; + letter-spacing: 0.1em; + padding: 45% 5%; + } + + .single-movie .hover-container { + border: 5px solid black; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + text-align: center; + } + + .text-hover { + display: none; + } + + .single-movie:hover .hover-container { + display: flex; + flex-direction: column; + text-align: center; + justify-content: flex-end; + background: rgba(0,0,0,.75); + } + + .single-movie:hover .hover-container .text-hover { + display: flex; + flex-direction: column; + text-align: center; + justify-content: flex-end; + } + + .cover-image { + width: 100%; + } + + @media (min-width: 0px) and (max-width: 570px) { + .single-movie { + width: 100%; + } + + } + + @media (min-width: 571px) and (max-width: 850px) { + .single-movie{ + width: 33.33%; + } + +} + \ No newline at end of file diff --git a/code/src/utils/urls.js b/code/src/utils/urls.js new file mode 100644 index 000000000..fe8972ed7 --- /dev/null +++ b/code/src/utils/urls.js @@ -0,0 +1,3 @@ +const apiKey = 'a2d2aecb5322eff67704d6c8635d60c1' + +export const LIST_URL = `https://api.themoviedb.org/3/movie/popular?api_key=${apiKey}&language=en-US&page=1` diff --git a/netlify.toml b/netlify.toml index e42219ead..db5d4f7b9 100644 --- a/netlify.toml +++ b/netlify.toml @@ -4,3 +4,8 @@ base = "code/" publish = "build/" command = "npm run build" + + [[redirects]] + from = "/* + to = "/index.html" + status = 200