39 changed files with 4103 additions and 72 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 |
||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
File diff suppressed because it is too large
@ -0,0 +1,52 @@ |
|||||
|
import {Line} from "react-chartjs-2"; |
||||
|
import {Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend} from "chart.js"; |
||||
|
|
||||
|
ChartJS.register( |
||||
|
CategoryScale, |
||||
|
LinearScale, |
||||
|
PointElement, |
||||
|
LineElement, |
||||
|
Title, |
||||
|
Tooltip, |
||||
|
Legend |
||||
|
); |
||||
|
|
||||
|
export default function LinearGraph({ dataToGraph, className, legend, borderColor="rgba(75, 192, 192, 1)" }) { |
||||
|
|
||||
|
const prepareData = () => { |
||||
|
if (!dataToGraph || dataToGraph.length === 0) { |
||||
|
return { |
||||
|
labels: [], |
||||
|
datasets: [] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const labels = dataToGraph.map(item => item.date.split("T")[0]); |
||||
|
const data = dataToGraph.map(item => item.count); |
||||
|
|
||||
|
return { |
||||
|
labels, |
||||
|
datasets: [{ |
||||
|
label: legend, |
||||
|
data, |
||||
|
fill: false, |
||||
|
pointRadius: 3, |
||||
|
borderColor: borderColor, |
||||
|
tension: 0, |
||||
|
stepped: false |
||||
|
}] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const data = prepareData(); |
||||
|
|
||||
|
const options = { |
||||
|
|
||||
|
} |
||||
|
return ( |
||||
|
<div className={className}> |
||||
|
<Line options={options} data={data} className="w-full border-red-500 " /> |
||||
|
</div> |
||||
|
) |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
export default function Tag({ tag, onSuppress, doShowControls=true }) { |
||||
|
|
||||
|
return ( |
||||
|
<div className="glassmorphism px-2 py-1 w-max flex flex-row items-center gap-2"> |
||||
|
<span className="font-inter text-white">#{tag}</span> |
||||
|
{doShowControls && ( |
||||
|
<span className="tag-controls cursor-pointer" onClick={onSuppress}> |
||||
|
<svg |
||||
|
className="w-6 h-6 fill-white" |
||||
|
stroke="#FFF" |
||||
|
viewBox="0 0 24 24" |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
> |
||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> |
||||
|
</svg> |
||||
|
|
||||
|
</span> |
||||
|
)} |
||||
|
</div> |
||||
|
) |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
|
||||
|
|
||||
|
export default function VideoStatListElement ({ video, onClick }) { |
||||
|
return ( |
||||
|
<div className="flex p-4 gap-4 glassmorphism cursor-pointer" onClick={onClick} > |
||||
|
<img |
||||
|
src={video.thumbnail} |
||||
|
alt="" |
||||
|
className="w-1/4 aspect-video rounded-sm" |
||||
|
/> |
||||
|
<div> |
||||
|
<h3 className="text-white text-2xl font-montserrat font-bold" >{video.title}</h3> |
||||
|
<p className="text-white text-lg font-montserrat font-normal">Vues: {video.views}</p> |
||||
|
<p className="text-white text-lg font-montserrat font-normal">Likes: {video.likes}</p> |
||||
|
<p className="text-white text-lg font-montserrat font-normal">Commentaires: {video.comments}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
import {useState} from "react"; |
||||
|
|
||||
|
|
||||
|
export default function CreateChannelModal({isOpen, onClose}) { |
||||
|
|
||||
|
const [name, setName] = useState(''); |
||||
|
const [description, setDescription] = useState(''); |
||||
|
|
||||
|
const token = localStorage.getItem('token'); |
||||
|
const userStored = localStorage.getItem('user'); |
||||
|
const user = userStored ? JSON.parse(userStored) : {}; |
||||
|
|
||||
|
const onSubmit = async (e) => { |
||||
|
e.preventDefault(); |
||||
|
|
||||
|
const request = await fetch(`/api/channels/`, { |
||||
|
method: 'POST', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization': `Bearer ${token}`, |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
"name": name, |
||||
|
"description": description, |
||||
|
"owner": user.id |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
if (!request.ok) { |
||||
|
console.error("Not able to create channel"); |
||||
|
return; // Prevent further execution if the request failed |
||||
|
} |
||||
|
|
||||
|
const data = await request.json(); |
||||
|
console.log(data); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
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 chaine</h2> |
||||
|
<label htmlFor="name" className="block text-xl text-white font-montserrat font-semibold mt-2" >Nom de la chaine</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
id="name" |
||||
|
name="name" |
||||
|
placeholder="Nom de la chaine" |
||||
|
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} |
||||
|
/> |
||||
|
|
||||
|
<label htmlFor="description" className="block text-xl text-white font-montserrat font-semibold mt-2" >Description</label> |
||||
|
<textarea |
||||
|
id="description" |
||||
|
name="description" |
||||
|
placeholder="Description de votre chaine" |
||||
|
className="block glassmorphism text-lg text-white font-inter w-full py-3 px-2 focus:outline-none" |
||||
|
onChange={(e) => setDescription(e.target.value)} |
||||
|
value={description} |
||||
|
> |
||||
|
</textarea> |
||||
|
<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,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> |
||||
|
); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,208 @@ |
|||||
|
import Navbar from "../components/Navbar.jsx"; |
||||
|
import {useEffect, useState} from "react"; |
||||
|
import {useNavigate, useParams} from "react-router-dom"; |
||||
|
import {useAuth} from "../contexts/AuthContext.jsx"; |
||||
|
import VideoStatListElement from "../components/VideoStatListElement.jsx"; |
||||
|
|
||||
|
|
||||
|
export default function ManageChannel() { |
||||
|
|
||||
|
const {id} = useParams(); |
||||
|
const {user} = useAuth(); |
||||
|
const navigate = useNavigate(); |
||||
|
|
||||
|
const [channel, setChannel] = useState(); |
||||
|
const [channelStats, setChannelStats] = useState(); |
||||
|
const [channelName, setChannelName] = useState(null); |
||||
|
const [description, setDescription] = useState(null); |
||||
|
const [editMode, setEditMode] = useState(false); |
||||
|
|
||||
|
const token = localStorage.getItem("token"); |
||||
|
const nonEditModeClasses = "text-2xl font-bold text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none text-center"; |
||||
|
const editModeClasses = nonEditModeClasses + " glassmorphism"; |
||||
|
|
||||
|
const nonEditModeClassesTextArea = "text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none w-full" |
||||
|
const editModeClassesTextArea = nonEditModeClassesTextArea + " glassmorphism h-48"; |
||||
|
|
||||
|
useEffect(() => { |
||||
|
fetchChannelData() |
||||
|
fetchChannelStats() |
||||
|
}, []); |
||||
|
|
||||
|
const fetchChannelData = async () => { |
||||
|
try { |
||||
|
const request = await fetch(`/api/channels/${id}`, { |
||||
|
method: "GET", |
||||
|
headers: { |
||||
|
"Authorization": `Bearer ${token}` |
||||
|
} |
||||
|
}) |
||||
|
const result = await request.json(); |
||||
|
setChannel(result); |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error("Error fetching channel data:", error); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
const fetchChannelStats = async () => { |
||||
|
try { |
||||
|
const request = await fetch(`/api/channels/${id}/stats`, { |
||||
|
method: "GET", |
||||
|
headers: { |
||||
|
"Authorization": `Bearer ${token}` |
||||
|
} |
||||
|
}) |
||||
|
const result = await request.json(); |
||||
|
setChannelStats(result); |
||||
|
} catch (error) { |
||||
|
console.error("Error fetching channel stats", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const handleUpdateChannel = async () => { |
||||
|
if (!editMode) return; |
||||
|
|
||||
|
try { |
||||
|
const response = await fetch(`/api/channels/${id}`, { |
||||
|
method: "PUT", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
"Authorization": `Bearer ${token}` |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
name: channelName || channel.name, |
||||
|
description: description || channel.description, |
||||
|
}) |
||||
|
}); |
||||
|
|
||||
|
if (response.ok) { |
||||
|
setEditMode(false); |
||||
|
fetchChannelData(); // Refresh channel data after update |
||||
|
} else { |
||||
|
console.error("Failed to update channel"); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error("Error updating channel:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient"> |
||||
|
<Navbar isSearchPage={false} /> |
||||
|
|
||||
|
<main className="pt-[118px] px-36 flex"> |
||||
|
|
||||
|
{/* LEFT SIDE */} |
||||
|
<form className="glassmorphism w-1/3 h-screen py-10 px-4"> |
||||
|
<img src={user.picture} className="w-1/3 aspect-square object-cover rounded-full mx-auto" alt=""/> |
||||
|
<label htmlFor="name" className={`text-2xl text-white mb-1 block font-montserrat ${editMode ? "block" : "hidden"} `}> |
||||
|
Nom de chaine |
||||
|
</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
id="name" |
||||
|
value={channelName || channelName === "" ? channelName : channel ? channel.name : "Chargement"} |
||||
|
className={(editMode ? editModeClasses : nonEditModeClasses)} |
||||
|
onChange={(e) => setChannelName(e.target.value)} |
||||
|
placeholder="Nom d'utilisateur" |
||||
|
disabled={!editMode} |
||||
|
/> |
||||
|
|
||||
|
<label htmlFor="name" className={`text-2xl text-white mb-1 block font-montserrat`}> |
||||
|
Description |
||||
|
</label> |
||||
|
<textarea |
||||
|
name="description" |
||||
|
id="" |
||||
|
className={(editMode ? editModeClassesTextArea : nonEditModeClassesTextArea)} |
||||
|
value={description || description === "" ? description : channel ? channel.description : "Chargement"} |
||||
|
onChange={(e) => setDescription(e.target.value)} |
||||
|
placeholder="Description de votre chaine" |
||||
|
disabled={!editMode} |
||||
|
></textarea> |
||||
|
|
||||
|
{ |
||||
|
editMode ? ( |
||||
|
<div className="mt-4"> |
||||
|
<button |
||||
|
type="button" |
||||
|
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer" |
||||
|
onClick={handleUpdateChannel} |
||||
|
> |
||||
|
Enregistrer |
||||
|
</button> |
||||
|
<button |
||||
|
type="button" |
||||
|
className="bg-red-500 p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer ml-3" |
||||
|
onClick={() => setEditMode(!editMode)} |
||||
|
> |
||||
|
Annuler |
||||
|
</button> |
||||
|
</div> |
||||
|
) : ( |
||||
|
<button |
||||
|
type="button" |
||||
|
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer mt-4" |
||||
|
onClick={() => setEditMode(!editMode)} |
||||
|
> |
||||
|
Modifier |
||||
|
</button> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
</form> |
||||
|
|
||||
|
{/* RIGHT SIDE */} |
||||
|
<div className="w-2/3 h-screen pl-10" > |
||||
|
{/* VIEW / SUBSCRIBERS STATS */} |
||||
|
|
||||
|
<div className="flex gap-4" > |
||||
|
<div className="glassmorphism flex-1 h-32 flex flex-col justify-center items-center" > |
||||
|
{/* TOTAL VIEWS */} |
||||
|
<p className="text-white text-xl font-montserrat font-semibold" >Vues totales</p> |
||||
|
<p className="text-white text-2xl font-montserrat font-bold" >{channelStats ? channelStats.views : "0"}</p> |
||||
|
</div> |
||||
|
<div className="glassmorphism flex-1 h-32 flex flex-col justify-center items-center" > |
||||
|
{/* TOTAL SUBSCRIBERS */} |
||||
|
<p className="text-white text-xl font-montserrat font-semibold" >Abonnés</p> |
||||
|
<p className="text-white text-2xl font-montserrat font-bold" >{channelStats ? channelStats.subscribers : "0"}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{/* VIDEOS */} |
||||
|
<div className="flex justify-between"> |
||||
|
<h2 className="text-white text-3xl font-montserrat font-bold mt-10" >Vidéos</h2> |
||||
|
<button |
||||
|
className="bg-primary px-2 py-1 rounded-sm text-white font-montserrat text-md font-semibold cursor-pointer mt-4" |
||||
|
onClick={() => navigate("/add-video")} |
||||
|
> |
||||
|
Ajouter une vidéo |
||||
|
</button> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
{ channel?.videos?.length > 0 ? ( |
||||
|
<div className="flex flex-col gap-4 mt-5"> |
||||
|
{channel.videos.map((video) => ( |
||||
|
<VideoStatListElement |
||||
|
video={video} |
||||
|
onClick={() => navigate("/manage-video/" + video.id)} |
||||
|
key={video.id} |
||||
|
/> |
||||
|
))} |
||||
|
</div> |
||||
|
) : ( |
||||
|
<p className="text-white text-xl font-montserrat mt-4">Aucune vidéo trouvée pour cette chaîne.</p> |
||||
|
)} |
||||
|
|
||||
|
|
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
</main> |
||||
|
|
||||
|
</div> |
||||
|
) |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,417 @@ |
|||||
|
import Navbar from "../components/Navbar.jsx"; |
||||
|
import {useParams} from "react-router-dom"; |
||||
|
import {useAuth} from "../contexts/AuthContext.jsx"; |
||||
|
import {useEffect, useState} from "react"; |
||||
|
import LinearGraph from "../components/LinearGraph.jsx"; |
||||
|
import Comment from "../components/Comment.jsx"; |
||||
|
import Tag from "../components/Tag.jsx"; |
||||
|
|
||||
|
|
||||
|
export default function ManageVideo() { |
||||
|
|
||||
|
const { user } = useAuth(); |
||||
|
const token = localStorage.getItem("token"); |
||||
|
const {id} = useParams(); |
||||
|
|
||||
|
const [video, setVideo] = useState(null); |
||||
|
const [likesPerDay, setLikesPerDay] = useState([]); |
||||
|
const [viewsPerDay, setViewsPerDay] = useState([]); |
||||
|
const [videoTitle, setVideoTitle] = useState(null); |
||||
|
const [description, setDescription] = useState(null); |
||||
|
const [visibility, setVisibility] = useState("private"); |
||||
|
const [editMode, setEditMode] = useState(false); |
||||
|
const [thumbnailPreview, setThumbnailPreview] = useState(null); |
||||
|
const [videoFile, setVideoFile] = useState(null); |
||||
|
|
||||
|
const nonEditModeClasses = "text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none"; |
||||
|
const editModeClasses = nonEditModeClasses + " glassmorphism"; |
||||
|
|
||||
|
const nonEditModeClassesTextArea = "text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none w-full" |
||||
|
const editModeClassesTextArea = nonEditModeClassesTextArea + " glassmorphism h-48"; |
||||
|
|
||||
|
const fetchVideo = async () => { |
||||
|
const request = await fetch(`/api/videos/${id}`, { |
||||
|
method: 'GET', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
} |
||||
|
}); |
||||
|
if (!request.ok) { |
||||
|
throw new Error("Failed to fetch video"); |
||||
|
} |
||||
|
const data = await request.json(); |
||||
|
setVideo(data); |
||||
|
setVisibility(data.visibility) |
||||
|
setVideoTitle(data.title); |
||||
|
setDescription(data.description); |
||||
|
} |
||||
|
const fetchLikesPerDay = async () => { |
||||
|
const request = await fetch(`/api/videos/${id}/likes/day`, { |
||||
|
method: 'GET', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
} |
||||
|
}); |
||||
|
if (!request.ok) { |
||||
|
throw new Error("Failed to fetch likes per day"); |
||||
|
} |
||||
|
const data = await request.json(); |
||||
|
setLikesPerDay(data.likes); |
||||
|
setViewsPerDay(data.views); |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (user) { |
||||
|
fetchVideo() |
||||
|
fetchLikesPerDay() |
||||
|
} |
||||
|
}, [user, id, token]); |
||||
|
|
||||
|
const onSubmit = async (e) => { |
||||
|
e.preventDefault(); |
||||
|
if (!editMode) return; |
||||
|
|
||||
|
const request = await fetch(`/api/videos/${id}`, { |
||||
|
method: 'PUT', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
title: videoTitle, |
||||
|
description: description, |
||||
|
visibility: visibility, |
||||
|
channel: video.channel |
||||
|
}) |
||||
|
}); |
||||
|
|
||||
|
if (!request.ok) { |
||||
|
console.error("Failed to update video"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const form = new FormData(); |
||||
|
if (videoFile) { |
||||
|
form.append('file', videoFile); |
||||
|
form.append('video', id); |
||||
|
form.append('channel', video.channel); |
||||
|
|
||||
|
const videoRequest = await fetch(`/api/videos/${id}/video`, { |
||||
|
method: 'PUT', |
||||
|
headers: { |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
}, |
||||
|
body: form |
||||
|
}); |
||||
|
|
||||
|
if (!videoRequest.ok) { |
||||
|
console.error("Failed to update video file"); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const data = await request.json(); |
||||
|
setVideo(data); |
||||
|
setEditMode(false); |
||||
|
} |
||||
|
|
||||
|
const handleThumbnailChange = async (e) => { |
||||
|
const file = e.target.files[0]; |
||||
|
if (file) { |
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (e) => { |
||||
|
setThumbnailPreview(e.target.result); |
||||
|
}; |
||||
|
reader.readAsDataURL(file); |
||||
|
} |
||||
|
|
||||
|
const formData = new FormData(); |
||||
|
formData.append('file', file); |
||||
|
formData.append('video', id); |
||||
|
formData.append('channel', video.channel); |
||||
|
|
||||
|
|
||||
|
|
||||
|
const request = await fetch(`/api/videos/thumbnail`, { |
||||
|
"method": 'POST', |
||||
|
"headers": { |
||||
|
"Authorization": `Bearer ${token}` |
||||
|
}, |
||||
|
body: formData |
||||
|
}) |
||||
|
if (!request.ok) { |
||||
|
console.error("Failed to upload thumbnail"); |
||||
|
return; |
||||
|
} |
||||
|
const data = await request.json(); |
||||
|
}; |
||||
|
|
||||
|
const onAddTag = async (e) => { |
||||
|
if (e.key !== 'Enter' || e.target.value.trim() === "") return; |
||||
|
|
||||
|
const newTag = e.target.value.trim(); |
||||
|
e.target.value = ""; |
||||
|
|
||||
|
const request = await fetch(`/api/videos/${id}/tags`, { |
||||
|
method: 'PUT', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
tags: [...video.tags, newTag], |
||||
|
channel: video.channel |
||||
|
}) |
||||
|
}); |
||||
|
if (!request.ok) { |
||||
|
console.error("Failed to add tag"); |
||||
|
return; |
||||
|
} |
||||
|
const data = await request.json(); |
||||
|
setVideo({ |
||||
|
...video, |
||||
|
tags: [...video.tags, newTag] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
const onSuppressTag = async (tag) => { |
||||
|
|
||||
|
const request = await fetch(`/api/videos/${id}/tags`, { |
||||
|
method: 'PUT', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization': `Bearer ${token}` |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
tags: video.tags.filter(t => t !== tag), |
||||
|
channel: video.channel |
||||
|
}) |
||||
|
}); |
||||
|
if (!request.ok) { |
||||
|
console.error("Failed to suppress tag"); |
||||
|
return; |
||||
|
} |
||||
|
const data = await request.json(); |
||||
|
const newTags = video.tags.filter(t => t !== tag); |
||||
|
setVideo({ |
||||
|
...video, |
||||
|
tags: newTags |
||||
|
}); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient"> |
||||
|
|
||||
|
<Navbar/> |
||||
|
|
||||
|
<main className="px-36 pb-36"> |
||||
|
|
||||
|
{ /* GRAPHS */ } |
||||
|
<div> |
||||
|
<div className="flex pt-[118px] gap-4" > |
||||
|
<LinearGraph |
||||
|
dataToGraph={viewsPerDay} |
||||
|
legend="Vues" |
||||
|
className="glassmorphism flex-1 h-[300px] p-4" |
||||
|
/> |
||||
|
<LinearGraph |
||||
|
dataToGraph={likesPerDay} |
||||
|
legend="Likes" |
||||
|
className="glassmorphism flex-1 h-[300px] p-4" |
||||
|
borderColor="#FF073A" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="flex gap-4 mt-4 "> |
||||
|
|
||||
|
{ /* LEFT SIDE */ } |
||||
|
<div className="flex-1" > |
||||
|
{ /* THUMBNAIL */ } |
||||
|
<label htmlFor="thumbnail" className="glassmorphism flex justify-center items-center py-5 relative overflow-hidden "> |
||||
|
<img src={thumbnailPreview || (video ? video.thumbnail : "")} alt="" className=" rounded-sm"/> |
||||
|
|
||||
|
<div className="absolute top-0 left-0 bg-[#00000080] w-full h-full flex justify-center items-center opacity-0 hover:opacity-100 transition" > |
||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" className="fill-white" viewBox="0 0 24 24"> |
||||
|
<path d="M19.045 7.401c.378-.378.586-.88.586-1.414s-.208-1.036-.586-1.414l-1.586-1.586c-.378-.378-.88-.586-1.414-.586s-1.036.208-1.413.585L4 13.585V18h4.413L19.045 7.401zm-3-3 1.587 1.585-1.59 1.584-1.586-1.585 1.589-1.584zM6 16v-1.585l7.04-7.018 1.586 1.586L7.587 16H6zm-2 4h16v2H4z"></path> |
||||
|
</svg> |
||||
|
</div> |
||||
|
|
||||
|
</label> |
||||
|
<input type="file" name="thumbnail" id="thumbnail" className="opacity-0 w-0 h-0 hidden" onChange={handleThumbnailChange} accept="image/*"/> |
||||
|
|
||||
|
{ /* VIDEO INFOS */ } |
||||
|
|
||||
|
<form className="glassmorphism p-4 mt-4 flex-1" > |
||||
|
<label htmlFor="name" className={`text-2xl text-white mb-1 block font-montserrat`}> |
||||
|
Titre de la vidéo |
||||
|
</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
id="name" |
||||
|
value={videoTitle || videoTitle === "" ? videoTitle : video ? video.title : "Chargement"} |
||||
|
onChange={(e) => setVideoTitle(e.target.value)} |
||||
|
className={(editMode ? editModeClasses : nonEditModeClasses) + " text-xl"} |
||||
|
placeholder="Titre de la vidéo" |
||||
|
disabled={!editMode} |
||||
|
/> |
||||
|
|
||||
|
<label htmlFor="name" className={`text-2xl text-white mb-1 block font-montserrat`}> |
||||
|
Description |
||||
|
</label> |
||||
|
<textarea |
||||
|
name="description" |
||||
|
id="" |
||||
|
className={(editMode ? editModeClassesTextArea : nonEditModeClassesTextArea)} |
||||
|
value={description || description === "" ? description : video ? video.description : "Chargement"} |
||||
|
onChange={(e) => setDescription(e.target.value)} |
||||
|
placeholder="Description de votre chaine" |
||||
|
disabled={!editMode} |
||||
|
></textarea> |
||||
|
|
||||
|
<label htmlFor="visibility" className="text-2xl text-white mb-1 block font-montserrat"> |
||||
|
Visibilité |
||||
|
</label> |
||||
|
<select |
||||
|
className={(editMode ? editModeClasses : nonEditModeClasses)} |
||||
|
value={visibility} |
||||
|
name="visibility" |
||||
|
onChange={(e) => setVisibility(e.target.value)} |
||||
|
disabled={!editMode} |
||||
|
> |
||||
|
<option value="public">Publique</option> |
||||
|
<option value="private">Privée</option> |
||||
|
</select> |
||||
|
|
||||
|
<label htmlFor="video" className={`flex gap-2 glassmorphism p-2 items-center mt-4 cursor-pointer ${editMode ? "block" : "hidden"}`}> |
||||
|
<video src={video ? video.file : ""} className="w-1/8 aspect-video rounded-sm" ></video> |
||||
|
<p className="text-2xl text-white mb-1 block font-montserrat">Fichier vidéo</p> |
||||
|
</label> |
||||
|
<input |
||||
|
type="file" |
||||
|
name="video" |
||||
|
id="video" |
||||
|
className="hidden" |
||||
|
accept="video/*" |
||||
|
onChange={(e) => setVideoFile(e.target.files[0])} |
||||
|
/> |
||||
|
|
||||
|
{ |
||||
|
editMode ? ( |
||||
|
<div className="mt-4"> |
||||
|
<button |
||||
|
type="button" |
||||
|
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer" |
||||
|
onClick={(e) => {onSubmit(e)}} |
||||
|
> |
||||
|
Enregistrer |
||||
|
</button> |
||||
|
<button |
||||
|
type="button" |
||||
|
className="bg-red-500 p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer ml-3" |
||||
|
onClick={() => setEditMode(!editMode)} |
||||
|
> |
||||
|
Annuler |
||||
|
</button> |
||||
|
</div> |
||||
|
) : ( |
||||
|
<button |
||||
|
type="button" |
||||
|
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer mt-4" |
||||
|
onClick={() => setEditMode(!editMode)} |
||||
|
> |
||||
|
Modifier |
||||
|
</button> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
</form> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
{ /* RIGHT SIDE */ } |
||||
|
<div className="flex-1"> |
||||
|
|
||||
|
<div className="flex gap-4"> |
||||
|
{ /* TOTAL VIEWS */ } |
||||
|
<div className="glassmorphism py-16 flex-1"> |
||||
|
<p className="text-2xl font-bold text-white mb-2 text-center">{video ? video.views : 0} vues</p> |
||||
|
</div> |
||||
|
|
||||
|
{ /* TOTAL LIKES */ } |
||||
|
<div className="glassmorphism py-16 flex-1"> |
||||
|
<p className="text-2xl font-bold text-white mb-2 text-center">{video ? video.likes : 0} likes</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{ /* COMMENTS */ } |
||||
|
<div className="glassmorphism p-4 mt-4 h-[500px] overflow-y-auto"> |
||||
|
|
||||
|
<h2 className="text-2xl font-bold text-white mb-4">Commentaires</h2> |
||||
|
|
||||
|
{video && video.comments && video.comments.length > 0 ? ( |
||||
|
video.comments.map((comment) => ( |
||||
|
<Comment comment={comment} doShowCommands={false} /> |
||||
|
)) |
||||
|
) : ( |
||||
|
<p className="text-gray-500">Aucun commentaire</p> |
||||
|
)} |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
{ /* TAGS */ } |
||||
|
<div className="glassmorphism p-4 mt-4"> |
||||
|
<h2 className="text-2xl font-bold text-white mb-4">Tags</h2> |
||||
|
<div className="flex flex-wrap gap-2"> |
||||
|
{ video && video.tags && video.tags.length > 0 ? ( |
||||
|
video.tags.map((tag) => ( |
||||
|
<Tag tag={tag} key={tag} onSuppress={() => onSuppressTag(tag)} /> |
||||
|
)) |
||||
|
) : ( |
||||
|
<p className="text-gray-500">Aucun tag</p> |
||||
|
)} |
||||
|
</div> |
||||
|
<input |
||||
|
type="text" |
||||
|
className="glassmorphism text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none mt-4" |
||||
|
placeholder="Ajouter un tag" |
||||
|
onKeyPress={(e) => onAddTag(e)} |
||||
|
/> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
{ /* LINK */ } |
||||
|
|
||||
|
<div className="glassmorphism p-4 mt-4"> |
||||
|
|
||||
|
<h2 className="text-2xl font-bold text-white mb-4">Lien de la vidéo</h2> |
||||
|
<p className="text-md font-normal text-white"> |
||||
|
<a href={`/video/${id}`} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline"> |
||||
|
{window.location.origin}/video/{id} |
||||
|
</a> |
||||
|
<button |
||||
|
className="ml-2 bg-primary text-white p-2 rounded-sm cursor-pointer" |
||||
|
onClick={() => { |
||||
|
navigator.clipboard.writeText(`${window.location.origin}/video/${id}`); |
||||
|
}} |
||||
|
> |
||||
|
Copier |
||||
|
</button> |
||||
|
</p> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
</main> |
||||
|
</div> |
||||
|
) |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
-----BEGIN CERTIFICATE----- |
||||
|
MIID5zCCAs+gAwIBAgIUXzNzqa/12lyIcoxXf+v371J3fWkwDQYJKoZIhvcNAQEL |
||||
|
BQAwgYIxCzAJBgNVBAYTAkZSMREwDwYDVQQIDAhOb3JtYW5keTENMAsGA1UEBwwE |
||||
|
Q2FlbjERMA8GA1UECgwIRnJhbWluZm8xFTATBgNVBAMMDFNhY2hhIEdVRVJJTjEn |
||||
|
MCUGCSqGSIb3DQEJARYYc2FjaGEuZ3VlcmluQHN1cGluZm8uY29tMB4XDTI1MDcy |
||||
|
MTEzMzgwMVoXDTI2MDcyMTEzMzgwMVowgYIxCzAJBgNVBAYTAkZSMREwDwYDVQQI |
||||
|
DAhOb3JtYW5keTENMAsGA1UEBwwEQ2FlbjERMA8GA1UECgwIRnJhbWluZm8xFTAT |
||||
|
BgNVBAMMDFNhY2hhIEdVRVJJTjEnMCUGCSqGSIb3DQEJARYYc2FjaGEuZ3Vlcmlu |
||||
|
QHN1cGluZm8uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvLg7 |
||||
|
nR0UqRZ7UadhI8jrUjRMV1SZj+ljxEnV6tDOVMsvafsym1MhDZHb+cyv8769yqPv |
||||
|
CKtIOQKhMH0PkSqau8szNlF1Tg/1UzT+Mkd4zvLvGE5+aW/oDMg7E2LMJZuCyO4X |
||||
|
9SzWDVA5+b1QFIw6vvb3mCkUOtVDkOFreBBwryZKcWJ0b8o1hT60oB2wr18P14j0 |
||||
|
0C2/TmHMtim0o4r3gKGvpatqt1fXJo0UlYOwTvfMrYhu2VHqsQ2qP7ocazXEWt5u |
||||
|
Alf1vNPkAenF0ZV/2UiaL41Q8GMoV1enDP7k7/qfgXvta/hOeYnLtmv5Qpi4XiWz |
||||
|
xKjSukTUD2sRtSX+YQIDAQABo1MwUTAdBgNVHQ4EFgQUVj9KtmjLFy4xWzkNI9Kq |
||||
|
NAxNsfUwHwYDVR0jBBgwFoAUVj9KtmjLFy4xWzkNI9KqNAxNsfUwDwYDVR0TAQH/ |
||||
|
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAGpUPMoF/ASIqOOfX5anDtaPvnslj |
||||
|
DuEVbU7Uoyc4EuSD343DPV7iMUKoFvVLPxJJqMLhSo3aEGJyqOF6q3fvq/vX2VE7 |
||||
|
9MhwS1t2DBGb5foWRosnT1EuqFU1/S0RJ/Y+GNcoY1PrUES4+r7zqqJJjwKOzneV |
||||
|
ktUVCdKl0C1gtw6W4Ajxse3fm9DNLxnZZXbyNqn+KbI8QdO0xSEl+gyiycvPu/NT |
||||
|
+EesdlFoYjO7gdA8dXkmu+Z7R61MYhE9Zvyop5KVMqgU8/Ym04UUWjWQYWWLMyuu |
||||
|
bxngE4XNEI5fhg+0e/I25xJJ9wVV/ZNAF4+XOylHz/CmU8V/SPKuGXBGHg== |
||||
|
-----END CERTIFICATE----- |
||||
@ -0,0 +1,28 @@ |
|||||
|
-----BEGIN PRIVATE KEY----- |
||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC8uDudHRSpFntR |
||||
|
p2EjyOtSNExXVJmP6WPESdXq0M5Uyy9p+zKbUyENkdv5zK/zvr3Ko+8Iq0g5AqEw |
||||
|
fQ+RKpq7yzM2UXVOD/VTNP4yR3jO8u8YTn5pb+gMyDsTYswlm4LI7hf1LNYNUDn5 |
||||
|
vVAUjDq+9veYKRQ61UOQ4Wt4EHCvJkpxYnRvyjWFPrSgHbCvXw/XiPTQLb9OYcy2 |
||||
|
KbSjiveAoa+lq2q3V9cmjRSVg7BO98ytiG7ZUeqxDao/uhxrNcRa3m4CV/W80+QB |
||||
|
6cXRlX/ZSJovjVDwYyhXV6cM/uTv+p+Be+1r+E55icu2a/lCmLheJbPEqNK6RNQP |
||||
|
axG1Jf5hAgMBAAECggEAAj+hmDRx6jafAAf67sqi3ZgEGEmBkXNeeLGBTPc/qhxd |
||||
|
ip6krTELnz8TE26RG5LYXzslasUNrn42nIImvBT5ZkcjcosKpWfEqQEAjc1PQovC |
||||
|
9eyKnKfw4TpUvvmiveT4T98vCYEOOqHE0/WTdlOoaBY/f+sZKQYu+1NMtAjFcg2r |
||||
|
vVqwsZb5vGyh7CKmIHZnz3UP8P+7G5digiNRne18pGnE2oTnSoQ3/QIqUWBs69DS |
||||
|
k5ew+CSyTLiUFFnMnE4adwyg6wAud5fBlzowF6UF2agToX7pxEaGxGvpBGG034kk |
||||
|
1UXaB/d5YwcsBeH+x5cNMLKZy4zqjoxEEW31Q466NQKBgQDtKk1R/slpTpRqvtBT |
||||
|
NC7InvjcCBXkXttylQHJRN9glqhmflEOe8iMW1/qRwBPlQgK1wq/sXySanVv2+gO |
||||
|
JGq8XNRLbHyG3YRyshdnJHP1HoWQE0uedD/rfqgkNaW5S1IvHrD7Q7tOvCrF+KbS |
||||
|
612pmIgNVzn+inafDXPhMZc4pQKBgQDLtQGAu2eK58ewndyL8+7+VHZSTEtKpt+h |
||||
|
G/U/ccv+6NGqdxI5YUkrJ7k6vV81VeRMvmN9uUS/i8znORFQmm6noRVkhXytwW5B |
||||
|
HXq2co4WRvv9b/XqcqS0GSYVPJ1u4YNH6lvtDZ4UWPyBzYl700GdHrGa+erT44yL |
||||
|
tnibHx9GDQKBgFW1J+Qt85O+9hvtgVPQU+fkq4K42VCCh0PNXavi2+cICyufEqPt |
||||
|
T/iJPQxpRE9+SD3CoPvNpHs1ReN60U3rEzenRIFNX2NNwoPAoHyBy/YVZac/keBd |
||||
|
mov8Zb9QM+fWtIiaytLDE3nMvph017T5ogucN+66SxcV6vBn6CzFwySRAoGAcUf2 |
||||
|
Tv1ohkGAtgIDrLx5cmvL5NZSpHAKOpDOoHqLA/W66v4OX2RviRUtF7JJ6OIb9GWH |
||||
|
9Fl8Fr0KtKbyrw1CbevRdrYY8JN52bIoFJ+9zjupVHXXnookd5boq7SqpAe6ttpo |
||||
|
RnplJ1GZEiIXy4lemp6AC/vhD/YhqWxOw4zaGl0CgYBslhqVt5F0EHf94p7NrCuY |
||||
|
hNHKHaNaULYP0VXKefQamt/ssDuktqb6DNSIvx2rbbB5+33nTlLTya67gimY1lKt |
||||
|
WeNB33/yBkCjfSP/J5UDD9mE/oPLt3vAOkOUgMCfp2IpC2Wez1QGqLHS260zpotP |
||||
|
VpgalHuSWtn8D4nO2pk1hg== |
||||
|
-----END PRIVATE KEY----- |
||||
Loading…
Reference in new issue