diff --git a/services/backend/generated/types/index.ts b/services/backend/generated/types/index.ts
new file mode 100644
index 0000000..d6564ad
--- /dev/null
+++ b/services/backend/generated/types/index.ts
@@ -0,0 +1,64 @@
+// Code generated by tygo. DO NOT EDIT.
+
+//////////
+// source: job.go
+
+export interface DownloadJob {
+ ID: string;
+ URL: string;
+ TIMESTAMP: string /* RFC3339 */;
+}
+export interface JobData {
+ ID: string;
+ JobID: string;
+ URL: string;
+ IsPlaylist: boolean;
+ STATUS: string;
+ PROGRESS: number /* int */;
+ CreatedAt: string /* RFC3339 */;
+ UpdatedAt: string /* RFC3339 */;
+}
+
+//////////
+// source: metadata.go
+
+export interface VideoMetadata {
+ id: string;
+ title: string;
+ uploader: string;
+ filesize: number /* int64 */;
+ duration: number /* float64 */;
+ format: string;
+ thumbnail: string;
+}
+export interface PlaylistMetadata {
+ id: string;
+ title: string;
+ description: string;
+}
+
+//////////
+// source: video.go
+
+/**
+ * Video represents metadata for a single video.
+ */
+export interface Video {
+ id: number /* int */; // Primary key in database
+ job_id: string; // ID associated with the download job
+ title: string; // Title of the video
+ uploader: string; // Uploader or channel name
+ file_path: string; // Path where the video file is stored
+ last_downloaded_at: string /* RFC3339 */; // Timestamp of when the video was last downloaded
+ length: number /* float64 */; // Duration of the video in seconds
+ size: number /* int64 */; // File size in bytes
+ quality: string; // Video quality
+}
+/**
+ * Playlist represents metadata for a playlist or channel.
+ */
+export interface Playlist {
+ id: string; // Playlist or channel ID
+ title: string; // Title of the playlist or channel
+ description: string; // Description of the playlist or channel
+}
diff --git a/web/app/page.tsx b/web/app/page.tsx
index e6449b0..35c47de 100644
--- a/web/app/page.tsx
+++ b/web/app/page.tsx
@@ -2,16 +2,17 @@
import {UrlInput} from "@/components/url-input";
import JobProgress from "@/components/job-progress";
+import Recent from "@/components/recent";
export default function Home() {
-
-
return (
-
-
-
-
-
-
- );
+
+
+
+
+
+
+
+ );
}
diff --git a/web/components/recent.tsx b/web/components/recent.tsx
new file mode 100644
index 0000000..f3b5800
--- /dev/null
+++ b/web/components/recent.tsx
@@ -0,0 +1,52 @@
+import React, {useEffect, useState} from 'react';
+import {Card, CardHeader, CardContent, CardTitle} from "@/components/ui/card";
+import {Progress} from "@/components/ui/progress";
+import {JobData} from "@/types";
+import useAppState from "@/store/appState";
+
+const Recent: React.FC = () => {
+ const [jobs, setJobs] = useState();
+ const [message, setMessage] = useState("");
+
+ useEffect(() => {
+ fetch(process.env.NEXT_PUBLIC_SERVER_URL + "/recent").then((res) => {
+ if (!res.ok) {
+ setMessage("No recent jobs found.");
+ return;
+ }
+ return res.json()
+ }).then((data) => {
+ setJobs(data.message);
+ });
+
+ const unsubscribe = useAppState.subscribe(
+ (state) => {
+ if (state.isDownloading) {
+ setMessage('')
+ unsubscribe();
+ }
+ }
+ );
+ }, []);
+
+ return (
+
+ {jobs && !message && jobs.map((job) => (
+
+
+ {job.URL}
+
+
+
+ Progress: ({job.PROGRESS}%)
+
+
+
+
+ ))}
+ {message &&
{message}
}
+
+ );
+};
+
+export default Recent;
diff --git a/web/components/url-input.tsx b/web/components/url-input.tsx
index 66c11d1..97d8091 100644
--- a/web/components/url-input.tsx
+++ b/web/components/url-input.tsx
@@ -7,12 +7,15 @@ import {Button} from "@/components/ui/button"
import {Input} from "@/components/ui/input"
import {AlertDestructive} from "@/components/alert-destructive";
import {toast} from "sonner";
+import useAppState from "@/store/appState";
export function UrlInput() {
const [url, setUrl] = useState("")
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
+
const SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL
+ const setIsDownloading = useAppState((state) => state.setIsDownloading);
const isValidYoutubeUrl = (url: string) => {
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/
@@ -27,6 +30,7 @@ export function UrlInput() {
}
setLoading(true)
+ setIsDownloading(true)
try {
const response = await fetch(`${SERVER_URL}/download`, {
method: "POST",
diff --git a/web/package.json b/web/package.json
index b641d41..d3602a8 100644
--- a/web/package.json
+++ b/web/package.json
@@ -24,7 +24,8 @@
"react-dom": "19.0.0-rc-69d4b800-20241021",
"sonner": "^1.7.0",
"tailwind-merge": "^2.5.5",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "zustand": "^5.0.2"
},
"devDependencies": {
"@types/node": "^22",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 01a8399..aacb3b1 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -56,6 +56,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.16)
+ zustand:
+ specifier: ^5.0.2
+ version: 5.0.2(@types/react@18.3.12)(react@19.0.0-rc-69d4b800-20241021)
devDependencies:
'@types/node':
specifier: ^22
@@ -2107,6 +2110,24 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ zustand@5.0.2:
+ resolution: {integrity: sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -3154,7 +3175,7 @@ snapshots:
debug: 4.3.7
enhanced-resolve: 5.17.1
eslint: 9.14.0(jiti@1.21.6)
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@1.21.6)))(eslint@9.14.0(jiti@1.21.6))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@1.21.6))
fast-glob: 3.3.2
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
@@ -3167,7 +3188,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@1.21.6)))(eslint@9.14.0(jiti@1.21.6)):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@1.21.6)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -3189,7 +3210,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.14.0(jiti@1.21.6)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@1.21.6)))(eslint@9.14.0(jiti@1.21.6))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@1.21.6))
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -4320,3 +4341,8 @@ snapshots:
yaml@2.6.0: {}
yocto-queue@0.1.0: {}
+
+ zustand@5.0.2(@types/react@18.3.12)(react@19.0.0-rc-69d4b800-20241021):
+ optionalDependencies:
+ '@types/react': 18.3.12
+ react: 19.0.0-rc-69d4b800-20241021
diff --git a/web/store/appState.ts b/web/store/appState.ts
new file mode 100644
index 0000000..09da902
--- /dev/null
+++ b/web/store/appState.ts
@@ -0,0 +1,13 @@
+import { create } from 'zustand';
+
+interface AppState {
+ isDownloading: boolean;
+ setIsDownloading: (value: boolean) => void;
+}
+
+const useAppState = create((set) => ({
+ isDownloading: false, // Initial state
+ setIsDownloading: (value) => set(() => ({ isDownloading: value })), // Mutator
+}));
+
+export default useAppState;
diff --git a/web/types/index.ts b/web/types/index.ts
new file mode 100644
index 0000000..d6564ad
--- /dev/null
+++ b/web/types/index.ts
@@ -0,0 +1,64 @@
+// Code generated by tygo. DO NOT EDIT.
+
+//////////
+// source: job.go
+
+export interface DownloadJob {
+ ID: string;
+ URL: string;
+ TIMESTAMP: string /* RFC3339 */;
+}
+export interface JobData {
+ ID: string;
+ JobID: string;
+ URL: string;
+ IsPlaylist: boolean;
+ STATUS: string;
+ PROGRESS: number /* int */;
+ CreatedAt: string /* RFC3339 */;
+ UpdatedAt: string /* RFC3339 */;
+}
+
+//////////
+// source: metadata.go
+
+export interface VideoMetadata {
+ id: string;
+ title: string;
+ uploader: string;
+ filesize: number /* int64 */;
+ duration: number /* float64 */;
+ format: string;
+ thumbnail: string;
+}
+export interface PlaylistMetadata {
+ id: string;
+ title: string;
+ description: string;
+}
+
+//////////
+// source: video.go
+
+/**
+ * Video represents metadata for a single video.
+ */
+export interface Video {
+ id: number /* int */; // Primary key in database
+ job_id: string; // ID associated with the download job
+ title: string; // Title of the video
+ uploader: string; // Uploader or channel name
+ file_path: string; // Path where the video file is stored
+ last_downloaded_at: string /* RFC3339 */; // Timestamp of when the video was last downloaded
+ length: number /* float64 */; // Duration of the video in seconds
+ size: number /* int64 */; // File size in bytes
+ quality: string; // Video quality
+}
+/**
+ * Playlist represents metadata for a playlist or channel.
+ */
+export interface Playlist {
+ id: string; // Playlist or channel ID
+ title: string; // Title of the playlist or channel
+ description: string; // Description of the playlist or channel
+}