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>
+  );
+}