committed by
GitHub
10 changed files with 271 additions and 12 deletions
@ -0,0 +1,90 @@ |
|||||
|
import {getClient} from "../utils/database.js"; |
||||
|
|
||||
|
export async function search(req, res) { |
||||
|
try { |
||||
|
console.log(req.query); |
||||
|
const query = req.query.q; |
||||
|
const type = req.query.type || 'all'; |
||||
|
const offset = req.query.offset || 0; |
||||
|
const limit = req.query.limit || 20; |
||||
|
const client = await getClient(); |
||||
|
|
||||
|
|
||||
|
if (!query) { |
||||
|
return res.status(400).json({ message: "Query parameter 'q' is required" }); |
||||
|
} |
||||
|
|
||||
|
if (type === 'videos') { |
||||
|
// Search video in database based on the query, video title, tags and author
|
||||
|
const videoNameQuery = `SELECT id FROM videos WHERE title ILIKE $1 OFFSET $3 LIMIT $2`; |
||||
|
const videoNameResult = await client.query(videoNameQuery, [`%${query}%`, limit, offset]); |
||||
|
|
||||
|
// Search video from tags
|
||||
|
const tagQuery = `SELECT id FROM tags WHERE name ILIKE $1 OFFSET $3 LIMIT $2`; |
||||
|
const tagResult = await client.query(tagQuery, [`%${query}%`, limit, offset]); |
||||
|
const tags = tagResult.rows.map(tag => tag.name); |
||||
|
|
||||
|
for (const tag of tags) { |
||||
|
const videoTagQuery = `SELECT id FROM videos WHERE id IN (SELECT video FROM video_tags WHERE tag = (SELECT id FROM tags WHERE name = $1)) OFFSET $3 LIMIT $2`; |
||||
|
const videoTagResult = await client.query(videoTagQuery, [tag, limit, offset]); |
||||
|
videoNameResult.rows.push(...videoTagResult.rows); |
||||
|
} |
||||
|
|
||||
|
// Search video from author
|
||||
|
const authorQuery = `SELECT videos.id FROM videos JOIN channels c ON videos.channel = c.id WHERE c.name ILIKE $1`; |
||||
|
const authorResult = await client.query(authorQuery, [`%${query}%`]); |
||||
|
|
||||
|
for (const author of authorResult.rows) { |
||||
|
if (!videoNameResult.rows.some(video => video.id === author.id)) { |
||||
|
videoNameResult.rows.push(author); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const videos = []; |
||||
|
|
||||
|
for (let video of videoNameResult.rows) { |
||||
|
video = video.id; // Extracting the video ID
|
||||
|
let videoDetails = {}; |
||||
|
|
||||
|
// Fetching video details
|
||||
|
const videoDetailsQuery = `SELECT id, title, description, thumbnail, channel, release_date FROM videos WHERE id = $1`; |
||||
|
const videoDetailsResult = await client.query(videoDetailsQuery, [video]); |
||||
|
if (videoDetailsResult.rows.length === 0) { |
||||
|
continue; // Skip if no video details found
|
||||
|
} |
||||
|
|
||||
|
videoDetails = videoDetailsResult.rows[0]; |
||||
|
// Setting the type
|
||||
|
videoDetails.type = 'video'; |
||||
|
|
||||
|
// Fetching views and likes
|
||||
|
const viewsQuery = `SELECT COUNT(*) AS view_count FROM history WHERE video = $1`; |
||||
|
const viewsResult = await client.query(viewsQuery, [video]); |
||||
|
videoDetails.views = viewsResult.rows[0].view_count; |
||||
|
|
||||
|
// GET CREATOR
|
||||
|
const creatorQuery = `SELECT c.id, c.name, c.owner FROM channels c JOIN videos v ON c.id = v.channel WHERE v.id = $1`; |
||||
|
const creatorResult = await client.query(creatorQuery, [video]); |
||||
|
videoDetails.creator = creatorResult.rows[0]; |
||||
|
|
||||
|
// GET CREATOR PROFILE PICTURE
|
||||
|
const profilePictureQuery = `SELECT picture FROM users WHERE id = $1`; |
||||
|
const profilePictureResult = await client.query(profilePictureQuery, [videoDetails.creator.owner]); |
||||
|
videoDetails.creator.profile_picture = profilePictureResult.rows[0].picture; |
||||
|
|
||||
|
videos.push(videoDetails); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
return res.status(200).json(videos); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error("Error in search controller:", error); |
||||
|
res.status(500).json({ message: "Internal server error" }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
import { Router } from 'express'; |
||||
|
import {search} from "../controllers/search.controller.js"; |
||||
|
|
||||
|
const router = Router(); |
||||
|
|
||||
|
router.get('/', search) |
||||
|
|
||||
|
export default router; |
||||
|
After Width: | Height: | Size: 441 B |
@ -0,0 +1,142 @@ |
|||||
|
import Navbar from "../components/Navbar.jsx"; |
||||
|
import {useState} from "react"; |
||||
|
|
||||
|
|
||||
|
export default function Account() { |
||||
|
|
||||
|
let user = JSON.parse(localStorage.getItem("user")) || {}; |
||||
|
|
||||
|
const [username, setUsername] = useState(user.username || ""); |
||||
|
const [email, setEmail] = useState(user.email || ""); |
||||
|
const [password, setPassword] = useState(""); |
||||
|
const [confirmPassword, setConfirmPassword] = useState(""); |
||||
|
const [isPictureEditActive, setIsPictureEditActive] = useState(false); |
||||
|
|
||||
|
const [userChannel, setUserChannel] = useState(null); |
||||
|
|
||||
|
const fetchUserChannel = async () => { |
||||
|
try { |
||||
|
const response = await fetch(`/api/channels/${user.id}`); |
||||
|
if (!response.ok) { |
||||
|
throw new Error("Failed to fetch user data"); |
||||
|
} |
||||
|
const data = await response.json(); |
||||
|
setUserChannel(data); |
||||
|
} catch (error) { |
||||
|
console.error("Error fetching user channel:", error); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
const [editMode, setEditMode] = useState(false); |
||||
|
|
||||
|
const nonEditModeClasses = "text-2xl font-bold text-white p-2 focus:text-white focus:outline-none w-full font-montserrat"; |
||||
|
const editModeClasses = nonEditModeClasses + " glassmorphism"; |
||||
|
|
||||
|
return ( |
||||
|
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient"> |
||||
|
<Navbar/> |
||||
|
|
||||
|
<main className="px-36 pt-[118px]"> |
||||
|
{/* Left side */} |
||||
|
|
||||
|
{/* Profile / Edit profile */} |
||||
|
<form className="glassmorphism w-1/3 p-10"> |
||||
|
<div className="relative w-1/3 aspect-square overflow-hidden mb-3 mx-auto" onMouseEnter={() => setIsPictureEditActive(true)} onMouseLeave={() => setIsPictureEditActive(false)} > |
||||
|
<label htmlFor="image"> |
||||
|
<img |
||||
|
src={user.picture} |
||||
|
className="w-full aspect-square rounded-full object-cover" |
||||
|
/> |
||||
|
<div className={`absolute w-full h-full bg-[#000000EF] flex items-center justify-center top-0 left-0 rounded-full ${(isPictureEditActive && editMode) ? "opacity-100 cursor-pointer" : "opacity-0 cursor-default"} ` } > |
||||
|
<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" accept="image/*" id="image" className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" disabled={!editMode}/> |
||||
|
</div> |
||||
|
<label htmlFor="name" className="text-2xl text-white mb-1 block font-montserrat"> |
||||
|
Nom d'utilisateur |
||||
|
</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
id="name" |
||||
|
value={username} |
||||
|
className={(editMode ? editModeClasses : nonEditModeClasses)} |
||||
|
onChange={(e) => setUsername(e.target.value)} |
||||
|
placeholder="Nom d'utilisateur" |
||||
|
disabled={!editMode} |
||||
|
/> |
||||
|
|
||||
|
<label htmlFor="email" className="text-2xl text-white mb-1 mt-4 block font-montserrat"> |
||||
|
Adresse e-mail |
||||
|
</label> |
||||
|
<input |
||||
|
type="email" |
||||
|
id="email" |
||||
|
value={email} |
||||
|
className={(editMode ? editModeClasses : nonEditModeClasses)} |
||||
|
onChange={(e) => setEmail(e.target.value)} |
||||
|
placeholder="Adresse mail" |
||||
|
disabled={!editMode} |
||||
|
/> |
||||
|
|
||||
|
{ editMode && ( |
||||
|
<> |
||||
|
<label htmlFor="password" className="text-2xl text-white mb-1 mt-4 block font-montserrat"> |
||||
|
Mot de passe |
||||
|
</label> |
||||
|
<input |
||||
|
type="password" |
||||
|
id="password" |
||||
|
value={password} |
||||
|
className={(editMode ? editModeClasses : nonEditModeClasses)} |
||||
|
onChange={(e) => setPassword(e.target.value)} |
||||
|
placeholder="**************" |
||||
|
disabled={!editMode} |
||||
|
/> |
||||
|
|
||||
|
<label htmlFor="confirm-password" className="text-2xl text-white mb-1 mt-4 block font-montserrat"> |
||||
|
Confirmer le mot de passe |
||||
|
</label> |
||||
|
<input |
||||
|
type="password" |
||||
|
id="confirm-password" |
||||
|
value={confirmPassword} |
||||
|
className={(editMode ? editModeClasses : nonEditModeClasses)} |
||||
|
onChange={(e) => setConfirmPassword(e.target.value)} |
||||
|
placeholder="" |
||||
|
disabled={!editMode} |
||||
|
/> |
||||
|
</> |
||||
|
) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
<div className="flex justify-center mt-5"> |
||||
|
<button |
||||
|
type="button" |
||||
|
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer" |
||||
|
onClick={() => setEditMode(!editMode)} |
||||
|
> |
||||
|
{editMode ? "Enregistrer" : "Modifier le profil"} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
|
||||
|
{ /* Right side */} |
||||
|
|
||||
|
{/* Channel */} |
||||
|
|
||||
|
{/* Playlists */} |
||||
|
|
||||
|
{/* History */} |
||||
|
|
||||
|
</main> |
||||
|
|
||||
|
</div> |
||||
|
) |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue