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