committed by
GitHub
5 changed files with 429 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,275 @@ |
|||||
|
import Navbar from "../components/Navbar.jsx"; |
||||
|
import {useEffect, useState} from "react"; |
||||
|
import Tag from "../components/Tag.jsx"; |
||||
|
|
||||
|
|
||||
|
export default function AddVideo() { |
||||
|
|
||||
|
const storedUser = localStorage.getItem("user"); |
||||
|
const user = storedUser ? JSON.parse(storedUser) : null; |
||||
|
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ée pour valider) 10 maximum" |
||||
|
onKeyDown={handleTagKeyDown} |
||||
|
|
||||
|
/> |
||||
|
<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-[480px] 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