Spaces:
Sleeping
Sleeping
Commit
·
98f877e
1
Parent(s):
ffc893c
major update
Browse files- frontend/src/app/category/[category]/CategoryPage.css +38 -15
- frontend/src/app/category/[category]/page.js +16 -24
- frontend/src/app/globals.css +7 -4
- frontend/src/app/layout.js +2 -0
- frontend/src/components/Card.css +1 -1
- frontend/src/components/CategoriesSection.css +48 -9
- frontend/src/components/CategoriesSection.js +11 -5
- frontend/src/components/Header.css +61 -0
- frontend/src/components/Header.js +23 -0
frontend/src/app/category/[category]/CategoryPage.css
CHANGED
|
@@ -3,8 +3,7 @@
|
|
| 3 |
flex-wrap: wrap;
|
| 4 |
gap: 10px;
|
| 5 |
justify-content: center;
|
| 6 |
-
|
| 7 |
-
height: 100%;
|
| 8 |
width: 100%;
|
| 9 |
}
|
| 10 |
|
|
@@ -13,38 +12,62 @@
|
|
| 13 |
text-align: center;
|
| 14 |
}
|
| 15 |
|
| 16 |
-
.
|
| 17 |
display: flex;
|
| 18 |
-
flex-
|
| 19 |
-
|
| 20 |
justify-content: center;
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
-
.loading {
|
|
|
|
| 27 |
font-size: 1.5rem;
|
| 28 |
color: #555;
|
| 29 |
}
|
| 30 |
|
| 31 |
.error {
|
| 32 |
font-size: 1.2rem;
|
| 33 |
-
color: red;
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
.load-more {
|
| 37 |
margin-top: 20px;
|
| 38 |
-
padding:
|
| 39 |
font-size: 1rem;
|
| 40 |
-
background-color:
|
| 41 |
color: white;
|
| 42 |
border: none;
|
| 43 |
-
border-radius:
|
| 44 |
cursor: pointer;
|
| 45 |
-
transition: background-color 0.3s;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
.load-more:hover {
|
| 49 |
-
background-color:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
|
|
|
| 3 |
flex-wrap: wrap;
|
| 4 |
gap: 10px;
|
| 5 |
justify-content: center;
|
| 6 |
+
height: auto;
|
|
|
|
| 7 |
width: 100%;
|
| 8 |
}
|
| 9 |
|
|
|
|
| 12 |
text-align: center;
|
| 13 |
}
|
| 14 |
|
| 15 |
+
.category-page-loading-container {
|
| 16 |
display: flex;
|
| 17 |
+
flex-direction: column;
|
| 18 |
+
align-items: center;
|
| 19 |
justify-content: center;
|
| 20 |
+
height: 100%;
|
| 21 |
+
color: var(--foreground);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.category-page-loader {
|
| 25 |
+
border: 8px solid rgba(255, 255, 255, 0.3);
|
| 26 |
+
border-top: 8px solid var(--foreground);
|
| 27 |
+
border-radius: 50%;
|
| 28 |
+
width: 60px;
|
| 29 |
+
height: 60px;
|
| 30 |
+
animation: spin 1s linear infinite;
|
| 31 |
}
|
| 32 |
|
| 33 |
+
.category-page-loading-text {
|
| 34 |
+
margin-top: 10px;
|
| 35 |
font-size: 1.5rem;
|
| 36 |
color: #555;
|
| 37 |
}
|
| 38 |
|
| 39 |
.error {
|
| 40 |
font-size: 1.2rem;
|
| 41 |
+
color: red;
|
| 42 |
+
margin-top: 20px;
|
| 43 |
}
|
| 44 |
|
| 45 |
.load-more {
|
| 46 |
margin-top: 20px;
|
| 47 |
+
padding: 12px 24px;
|
| 48 |
font-size: 1rem;
|
| 49 |
+
background-color: var(--foreground-2);
|
| 50 |
color: white;
|
| 51 |
border: none;
|
| 52 |
+
border-radius: 25px; /* Increased border-radius for a modern look */
|
| 53 |
cursor: pointer;
|
| 54 |
+
transition: background-color 0.3s, transform 0.3s; /* Added transform transition */
|
| 55 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); /* Added shadow for depth */
|
| 56 |
+
display: flex;
|
| 57 |
+
align-items: center;
|
| 58 |
+
justify-content: center; /* Center text inside button */
|
| 59 |
}
|
| 60 |
|
| 61 |
.load-more:hover {
|
| 62 |
+
background-color: var(--foreground-3);
|
| 63 |
+
transform: scale(1.05); /* Slightly increase size on hover */
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.load-more:active {
|
| 67 |
+
transform: scale(0.95); /* Shrink on click for feedback */
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
@keyframes spin {
|
| 71 |
+
0% { transform: rotate(0deg); }
|
| 72 |
+
100% { transform: rotate(360deg); }
|
| 73 |
}
|
frontend/src/app/category/[category]/page.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
-
|
| 2 |
import Card from "@/components/Card";
|
| 3 |
import './CategoryPage.css';
|
| 4 |
-
import { useEffect, useState,
|
| 5 |
-
import { use } from 'react';
|
| 6 |
|
| 7 |
export default function CategoryPage({ params }) {
|
| 8 |
const { category } = use(params); // Unwrap params using React.use()
|
|
@@ -11,7 +10,7 @@ export default function CategoryPage({ params }) {
|
|
| 11 |
const [error, setError] = useState(null);
|
| 12 |
const [page, setPage] = useState(1);
|
| 13 |
const [hasMore, setHasMore] = useState(true);
|
| 14 |
-
const [maxPage, setMaxPage] = useState(Infinity);
|
| 15 |
|
| 16 |
useEffect(() => {
|
| 17 |
const fetchMusicByCategory = async () => {
|
|
@@ -25,18 +24,13 @@ export default function CategoryPage({ params }) {
|
|
| 25 |
}
|
| 26 |
|
| 27 |
const data = await response.json();
|
| 28 |
-
|
| 29 |
-
// Update music items based on response, avoiding duplicates
|
| 30 |
setMusicItems(prevItems => {
|
| 31 |
const newItems = data.files.filter(file => !prevItems.includes(file));
|
| 32 |
return [...prevItems, ...newItems];
|
| 33 |
});
|
| 34 |
|
| 35 |
-
// Calculate maxPage based on total_files and limit, then set it
|
| 36 |
const calculatedMaxPage = Math.ceil(data.total_files / data.limit);
|
| 37 |
setMaxPage(calculatedMaxPage);
|
| 38 |
-
|
| 39 |
-
// Update hasMore based on whether we have reached maxPage
|
| 40 |
setHasMore(page < calculatedMaxPage);
|
| 41 |
} catch (err) {
|
| 42 |
setError(err.message);
|
|
@@ -48,40 +42,33 @@ export default function CategoryPage({ params }) {
|
|
| 48 |
fetchMusicByCategory();
|
| 49 |
}, [category, page]);
|
| 50 |
|
| 51 |
-
// Load more when scrolled to the bottom of the app container
|
| 52 |
useEffect(() => {
|
| 53 |
-
const appContainer = document.querySelector('.app-container');
|
| 54 |
if (!appContainer) return;
|
| 55 |
|
| 56 |
const handleScroll = () => {
|
| 57 |
const { scrollTop, clientHeight, scrollHeight } = appContainer;
|
| 58 |
-
|
| 59 |
-
// Check if we are at the bottom of the scrollable container
|
| 60 |
if (scrollHeight - scrollTop <= clientHeight + 10 && hasMore && !loading) {
|
| 61 |
loadMore();
|
| 62 |
}
|
| 63 |
};
|
| 64 |
|
| 65 |
appContainer.addEventListener('scroll', handleScroll);
|
| 66 |
-
|
| 67 |
return () => {
|
| 68 |
appContainer.removeEventListener('scroll', handleScroll);
|
| 69 |
};
|
| 70 |
}, [hasMore, loading]);
|
| 71 |
|
| 72 |
const loadMore = () => {
|
| 73 |
-
if (hasMore && page < maxPage) {
|
| 74 |
setPage(prevPage => prevPage + 1);
|
| 75 |
}
|
| 76 |
};
|
| 77 |
|
| 78 |
-
// Initial loading logic to fill the app-container
|
| 79 |
useEffect(() => {
|
| 80 |
-
const appContainer = document.querySelector('.app-container');
|
| 81 |
if (appContainer) {
|
| 82 |
const { scrollHeight, clientHeight } = appContainer;
|
| 83 |
-
|
| 84 |
-
// If content height is less than or equal to client height, load more
|
| 85 |
if (scrollHeight <= clientHeight && hasMore) {
|
| 86 |
loadMore();
|
| 87 |
}
|
|
@@ -89,7 +76,12 @@ export default function CategoryPage({ params }) {
|
|
| 89 |
}, [musicItems, hasMore]);
|
| 90 |
|
| 91 |
if (loading && musicItems.length === 0) {
|
| 92 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
if (error) {
|
|
@@ -97,16 +89,16 @@ export default function CategoryPage({ params }) {
|
|
| 97 |
}
|
| 98 |
|
| 99 |
return (
|
| 100 |
-
<div className="category-page
|
| 101 |
<h1>{category.charAt(0).toUpperCase() + category.slice(1)} Music</h1>
|
| 102 |
<div className="music-card-section">
|
| 103 |
{musicItems.map((file, index) => (
|
| 104 |
<Card
|
| 105 |
-
key={`${file}-${index}`}
|
| 106 |
-
src={file}
|
| 107 |
/>
|
| 108 |
))}
|
| 109 |
-
{loading && <div className="load-more">Loading more...</div>}
|
| 110 |
</div>
|
| 111 |
</div>
|
| 112 |
);
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
import Card from "@/components/Card";
|
| 3 |
import './CategoryPage.css';
|
| 4 |
+
import { useEffect, useState, use } from 'react';
|
|
|
|
| 5 |
|
| 6 |
export default function CategoryPage({ params }) {
|
| 7 |
const { category } = use(params); // Unwrap params using React.use()
|
|
|
|
| 10 |
const [error, setError] = useState(null);
|
| 11 |
const [page, setPage] = useState(1);
|
| 12 |
const [hasMore, setHasMore] = useState(true);
|
| 13 |
+
const [maxPage, setMaxPage] = useState(Infinity);
|
| 14 |
|
| 15 |
useEffect(() => {
|
| 16 |
const fetchMusicByCategory = async () => {
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
const data = await response.json();
|
|
|
|
|
|
|
| 27 |
setMusicItems(prevItems => {
|
| 28 |
const newItems = data.files.filter(file => !prevItems.includes(file));
|
| 29 |
return [...prevItems, ...newItems];
|
| 30 |
});
|
| 31 |
|
|
|
|
| 32 |
const calculatedMaxPage = Math.ceil(data.total_files / data.limit);
|
| 33 |
setMaxPage(calculatedMaxPage);
|
|
|
|
|
|
|
| 34 |
setHasMore(page < calculatedMaxPage);
|
| 35 |
} catch (err) {
|
| 36 |
setError(err.message);
|
|
|
|
| 42 |
fetchMusicByCategory();
|
| 43 |
}, [category, page]);
|
| 44 |
|
|
|
|
| 45 |
useEffect(() => {
|
| 46 |
+
const appContainer = document.querySelector('.app-container');
|
| 47 |
if (!appContainer) return;
|
| 48 |
|
| 49 |
const handleScroll = () => {
|
| 50 |
const { scrollTop, clientHeight, scrollHeight } = appContainer;
|
|
|
|
|
|
|
| 51 |
if (scrollHeight - scrollTop <= clientHeight + 10 && hasMore && !loading) {
|
| 52 |
loadMore();
|
| 53 |
}
|
| 54 |
};
|
| 55 |
|
| 56 |
appContainer.addEventListener('scroll', handleScroll);
|
|
|
|
| 57 |
return () => {
|
| 58 |
appContainer.removeEventListener('scroll', handleScroll);
|
| 59 |
};
|
| 60 |
}, [hasMore, loading]);
|
| 61 |
|
| 62 |
const loadMore = () => {
|
| 63 |
+
if (hasMore && page < maxPage) {
|
| 64 |
setPage(prevPage => prevPage + 1);
|
| 65 |
}
|
| 66 |
};
|
| 67 |
|
|
|
|
| 68 |
useEffect(() => {
|
| 69 |
+
const appContainer = document.querySelector('.app-container');
|
| 70 |
if (appContainer) {
|
| 71 |
const { scrollHeight, clientHeight } = appContainer;
|
|
|
|
|
|
|
| 72 |
if (scrollHeight <= clientHeight && hasMore) {
|
| 73 |
loadMore();
|
| 74 |
}
|
|
|
|
| 76 |
}, [musicItems, hasMore]);
|
| 77 |
|
| 78 |
if (loading && musicItems.length === 0) {
|
| 79 |
+
return (
|
| 80 |
+
<div className="category-page-loading-container">
|
| 81 |
+
<div className="category-page-loader"></div>
|
| 82 |
+
<span className="category-page-loading-text">Loading music...</span>
|
| 83 |
+
</div>
|
| 84 |
+
);
|
| 85 |
}
|
| 86 |
|
| 87 |
if (error) {
|
|
|
|
| 89 |
}
|
| 90 |
|
| 91 |
return (
|
| 92 |
+
<div className="category-page">
|
| 93 |
<h1>{category.charAt(0).toUpperCase() + category.slice(1)} Music</h1>
|
| 94 |
<div className="music-card-section">
|
| 95 |
{musicItems.map((file, index) => (
|
| 96 |
<Card
|
| 97 |
+
key={`${file}-${index}`}
|
| 98 |
+
src={file}
|
| 99 |
/>
|
| 100 |
))}
|
| 101 |
+
{loading && <div className="load-more">Loading more...</div>}
|
| 102 |
</div>
|
| 103 |
</div>
|
| 104 |
);
|
frontend/src/app/globals.css
CHANGED
|
@@ -4,12 +4,15 @@
|
|
| 4 |
|
| 5 |
:root {
|
| 6 |
--background: #080523;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
--foreground: #ededed;
|
| 8 |
-
--background-secondary: #060a52;
|
| 9 |
--foreground-secondary: #1d85b9;
|
| 10 |
-
--background-2: #2a3477;
|
| 11 |
--foreground-2: #0b5da9;
|
| 12 |
-
--foreground-3: #
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
html,
|
|
@@ -22,7 +25,7 @@ body {
|
|
| 22 |
|
| 23 |
body {
|
| 24 |
color: var(--foreground);
|
| 25 |
-
background: var(--background);
|
| 26 |
font-family: Arial, Helvetica, sans-serif;
|
| 27 |
display: flex;
|
| 28 |
flex-direction: column;
|
|
|
|
| 4 |
|
| 5 |
:root {
|
| 6 |
--background: #080523;
|
| 7 |
+
--background-secondary: #121029;
|
| 8 |
+
--background-2: #1e2b57;
|
| 9 |
+
--background-3: #0d0e1e;
|
| 10 |
+
--background-4: #313c51;
|
| 11 |
--foreground: #ededed;
|
|
|
|
| 12 |
--foreground-secondary: #1d85b9;
|
|
|
|
| 13 |
--foreground-2: #0b5da9;
|
| 14 |
+
--foreground-3: #25253c;
|
| 15 |
+
--foreground-4: #878787;
|
| 16 |
}
|
| 17 |
|
| 18 |
html,
|
|
|
|
| 25 |
|
| 26 |
body {
|
| 27 |
color: var(--foreground);
|
| 28 |
+
background: var(--background-3);
|
| 29 |
font-family: Arial, Helvetica, sans-serif;
|
| 30 |
display: flex;
|
| 31 |
flex-direction: column;
|
frontend/src/app/layout.js
CHANGED
|
@@ -6,6 +6,7 @@ import "./globals.css";
|
|
| 6 |
import MusicPlayer from "@/components/MusicPlayer";
|
| 7 |
import { MusicPlayerProvider } from "@/context/MusicPlayerContext";
|
| 8 |
import { AppProgressBar as ProgressBar } from 'next-nprogress-bar';
|
|
|
|
| 9 |
|
| 10 |
const geistSans = localFont({
|
| 11 |
src: "./fonts/GeistVF.woff",
|
|
@@ -31,6 +32,7 @@ export default function RootLayout({ children }) {
|
|
| 31 |
options={{ showSpinner: false }}
|
| 32 |
shallowRouting
|
| 33 |
/>
|
|
|
|
| 34 |
<div className="app-container">{children}</div>
|
| 35 |
<footer className="bottom-0 flex items-center w-full">
|
| 36 |
<MusicPlayer />
|
|
|
|
| 6 |
import MusicPlayer from "@/components/MusicPlayer";
|
| 7 |
import { MusicPlayerProvider } from "@/context/MusicPlayerContext";
|
| 8 |
import { AppProgressBar as ProgressBar } from 'next-nprogress-bar';
|
| 9 |
+
import Header from "@/components/Header";
|
| 10 |
|
| 11 |
const geistSans = localFont({
|
| 12 |
src: "./fonts/GeistVF.woff",
|
|
|
|
| 32 |
options={{ showSpinner: false }}
|
| 33 |
shallowRouting
|
| 34 |
/>
|
| 35 |
+
<Header/>
|
| 36 |
<div className="app-container">{children}</div>
|
| 37 |
<footer className="bottom-0 flex items-center w-full">
|
| 38 |
<MusicPlayer />
|
frontend/src/components/Card.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
.music-card {
|
| 2 |
width: 250px;
|
| 3 |
height: 40;
|
| 4 |
-
background-image: linear-gradient(var(--background-
|
| 5 |
border-top-left-radius: 20px;
|
| 6 |
border-bottom-left-radius: 20px;
|
| 7 |
border-top-right-radius: 5px;
|
|
|
|
| 1 |
.music-card {
|
| 2 |
width: 250px;
|
| 3 |
height: 40;
|
| 4 |
+
background-image: linear-gradient(var(--background-4), var(--foreground-3));
|
| 5 |
border-top-left-radius: 20px;
|
| 6 |
border-bottom-left-radius: 20px;
|
| 7 |
border-top-right-radius: 5px;
|
frontend/src/components/CategoriesSection.css
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
|
|
|
|
|
| 1 |
.categories-section {
|
| 2 |
display: flex;
|
| 3 |
-
gap:
|
| 4 |
flex-wrap: wrap;
|
| 5 |
-
padding:
|
| 6 |
justify-content: center;
|
| 7 |
}
|
| 8 |
|
| 9 |
.category-card {
|
| 10 |
-
width:
|
| 11 |
-
height:
|
| 12 |
padding: 10px;
|
| 13 |
background-image: linear-gradient(var(--foreground-secondary), var(--foreground-3));
|
| 14 |
border-radius: 10px;
|
|
@@ -17,17 +19,54 @@
|
|
| 17 |
display: flex;
|
| 18 |
justify-content: center;
|
| 19 |
align-items: center;
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
.category-title {
|
| 23 |
font-size: 1rem;
|
| 24 |
text-transform: capitalize;
|
| 25 |
font-weight: 600;
|
| 26 |
-
cursor: pointer;
|
| 27 |
-
transition: scale .3s ease;
|
| 28 |
overflow-wrap: break-word;
|
| 29 |
}
|
| 30 |
|
| 31 |
-
.category-card:hover
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* CategoriesSection.css */
|
| 2 |
+
|
| 3 |
.categories-section {
|
| 4 |
display: flex;
|
| 5 |
+
gap: 15px;
|
| 6 |
flex-wrap: wrap;
|
| 7 |
+
padding: 20px;
|
| 8 |
justify-content: center;
|
| 9 |
}
|
| 10 |
|
| 11 |
.category-card {
|
| 12 |
+
width: 100px;
|
| 13 |
+
height: 80px;
|
| 14 |
padding: 10px;
|
| 15 |
background-image: linear-gradient(var(--foreground-secondary), var(--foreground-3));
|
| 16 |
border-radius: 10px;
|
|
|
|
| 19 |
display: flex;
|
| 20 |
justify-content: center;
|
| 21 |
align-items: center;
|
| 22 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 23 |
+
cursor: pointer;
|
| 24 |
}
|
| 25 |
|
| 26 |
.category-title {
|
| 27 |
font-size: 1rem;
|
| 28 |
text-transform: capitalize;
|
| 29 |
font-weight: 600;
|
|
|
|
|
|
|
| 30 |
overflow-wrap: break-word;
|
| 31 |
}
|
| 32 |
|
| 33 |
+
.category-card:hover {
|
| 34 |
+
transform: translateY(-5px);
|
| 35 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.loading-container {
|
| 39 |
+
display: flex;
|
| 40 |
+
flex-direction: column;
|
| 41 |
+
align-items: center;
|
| 42 |
+
justify-content: center;
|
| 43 |
+
height: 100px; /* Adjust height as needed */
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.loading-spinner {
|
| 47 |
+
border: 4px solid rgba(255, 255, 255, 0.3);
|
| 48 |
+
border-top: 4px solid var(--foreground);
|
| 49 |
+
border-radius: 50%;
|
| 50 |
+
width: 40px; /* Size of the spinner */
|
| 51 |
+
height: 40px; /* Size of the spinner */
|
| 52 |
+
animation: spin 1s linear infinite;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.loading-text {
|
| 56 |
+
margin-top: 10px;
|
| 57 |
+
color: var(--foreground);
|
| 58 |
+
font-weight: 500;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Spinner animation */
|
| 62 |
+
@keyframes spin {
|
| 63 |
+
0% { transform: rotate(0deg); }
|
| 64 |
+
100% { transform: rotate(360deg); }
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.error-message {
|
| 68 |
+
color: var(--foreground);
|
| 69 |
+
font-weight: 500;
|
| 70 |
+
text-align: center;
|
| 71 |
+
margin: 20px;
|
| 72 |
+
}
|
frontend/src/components/CategoriesSection.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
"use client";
|
| 2 |
import { useEffect, useState } from "react";
|
| 3 |
import "./CategoriesSection.css";
|
|
@@ -28,11 +29,16 @@ const CategoriesSection = () => {
|
|
| 28 |
}, []);
|
| 29 |
|
| 30 |
if (loading) {
|
| 31 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
if (error) {
|
| 35 |
-
return <div>Error: {error}</div>;
|
| 36 |
}
|
| 37 |
|
| 38 |
return (
|
|
@@ -47,9 +53,9 @@ const CategoriesSection = () => {
|
|
| 47 |
const CategoryCard = ({ label }) => {
|
| 48 |
return (
|
| 49 |
<Link href={`/category/${label}`}>
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
</Link>
|
| 54 |
);
|
| 55 |
};
|
|
|
|
| 1 |
+
// CategoriesSection.js
|
| 2 |
"use client";
|
| 3 |
import { useEffect, useState } from "react";
|
| 4 |
import "./CategoriesSection.css";
|
|
|
|
| 29 |
}, []);
|
| 30 |
|
| 31 |
if (loading) {
|
| 32 |
+
return (
|
| 33 |
+
<div className="loading-container">
|
| 34 |
+
<div className="loading-spinner"></div>
|
| 35 |
+
<p className="loading-text">Loading categories...</p>
|
| 36 |
+
</div>
|
| 37 |
+
);
|
| 38 |
}
|
| 39 |
|
| 40 |
if (error) {
|
| 41 |
+
return <div className="error-message">Error: {error}</div>;
|
| 42 |
}
|
| 43 |
|
| 44 |
return (
|
|
|
|
| 53 |
const CategoryCard = ({ label }) => {
|
| 54 |
return (
|
| 55 |
<Link href={`/category/${label}`}>
|
| 56 |
+
<div className="category-card">
|
| 57 |
+
<label className="category-title">{label}</label>
|
| 58 |
+
</div>
|
| 59 |
</Link>
|
| 60 |
);
|
| 61 |
};
|
frontend/src/components/Header.css
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Header.css */
|
| 2 |
+
|
| 3 |
+
.header {
|
| 4 |
+
background-color: var(--background);
|
| 5 |
+
padding: 15px 10px;
|
| 6 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.header-container {
|
| 10 |
+
display: flex;
|
| 11 |
+
justify-content: space-between;
|
| 12 |
+
align-items: center;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.logo {
|
| 16 |
+
color: var(--foreground);
|
| 17 |
+
font-size: 24px;
|
| 18 |
+
font-weight: bold;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.header-music{
|
| 22 |
+
font-weight: 300;
|
| 23 |
+
font-size: 1rem;
|
| 24 |
+
color: var(--foreground-4);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.nav {
|
| 28 |
+
display: flex;
|
| 29 |
+
gap: 20px;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.nav-link {
|
| 33 |
+
color: var(--foreground);
|
| 34 |
+
text-decoration: none;
|
| 35 |
+
transition: color 0.3s ease;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.nav-link:hover {
|
| 39 |
+
color: var(--foreground-2);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.search-container {
|
| 43 |
+
position: relative;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.search-input {
|
| 47 |
+
padding: 8px 12px;
|
| 48 |
+
border: 1px solid var(--foreground-3);
|
| 49 |
+
border-radius: 4px;
|
| 50 |
+
background-color: transparent;
|
| 51 |
+
color: var(--foreground);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.search-input::placeholder {
|
| 55 |
+
color: var(--foreground-4);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.search-input:focus {
|
| 59 |
+
outline: none;
|
| 60 |
+
border-color: var(--foreground-2);
|
| 61 |
+
}
|
frontend/src/components/Header.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Header.js
|
| 2 |
+
import React from "react";
|
| 3 |
+
import Link from "next/link";
|
| 4 |
+
import "./Header.css"; // Ensure to import the CSS file
|
| 5 |
+
|
| 6 |
+
const Header = () => {
|
| 7 |
+
return (
|
| 8 |
+
<header className="header">
|
| 9 |
+
<div className="header-container">
|
| 10 |
+
<h1 className="logo">
|
| 11 |
+
Nexus <sup className="header-music"> Music</sup>
|
| 12 |
+
</h1>
|
| 13 |
+
<nav className="nav">
|
| 14 |
+
<Link href="/" className="nav-link">
|
| 15 |
+
Home
|
| 16 |
+
</Link>
|
| 17 |
+
</nav>
|
| 18 |
+
</div>
|
| 19 |
+
</header>
|
| 20 |
+
);
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
export default Header;
|