31 changed files with 3011 additions and 235 deletions
@ -0,0 +1,30 @@ |
|||||
|
import nodemailer from "nodemailer"; |
||||
|
|
||||
|
function getTransporter() { |
||||
|
return nodemailer.createTransport({ |
||||
|
host: "smtp.gmail.com", |
||||
|
port: 587, |
||||
|
secure: false, |
||||
|
auth: { |
||||
|
user: process.env.GMAIL_USER, |
||||
|
pass: "yuuu kvoi ytrf blla", |
||||
|
}, |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
export function sendEmail(to, subject, text, html = null) { |
||||
|
const transporter = getTransporter(); |
||||
|
const mailOptions = { |
||||
|
from: process.env.GMAIL_USER, |
||||
|
to, |
||||
|
subject, |
||||
|
text, |
||||
|
}; |
||||
|
|
||||
|
// Add HTML if provided
|
||||
|
if (html) { |
||||
|
mailOptions.html = html; |
||||
|
} |
||||
|
|
||||
|
return transporter.sendMail(mailOptions); |
||||
|
} |
||||
File diff suppressed because it is too large
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 764 B |
|
After Width: | Height: | Size: 487 B |
|
After Width: | Height: | Size: 260 B |
@ -0,0 +1,17 @@ |
|||||
|
|
||||
|
|
||||
|
export default function PlaylistVideoCard({ video, playlistId, navigation, currentVideo }) { |
||||
|
return ( |
||||
|
<div className="glassmorphism flex items-center gap-2 p-2" onClick={() => navigation(`/video/${video.id}?playlistId=${playlistId}`)} > |
||||
|
<div className="relative" > |
||||
|
<img src={video.thumbnail} alt={video.title} className="h-16 object-cover rounded-sm" /> |
||||
|
{currentVideo == video.id && ( |
||||
|
<div className="absolute inset-0 bg-black opacity-80 rounded-sm w-full h-full flex items-center justify-center"> |
||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="fill-white"><path d="M7 6v12l10-6z"></path></svg> |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
<h3 className="font-montserrat font-medium text-white">{video.title}</h3> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
import { useState } from "react"; |
||||
|
import { createPlaylist } from "../services/playlist.service.js"; |
||||
|
|
||||
|
export default function CreatePlaylistModal({ isOpen, onClose, addAlert }) { |
||||
|
|
||||
|
const [name, setName] = useState(''); |
||||
|
|
||||
|
const onSubmit = async (e) => { |
||||
|
e.preventDefault(); |
||||
|
const token = localStorage.getItem("token"); |
||||
|
const body = { name }; |
||||
|
await createPlaylist(body, token, addAlert); |
||||
|
onClose(); |
||||
|
}; |
||||
|
|
||||
|
return isOpen && ( |
||||
|
|
||||
|
<div className="bg-[#00000080] fixed top-0 left-0 w-screen h-screen flex flex-col items-center justify-center" > |
||||
|
<div className="glassmorphism p-4 w-1/4" > |
||||
|
<h2 className="text-2xl text-white font-montserrat font-bold" >Créer une playlist</h2> |
||||
|
<label htmlFor="name" className="block text-xl text-white font-montserrat font-semibold mt-2" >Nom de la playlist</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
id="name" |
||||
|
name="name" |
||||
|
placeholder="Nom de la playlist" |
||||
|
className="block glassmorphism text-lg text-white font-inter w-full py-3 px-2 focus:outline-none" |
||||
|
onChange={(e) => setName(e.target.value)} |
||||
|
value={name} |
||||
|
/> |
||||
|
<button |
||||
|
className="bg-primary mt-2 py-2 px-3 rounded-sm text-white font-montserrat text-2xl font-semibold cursor-pointer" |
||||
|
onClick={(e) => onSubmit(e) } |
||||
|
> |
||||
|
Valider |
||||
|
</button> |
||||
|
<button |
||||
|
className="bg-red-500 ml-2 mt-2 py-2 px-3 rounded-sm text-white font-montserrat text-2xl font-semibold cursor-pointer" |
||||
|
onClick={onClose} |
||||
|
> |
||||
|
Annuler |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
import React, { useState } from 'react'; |
||||
|
|
||||
|
export default function EmailVerificationModal({ isOpen, onSubmit, onClose }) { |
||||
|
|
||||
|
const [verificationCode, setVerificationCode] = useState(''); |
||||
|
|
||||
|
return isOpen && ( |
||||
|
<div className="bg-[#00000080] fixed top-0 left-0 w-screen h-screen flex flex-col items-center justify-center" > |
||||
|
<div className="glassmorphism p-4 w-1/4" > |
||||
|
<h2 className="text-lg font-bold mb-2 font-montserrat text-white">Vérification de l'email</h2> |
||||
|
<p className="font-montserrat text-white">Un email de vérification a été envoyé à votre adresse email. Veuillez vérifier votre boîte de réception.</p> |
||||
|
<input |
||||
|
type="text" |
||||
|
placeholder="Entrez le code de vérification" |
||||
|
className="glassmorphism w-full px-4 py-2 mt-4 text-white" |
||||
|
value={verificationCode} |
||||
|
onChange={(e) => setVerificationCode(e.target.value)} |
||||
|
/> |
||||
|
<button className="bg-primary px-3 py-2 rounded-sm text-white font-montserrat text-lg font-semibold cursor-pointer mt-2" onClick={() => { |
||||
|
console.log("Verification code submitted:", verificationCode); |
||||
|
onSubmit(verificationCode) |
||||
|
}}>Vérifier</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
|
||||
|
|
||||
|
export default function VerificationModal({title, onConfirm, onCancel, isOpen}) { |
||||
|
if (!isOpen) return null; |
||||
|
|
||||
|
return ( |
||||
|
<div className="fixed inset-0 flex items-center justify-center"> |
||||
|
<div className="glassmorphism p-6"> |
||||
|
<h2 className="text-lg text-white font-semibold mb-4">{title}</h2> |
||||
|
<div className="flex justify-end"> |
||||
|
<button |
||||
|
className="bg-primary px-3 py-2 rounded-sm text-white font-montserrat text-lg font-semibold cursor-pointer" |
||||
|
onClick={() => onConfirm()} |
||||
|
> |
||||
|
Confirmer |
||||
|
</button> |
||||
|
<button |
||||
|
className="bg-red-500 ml-4 px-3 py-2 rounded-sm text-white font-montserrat text-lg font-semibold cursor-pointer" |
||||
|
onClick={() => onCancel()} |
||||
|
> |
||||
|
Annuler |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
import { useParams } from "react-router-dom"; |
||||
|
import Navbar from "../components/Navbar"; |
||||
|
import { useEffect, useState } from "react"; |
||||
|
import { getPlaylistById, deletePlaylist, deleteVideo } from "../services/playlist.service.js"; |
||||
|
import VideoCard from "../components/VideoCard.jsx"; |
||||
|
import VerificationModal from "../modals/VerificationModal.jsx"; |
||||
|
import { useNavigate } from "react-router-dom"; |
||||
|
|
||||
|
export default function Playlist() { |
||||
|
|
||||
|
const { id } = useParams(); |
||||
|
const navigate = useNavigate(); |
||||
|
|
||||
|
const [alerts, setAlerts] = useState([]); |
||||
|
const [playlist, setPlaylist] = useState(null); |
||||
|
const [isDeletePlaylistModalOpen, setIsDeletePlaylistModalOpen] = useState(false); |
||||
|
|
||||
|
const fetchPlaylistDetails = async () => { |
||||
|
const token = localStorage.getItem("token"); |
||||
|
const data = await getPlaylistById(id, token, addAlert); |
||||
|
setPlaylist(data); |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
fetchPlaylistDetails(); |
||||
|
}, [id]); |
||||
|
|
||||
|
const addAlert = (type, message) => { |
||||
|
const newAlert = { type, message, id: Date.now() }; // Add unique ID |
||||
|
setAlerts([...alerts, newAlert]); |
||||
|
}; |
||||
|
|
||||
|
const onCloseAlert = (alertToRemove) => { |
||||
|
setAlerts(alerts.filter(alert => alert !== alertToRemove)); |
||||
|
}; |
||||
|
|
||||
|
const onDeletePlaylist = async () => { |
||||
|
const token = localStorage.getItem("token"); |
||||
|
await deletePlaylist(id, token, addAlert); |
||||
|
setIsDeletePlaylistModalOpen(false); |
||||
|
navigate("/profile"); |
||||
|
} |
||||
|
|
||||
|
const onDeleteVideo = async (videoId) => { |
||||
|
const token = localStorage.getItem("token"); |
||||
|
await deleteVideo(id, videoId, token, addAlert); |
||||
|
fetchPlaylistDetails(); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient"> |
||||
|
<Navbar isSearchPage={false} alerts={alerts} onCloseAlert={onCloseAlert} /> |
||||
|
|
||||
|
<main className="px-36 w-full pt-[118px]"> |
||||
|
|
||||
|
<h1 className="font-bold font-montserrat text-3xl text-white" >{playlist && playlist.name}</h1> |
||||
|
{/* CONTROLS */} |
||||
|
<div className="mt-4"> |
||||
|
<button |
||||
|
className="bg-primary px-3 py-2 rounded-sm text-white font-montserrat text-lg font-semibold cursor-pointer" |
||||
|
onClick={() => console.log("Modifier playlist")} |
||||
|
> |
||||
|
modifier |
||||
|
</button> |
||||
|
<button |
||||
|
className="bg-red-500 ml-4 px-3 py-2 rounded-sm text-white font-montserrat text-lg font-semibold cursor-pointer" |
||||
|
onClick={() => setIsDeletePlaylistModalOpen(true)} |
||||
|
> |
||||
|
supprimer |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div className="grid grid-cols-4 gap-8 mt-12"> |
||||
|
{ |
||||
|
playlist && playlist.videos && playlist.videos.length > 0 ? playlist.videos.map(video => ( |
||||
|
<VideoCard |
||||
|
key={video.id} |
||||
|
video={video} |
||||
|
showControls={true} |
||||
|
onDelete={onDeleteVideo} |
||||
|
link={`/video/${video.id}?playlistId=${playlist.id}`} |
||||
|
/> |
||||
|
)) : ( |
||||
|
<p className="text-white">Aucun vidéo trouvée dans cette playlist.</p> |
||||
|
) |
||||
|
} |
||||
|
</div> |
||||
|
</main> |
||||
|
<VerificationModal |
||||
|
title="Confirmer la suppression" |
||||
|
onConfirm={() => onDeletePlaylist()} |
||||
|
onCancel={() => setIsDeletePlaylistModalOpen(false)} |
||||
|
isOpen={isDeletePlaylistModalOpen} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,104 @@ |
|||||
|
|
||||
|
export async function createPlaylist(body, token, addAlert) { |
||||
|
try { |
||||
|
const response = await fetch(`https://localhost/api/playlists`, { |
||||
|
method: 'POST', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
}, |
||||
|
body: JSON.stringify(body) |
||||
|
}); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
throw new Error('Failed to create playlist'); |
||||
|
} |
||||
|
|
||||
|
const data = await response.json(); |
||||
|
addAlert('success', 'Playlist created successfully'); |
||||
|
return data; |
||||
|
} catch (error) { |
||||
|
addAlert('error', error.message); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export async function addToPlaylist(id, body, token, addAlert) { |
||||
|
try { |
||||
|
const response = await fetch(`https://localhost/api/playlists/${id}`, { |
||||
|
method: 'POST', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
}, |
||||
|
body: JSON.stringify(body) |
||||
|
}); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
throw new Error('Failed to add video to playlist'); |
||||
|
} |
||||
|
|
||||
|
const data = await response.json(); |
||||
|
addAlert('success', 'Vidéo ajoutée à la playlist avec succès'); |
||||
|
return data; |
||||
|
} catch (error) { |
||||
|
addAlert('error', "Erreur lors de l'ajout à la playlist"); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export async function getPlaylistById(id, token, addAlert) { |
||||
|
try { |
||||
|
const response = await fetch(`https://localhost/api/playlists/${id}`, { |
||||
|
headers: { |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
throw new Error('Failed to fetch playlist'); |
||||
|
} |
||||
|
|
||||
|
const data = await response.json(); |
||||
|
return data; |
||||
|
} catch (error) { |
||||
|
addAlert('error', error.message); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export async function deletePlaylist(id, token, addAlert) { |
||||
|
try { |
||||
|
const response = await fetch(`https://localhost/api/playlists/${id}`, { |
||||
|
method: 'DELETE', |
||||
|
headers: { |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
throw new Error('Failed to delete playlist'); |
||||
|
} |
||||
|
|
||||
|
addAlert('success', 'Playlist deleted successfully'); |
||||
|
} catch (error) { |
||||
|
addAlert('error', error.message); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export async function deleteVideo(playlistId, videoId, token, addAlert) { |
||||
|
try { |
||||
|
const response = await fetch(`https://localhost/api/playlists/${playlistId}/video/${videoId}`, { |
||||
|
method: 'DELETE', |
||||
|
headers: { |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
throw new Error('Failed to delete video'); |
||||
|
} |
||||
|
|
||||
|
addAlert('success', 'Video deleted successfully'); |
||||
|
} catch (error) { |
||||
|
addAlert('error', error.message); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue