Merge pull request '(add+update) : UI faite avec tailwind + connexion l'API)' (#1) from dev into master
Reviewed-on: #1
This commit is contained in:
commit
1964347da6
@ -4,7 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
@ -12,17 +12,21 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react-swc": "^4.0.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"vite": "^7.1.2"
|
||||
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
40
src/App.tsx
40
src/App.tsx
@ -1,35 +1,15 @@
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import Home from "./pages/Home";
|
||||
import Layout from "./components/layout/Layout";
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Home />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
11
src/components/layout/Layout.tsx
Normal file
11
src/components/layout/Layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
144
src/features/youtube/YoutubeForm.tsx
Normal file
144
src/features/youtube/YoutubeForm.tsx
Normal 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 l’envoi.");
|
||||
} 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 l’historique
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
10
src/main.tsx
10
src/main.tsx
@ -1,10 +1,14 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App.tsx'
|
||||
import "./styles/index.css";
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
25
src/services/apiClient.ts
Normal file
25
src/services/apiClient.ts
Normal 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
25
src/styles/index.css
Normal 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
5
src/types/api.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ApiResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
jobId?: string; // si tu veux suivre le téléchargement
|
||||
}
|
||||
0
src/types/global.d.ts
vendored
0
src/types/global.d.ts
vendored
28
tailwind.config.js
Normal file
28
tailwind.config.js
Normal 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: [],
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user