Skip to content

Commit

Permalink
Implementing frontend features
Browse files Browse the repository at this point in the history
This includes everything under Level 100, 150 and 199, except for adding MUI support - frontend is still running without external dependencies

This does not yet include the generated Fly.io deployment files though, they will be added in a later commit
  • Loading branch information
sztupy committed Dec 5, 2023
1 parent fe94026 commit e60be3a
Show file tree
Hide file tree
Showing 17 changed files with 2,291 additions and 156 deletions.
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

0 comments on commit e60be3a

Please sign in to comment.