5 changed files with 428 additions and 3 deletions
@ -0,0 +1,9 @@ |
|||
/backend/app/uploads/ |
|||
# Ignore all files in the uploads directory |
|||
|
|||
/frontend/node_modules |
|||
/backend/node_modules |
|||
# Ignore node_modules directories in both frontend and backend |
|||
|
|||
/frontend/dist |
|||
# Ignore the build output directory for the frontend |
|||
@ -0,0 +1,274 @@ |
|||
import Navbar from "../components/Navbar.jsx"; |
|||
import {useEffect, useState} from "react"; |
|||
import Tag from "../components/Tag.jsx"; |
|||
|
|||
|
|||
export default function AddVideo() { |
|||
|
|||
const user = JSON.parse(localStorage.getItem("user")); |
|||
const token = localStorage.getItem("token"); |
|||
|
|||
const [videoTitle, setVideoTitle] = useState(""); |
|||
const [videoDescription, setVideoDescription] = useState(""); |
|||
const [videoTags, setVideoTags] = useState([]); |
|||
const [visibility, setVisibility] = useState("public"); // Default visibility |
|||
const [videoThumbnail, setVideoThumbnail] = useState(null); |
|||
const [videoFile, setVideoFile] = useState(null); |
|||
const [channel, setChannel] = useState(null); // Assuming user.channel is the channel ID |
|||
|
|||
useEffect(() => { |
|||
fetchChannel(); |
|||
}, []) |
|||
|
|||
const fetchChannel = async () => { |
|||
try { |
|||
const response = await fetch(`/api/users/${user.id}/channel`, { |
|||
headers: { |
|||
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication |
|||
}, |
|||
}); |
|||
if (!response.ok) { |
|||
throw new Error("Erreur lors de la récupération de la chaîne"); |
|||
} |
|||
const data = await response.json(); |
|||
setChannel(data.channel); |
|||
} catch (error) { |
|||
console.error("Erreur lors de la récupération de la chaîne :", error); |
|||
} |
|||
} |
|||
|
|||
const handleTagKeyDown = (e) => { |
|||
if (e.key === 'Enter' && videoTags.length < 10) { |
|||
e.preventDefault(); |
|||
const newTag = e.target.value.trim(); |
|||
if (newTag && !videoTags.includes(newTag)) { |
|||
setVideoTags([...videoTags, newTag]); |
|||
e.target.value = ''; |
|||
} |
|||
} |
|||
} |
|||
const handleTagRemove = (tagToRemove) => { |
|||
setVideoTags(videoTags.filter(tag => tag !== tagToRemove)); |
|||
}; |
|||
|
|||
// This function handles the submission of the video form |
|||
const handleSubmit = async (e) => { |
|||
e.preventDefault(); |
|||
|
|||
if (!videoTitle || !videoDescription || !videoThumbnail || !videoFile) { |
|||
alert("Veuillez remplir tous les champs requis."); |
|||
return; |
|||
} |
|||
if (!channel || !channel.id) { |
|||
alert("Erreur: aucune chaîne trouvée. Veuillez actualiser la page."); |
|||
return; |
|||
} |
|||
if (videoTags.length > 10) { |
|||
alert("Vous ne pouvez pas ajouter plus de 10 tags."); |
|||
return; |
|||
} |
|||
|
|||
const formData = new FormData(); |
|||
formData.append("title", videoTitle); |
|||
formData.append("description", videoDescription); |
|||
formData.append("channel", channel.id.toString()); // Ensure it's a string |
|||
formData.append("visibility", visibility); |
|||
formData.append("file", videoFile); |
|||
|
|||
const request = await fetch("/api/videos", { |
|||
method: "POST", |
|||
headers: { |
|||
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication |
|||
}, |
|||
body: formData |
|||
}); |
|||
if (!request.ok) { |
|||
const errorData = await request.json(); |
|||
console.error("Backend validation errors:", errorData); |
|||
|
|||
// Display specific validation errors if available |
|||
if (errorData.errors && errorData.errors.length > 0) { |
|||
const errorMessages = errorData.errors.map(error => |
|||
`${error.path}: ${error.msg}` |
|||
).join('\n'); |
|||
alert(`Erreurs de validation:\n${errorMessages}`); |
|||
} else { |
|||
alert(`Erreur lors de l'ajout de la vidéo : ${errorData.message || 'Erreur inconnue'}`); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
// If the video was successfully created, we can now upload the thumbnail |
|||
const response = await request.json(); |
|||
const videoId = response.id; |
|||
const thumbnailFormData = new FormData(); |
|||
thumbnailFormData.append("video", videoId); |
|||
thumbnailFormData.append("file", videoThumbnail); |
|||
thumbnailFormData.append("channel", channel.id.toString()); |
|||
const thumbnailRequest = await fetch("/api/videos/thumbnail", { |
|||
method: "POST", |
|||
headers: { |
|||
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication |
|||
}, |
|||
body: thumbnailFormData |
|||
}); |
|||
if (!thumbnailRequest.ok) { |
|||
const errorData = await thumbnailRequest.json(); |
|||
console.error("Backend validation errors:", errorData); |
|||
alert(`Erreur lors de l'ajout de la miniature : ${errorData.message || 'Erreur inconnue'}`); |
|||
return; |
|||
} |
|||
|
|||
// if the thumbnail was successfully uploaded, we can send the tags |
|||
const tagsRequest = await fetch(`/api/videos/${videoId}/tags`, { |
|||
method: "PUT", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication |
|||
}, |
|||
body: JSON.stringify({ tags: videoTags, channel: channel.id.toString() }) // Ensure channel ID is a string |
|||
}); |
|||
if (!tagsRequest.ok) { |
|||
const errorData = await tagsRequest.json(); |
|||
console.error("Backend validation errors:", errorData); |
|||
alert(`Erreur lors de l'ajout des tags : ${errorData.message || 'Erreur inconnue'}`); |
|||
return; |
|||
} |
|||
// If everything is successful, redirect to the video management page |
|||
alert("Vidéo ajoutée avec succès !"); |
|||
|
|||
|
|||
|
|||
}; |
|||
|
|||
return ( |
|||
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient"> |
|||
<Navbar isSearchPage={false} /> |
|||
|
|||
<main className="px-36 pt-[118px]"> |
|||
<h1 className="font-montserrat text-2xl font-black text-white"> |
|||
Ajouter une vidéo |
|||
</h1> |
|||
|
|||
<div className="flex gap-8 mt-8"> |
|||
{/* Left side: Form for adding video details */} |
|||
<form className="flex-1"> |
|||
<label className="block text-white text-xl font-montserrat font-semibold mb-2" htmlFor="videoTitle">Titre de la vidéo</label> |
|||
<input |
|||
type="text" |
|||
id="videoTitle" |
|||
name="videoTitle" |
|||
className="w-full p-2 mb-4 glassmorphism focus:outline-none font-inter text-xl text-white" |
|||
placeholder="Entrez le titre de la vidéo" |
|||
value={videoTitle} |
|||
onChange={(e) => setVideoTitle(e.target.value)} |
|||
required |
|||
/> |
|||
|
|||
<label className="block text-white text-xl font-montserrat font-semibold mb-2" htmlFor="videoDescription">Description de la vidéo</label> |
|||
<textarea |
|||
id="videoDescription" |
|||
name="videoDescription" |
|||
className="w-full p-2 mb-4 glassmorphism focus:outline-none font-inter text-xl text-white" |
|||
placeholder="Entrez la description de la vidéo" |
|||
rows="4" |
|||
value={videoDescription} |
|||
onChange={(e) => setVideoDescription(e.target.value)} |
|||
required |
|||
></textarea> |
|||
|
|||
<label className="block text-white text-xl font-montserrat font-semibold mb-1" htmlFor="videoDescription">Tags</label> |
|||
<input |
|||
type="text" |
|||
id="videoTags" |
|||
name="videoTags" |
|||
className="w-full p-2 mb-4 glassmorphism focus:outline-none font-inter text-xl text-white" |
|||
placeholder="Entrez les tags de la vidéo (entré pour valider) 10 maximums" |
|||
onKeyDown={handleTagKeyDown} |
|||
required |
|||
/> |
|||
<div className="flex flex-wrap gap-2 mb-2"> |
|||
{videoTags.map((tag, index) => ( |
|||
<Tag tag={tag} doShowControls={true} key={index} onSuppress={() => handleTagRemove(tag)} /> |
|||
))} |
|||
</div> |
|||
|
|||
<label className="block text-white text-xl font-montserrat font-semibold mb-2" htmlFor="visibility">Visibilité</label> |
|||
<select |
|||
name="visibility" |
|||
id="visibility" |
|||
className="w-full p-2 mb-4 glassmorphism focus:outline-none font-inter text-xl text-white" |
|||
value={visibility} |
|||
onChange={(e) => setVisibility(e.target.value)} |
|||
> |
|||
<option value="public">Public</option> |
|||
<option value="private">Privé</option> |
|||
</select> |
|||
|
|||
<label className="block text-white text-xl font-montserrat font-semibold mb-2" htmlFor="videoThumbnail">Miniature</label> |
|||
<input |
|||
type="file" |
|||
id="videoThumbnail" |
|||
name="videoThumbnail" |
|||
className="w-full p-2 mb-4 glassmorphism focus:outline-none font-inter text-xl text-white" |
|||
accept="image/*" |
|||
onChange={(e) => setVideoThumbnail(e.target.files[0])} |
|||
required |
|||
/> |
|||
|
|||
<label className="block text-white text-xl font-montserrat font-semibold mb-2" htmlFor="videoFile">Fichier vidéo</label> |
|||
<input |
|||
type="file" |
|||
id="videoFile" |
|||
name="videoFile" |
|||
className="w-full p-2 mb-4 glassmorphism focus:outline-none font-inter text-xl text-white" |
|||
accept="video/*" |
|||
onChange={(e) => setVideoFile(e.target.files[0])} |
|||
required |
|||
/> |
|||
|
|||
<button |
|||
type="submit" |
|||
className="bg-primary text-white font-montserrat p-3 rounded-lg text-2xl font-bold w-full cursor-pointer" |
|||
onClick={(e) => { handleSubmit(e) }} |
|||
> |
|||
Ajouter la vidéo |
|||
</button> |
|||
|
|||
</form> |
|||
|
|||
{/* Right side: Preview of the video being added */} |
|||
<div className="flex-1 flex justify-center"> |
|||
<div className="glassmorphism p-4 rounded-lg"> |
|||
<img |
|||
src={videoThumbnail ? URL.createObjectURL(videoThumbnail) : "https://placehold.co/1280x720"} alt={videoTitle} |
|||
className="w-[480] h-auto mb-4 rounded-lg" |
|||
/> |
|||
<h2 className="text-white text-xl font-montserrat font-semibold mb-2">{videoTitle || "Titre de la vidéo"}</h2> |
|||
|
|||
<div className="glassmorphism p-4 rounded-sm"> |
|||
<p className="text-white font-inter mb-2"> |
|||
{videoDescription || "Description de la vidéo"} |
|||
</p> |
|||
|
|||
<div className="flex flex-wrap gap-2"> |
|||
{videoTags.length > 0 ? ( |
|||
videoTags.map((tag, index) => ( |
|||
<Tag tag={tag} doShowControls={false} key={index} /> |
|||
)) |
|||
) : ( |
|||
<span className="text-gray-400">Aucun tag ajouté</span> |
|||
)} |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
</main> |
|||
|
|||
</div> |
|||
); |
|||
|
|||
} |
|||
Loading…
Reference in new issue