first
This commit is contained in:
commit
826cf22c97
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
cache.json
|
||||||
|
package-lock.json
|
210
README.md
Normal file
210
README.md
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
# Raven Scott Music
|
||||||
|
|
||||||
|
**Raven Scott Music** is a sophisticated, dark-themed web application designed to curate and display music playlists from SoundCloud across three genres: Metal, Alt Rock, and Rap. Built with Node.js, Express, and EJS, this project combines server-side rendering, efficient caching, and a responsive front-end to deliver a seamless music discovery experience. Whether you're a developer looking to explore the code or a music enthusiast browsing tracks, this app offers a robust foundation with plenty of room for customization.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Features](#features)
|
||||||
|
- [Live Demo](#live-demo)
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Technical Overview](#technical-overview)
|
||||||
|
- [Backend](#backend)
|
||||||
|
- [Frontend](#frontend)
|
||||||
|
- [Caching Mechanism](#caching-mechanism)
|
||||||
|
- [File Structure](#file-structure)
|
||||||
|
- [Tech Stack Rationale](#tech-stack-rationale)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Deployment](#deployment)
|
||||||
|
- [Development](#development)
|
||||||
|
- [Enhancement Ideas](#enhancement-ideas)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [License](#license)
|
||||||
|
- [Acknowledgments](#acknowledgments)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Genre-Specific Playlists**: Dynamically fetches and displays tracks from SoundCloud playlists for Metal, Alt Rock, and Rap.
|
||||||
|
- **Efficient Caching**: Stores playlist data in local JSON files, refreshing only after a week to minimize API calls and improve load times.
|
||||||
|
- **Responsive UI**: Leverages Bootstrap 4.5.2 and custom CSS for a mobile-friendly, dark-mode interface with orange accents.
|
||||||
|
- **Track Pages**: Dedicated pages for each track with embedded SoundCloud players, titles, and descriptions.
|
||||||
|
- **SEO Optimization**: Generates a `sitemap.xml` dynamically for better search engine visibility.
|
||||||
|
- **JSON API**: Provides a RESTful endpoint (`/json/:genre`) for programmatic access to track data.
|
||||||
|
- **Custom Styling**: Features a slim dark-mode scrollbar, animated navbar hover effects, and a cohesive aesthetic tailored for music lovers.
|
||||||
|
- **Sorting Logic**: Tracks are sorted by play count (descending) and then by publication date (newest first) for a curated experience.
|
||||||
|
|
||||||
|
## Live Demo
|
||||||
|
|
||||||
|
Explore the live site at [raven-scott.rocks](https://raven-scott.rocks). Check out the Metal, Alt Rock, and Rap sections, dive into individual track pages, or inspect the sitemap at `/sitemap.xml`.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
To run this project locally, ensure you have:
|
||||||
|
- **[Node.js](https://nodejs.org/)**: Version 14.x or higher (includes npm).
|
||||||
|
- **[npm](https://www.npmjs.com/)**: Package manager for installing dependencies.
|
||||||
|
- **SoundCloud Access**: The playlists specified in `music_site.js` must be public and accessible via the `soundcloud-scraper` library.
|
||||||
|
- **Git**: For cloning the repository (optional but recommended).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. **Clone the Repository**:
|
||||||
|
```bash
|
||||||
|
git clone https://git.ssh.surf/snxraven/ravenscott-rocks.git
|
||||||
|
cd ravenscott-rocks
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install Dependencies**:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
This installs `express`, `soundcloud-scraper`, `ejs`, and other required packages listed in `package.json`.
|
||||||
|
|
||||||
|
3. **Optional: Environment Variables**:
|
||||||
|
- Create a `.env` file in the root directory to customize the port:
|
||||||
|
```bash
|
||||||
|
PORT=6767
|
||||||
|
```
|
||||||
|
- Alternatively, set the `PORT` variable in your terminal:
|
||||||
|
```bash
|
||||||
|
export PORT=6767 # Linux/macOS
|
||||||
|
set PORT=6767 # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Start the Server**:
|
||||||
|
```bash
|
||||||
|
node music_site.js
|
||||||
|
```
|
||||||
|
The app will run on `http://localhost:6767` (or your specified port). Open this URL in your browser to begin exploring.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- **Home Page (`/`)**: Displays all genre playlists with embedded SoundCloud players and "More Details" links.
|
||||||
|
- **Genre Navigation (`/#genre`)**: Use the navbar to jump to specific genres (e.g., `#metal`, `#altrock`, `#rap`).
|
||||||
|
- **Track Details (`/:genre/track/:slug`)**: Click "More Details" to view a track’s page with its SoundCloud player and description.
|
||||||
|
- **JSON API (`/json/:genre`)**: Fetch raw track data for a genre (e.g., `http://localhost:6767/json/metal`) in JSON format.
|
||||||
|
- **Sitemap (`/sitemap.xml`)**: Access a dynamically generated sitemap for SEO purposes.
|
||||||
|
|
||||||
|
Example API response:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Heavy Riffs",
|
||||||
|
"description": "A brutal metal track.",
|
||||||
|
"url": "https://soundcloud.com/snxraven/heavy-riffs",
|
||||||
|
"playCount": 1200,
|
||||||
|
"publishedAt": "2023-05-10T12:00:00Z",
|
||||||
|
"slug": "heavy-riffs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Overview
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Framework**: Uses Express.js to handle routing, static file serving, and API endpoints.
|
||||||
|
- **SoundCloud Integration**: The `soundcloud-scraper` library fetches playlist data, including track titles, URLs, play counts, and publication dates.
|
||||||
|
- **Routing**:
|
||||||
|
- `/`: Renders the home page with all genres.
|
||||||
|
- `/:genre`: Redirects to the home page’s genre section (e.g., `/#metal`).
|
||||||
|
- `/:genre/track/:slug`: Renders individual track pages.
|
||||||
|
- `/json/:genre`: Returns track data in JSON.
|
||||||
|
- `/sitemap.xml`: Generates an XML sitemap.
|
||||||
|
- **Slug Generation**: Converts track titles into URL-friendly slugs (e.g., "Heavy Riffs" → `heavy-riffs`).
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Templating**: EJS renders dynamic HTML with data from the backend.
|
||||||
|
- **Styling**:
|
||||||
|
- Bootstrap 4.5.2 (via CDN) provides the grid system and card components.
|
||||||
|
- Custom CSS adds a dark theme, orange buttons, a fixed navbar with hover animations, and a slim scrollbar.
|
||||||
|
- **Scripts**: jQuery, Popper.js, and Bootstrap JS (via CDN) enable navbar toggling and other interactive elements.
|
||||||
|
|
||||||
|
### Caching Mechanism
|
||||||
|
- **Purpose**: Reduces API calls to SoundCloud by storing playlist data locally.
|
||||||
|
- **Implementation**:
|
||||||
|
- Cache files (`cache_metal.json`, etc.) store tracks and a timestamp.
|
||||||
|
- The `getTracks` function checks if the cache is older than 1 week (configurable via `oneWeekInMs`).
|
||||||
|
- If outdated or missing, it fetches fresh data and updates the cache.
|
||||||
|
- **Sorting**: Tracks are sorted by play count (highest first) and then by publication date (newest first).
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
raven-scott-music/
|
||||||
|
├── public/ # Static assets
|
||||||
|
│ └── css/ # Custom CSS (currently empty; styles are inline in EJS)
|
||||||
|
├── views/ # EJS templates
|
||||||
|
│ ├── index.ejs # Home page with genre sections
|
||||||
|
│ ├── layout.ejs # Base layout (not fully utilized in this setup)
|
||||||
|
│ └── track.ejs # Individual track page
|
||||||
|
├── cache_metal.json # Cached Metal tracks (auto-generated)
|
||||||
|
├── cache_altrock.json # Cached Alt Rock tracks (auto-generated)
|
||||||
|
├── cache_rap.json # Cached Rap tracks (auto-generated)
|
||||||
|
├── music_site.js # Core server logic and routing
|
||||||
|
├── package.json # Project metadata and dependencies
|
||||||
|
├── package-lock.json # Dependency lock file
|
||||||
|
└── README.md # This documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack Rationale
|
||||||
|
|
||||||
|
- **Node.js & Express**: Lightweight and fast for building a server-side application with minimal overhead.
|
||||||
|
- **soundcloud-scraper**: Chosen for its simplicity and ability to scrape public playlist data without requiring an official API key.
|
||||||
|
- **EJS**: Offers server-side rendering with easy integration into Express, ideal for dynamic content like playlists.
|
||||||
|
- **Bootstrap**: Speeds up development with pre-built components and ensures responsiveness out of the box.
|
||||||
|
- **File-System Caching**: A simple, effective solution for persistence without the complexity of a database.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **Playlists**: Modify the `playlists` object in `music_site.js`:
|
||||||
|
```javascript
|
||||||
|
const playlists = {
|
||||||
|
metal: { url: 'https://soundcloud.com/snxraven/sets/raven-scott-metal', ... },
|
||||||
|
// Add new genres here
|
||||||
|
};
|
||||||
|
```
|
||||||
|
- **Cache Refresh**: Adjust `oneWeekInMs` (in milliseconds) in `getTracks`:
|
||||||
|
```javascript
|
||||||
|
const oneWeekInMs = 7 * 24 * 60 * 60 * 1000; // Change to 24 * 60 * 60 * 1000 for daily refresh
|
||||||
|
```
|
||||||
|
- **Port**: Set via `.env` or directly in `music_site.js`:
|
||||||
|
```javascript
|
||||||
|
const PORT = process.env.PORT || 6767;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
1. Fork and clone the repo.
|
||||||
|
2. Install dependencies (`npm install`).
|
||||||
|
3. Run locally (`node music_site.js`).
|
||||||
|
4. Make changes and test in your browser.
|
||||||
|
|
||||||
|
### Enhancement Ideas
|
||||||
|
- **Database Integration**: Replace file caching with MongoDB or SQLite for scalability.
|
||||||
|
- **Search Functionality**: Add a search bar to filter tracks by title or description.
|
||||||
|
- **User Accounts**: Implement authentication (e.g., with Passport.js) for favoriting tracks.
|
||||||
|
- **Additional Platforms**: Integrate Spotify or YouTube using their APIs.
|
||||||
|
- **Analytics**: Track page views or play counts with a tool like Google Analytics.
|
||||||
|
- **Lazy Loading**: Optimize the home page by loading tracks incrementally.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **SoundCloud Fetch Fails**:
|
||||||
|
- Verify playlist URLs in `music_site.js` are correct and public.
|
||||||
|
- Update `soundcloud-scraper` (`npm install soundcloud-scraper@latest`).
|
||||||
|
- **Cache Issues**:
|
||||||
|
- Delete `cache_*.json` files and restart the server to force a refresh.
|
||||||
|
- Check file permissions if cache files aren’t being written.
|
||||||
|
- **Port Conflicts**:
|
||||||
|
- Change the `PORT` value or kill the conflicting process:
|
||||||
|
```bash
|
||||||
|
lsof -i :6767 # Linux/macOS
|
||||||
|
kill -9 <PID>
|
||||||
|
```
|
||||||
|
- **404 Errors**:
|
||||||
|
- Ensure genre or slug matches the data (case-sensitive).
|
||||||
|
- Check console logs for errors during playlist fetching.
|
||||||
|
- **Styling Problems**:
|
||||||
|
- Clear browser cache if CSS changes don’t appear.
|
||||||
|
- Verify CDN links for Bootstrap and jQuery are accessible.
|
1
cache.json.save
Normal file
1
cache.json.save
Normal file
File diff suppressed because one or more lines are too long
1
cache_altrock.json
Normal file
1
cache_altrock.json
Normal file
File diff suppressed because one or more lines are too long
1
cache_metal.json
Normal file
1
cache_metal.json
Normal file
File diff suppressed because one or more lines are too long
1
cache_rap.json
Normal file
1
cache_rap.json
Normal file
File diff suppressed because one or more lines are too long
168
music_site.js
Normal file
168
music_site.js
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const path = require('path');
|
||||||
|
const SoundCloud = require('soundcloud-scraper');
|
||||||
|
const client = new SoundCloud.Client();
|
||||||
|
const fs = require('fs');
|
||||||
|
const PORT = process.env.PORT || 6767;
|
||||||
|
|
||||||
|
// Define genre playlists
|
||||||
|
const playlists = {
|
||||||
|
metal: {
|
||||||
|
url: 'https://soundcloud.com/snxraven/sets/raven-scott-metal',
|
||||||
|
cacheFile: path.join(__dirname, 'cache_metal.json'),
|
||||||
|
tracks: []
|
||||||
|
},
|
||||||
|
altrock: {
|
||||||
|
url: 'https://soundcloud.com/snxraven/sets/raven-scott-alt-rock',
|
||||||
|
cacheFile: path.join(__dirname, 'cache_altrock.json'),
|
||||||
|
tracks: []
|
||||||
|
},
|
||||||
|
rap: {
|
||||||
|
url: 'https://soundcloud.com/snxraven/sets/raven-scott-rap',
|
||||||
|
cacheFile: path.join(__dirname, 'cache_rap.json'),
|
||||||
|
tracks: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to create a slug from track title
|
||||||
|
function generateSlug(title) {
|
||||||
|
return title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/(^-|-$)/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to read the cache
|
||||||
|
function readCache(cacheFile) {
|
||||||
|
if (fs.existsSync(cacheFile)) {
|
||||||
|
const data = fs.readFileSync(cacheFile, 'utf8');
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to save cache
|
||||||
|
function saveCache(cacheFile, data) {
|
||||||
|
fs.writeFileSync(cacheFile, JSON.stringify(data), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch playlist tracks from SoundCloud
|
||||||
|
async function fetchPlaylist(playlistUrl) {
|
||||||
|
const playlist = await client.getPlaylist(playlistUrl);
|
||||||
|
|
||||||
|
return playlist.tracks.map(track => ({
|
||||||
|
title: track.title,
|
||||||
|
description: track.description || 'No description available',
|
||||||
|
url: track.url,
|
||||||
|
playCount: track.playCount || 0,
|
||||||
|
publishedAt: track.publishedAt || new Date().toISOString(),
|
||||||
|
slug: generateSlug(track.title)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tracks for a specific genre
|
||||||
|
async function getTracks(genre, fetch = false) {
|
||||||
|
const playlist = playlists[genre];
|
||||||
|
const cache = readCache(playlist.cacheFile);
|
||||||
|
const oneWeekInMs = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (fetch || !cache || (now - cache.timestamp) > oneWeekInMs) {
|
||||||
|
playlist.tracks = await fetchPlaylist(playlist.url);
|
||||||
|
saveCache(playlist.cacheFile, { tracks: playlist.tracks, timestamp: now });
|
||||||
|
} else {
|
||||||
|
playlist.tracks = cache.tracks.map(track => ({
|
||||||
|
...track,
|
||||||
|
slug: track.slug || generateSlug(track.title)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by playCount first, then by publishedAt
|
||||||
|
playlist.tracks.sort((a, b) => {
|
||||||
|
if (b.playCount !== a.playCount) return b.playCount - a.playCount;
|
||||||
|
return new Date(b.publishedAt) - new Date(a.publishedAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
return playlist.tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve static files from public directory
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// Set EJS as templating engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Home page route
|
||||||
|
app.get('/', async (req, res) => {
|
||||||
|
const genreTracks = {};
|
||||||
|
for (const genre in playlists) {
|
||||||
|
genreTracks[genre] = await getTracks(genre);
|
||||||
|
}
|
||||||
|
res.render('index', { genreTracks });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect /genre to /#genre
|
||||||
|
app.get('/:genre', async (req, res) => {
|
||||||
|
const { genre } = req.params;
|
||||||
|
if (playlists[genre]) {
|
||||||
|
res.redirect(`/#${genre}`);
|
||||||
|
} else {
|
||||||
|
res.status(404).send('Genre not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Individual track page route
|
||||||
|
app.get('/:genre/track/:slug', async (req, res) => {
|
||||||
|
const { genre, slug } = req.params;
|
||||||
|
if (!playlists[genre]) {
|
||||||
|
return res.status(404).send('Genre not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracks = await getTracks(genre);
|
||||||
|
const track = tracks.find(t => t.slug === slug);
|
||||||
|
|
||||||
|
if (!track) {
|
||||||
|
return res.status(404).send('Track not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('track', { track, genre });
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON endpoint for specific genre
|
||||||
|
app.get('/json/:genre', async (req, res) => {
|
||||||
|
const { genre } = req.params;
|
||||||
|
if (!playlists[genre]) {
|
||||||
|
return res.status(404).json({ error: 'Genre not found' });
|
||||||
|
}
|
||||||
|
const tracks = await getTracks(genre);
|
||||||
|
res.json(tracks);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sitemap endpoint
|
||||||
|
app.get('/sitemap.xml', async (req, res) => {
|
||||||
|
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
||||||
|
sitemap += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`;
|
||||||
|
|
||||||
|
// Home page
|
||||||
|
sitemap += `<url>\n <loc>https://raven-scott.rocks/</loc>\n <priority>1.0</priority>\n</url>\n`;
|
||||||
|
|
||||||
|
// Track pages for each genre
|
||||||
|
for (const genre in playlists) {
|
||||||
|
const tracks = await getTracks(genre);
|
||||||
|
tracks.forEach(track => {
|
||||||
|
sitemap += `<url>\n <loc>https://raven-scott.rocks/${genre}/track/${track.slug}</loc>\n <priority>0.8</priority>\n</url>\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sitemap += `</urlset>`;
|
||||||
|
|
||||||
|
res.header('Content-Type', 'application/xml');
|
||||||
|
res.send(sitemap);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen on the specified port
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server is running on port ${PORT}`);
|
||||||
|
});
|
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "raven-scott-metal-website",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Metal themed website for Raven Scott's music.",
|
||||||
|
"main": "app.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node app.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"cheerio": "^1.0.0-rc.10",
|
||||||
|
"ejs": "^3.1.8",
|
||||||
|
"express": "^4.18.1",
|
||||||
|
"puppeteer": "^23.6.0",
|
||||||
|
"rss-to-json": "^2.1.1",
|
||||||
|
"sitemap": "^8.0.0",
|
||||||
|
"soundcloud-scraper": "^5.0.3"
|
||||||
|
}
|
||||||
|
}
|
181
views/index.ejs
Normal file
181
views/index.ejs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Great Scott Music</title>
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Metal Mania', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #ff5500;
|
||||||
|
border-color: #ff5500;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #ff3300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar Styling */
|
||||||
|
.navbar {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||||
|
border-bottom: 2px solid #ff5500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
color: #ff5500 !important;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand:hover {
|
||||||
|
color: #ff3300 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-item {
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: white !important;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 0.5rem 1rem !important;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: #ff5500 !important;
|
||||||
|
background-color: rgba(255, 85, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
background-color: #ff5500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover::after {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler {
|
||||||
|
border-color: #ff5500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler-icon {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23ff5500' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom slim dark mode scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #4a4a4a;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #4a4a4a #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for section titles being hidden under navbar */
|
||||||
|
section {
|
||||||
|
padding-top: 90px; /* Adjust this value based on navbar height */
|
||||||
|
margin-top: -90px; /* Negative margin to offset the padding */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark fixed-top">
|
||||||
|
<a class="navbar-brand" href="https://raven-scott.fyi">Raven Scott Music</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<% for (const genre in genreTracks) { %>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#<%= genre %>">
|
||||||
|
<%= genre.charAt(0).toUpperCase() + genre.slice(1) %>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<% } %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-5 pt-5">
|
||||||
|
<% for (const genre in genreTracks) { %>
|
||||||
|
<section id="<%= genre %>">
|
||||||
|
<h2 class="text-center mb-4">
|
||||||
|
<%= genre.charAt(0).toUpperCase() + genre.slice(1) %> Tracks
|
||||||
|
</h2>
|
||||||
|
<div class="row">
|
||||||
|
<% genreTracks[genre].forEach(track => { %>
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<%= track.title %>
|
||||||
|
</h5>
|
||||||
|
<p class="card-text"></p>
|
||||||
|
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay"
|
||||||
|
src="https://w.soundcloud.com/player/?url=<%= track.url %>&color=%23ff5500&auto_play=false&show_artwork=true"
|
||||||
|
loading="lazy">
|
||||||
|
</iframe>
|
||||||
|
<a href="/<%= genre %>/track/<%= track.slug %>" class="btn btn-primary mt-3">More Details</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
20
views/layout.ejs
Normal file
20
views/layout.ejs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title || 'Raven Scott Metal' %></title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1><%= title || 'Raven Scott Metal' %></h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<%- include('content') %> <!-- This includes the body content -->
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>© 2024 Raven Scott</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
86
views/track.ejs
Normal file
86
views/track.ejs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>
|
||||||
|
<%= track.title %>
|
||||||
|
</title>
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Metal Mania', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #ff5500;
|
||||||
|
border-color: #ff5500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #ff3300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom slim dark mode scrollbar for webkit browsers (Chrome, Safari, Edge) */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
/* Slim width for the scrollbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1e1e1e;
|
||||||
|
/* Dark background for the scrollbar track */
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #4a4a4a;
|
||||||
|
/* Slightly lighter thumb color */
|
||||||
|
border-radius: 10px;
|
||||||
|
/* Rounded corners for the scrollbar */
|
||||||
|
border: 2px solid #1e1e1e;
|
||||||
|
/* Matches the track background to create a gap effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #555555;
|
||||||
|
/* Lighter on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for Firefox */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #4a4a4a #1e1e1e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="card-title">
|
||||||
|
<%= track.title %>
|
||||||
|
</h1>
|
||||||
|
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay"
|
||||||
|
src="https://w.soundcloud.com/player/?url=<%= track.url %>&color=%23ff5500&auto_play=false&show_artwork=true">
|
||||||
|
</iframe>
|
||||||
|
<p class="card-text">
|
||||||
|
<%- (track.description && track.description.trim()) ? track.description.replace(/\n/g, '<br>' )
|
||||||
|
: 'No description available for this track.' %>
|
||||||
|
<BR>
|
||||||
|
<a href="/" class="btn btn-primary mt-3">Back</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user