diff --git a/client/src/App.jsx b/client/src/App.jsx index 21656a5951..8031e2a983 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,10 +1,50 @@ import Videos from "./Videos/Videos"; +import { useState, useEffect, useRef } from "react"; +import VideoForm from "./VideoForm/VideoForm"; +import "./App.scss"; const App = () => { + const [videos, setVideos] = useState([]); + const [fetchedVideos, setFetchedVideos] = useState(false); + + useEffect(() => { + fetch("/api/videos") + .then((res) => res.json()) + .then((data) => { + setVideos(data); + }); + setFetchedVideos(false); + }, [fetchedVideos]); + + const handleSubmit = async (e) => { + e.preventDefault(); + const formData = Object.fromEntries(new FormData(e.target)); + const response = await fetch("/api/videos", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + if (response.ok) { + setFetchedVideos(true); + + } + + if (!response.ok) { + throw new Error("Failed to add video"); + } + + const responseData = await response.json(); + e.target.reset(); + }; + return ( <>

Video Recommendations

- + + ); }; diff --git a/client/src/App.scss b/client/src/App.scss new file mode 100644 index 0000000000..fbeffbb342 --- /dev/null +++ b/client/src/App.scss @@ -0,0 +1,8 @@ + +.container-box{ + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/client/src/VideoForm/VideoForm.jsx b/client/src/VideoForm/VideoForm.jsx new file mode 100644 index 0000000000..293f3fc26d --- /dev/null +++ b/client/src/VideoForm/VideoForm.jsx @@ -0,0 +1,20 @@ +import "./VideoForm.scss"; +const VideoForm = ({ handleSubmit }) => { + return ( +
+ + + +
+ ); +}; + +export default VideoForm; diff --git a/client/src/VideoForm/VideoForm.scss b/client/src/VideoForm/VideoForm.scss new file mode 100644 index 0000000000..3ab57123cd --- /dev/null +++ b/client/src/VideoForm/VideoForm.scss @@ -0,0 +1,37 @@ +.form { + height: 10rem; + background: rgb(137, 125, 137); + display: flex; + gap: 2rem; + justify-content: center; + align-items: center; + + &__label { + color: black; + display: flex; + gap: 1rem; + justify-content: center; + align-items: center; + } + &__input { + height: 2.5rem; + padding: 1rem; + width: 18rem; + border-radius: 0.2rem; + border: none; + } + &__btn { + cursor: pointer; + height: 2.5rem; + width: 10rem; + background-color: rgb(38, 25, 38); + color: white; + font-weight: 700; + border: 0; + border-radius: 1rem; + &:hover{ + font-size: 0.9rem; + background-color: rgb(74, 57, 74); + } + } +} diff --git a/client/src/Videos/Videos.jsx b/client/src/Videos/Videos.jsx index 5304d830a5..d99c688bab 100644 --- a/client/src/Videos/Videos.jsx +++ b/client/src/Videos/Videos.jsx @@ -1,23 +1,7 @@ -import { useState, useEffect, useRef } from "react"; import "./videos.scss"; -const Videos = () => { - const [videos, setVideos] = useState([]); - const fetchedVideos = useRef(true); - - useEffect(() => { - if (fetchedVideos.current) { - fetchedVideos.current = false; - fetch("/api/videos") - .then((res) => res.json()) - .then((data) => { - setVideos(data); - fetchedVideos.current = true; - }); - } - }, []); - - const mapVideos = videos.map((video, index) => { +const Videos = (props) => { + const mapVideos = props.video.map((video, index) => { return (

{video.title}

@@ -30,7 +14,11 @@ const Videos = () => { ); }); - return

{mapVideos}

; + return ( + <> +
{mapVideos}
+ + ); }; export default Videos; diff --git a/client/src/Videos/videos.scss b/client/src/Videos/videos.scss index 6c00affe40..4d0aa95a53 100644 --- a/client/src/Videos/videos.scss +++ b/client/src/Videos/videos.scss @@ -1,6 +1,12 @@ .container { margin-top: 2rem; padding-left: 1rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + max-width: 25rem; + height: 15rem; &_video { border-radius: 1rem; diff --git a/db/initdb.sql b/db/initdb.sql index bb40b5058f..844d4b122c 100644 --- a/db/initdb.sql +++ b/db/initdb.sql @@ -2,16 +2,86 @@ DROP TABLE IF EXISTS videos CASCADE; CREATE TABLE videos ( id SERIAL PRIMARY KEY, - title VARCHAR, - src VARCHAR); - -INSERT INTO videos (title,src) VALUES ('Never Gonna Give You Up','https://www.youtube.com/embed/dQw4w9WgXcQ?si=sdvqEritjOTwN2Af'); -INSERT INTO videos (title,src) VALUES ('The Coding Train','https://www.youtube.com/embed/HerCR8bw_GE?si=5Xfqx9K1JMB_QCBh'); -INSERT INTO videos (title,src) VALUES ('Mac & Cheese | Basics with Babish','https://www.youtube.com/embed/FUeyrEN14Rk?si=dUHtCerjTKIdgK5u'); -INSERT INTO videos (title,src) VALUES ('Videos for Cats to Watch - 8 Hour Bird Bonanza','https://www.youtube.com/embed/xbs7FT7dXYc?si=W9bjQcH1cYbIlnY3'); -INSERT INTO videos (title,src) VALUES ('The Complete London 2012 Opening Ceremony | London 2012 Olympic Games','https://www.youtube.com/embed/4As0e4de-rI?si=QvvaM7T6gj31cQ6z'); -INSERT INTO videos (title,src) VALUES ('Learn Unity - Beginners Game Development Course','https://www.youtube.com/embed/gB1F9G0JXOo?si=zh21-opwR7otFnSZ'); -INSERT INTO videos (title,src) VALUES ('Cracking Enigma in 2021 - Computerphile','https://www.youtube.com/embed/RzWB5jL5RX0?si=OuYo20zJalIFBT2w'); -INSERT INTO videos (title,src) VALUES ('Coding Adventure: Chess AI','https://www.youtube.com/embed/U4ogK0MIzqk?si=xICbZlD8Hm9nCyWy'); -INSERT INTO videos (title,src) VALUES ('Coding Adventure: Ant and Slime Simulations','https://www.youtube.com/embed/X-iSQQgOd1A?si=bZUPXmKxC43YzERA'); -INSERT INTO videos (title,src) VALUES ('Why the Tour de France is so brutal','https://www.youtube.com/embed/ZacOS8NBK6U?si=nfaj6AHw0aaE-c7C'); \ No newline at end of file + title VARCHAR, + src VARCHAR +); + +INSERT INTO + videos (title, src) +VALUES + ( + 'Never Gonna Give You Up', + 'https://www.youtube.com/embed/dQw4w9WgXcQ?si=sdvqEritjOTwN2Af' + ); + +INSERT INTO + videos (title, src) +VALUES + ( + 'The Coding Train', + 'https://www.youtube.com/embed/HerCR8bw_GE?si=5Xfqx9K1JMB_QCBh' + ); + +INSERT INTO + videos (title, src) +VALUES + ( + 'Mac & Cheese | Basics with Babish', + 'https://www.youtube.com/embed/FUeyrEN14Rk?si=dUHtCerjTKIdgK5u' + ); + +INSERT INTO + videos (title, src) +VALUES + ( + 'Videos for Cats to Watch - 8 Hour Bird Bonanza', + 'https://www.youtube.com/embed/xbs7FT7dXYc?si=W9bjQcH1cYbIlnY3' + ); + +INSERT INTO + videos (title, src) +VALUES + ( + 'The Complete London 2012 Opening Ceremony | London 2012 Olympic Games', + 'https://www.youtube.com/embed/4As0e4de-rI?si=QvvaM7T6gj31cQ6z' + ); + +INSERT INTO + videos (title, src) +VALUES + ( + 'Learn Unity - Beginners Game Development Course', + 'https://www.youtube.com/embed/gB1F9G0JXOo?si=zh21-opwR7otFnSZ' + ); + +INSERT INTO + videos (title, src) +VALUES + ( + 'Cracking Enigma in 2021 - Computerphile', + 'https://www.youtube.com/embed/RzWB5jL5RX0?si=OuYo20zJalIFBT2w' + ); + +INSERT INTO + videos (title, src) +VALUES + ( + 'Coding Adventure: Chess AI', + 'https://www.youtube.com/embed/U4ogK0MIzqk?si=xICbZlD8Hm9nCyWy' + ); + +INSERT INTO + videos (title, src) +VALUES + ( + 'Coding Adventure: Ant and Slime Simulations', + 'https://www.youtube.com/embed/X-iSQQgOd1A?si=bZUPXmKxC43YzERA' + ); + +INSERT INTO + videos (title, src) +VALUES + ( + 'Why the Tour de France is so brutal', + 'https://www.youtube.com/embed/ZacOS8NBK6U?si=nfaj6AHw0aaE-c7C' + ); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6886da3202..e133f6d6bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "e2e" ], "dependencies": { + "react-hook-form": "^7.51.3", "sass": "^1.75.0", "serverless-http": "^3.2.0" }, @@ -8970,6 +8971,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.51.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz", + "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index 5b7d4b4853..4893e5241b 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "npm": ">=10" }, "dependencies": { + "react-hook-form": "^7.51.3", "sass": "^1.75.0", "serverless-http": "^3.2.0" } diff --git a/server/api.js b/server/api.js index 38b523a9f7..2a423492d3 100644 --- a/server/api.js +++ b/server/api.js @@ -7,19 +7,24 @@ router.get("/videos", async (_, res) => { result ? res.send(result.rows) : res - .status(500) - .send({ success: "false", error: "Could not connect to database" }); + .status(500) + .send({ success: "false", error: "Could not connect to database" }); }); router.post("/videos", async (req, res) => { - const newVideo = await db.query( - `INSERT INTO videos (title, src) VALUES ('${req.body.title}', '${req.body.src}')` - ); - res.send( - newVideo - ? res.send({ success: "Video added successfully" }) - : res.send({ error: "Video could not be added" }) - ); + try { + await db.query(`INSERT INTO videos (title, src) VALUES ($1, $2)`, [req.body.title, req.body.src]) + res.send({ + success: true, + message: `Video added successfully: ${req.body.title}, ${req.body.src}` + }); + } catch (error) { + res.status(500).send({ + success: false, + error: "Video could not be added" + }); + } }); + export default router;