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

[Joint test] Implementing frontend features #7

Merged
merged 1 commit into from
Dec 5, 2023
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
15 changes: 15 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"env": {
"es6": true,
"node": true,
"jest/globals": true
},
"extends": ["@codeyourfuture/standard"],
"parser": "@babel/eslint-parser",
"root": true,
"rules": {
"indent": "off",
"operator-linebreak": "off"
},
"plugins": ["jest"]
}
2 changes: 1 addition & 1 deletion .github/workflows/enforce-linting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ run-name: Enforce lint passes on committed files

on:
workflow_dispatch:
# pull_request:
pull_request:

jobs:
run-linter:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-client-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ run-name: Enforce tests pass on committed files

on:
workflow_dispatch:
# pull_request:
pull_request:

jobs:
run-client-tests:
Expand Down
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"useTabs": true
}
134 changes: 134 additions & 0 deletions client/src/App.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,141 @@
import { useState, useEffect } from "react";

import VideoList from "./components/VideoList";
import VideoSubmission from "./components/VideoSubmission";
import OrderingSelector from "./components/OrderingSelector";

const VIDEO_LIST = [
{
id: 523523,
title: "Never Gonna Give You Up",
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
rating: 23,
},
{
id: 523427,
title: "The Coding Train",
url: "https://www.youtube.com/watch?v=HerCR8bw_GE",
rating: 230,
},
{
id: 82653,
title: "Mac & Cheese | Basics with Babish",
url: "https://www.youtube.com/watch?v=FUeyrEN14Rk",
rating: 2111,
},
{
id: 858566,
title: "Videos for Cats to Watch - 8 Hour Bird Bonanza",
url: "https://www.youtube.com/watch?v=xbs7FT7dXYc",
rating: 11,
},
{
id: 453538,
title:
"The Complete London 2012 Opening Ceremony | London 2012 Olympic Games",
url: "https://www.youtube.com/watch?v=4As0e4de-rI",
rating: 3211,
},
{
id: 283634,
title: "Learn Unity - Beginner's Game Development Course",
url: "https://www.youtube.com/watch?v=gB1F9G0JXOo",
rating: 211,
},
{
id: 562824,
title: "Cracking Enigma in 2021 - Computerphile",
url: "https://www.youtube.com/watch?v=RzWB5jL5RX0",
rating: 111,
},
{
id: 442452,
title: "Coding Adventure: Chess AI",
url: "https://www.youtube.com/watch?v=U4ogK0MIzqk",
rating: 671,
},
{
id: 536363,
title: "Coding Adventure: Ant and Slime Simulations",
url: "https://www.youtube.com/watch?v=X-iSQQgOd1A",
rating: 76,
},
{
id: 323445,
title: "Why the Tour de France is so brutal",
url: "https://www.youtube.com/watch?v=ZacOS8NBK6U",
rating: 73,
},
];

const App = () => {
let [videos, setVideos] = useState([]);
let [order, setOrder] = useState("id");

function orderVideos(videos, order, initial) {
switch (order) {
case "rating_asc":
return videos.sort((a, b) => a.rating - b.rating);
case "rating_desc":
return videos.sort((a, b) => b.rating - a.rating);
case "random":
if (initial) {
return videos.sort(() => 0.5 - Math.random());
} else {
return videos; // this should only sort once when getting from backend, then keep it as-is
}
case "id":
return videos.sort((a, b) => a.id - b.id);
}
}

useEffect(() => {
setVideos([...orderVideos(VIDEO_LIST, order, true)]);
}, [setVideos, order]);

const addVideo = function (title, url) {
const newVideo = {
id: Math.max(...videos.map((v) => v.id)) + 1,
title: title,
url: url,
rating: 0,
};
VIDEO_LIST.push(newVideo);
setVideos([...orderVideos(VIDEO_LIST, order)]);
};

const updateVideo = function (video, action) {
const deleteVideo = async (selectedVideo) => {
VIDEO_LIST.splice(VIDEO_LIST.indexOf(selectedVideo), 1);
setVideos([...orderVideos(VIDEO_LIST, order)]);
};

const voteOnVideo = async (selectedVideo, action) => {
selectedVideo.rating += action == "up" ? 1 : -1;
setVideos([...orderVideos(VIDEO_LIST, order)]);
};

let selectedVideo = VIDEO_LIST.find((e) => e.id === video.id);

if (selectedVideo) {
switch (action) {
case "up":
case "down":
voteOnVideo(selectedVideo, action);
break;
case "delete":
deleteVideo(selectedVideo);
break;
}
}
};

return (
<>
<h1>Video Recommendations</h1>
<OrderingSelector order={order} setOrder={setOrder} />
<VideoList videos={videos} updateVideo={updateVideo} />
<VideoSubmission addVideo={addVideo} />
</>
);
};
Expand Down
45 changes: 45 additions & 0 deletions client/src/components/OrderingSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export default function OrderingSelector({ order, setOrder }) {
return (
<div className="order-selector">
<h3>Select order of videos</h3>
<ul>
<li>
<button
disabled={order == "rating_asc"}
onClick={() => setOrder("rating_asc")}
title="Ascending by vote"
>
⬆️
</button>
</li>
<li>
<button
disabled={order == "rating_desc"}
onClick={() => setOrder("rating_desc")}
title="Descending by vote"
>
⬇️
</button>
</li>
<li>
<button
disabled={order == "id"}
onClick={() => setOrder("id")}
title="Creation order"
>
➡️
</button>
</li>
<li>
<button
disabled={order == "random"}
onClick={() => setOrder("random")}
title="Random order"
>
🔀
</button>
</li>
</ul>
</div>
);
}
42 changes: 42 additions & 0 deletions client/src/components/Video.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// source: https://stackoverflow.com/questions/3452546/how-do-i-get-the-youtube-video-id-from-a-url
function youtubeLinkParser(url) {
let regExp =
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
let match = url.match(regExp);
return match && match[7].length == 11 ? match[7] : false;
}

export default function Video({ video, updateVideo }) {
const youtubeId = youtubeLinkParser(video.url);

return (
<li className="video">
<h2>
<a href={video.url} tabIndex={0}>
{video.title}
</a>
</h2>
{youtubeId && (
<iframe
src={`https://www.youtube.com/embed/${youtubeId}`}
title={video.title}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
)}
<h3>Rating</h3>
<div title="Rating" className="rating">
{video.rating}
</div>
<h3>Controls</h3>
<div className="controls">
<button onClick={() => updateVideo(video, "delete")}>
Remove video
</button>
<button onClick={() => updateVideo(video, "up")}>Up Vote</button>
<button onClick={() => updateVideo(video, "down")}>Down Vote</button>
</div>
</li>
);
}
11 changes: 11 additions & 0 deletions client/src/components/VideoList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Video from "./Video";

export default function VideoList({ videos, updateVideo }) {
return (
<ul id="videos">
{videos.map((video) => (
<Video key={video.id} video={video} updateVideo={updateVideo} />
))}
</ul>
);
}
63 changes: 63 additions & 0 deletions client/src/components/VideoSubmission.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useState } from "react";

export default function VideoSubmisson({ addVideo }) {
const [title, setTitle] = useState("");
const [url, setUrl] = useState("");
const [changed, setChanged] = useState(false);

const titleValid = !changed || title !== "";
const urlValid =
!changed || url.match(/youtube.com\/watch\?v=[a-zA-Z0-9-]{11}/);

const updateField = function (e) {
setChanged(true);

switch (e.target.name) {
case "title":
setTitle(e.target.value);
break;
case "url":
setUrl(e.target.value);
break;
}
};

const submitVideo = function (e) {
e.preventDefault();

if (changed && titleValid && urlValid) {
addVideo(title, url);

setChanged(false);
setTitle("");
setUrl("");
}
};

return (
<div id="submit-video">
<h2>Submit a new video</h2>
<form aria-label="Submit a new video" onSubmit={submitVideo}>
<label htmlFor="title">Title: </label>
<input
type="text"
name="title"
id="title"
value={title}
onChange={updateField}
className={titleValid ? "valid" : "invalid"}
/>
<label htmlFor="url">Url: </label>
<input
type="text"
name="url"
id="url"
value={url}
onChange={updateField}
className={urlValid ? "valid" : "invalid"}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
Loading