Browse Source

Merge pull request #3 from Astri4-4/developpement

Add profile page
fix/clean
Sacha GUERIN 5 months ago
committed by GitHub
parent
commit
f79b681df8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 90
      backend/app/controllers/search.controller.js
  2. 8
      backend/app/routes/search.route.js
  3. 5
      backend/requests/video.http
  4. 2
      backend/server.js
  5. 1
      frontend/src/assets/svg/edit.svg
  6. 20
      frontend/src/components/Navbar.jsx
  7. 4
      frontend/src/components/ProtectedRoute.jsx
  8. 142
      frontend/src/pages/Account.jsx
  9. 2
      frontend/src/pages/Video.jsx
  10. 9
      frontend/src/routes/routes.jsx

90
backend/app/controllers/search.controller.js

@ -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" });
}
}

8
backend/app/routes/search.route.js

@ -0,0 +1,8 @@
import { Router } from 'express';
import {search} from "../controllers/search.controller.js";
const router = Router();
router.get('/', search)
export default router;

5
backend/requests/video.http

@ -27,4 +27,7 @@ Authorization: Bearer {{token}}
"Redstone" "Redstone"
], ],
"channel": 2 "channel": 2
} }
###
GET http://localhost/api/search?q=minecraft&type=videos&offset=0&limit=10

2
backend/server.js

@ -10,6 +10,7 @@ import cors from "cors";
import PlaylistRoute from "./app/routes/playlist.route.js"; import PlaylistRoute from "./app/routes/playlist.route.js";
import {initDb} from "./app/utils/database.js"; import {initDb} from "./app/utils/database.js";
import MediaRoutes from "./app/routes/media.routes.js"; import MediaRoutes from "./app/routes/media.routes.js";
import SearchRoute from "./app/routes/search.route.js";
console.clear(); console.clear();
dotenv.config(); dotenv.config();
@ -32,6 +33,7 @@ app.use("/api/comments/", CommentRoute);
app.use("/api/playlists", PlaylistRoute); app.use("/api/playlists", PlaylistRoute);
app.use("/api/recommendations", RecommendationRoute); app.use("/api/recommendations", RecommendationRoute);
app.use("/api/media", MediaRoutes); app.use("/api/media", MediaRoutes);
app.use("/api/search", SearchRoute);
const port = process.env.PORT; const port = process.env.PORT;

1
frontend/src/assets/svg/edit.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="fill: rgba(255, 255, 255, 1);transform: ;msFilter:;"><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>

After

Width:  |  Height:  |  Size: 441 B

20
frontend/src/components/Navbar.jsx

@ -19,15 +19,17 @@ export default function Navbar({ isSearchPage = false }) {
{isAuthenticated ? ( {isAuthenticated ? (
<> <>
<li><a href="/">Abonnements</a></li> <li><a href="/">Abonnements</a></li>
<li className="flex items-center space-x-4"> <li>
<span className="text-2xl">{user?.username}</span> <a href="/profile" className="flex items-center space-x-4">
{user?.picture && ( <span className="text-2xl">{user?.username}</span>
<img {user?.picture && (
src={`${user.picture}`} <img
alt="Profile" src={`${user.picture}`}
className="w-8 h-8 rounded-full object-cover" alt="Profile"
/> className="w-8 h-8 rounded-full object-cover"
)} />
)}
</a>
</li> </li>
<li> <li>
<button <button

4
frontend/src/components/ProtectedRoute.jsx

@ -18,9 +18,11 @@ const ProtectedRoute = ({ children, requireAuth = true }) => {
} }
if (!requireAuth && isAuthenticated) { if (!requireAuth && isAuthenticated) {
return <Navigate to="/" replace />; return <Navigate to="/login" replace />;
} }
return children; return children;
}; };

142
frontend/src/pages/Account.jsx

@ -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>
)
}

2
frontend/src/pages/Video.jsx

@ -311,7 +311,7 @@ export default function Video() {
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient"> <div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar isSearchPage={false} /> <Navbar isSearchPage={false} />
<main className="px-36 w-full flex justify-between pt-[118px]"> <main className="px-36 w-full flex justify-between pt-[118px]">
{video ? ( {video ? (
<> <>
{/* Video player section */} {/* Video player section */}

9
frontend/src/routes/routes.jsx

@ -3,6 +3,7 @@ import Login from '../pages/Login.jsx'
import Register from '../pages/Register.jsx' import Register from '../pages/Register.jsx'
import Video from '../pages/Video.jsx' import Video from '../pages/Video.jsx'
import ProtectedRoute from '../components/ProtectedRoute.jsx' import ProtectedRoute from '../components/ProtectedRoute.jsx'
import Account from "../pages/Account.jsx";
const routes = [ const routes = [
{ path: "/", element: <Home/> }, { path: "/", element: <Home/> },
@ -26,6 +27,14 @@ const routes = [
path: "/video/:id", path: "/video/:id",
element: <Video /> element: <Video />
}, },
{
path: "/profile",
element: (
<ProtectedRoute requireAuth={true}>
<Account/>
</ProtectedRoute>
)
}
] ]
export default routes; export default routes;
Loading…
Cancel
Save