diff --git a/backend/app/controllers/search.controller.js b/backend/app/controllers/search.controller.js new file mode 100644 index 0000000..f937d99 --- /dev/null +++ b/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" }); + } +} \ No newline at end of file diff --git a/backend/app/routes/search.route.js b/backend/app/routes/search.route.js new file mode 100644 index 0000000..ea24b0a --- /dev/null +++ b/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; \ No newline at end of file diff --git a/backend/logs/access.log b/backend/logs/access.log index c7d1053..374dc01 100644 --- a/backend/logs/access.log +++ b/backend/logs/access.log @@ -877,3 +877,92 @@ [2025-07-20 08:55:02.625] [undefined] GET(/:id/like): likes found, removing like for video 3 with status 200 [2025-07-20 08:55:03.148] [undefined] GET(/:id/like): try to toggle like on video 3 [2025-07-20 08:55:03.155] [undefined] GET(/:id/like): no likes found adding likes for video 3 with status 200 +[2025-07-20 09:05:22.309] [undefined] DELETE(/:id): try to delete comment 16 +[2025-07-20 09:05:22.333] [undefined] DELETE(/:id): successfully deleted comment with status 200 +[2025-07-20 09:05:23.414] [undefined] DELETE(/:id): try to delete comment 15 +[2025-07-20 09:05:23.421] [undefined] DELETE(/:id): successfully deleted comment with status 200 +[2025-07-20 09:05:24.144] [undefined] DELETE(/:id): try to delete comment 14 +[2025-07-20 09:05:24.149] [undefined] DELETE(/:id): successfully deleted comment with status 200 +[2025-07-20 09:05:25.108] [undefined] GET(/:id): try to get video 3 +[2025-07-20 09:05:25.116] [undefined] GET(/:id): successfully get video 3 with status 200 +[2025-07-20 09:05:25.125] [undefined] GET(/:id/similar): try to get similar videos for video 3 +[2025-07-20 09:05:25.136] [undefined] GET(/:id/similar): successfully retrieved similar videos for video 3 with status 200 +[2025-07-20 09:05:25.153] [undefined] GET(/:id/views): try to add views for video 3 +[2025-07-20 09:05:25.163] [undefined] GET(/:id/views): successfully added views for video 3 with status 200 +[2025-07-20 09:05:27.505] [undefined] POST(/): try to post comment +[2025-07-20 09:05:27.512] [undefined] POST(/): successfully post comment with status 200 +[2025-07-20 09:05:30.109] [undefined] GET(/:id): try to get video 3 +[2025-07-20 09:05:30.117] [undefined] GET(/:id): successfully get video 3 with status 200 +[2025-07-20 09:05:30.126] [undefined] GET(/:id/similar): try to get similar videos for video 3 +[2025-07-20 09:05:30.147] [undefined] GET(/:id/similar): successfully retrieved similar videos for video 3 with status 200 +[2025-07-20 09:05:30.161] [undefined] GET(/:id/views): try to add views for video 3 +[2025-07-20 09:05:30.170] [undefined] GET(/:id/views): successfully added views for video 3 with status 200 +[2025-07-20 09:05:44.680] [undefined] GET(/:id): try to get video 3 +[2025-07-20 09:05:44.689] [undefined] GET(/:id): successfully get video 3 with status 200 +[2025-07-20 09:05:44.698] [undefined] GET(/:id/similar): try to get similar videos for video 3 +[2025-07-20 09:05:44.708] [undefined] GET(/:id/similar): successfully retrieved similar videos for video 3 with status 200 +[2025-07-20 09:05:44.720] [undefined] GET(/:id/views): try to add views for video 3 +[2025-07-20 09:05:44.729] [undefined] GET(/:id/views): successfully added views for video 3 with status 200 +[2025-07-20 09:05:46.875] [undefined] DELETE(/:id): try to delete comment 17 +[2025-07-20 09:05:46.899] [undefined] DELETE(/:id): successfully deleted comment with status 200 +[2025-07-20 09:05:48.713] [undefined] POST(/): try to post comment +[2025-07-20 09:05:48.719] [undefined] POST(/): successfully post comment with status 200 +[2025-07-20 09:05:49.896] [undefined] DELETE(/:id): comment not found with status 404 +[2025-07-20 09:05:50.580] [undefined] GET(/:id): try to get video 3 +[2025-07-20 09:05:50.588] [undefined] GET(/:id): successfully get video 3 with status 200 +[2025-07-20 09:05:50.600] [undefined] GET(/:id/similar): try to get similar videos for video 3 +[2025-07-20 09:05:50.610] [undefined] GET(/:id/similar): successfully retrieved similar videos for video 3 with status 200 +[2025-07-20 09:05:50.627] [undefined] GET(/:id/views): try to add views for video 3 +[2025-07-20 09:05:50.637] [undefined] GET(/:id/views): successfully added views for video 3 with status 200 +[2025-07-20 09:05:52.088] [undefined] DELETE(/:id): try to delete comment 18 +[2025-07-20 09:05:52.093] [undefined] DELETE(/:id): successfully deleted comment with status 200 +[2025-07-20 09:05:55.768] [undefined] DELETE(/:id): comment not found with status 404 +[2025-07-20 09:05:57.239] [undefined] GET(/:id): try to get video 3 +[2025-07-20 09:05:57.248] [undefined] GET(/:id): successfully get video 3 with status 200 +[2025-07-20 09:05:57.274] [undefined] GET(/:id/similar): try to get similar videos for video 3 +[2025-07-20 09:05:57.284] [undefined] GET(/:id/similar): successfully retrieved similar videos for video 3 with status 200 +[2025-07-20 09:05:57.304] [undefined] GET(/:id/views): try to add views for video 3 +[2025-07-20 09:05:57.315] [undefined] GET(/:id/views): successfully added views for video 3 with status 200 +[2025-07-20 09:06:00.852] [undefined] GET(/:id): try to get video 2 +[2025-07-20 09:06:00.860] [undefined] GET(/:id): successfully get video 2 with status 200 +[2025-07-20 09:06:00.871] [undefined] GET(/:id/similar): try to get similar videos for video 2 +[2025-07-20 09:06:00.879] [undefined] GET(/:id/similar): successfully retrieved similar videos for video 2 with status 200 +[2025-07-20 09:06:00.889] [undefined] GET(/:id/views): try to add views for video 2 +[2025-07-20 09:06:00.894] [undefined] GET(/:id/views): successfully added views for video 2 with status 200 +[2025-07-20 09:06:02.666] [undefined] DELETE(/:id): try to delete comment 3 +[2025-07-20 09:06:02.673] [undefined] DELETE(/:id): successfully deleted comment with status 200 +[2025-07-20 09:06:03.566] [undefined] GET(/:id): try to get video 2 +[2025-07-20 09:06:03.574] [undefined] GET(/:id): successfully get video 2 with status 200 +[2025-07-20 09:06:03.583] [undefined] GET(/:id/similar): try to get similar videos for video 2 +[2025-07-20 09:06:03.592] [undefined] GET(/:id/similar): successfully retrieved similar videos for video 2 with status 200 +[2025-07-20 09:06:03.604] [undefined] GET(/:id/views): try to add views for video 2 +[2025-07-20 09:06:03.611] [undefined] GET(/:id/views): successfully added views for video 2 with status 200 +[2025-07-20 09:06:06.078] [undefined] GET(/:id/like): try to toggle like on video 2 +[2025-07-20 09:06:06.085] [undefined] GET(/:id/like): no likes found adding likes for video 2 with status 200 +[2025-07-20 09:06:06.843] [undefined] GET(/:id/like): try to toggle like on video 2 +[2025-07-20 09:06:06.850] [undefined] GET(/:id/like): likes found, removing like for video 2 with status 200 +[2025-07-20 09:06:07.203] [undefined] GET(/:id/like): try to toggle like on video 2 +[2025-07-20 09:06:07.210] [undefined] GET(/:id/like): no likes found adding likes for video 2 with status 200 +[2025-07-20 09:06:08.133] [undefined] POST(/:id/subscribe): try to toggle subscription for channel with id 2 +[2025-07-20 09:06:08.139] [undefined] POST(/:id/subscribe): Successfully unsubscribed from channel with status 200 +[2025-07-20 09:06:08.526] [undefined] POST(/:id/subscribe): try to toggle subscription for channel with id 2 +[2025-07-20 09:06:08.532] [undefined] POST(/:id/subscribe): Successfully subscribed to channel with status 200 +[2025-07-20 13:49:36.061] [undefined] GET(/:id): failed because video not found with status 404 +[2025-07-20 13:49:36.080] [undefined] GET(/:id/similar): failed because video not found with status 404 +[2025-07-20 13:49:36.099] [undefined] GET(/:id/views): failed because video not found with status 404 +[2025-07-20 13:49:59.436] [undefined] GET(/:id): failed because video not found with status 404 +[2025-07-20 13:49:59.448] [undefined] GET(/:id/similar): failed because video not found with status 404 +[2025-07-20 13:49:59.462] [undefined] GET(/:id/views): failed because video not found with status 404 +[2025-07-20 13:50:00.050] [undefined] GET(/:id): failed because video not found with status 404 +[2025-07-20 13:50:00.074] [undefined] GET(/:id/similar): failed because video not found with status 404 +[2025-07-20 13:50:00.085] [undefined] GET(/:id/views): failed because video not found with status 404 +[2025-07-20 13:50:11.019] [undefined] GET(/:id): failed because video not found with status 404 +[2025-07-20 13:50:11.053] [undefined] GET(/:id/similar): failed because video not found with status 404 +[2025-07-20 13:50:11.065] [undefined] GET(/:id/views): failed because video not found with status 404 +[2025-07-20 13:50:19.640] [undefined] POST(/login): try to login with username 'astria' +[2025-07-20 13:50:19.647] [undefined] POST(/login): failed to login with status 401 +[2025-07-20 13:50:46.734] [undefined] POST(/): try to register a user with username: astria and email: sachaguerin.sg@gmail.com +[2025-07-20 13:50:46.783] [undefined] POST(/): successfully registered with status 200 +[2025-07-20 13:50:46.787] [undefined] POST(/login): try to login with username 'astria' +[2025-07-20 13:50:46.837] [undefined] POST(/login): Successfully logged in with status 200 +[2025-07-20 13:51:19.037] [undefined] POST(/): failed to retrieve channel because it doesn't exist with status 404 diff --git a/backend/requests/video.http b/backend/requests/video.http index 4c9aea6..f62d269 100644 --- a/backend/requests/video.http +++ b/backend/requests/video.http @@ -27,4 +27,7 @@ Authorization: Bearer {{token}} "Redstone" ], "channel": 2 -} \ No newline at end of file +} + +### +GET http://localhost/api/search?q=minecraft&type=videos&offset=0&limit=10 \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index c588fb8..6647ab1 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,6 +10,7 @@ import cors from "cors"; import PlaylistRoute from "./app/routes/playlist.route.js"; import {initDb} from "./app/utils/database.js"; import MediaRoutes from "./app/routes/media.routes.js"; +import SearchRoute from "./app/routes/search.route.js"; console.clear(); dotenv.config(); @@ -32,6 +33,7 @@ app.use("/api/comments/", CommentRoute); app.use("/api/playlists", PlaylistRoute); app.use("/api/recommendations", RecommendationRoute); app.use("/api/media", MediaRoutes); +app.use("/api/search", SearchRoute); const port = process.env.PORT; diff --git a/frontend/src/assets/svg/edit.svg b/frontend/src/assets/svg/edit.svg new file mode 100644 index 0000000..b302306 --- /dev/null +++ b/frontend/src/assets/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 174f22a..27249f1 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -19,15 +19,17 @@ export default function Navbar({ isSearchPage = false }) { {isAuthenticated ? ( <>