(add+update) : UI faite avec tailwind + connexion l'API)

This commit is contained in:
root 2025-09-14 11:20:17 +00:00
parent 8772d14a62
commit 61376351e2
16 changed files with 276 additions and 103 deletions

View File

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
@ -12,17 +12,21 @@
}, },
"dependencies": { "dependencies": {
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1" "react-dom": "^19.1.1",
"react-router-dom": "^7.9.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"@types/react": "^19.1.10", "@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",
"@vitejs/plugin-react-swc": "^4.0.0", "@vitejs/plugin-react-swc": "^4.0.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.33.0", "eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0", "globals": "^16.3.0",
"postcss": "^8.5.6",
"tailwindcss": "3.4.17",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.39.1", "typescript-eslint": "^8.39.1",
"vite": "^7.1.2" "vite": "^7.1.2"

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,35 +1,15 @@
import { useState } from 'react' import { Routes, Route } from "react-router-dom";
import reactLogo from './assets/react.svg' import Home from "./pages/Home";
import viteLogo from '/vite.svg' import Layout from "./components/layout/Layout";
import './App.css'
function App() { function App() {
const [count, setCount] = useState(0)
return ( return (
<> <Routes>
<div> <Route path="/" element={<Layout />}>
<a href="https://vite.dev" target="_blank"> <Route index element={<Home />} />
<img src={viteLogo} className="logo" alt="Vite logo" /> </Route>
</a> </Routes>
<a href="https://react.dev" target="_blank"> );
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
} }
export default App export default App;

View File

@ -0,0 +1,11 @@
import { Outlet } from "react-router-dom";
export default function Layout() {
return (
<div className="min-h-screen bg-background text-text flex flex-col">
<main className="flex-1 flex items-center justify-center p-6">
<Outlet />
</main>
</div>
);
}

View File

@ -0,0 +1,144 @@
import { useState } from "react";
import { sendYoutubeUrl } from "../../services/apiClient";
interface VideoJob {
id: string;
url: string;
format: string;
status: "pending" | "processing" | "done" | "error";
}
export default function YoutubeForm() {
const [url, setUrl] = useState("");
const [format, setFormat] = useState("mp3"); // format par défaut
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [jobs, setJobs] = useState<VideoJob[]>([]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage(null);
try {
const res = await sendYoutubeUrl(url, format);
const newJob: VideoJob = {
id: res.jobId || Date.now().toString(),
url,
format,
status: "processing",
};
setJobs((prev) => [newJob, ...prev]);
setMessage(res.message || "Vidéo envoyée avec succès !");
setUrl("");
} catch (err: any) {
setMessage(err.message || "Erreur lors de lenvoi.");
} finally {
setLoading(false);
}
};
const clearHistory = () => {
setJobs([]);
setMessage("Historique vidé ✅");
};
return (
<div className="flex flex-col items-center w-full max-w-2xl mx-auto bg-surface p-8 rounded-xl shadow-lg">
<h1 className="text-2xl font-bold text-primary mb-4">
Youtube To Jellyfin
</h1>
<p className="text-muted text-sm mb-6">
Collez un lien YouTube ci-dessous et choisissez le format. La vidéo sera
téléchargée, convertie et envoyée automatiquement sur votre serveur.
</p>
<form
onSubmit={handleSubmit}
className="flex flex-col sm:flex-row gap-4 w-full"
>
<input
type="url"
placeholder="https://youtube.com/watch?v=..."
value={url}
onChange={(e) => setUrl(e.target.value)}
className="flex-1 px-4 py-3 rounded-lg bg-surfaceLight text-text
placeholder-gray-500 border border-gray-700
focus:outline-none focus:ring-2 focus:ring-primary"
/>
<select
value={format}
onChange={(e) => setFormat(e.target.value)}
className="px-4 py-3 rounded-lg bg-surfaceLight text-text border border-gray-700 focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="mp3">MP3</option>
<option value="flac">FLAC</option>
<option value="wav">WAV</option>
</select>
<button
type="submit"
disabled={loading}
className={`px-6 py-3 rounded-lg font-semibold transition
${loading
? "bg-primary/70 text-black animate-pulse cursor-not-allowed"
: "bg-primary text-black hover:opacity-90 cursor-pointer"}`}
>
{loading ? (
<div className="flex items-center gap-2">
<span className="h-4 w-4 border-2 border-black border-t-transparent rounded-full animate-spin"></span>
Envoi...
</div>
) : (
"Télécharger & Envoyer"
)}
</button>
</form>
{message && (
<p className="mt-4 text-sm text-secondary font-medium">{message}</p>
)}
{jobs.length > 0 && (
<div className="mt-8 w-full">
<div className="flex justify-between items-center mb-3">
<h2 className="text-lg font-semibold text-text">Vidéos envoyées</h2>
<button
onClick={clearHistory}
className="text-sm text-red-400 hover:text-red-300 transition"
>
Vider lhistorique
</button>
</div>
<ul className="space-y-3">
{jobs.map((job) => (
<li
key={job.id}
className="p-4 rounded-lg bg-surfaceLight shadow flex justify-between items-center opacity-0 animate-fadeIn"
>
<span className="truncate text-sm">
{job.url} <span className="text-muted">({job.format})</span>
</span>
<span
className={`text-xs font-semibold px-2 py-1 rounded ${
job.status === "done"
? "bg-green-600 text-white"
: job.status === "processing"
? "bg-yellow-600 text-white"
: job.status === "error"
? "bg-red-600 text-white"
: "bg-gray-600 text-white"
}`}
>
{job.status}
</span>
</li>
))}
</ul>
</div>
)}
</div>
);
}

View File

@ -1,68 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -1,10 +1,14 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css'
import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx' import App from './App.tsx'
import "./styles/index.css";
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <BrowserRouter>
</StrictMode>, <App />
</BrowserRouter>
</StrictMode>
) )

View File

@ -0,0 +1,9 @@
import YoutubeForm from "../features/youtube/YoutubeForm";
export default function Home() {
return (
<div className="flex flex-col items-center justify-center min-h-[70vh] text-center">
<YoutubeForm />
</div>
);
}

View File

View File

25
src/services/apiClient.ts Normal file
View File

@ -0,0 +1,25 @@
export interface ApiResponse {
success: boolean;
message: string;
jobId?: string;
}
const API_URL = import.meta.env.VITE_API_URL;
const API_KEY = import.meta.env.VITE_API_KEY;
export async function sendYoutubeUrl(url: string, format: string): Promise<ApiResponse> {
const res = await fetch(`${API_URL}/api/download`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_KEY}`,
},
body: JSON.stringify({ url, format }),
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
return res.json();
}

25
src/styles/index.css Normal file
View File

@ -0,0 +1,25 @@
/* eslint-disable @tailwindcss/no-custom-at-rules */
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.4s ease-out forwards;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

5
src/types/api.ts Normal file
View File

@ -0,0 +1,5 @@
export interface ApiResponse {
success: boolean;
message: string;
jobId?: string; // si tu veux suivre le téléchargement
}

View File

28
tailwind.config.js Normal file
View File

@ -0,0 +1,28 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
container: {
center: true,
padding: "1rem",
},
colors: {
background: "#121212",
surface: "#1E1E1E",
surfaceLight: "#2A2A2A",
primary: "#BB86FC",
secondary: "#03DAC6",
text: {
DEFAULT: "#E0E0E0",
muted: "#A0A0A0",
},
},
},
},
plugins: [],
}