From a53f5a2ad28886775cf4223ac962186bbec1f26d Mon Sep 17 00:00:00 2001 From: Hadeed Ahmad <me@hadeedahmad.com> Date: Wed, 9 Apr 2025 05:46:19 +0500 Subject: [PATCH] Add frontend for playing latest video --- backend/app/main.py | 6 +++ frontend/package-lock.json | 82 +++++++++++++++++++++++++++++++++++++- frontend/package.json | 3 +- frontend/src/app.jsx | 79 ++++++++++++++++++++++++++++++++++-- 4 files changed, 164 insertions(+), 6 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index cdd3cf6..8913d39 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,8 +1,14 @@ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware import utils app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173"] +) + @app.get("/video") def get_video(channel_name: str): diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9b8982a..fff2d1f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6,7 +6,8 @@ "": { "dependencies": { "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-youtube": "^10.1.0" }, "devDependencies": { "autoprefixer": "^10.4.19", @@ -1171,6 +1172,15 @@ "node": ">=4" } }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1257,6 +1267,12 @@ "node": ">=6" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1544,6 +1560,12 @@ "dev": true, "license": "MIT" }, + "node_modules/load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1613,6 +1635,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -1675,7 +1703,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2075,6 +2102,17 @@ } } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2121,6 +2159,29 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-youtube": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz", + "integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "3.1.3", + "prop-types": "15.8.1", + "youtube-player": "5.5.2" + }, + "engines": { + "node": ">= 14.x" + }, + "peerDependencies": { + "react": ">=0.14.1" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2285,6 +2346,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sister": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", + "integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==", + "license": "BSD-3-Clause" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2752,6 +2819,17 @@ "engines": { "node": ">= 14" } + }, + "node_modules/youtube-player": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz", + "integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==", + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^2.6.6", + "load-script": "^1.0.0", + "sister": "^3.0.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 3b125b6..5f8bca7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,8 @@ "type": "module", "dependencies": { "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-youtube": "^10.1.0" }, "devDependencies": { "autoprefixer": "^10.4.19", diff --git a/frontend/src/app.jsx b/frontend/src/app.jsx index 040a07e..16b65e4 100644 --- a/frontend/src/app.jsx +++ b/frontend/src/app.jsx @@ -1,3 +1,76 @@ -export default () => ( - <h1 className="text-4xl font-extrabold">Using React and Tailwind</h1> -); +import { useState } from "react"; +import YouTube from "react-youtube"; + +export default function App() { + const [channelName, setChannelName] = useState(""); + const [videoId, setVideoId] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function fetchLatestVideo() { + if (!channelName) return; + setLoading(true); + setError(""); + setVideoId(null); + + try { + const res = await fetch( + `http://localhost:8000/video?channel_name=${channelName}`, + ); + if (!res.ok) throw new Error("Channel not found or API error"); + const data = await res.json(); + setVideoId(data.id); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + } + + const playerOptions = { + playerVars: { + autoplay: 0, + rel: 0, + }, + }; + + return ( + <div className="flex min-h-screen flex-col items-center justify-center bg-gray-100 p-6"> + <h1 className="mb-6 text-4xl font-extrabold"> + YouTube Latest Video Finder + </h1> + + <form + onSubmit={(e) => { + e.preventDefault(); + fetchLatestVideo(); + }} + className="flex flex-col items-center gap-4 sm:flex-row" + > + <input + type="text" + value={channelName} + onChange={(e) => setChannelName(e.target.value)} + placeholder="Enter channel name" + className="w-72 rounded-md border border-gray-300 px-4 py-2" + /> + + <button + type="submit" + disabled={loading} + className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50" + > + {loading ? "Loading..." : "Get Latest Video"} + </button> + </form> + + {error && <p className="mt-4 text-sm text-red-600">{error}</p>} + + {videoId && ( + <div className="mt-8 flex w-full justify-center"> + <YouTube videoId={videoId} opts={playerOptions} /> + </div> + )} + </div> + ); +}