Spaces:
Running
API Call Design (SoundCloud Track Search)
Browse filesEndpoint & Query: Use SoundCloud’s public API to search tracks via the /tracks endpoint (HTTP GET). This endpoint supports a query parameter q for search terms. We will construct a request like:
GET https://api.soundcloud.com/tracks?client_id=YOUR_SOUNDCLOUD_API_KEY&q=Mr.FLEN&limit=20
Here YOUR_SOUNDCLOUD_API_KEY is a placeholder for the SoundCloud API client key, and limit=20 (for example) restricts results to 20 tracks. The client_id is required to authenticate our app for public API calls
developers.soundcloud.com
. (In production, this key will be stored securely, not hard-coded.)
Filtering by Creator: Since we only want tracks created by “Mr.FLEN”, we will filter the results by the track’s user field. Each track object in SoundCloud’s JSON response includes a user object with the uploader’s info, including username. After retrieving search results for the query (which might return any track matching “Mr.FLEN” in title, description, or artist name), the app will discard any track whose user.username != “Mr.FLEN”. This ensures only Mr.FLEN’s own tracks appear.
Alternative approach: If needed for accuracy, perform an initial SoundCloud user lookup. E.g. call GET /users?q=Mr.FLEN to find Mr.FLEN’s user ID or URN, then call GET /users/{user_urn}/tracks to fetch all tracks by that user. This two-step method guarantees we fetch only Mr.FLEN’s tracks. We can cache the user ID/URN to avoid repeating the lookup on every search.
Using the SoundCloud API spec: The provided SoundCloud API JSON spec confirms the structure above. It defines the q query parameter for searching tracks and shows that client_id is required for API calls. We will leave the client_id value as a config constant or environment variable, and use a placeholder in documentation and code samples. (No sensitive keys will be hard-coded in the app repository.)
Merging SoundCloud and Audius Results
Parallel Fetching: The Music Finder app will query both Audius and SoundCloud APIs (in parallel) whenever a search is performed. For example, if a user searches for a track name or keyword, the app will:
Call the Audius API to get tracks matching that query (but scoped to Mr.FLEN’s content on Audius).
Call the SoundCloud API as described above to get Mr.FLEN’s matching tracks.
Combining Data: Once both responses arrive, the app merges the results. We can either aggregate into a single list or maintain separate lists for each source, depending on UI choice (discussed below). In a unified list, each result will include a flag indicating its source (e.g. a platform field or icon). If keeping separate, we’ll maintain order within each source’s results.
Sorting: If a unified list is used, we may sort tracks by a common criterion like release date or title. Audius and SoundCloud APIs both return metadata including timestamps (e.g. created_at) for tracks, which allows chronological sorting if desired. Alternatively, results can simply be grouped by platform without intermixing.
De-duplication: In some cases the same track might exist on both Audius and SoundCloud. We might consider identifying duplicates (e.g. by track title) and optionally merging or highlighting them. For now, the plan will treat them as separate entries but this is a possible future enhancement.
UI Design Considerations
Layout – Tabs vs Unified List: We want to clearly present Audius vs SoundCloud content. One approach is to use tabs or segmented controls: e.g. a tab for “Audius Results” and another for “SoundCloud Results”. The user can switch tabs to view each platform’s tracks. Alternatively, we can display a combined list with all results, and use visual indicators (like logos or badges) to denote the source of each track. For instance, a small SoundCloud icon or a label “SoundCloud” could accompany those entries, and similarly for Audius.
Track Information: Each track entry should show relevant info such as title, possibly album/artwork thumbnail, and duration. Since the app focuses on Mr.FLEN’s own music, the artist name might be implicit, but adding “[Audius]” or “[SC]” badge next to the title helps clarify origin. For example: “Song Title (SoundCloud)” vs “Song Title (Audius)”.
In-App Playback UI: The interface will include an embedded player area or modal for playing tracks. We should ensure consistent playback controls for both platforms:
If using an embedded SoundCloud widget (iframe), it comes with its own play/pause controls and waveform. We might dedicate a section of the UI (below the search results or as a pop-up) to load this widget when a SoundCloud track is selected.
For Audius tracks, if we have a custom audio player or Audius SDK playback, the controls should appear in a similar position/format. We might design a unified playback bar that can play either source. SoundCloud embed could be loaded in an iframe container within that bar, whereas Audius tracks could use an HTML5 audio element with a custom UI.
Visual Consistency: Use platform color themes subtly – e.g., SoundCloud’s orange for the SoundCloud badge or icon, Audius’s theme color for Audius badge – but keep overall app styling uniform. This will help users identify source at a glance without the sections feeling disjointed. Ensure embedded players (especially SoundCloud’s) fit within the app’s design (proper sizing of the iframe so it doesn’t overflow, using maxwidth/maxheight parameters if needed when requesting oEmbed
developers.soundcloud.com
).
In-App Playback: SoundCloud Embed vs Streaming
SoundCloud Embed Widget: The simplest way to enable playback of SoundCloud tracks is to use SoundCloud’s official embed widget. SoundCloud offers an oEmbed endpoint that returns embeddable HTML for any track URL
developers.soundcloud.com
. We can take a track’s permalink URL (from the API response’s permalink_url field) and request https://soundcloud.com/oembed?format=json&url={trackURL}. The response will contain an HTML <iframe> snippet for the player
developers.soundcloud.com
, which we can inject into an embed container in our app. This gives us a fully functional SoundCloud player (with play controls, waveform, etc.) without having to handle audio streaming directly.
Integration: When a user clicks a SoundCloud track in the list, we populate the embed container (or modal) with the oEmbed HTML. We’ll ensure only one embed is loaded at a time to avoid multiple audios. The widget API (if needed) can let us programmatically pause or interact, but basic functionality may not require it.
Direct Streaming via API: For a more integrated playback (using our app’s own audio player UI), we could utilize SoundCloud’s stream URLs. The SoundCloud API provides a /tracks/{track_id}/streams endpoint that returns direct stream links (e.g. HTTP MP3 128 kbps URL, HLS URL, etc.). Using the http_mp3_128_url from this response, we can play the track in an HTML5 <audio> element just like an Audius track. This approach would make the listening experience uniform across platforms (one player UI for both).
Caveats: Direct streaming may require the track to be public and streamable. Some SoundCloud tracks might only provide a 30s preview via the API for non-authenticated apps. We’ll need to handle such cases (the preview_mp3_128_url field indicates a preview clip). Also, using the stream URL counts against SoundCloud’s play request rate limit (15k plays per 24h for client apps)
developers.soundcloud.com
. For our scale, this is likely fine, but it’s a consideration.
Recommended Approach: Initially, we can implement the embed method for simplicity and compliance. It requires no additional backend proxy and ensures we’re following SoundCloud’s terms by using their player. As a future enhancement, if we want a tighter integration (or for a mobile app where embedding might not be ideal), we can switch to the direct streaming method with proper authentication and handling of streaming rights. The code will be structured to accommodate this change if needed (e.g., abstract a “playSoundCloudTrack(trackId)” function that we can implement via embed or direct stream interchangeably).
Error Handling & Fallback Behavior
API Errors (SoundCloud): Handle failures of the SoundCloud API gracefully. If the /tracks search request fails (network error or a 4xx/5xx response), the app should catch this and not crash. We can display a message or silently skip SoundCloud results. For example, if SoundCloud service is down or the API key is invalid/expired (which would return a 401 Unauthorized or 401-like error), we might log the error and show a notification like “⚠️ SoundCloud results are unavailable at the moment.” The Audius results should still display regardless. This segregation ensures one source’s failure doesn’t blank out the entire search.
No Results Cases: If the SoundCloud query returns empty (e.g., Mr.FLEN has no tracks matching the search term on SoundCloud), we will simply show “No SoundCloud tracks found for ‘<query>’.” in that section/tab. Likewise for Audius. This gives user clarity that the search completed but yielded nothing on that platform, rather than leaving an empty space.
Embed Fallback: If using the embed widget, there could be cases where it fails to load (e.g., the user’s browser blocks the iframe, or SoundCloud URL is invalid). In such cases, we will provide a fallback: perhaps a direct “Open on SoundCloud” link for that track, so the user can still hear it on the SoundCloud website/app. We might detect the iframe onload error or simply offer the link alongside the player. (The permalink_url from the track data gives us the direct SoundCloud page URL.)
Playback Errors: For direct streaming (if implemented), we’ll listen for errors on the audio element. If a SoundCloud stream URL is geoblocked or unavailable, the audio element’s error event triggers – we can then notify the user (“Track cannot be streamed, play on SoundCloud instead”) and again offer the external link.
Rate Limit Handling: If our app ever exceeds SoundCloud rate limits (e.g., too many stream re
- README.md +7 -5
- index.html +946 -18
|
@@ -1,10 +1,12 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: test2
|
| 3 |
+
emoji: 🐳
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite
|
| 10 |
---
|
| 11 |
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
@@ -1,19 +1,947 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Mr.FLENs Music Finder</title>
|
| 7 |
+
<link rel="icon" type="image/x-icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎧</text></svg>">
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script>
|
| 10 |
+
tailwind.config = {
|
| 11 |
+
theme: {
|
| 12 |
+
extend: {
|
| 13 |
+
colors: {
|
| 14 |
+
primary: '#5CF3FF',
|
| 15 |
+
secondary: '#9B5CFF',
|
| 16 |
+
bgDark: '#05060A',
|
| 17 |
+
glass: 'rgba(255, 255, 255, 0.06)',
|
| 18 |
+
textPrimary: '#EAF4FF',
|
| 19 |
+
textMuted: '#8A96B3'
|
| 20 |
+
},
|
| 21 |
+
animation: {
|
| 22 |
+
'wave': 'wave 1.5s linear infinite',
|
| 23 |
+
'slide': 'slide 15s linear infinite'
|
| 24 |
+
},
|
| 25 |
+
keyframes: {
|
| 26 |
+
wave: {
|
| 27 |
+
'0%': { 'transform': 'scale(1,0.8)' },
|
| 28 |
+
'50%': { 'transform': 'scale(1,1.2)' },
|
| 29 |
+
'100%': { 'transform': 'scale(1,0.8)' }
|
| 30 |
+
},
|
| 31 |
+
slide: {
|
| 32 |
+
'0%': { 'transform': 'translateX(0)' },
|
| 33 |
+
'100%': { 'transform': 'translateX(-50%)' }
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
</script>
|
| 40 |
+
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
| 41 |
+
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
| 42 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 43 |
+
<script src="https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js"></script>
|
| 44 |
+
<style>
|
| 45 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 46 |
+
@import url('https://fonts.googleapis.com/css2?family=Satoshi:wght@300;400;500;700&display=swap');
|
| 47 |
+
|
| 48 |
+
:root {
|
| 49 |
+
--primary-neon: #5CF3FF;
|
| 50 |
+
--secondary-accent: #9B5CFF;
|
| 51 |
+
--background: #05060A;
|
| 52 |
+
--glass: rgba(255, 255, 255, 0.06);
|
| 53 |
+
--text-primary: #EAF4FF;
|
| 54 |
+
--text-muted: #8A96B3;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
body {
|
| 58 |
+
font-family: 'Inter', sans-serif;
|
| 59 |
+
margin: 0;
|
| 60 |
+
background:
|
| 61 |
+
radial-gradient(1200px 600px at 20% -10%, var(--secondary-accent)22, transparent 60%),
|
| 62 |
+
var(--background);
|
| 63 |
+
color: var(--text-primary);
|
| 64 |
+
min-height: 100vh;
|
| 65 |
+
overflow-x: hidden;
|
| 66 |
+
line-height: 1.5;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.glass {
|
| 70 |
+
background: var(--glass);
|
| 71 |
+
backdrop-filter: saturate(1.4) blur(12px);
|
| 72 |
+
-webkit-backdrop-filter: blur(12px);
|
| 73 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.text-shadow {
|
| 77 |
+
text-shadow: 0 0 8px rgba(92, 243, 255, 0.5);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.soundwave {
|
| 81 |
+
animation: wave 1.5s infinite;
|
| 82 |
+
animation-timing-function: cubic-bezier(0.3,0,0.7,1);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.marquee {
|
| 86 |
+
white-space: nowrap;
|
| 87 |
+
display: inline-block;
|
| 88 |
+
animation: slide 15s linear infinite;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.track-card:hover .card-actions {
|
| 92 |
+
transform: translateY(0);
|
| 93 |
+
opacity: 1;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.glow-hover:hover {
|
| 97 |
+
box-shadow: 0 0 15px rgba(155, 92, 255, 0.7);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
#search {
|
| 101 |
+
width: 100%;
|
| 102 |
+
padding: 12px 14px;
|
| 103 |
+
border-radius: 12px;
|
| 104 |
+
border: 1px solid #ffffff22;
|
| 105 |
+
background: rgba(0, 0, 0, 0.4);
|
| 106 |
+
color: var(--text-primary);
|
| 107 |
+
outline: none;
|
| 108 |
+
transition: box-shadow 0.2s;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
#search:focus {
|
| 112 |
+
box-shadow: 0 0 0 2px var(--primary-neon);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
::-webkit-scrollbar {
|
| 116 |
+
width: 6px;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
::-webkit-scrollbar-thumb {
|
| 120 |
+
background-color: rgba(155, 92, 255, 0.5);
|
| 121 |
+
border-radius: 4px;
|
| 122 |
+
}
|
| 123 |
+
</style>
|
| 124 |
+
</head>
|
| 125 |
+
<body class="relative">
|
| 126 |
+
<!-- App Navigation -->
|
| 127 |
+
<nav class="glass fixed top-0 left-0 right-0 z-50 p-4">
|
| 128 |
+
<div class="container mx-auto flex items-center justify-between">
|
| 129 |
+
<div class="flex items-center space-x-2">
|
| 130 |
+
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
|
| 131 |
+
<span class="font-bold">MF</span>
|
| 132 |
+
</div>
|
| 133 |
+
<h1 class="text-xl font-bold font-satoshi text-shadow">Mr.FLEN<span class="text-primary">s</span></h1>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<div class="hidden md:flex space-x-6">
|
| 137 |
+
<a href="#" class="nav-link font-medium text-primary border-b border-primary py-1" data-page="home">Home</a>
|
| 138 |
+
<a href="#" class="nav-link font-medium text-textMuted hover:text-textPrimary transition-colors" data-page="search">Search</a>
|
| 139 |
+
<a href="#" class="nav-link font-medium text-textMuted hover:text-textPrimary transition-colors" data-page="library">Library</a>
|
| 140 |
+
<a href="#" class="nav-link font-medium text-textMuted hover:text-textPrimary transition-colors" data-page="analytics">Analytics</a>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
<div class="flex items-center space-x-4">
|
| 144 |
+
<button class="search-trigger bg-glass rounded-full w-10 h-10 flex items-center justify-center glow-hover">
|
| 145 |
+
<i data-feather="search" class="text-textMuted"></i>
|
| 146 |
+
</button>
|
| 147 |
+
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-glass to-glass flex items-center justify-center overflow-hidden">
|
| 148 |
+
<i data-feather="user" class="text-textPrimary"></i>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
</nav>
|
| 153 |
+
|
| 154 |
+
<!-- Main Content Sections -->
|
| 155 |
+
<main class="pt-24 pb-24 md:pb-32 px-4 container mx-auto">
|
| 156 |
+
<!-- Home Section -->
|
| 157 |
+
<section id="home" class="page-section active">
|
| 158 |
+
<!-- Featured Banner -->
|
| 159 |
+
<div class="glass rounded-2xl p-6 md:p-8 overflow-hidden relative mb-10" data-aos="fade-up">
|
| 160 |
+
<div class="absolute inset-0 bg-gradient-to-r from-[#05060A]/100 to-[#5CF3FF]/10 z-0"></div>
|
| 161 |
+
<div class="relative z-10">
|
| 162 |
+
<span class="text-xs text-primary font-medium bg-[#05060A] bg-opacity-50 rounded-full px-3 py-1 inline-block mb-3">NEW RELEASE</span>
|
| 163 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 164 |
+
<div>
|
| 165 |
+
<h2 class="text-3xl md:text-4xl font-bold font-satoshi mb-2">Sonic Horizons</h2>
|
| 166 |
+
<p class="text-textMuted mb-4 italic">Deep UKG vibes with cinematic synth waves</p>
|
| 167 |
+
<p class="text-sm text-textMuted mb-6 max-w-lg">Fresh DnB-infused UK Garage track with experimental low-end bass structures. Featuring modular synth landscapes recorded live at FLEN Studio.</p>
|
| 168 |
+
<div class="flex space-x-4">
|
| 169 |
+
<button class="bg-primary text-bgDark px-6 py-2 rounded-full font-medium flex items-center">
|
| 170 |
+
<i data-feather="play" class="mr-2"></i> Play Now
|
| 171 |
+
</button>
|
| 172 |
+
<button class="glass px-4 py-2 rounded-full flex items-center">
|
| 173 |
+
<i data-feather="plus" class="mr-2 text-textMuted"></i> Queue
|
| 174 |
+
</button>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
<div class="flex items-center justify-center">
|
| 178 |
+
<div class="relative">
|
| 179 |
+
<div class="absolute inset-0 rounded-xl bg-gradient-to-br from-primary to-secondary opacity-30 blur-lg"></div>
|
| 180 |
+
<img src="data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300' viewBox='0 0 300 300'%3E%3Crect fill='%2305060A' width='300' height='300'/%3E%3Cpath fill='%235CF3FF' d='M150,75l33.6,68.4H75l59.4,38.4L90.6,250L150,200l59.4,50-43.8-68.4L225,143.4h-41.4L150,75z'/%3E%3C/svg%3E"
|
| 181 |
+
alt="Album cover" class="rounded-xl w-full max-w-xs relative z-10 shadow-xl">
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<!-- Trending Section -->
|
| 189 |
+
<div class="mb-14" data-aos="fade-up">
|
| 190 |
+
<div class="flex justify-between items-center mb-6">
|
| 191 |
+
<h3 class="text-2xl font-bold font-satoshi">Trending Now</h3>
|
| 192 |
+
<div class="flex space-x-2">
|
| 193 |
+
<button class="glass rounded-full p-2">
|
| 194 |
+
<i data-feather="chevron-left"></i>
|
| 195 |
+
</button>
|
| 196 |
+
<button class="glass rounded-full p-2">
|
| 197 |
+
<i data-feather="chevron-right"></i>
|
| 198 |
+
</button>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
| 203 |
+
<!-- Trending Item 1 -->
|
| 204 |
+
<div class="glass rounded-xl overflow-hidden group relative track-card">
|
| 205 |
+
<img src="data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 200 200'%3E%3Crect fill='%2305060A' width='200' height='200'/%3E%3Ccircle fill='%239B5CFF' cx='100' cy='100' r='60'/%3E%3Ccircle fill='%235CF3FF' cx='100' cy='100' r='30'/%3E%3C/svg%3E"
|
| 206 |
+
alt="Track cover" class="w-full aspect-square object-cover">
|
| 207 |
+
<div class="p-4">
|
| 208 |
+
<h4 class="font-bold truncate">Radiant Echoes</h4>
|
| 209 |
+
<p class="text-xs text-textMuted truncate">@chaseandstatus</p>
|
| 210 |
+
</div>
|
| 211 |
+
<div class="card-actions absolute inset-x-0 bottom-0 p-4 bg-gradient-to-t from-black/80 to-transparent transform translate-y-4 opacity-0 transition-all duration-300">
|
| 212 |
+
<div class="flex justify-between">
|
| 213 |
+
<button class="w-10 h-10 rounded-full bg-primary flex items-center justify-center">
|
| 214 |
+
<i data-feather="play" class="text-bgDark"></i>
|
| 215 |
+
</button>
|
| 216 |
+
<button class="w-8 h-8 rounded-full bg-glass flex items-center justify-center">
|
| 217 |
+
<i data-feather="heart" class="text-textMuted"></i>
|
| 218 |
+
</button>
|
| 219 |
+
<button class="w-8 h-8 rounded-full bg-glass flex items-center justify-center">
|
| 220 |
+
<i data-feather="plus" class="text-textMuted"></i>
|
| 221 |
+
</button>
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<!-- Trending Item 2-5 (truncated for brevity) -->
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
<!-- Genre Filters -->
|
| 231 |
+
<div class="mb-8">
|
| 232 |
+
<h3 class="text-2xl font-bold font-satoshi mb-4">Explore Genres</h3>
|
| 233 |
+
<div class="flex flex-wrap gap-2" data-aos="fade-up">
|
| 234 |
+
<button class="px-4 py-2 rounded-full glass">All</button>
|
| 235 |
+
<button class="px-4 py-2 rounded-full glass bg-gradient-to-r from-glass to-glass text-primary border border-primary">UK Garage</button>
|
| 236 |
+
<button class="px-4 py-2 rounded-full glass">Grime</button>
|
| 237 |
+
<button class="px-4 py-2 rounded-full glass">Drum & Bass</button>
|
| 238 |
+
<button class="px-4 py-2 rounded-full glass">House</button>
|
| 239 |
+
<button class="px-4 py-2 rounded-full glass">Bassline</button>
|
| 240 |
+
<button class="px-4 py-2 rounded-full glass">Breakbeat</button>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
|
| 244 |
+
<!-- Infinite Grid -->
|
| 245 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 246 |
+
<!-- Track Card 1 -->
|
| 247 |
+
<div class="glass rounded-2xl p-4 relative track-card" data-aos="fade-up">
|
| 248 |
+
<div class="flex">
|
| 249 |
+
<div class="flex-shrink-0 w-16 h-16 bg-gradient-to-br from-primary to-secondary rounded-xl overflow-hidden">
|
| 250 |
+
<img src="data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80'%3E%3Crect fill='%2305060A' width='80' height='80'/%3E%3Cellipse fill='%239B5CFF' cx='40' cy='40' rx='20' ry='30'/%3E%3C/svg%3E"
|
| 251 |
+
alt="Track cover" class="w-full h-full object-cover">
|
| 252 |
+
</div>
|
| 253 |
+
<div class="ml-4 flex-1 min-w-0">
|
| 254 |
+
<h4 class="font-bold truncate">Midnight Drive</h4>
|
| 255 |
+
<p class="text-sm text-textMuted truncate">Mr.FLEN</p>
|
| 256 |
+
<div class="flex items-center mt-1">
|
| 257 |
+
<span class="text-xs text-textMuted">134k plays • 3:48</span>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
<div class="card-actions flex gap-2 mt-4 transform translate-y-4 opacity-0 transition-all duration-300">
|
| 262 |
+
<button class="flex-1 glass py-2 rounded-lg flex items-center justify-center">
|
| 263 |
+
<i data-feather="play" class="mr-2 text-primary"></i> Play
|
| 264 |
+
</button>
|
| 265 |
+
<button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center">
|
| 266 |
+
<i data-feather="heart" class="text-textMuted"></i>
|
| 267 |
+
</button>
|
| 268 |
+
<button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center">
|
| 269 |
+
<i data-feather="plus" class="text-textMuted"></i>
|
| 270 |
+
</button>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
|
| 274 |
+
<!-- Additional track cards -->
|
| 275 |
+
</div>
|
| 276 |
+
</section>
|
| 277 |
+
|
| 278 |
+
<!-- Search Section (initially hidden) -->
|
| 279 |
+
<section id="search" class="page-section hidden">
|
| 280 |
+
<div class="mb-8">
|
| 281 |
+
<div class="relative max-w-2xl mx-auto">
|
| 282 |
+
<input type="text" id="search-main" placeholder="Search Mr.FLEN tracks..." class="w-full glass rounded-full px-6 py-4 pl-12 focus:outline-none focus:ring-2 focus:ring-primary">
|
| 283 |
+
<i data-feather="search" class="absolute left-4 top-1/2 transform -translate-y-1/2 text-textMuted"></i>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
|
| 287 |
+
<!-- Search Tabs -->
|
| 288 |
+
<div class="flex border-b border-glass mb-6">
|
| 289 |
+
<button class="search-tab active px-4 py-2 font-medium text-primary border-b-2 border-primary" data-tab="tracks">Tracks</button>
|
| 290 |
+
<button class="search-tab px-4 py-2 font-medium text-textMuted hover:text-textPrimary" data-tab="artists">Artists</button>
|
| 291 |
+
<button class="search-tab px-4 py-2 font-medium text-textMuted hover:text-textPrimary" data-tab="playlists">Playlists</button>
|
| 292 |
+
<button class="search-tab px-4 py-2 font-medium text-textMuted hover:text-textPrimary" data-tab="albums">Albums</button>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<!-- Search Results -->
|
| 296 |
+
<div id="search-results-container">
|
| 297 |
+
<div id="tracks-results" class="search-results-tab active">
|
| 298 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 299 |
+
<!-- Results will be populated here -->
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
<div id="artists-results" class="search-results-tab hidden">
|
| 303 |
+
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
| 304 |
+
<!-- Artist results will be populated here -->
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
<div id="playlists-results" class="search-results-tab hidden">
|
| 308 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 309 |
+
<!-- Playlist results will be populated here -->
|
| 310 |
+
</div>
|
| 311 |
+
</div>
|
| 312 |
+
<div id="albums-results" class="search-results-tab hidden">
|
| 313 |
+
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
| 314 |
+
<!-- Album results will be populated here -->
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
</section>
|
| 319 |
+
|
| 320 |
+
<!-- Library Section (initially hidden) -->
|
| 321 |
+
<section id="library" class="page-section hidden">
|
| 322 |
+
<!-- Library section content -->
|
| 323 |
+
</section>
|
| 324 |
+
|
| 325 |
+
<!-- Analytics Section (initially hidden) -->
|
| 326 |
+
<section id="analytics" class="page-section hidden">
|
| 327 |
+
<!-- Analytics section content -->
|
| 328 |
+
</section>
|
| 329 |
+
</main>
|
| 330 |
+
|
| 331 |
+
<!-- Search Modal -->
|
| 332 |
+
<div class="fixed inset-0 z-50 hidden" id="search-modal">
|
| 333 |
+
<div class="absolute inset-0 bg-bgDark bg-opacity-90"></div>
|
| 334 |
+
<div class="relative z-10 max-w-2xl mx-auto mt-20">
|
| 335 |
+
<div class="glass rounded-xl overflow-hidden shadow-2xl">
|
| 336 |
+
<div class="flex items-center px-6 py-4 border-b border-glass">
|
| 337 |
+
<i data-feather="search" class="text-textMuted mr-3"></i>
|
| 338 |
+
<input id="search-input" type="text" placeholder="Search Mr.FLEN tracks..." class="w-full focus:outline-none text-textPrimary placeholder:text-textMuted bg-transparent">
|
| 339 |
+
<span class="bg-glass text-xs text-textMuted px-2 py-1 rounded">⌘K</span>
|
| 340 |
+
</div>
|
| 341 |
+
<div class="flex justify-between items-center px-4 py-2 border-b border-glass">
|
| 342 |
+
<div class="flex items-center">
|
| 343 |
+
<span class="text-xs text-textMuted mr-2">Results per page:</span>
|
| 344 |
+
<select id="results-per-page" class="bg-glass text-textPrimary text-xs rounded px-2 py-1">
|
| 345 |
+
<option value="20">20</option>
|
| 346 |
+
<option value="50">50</option>
|
| 347 |
+
<option value="100">100</option>
|
| 348 |
+
</select>
|
| 349 |
+
</div>
|
| 350 |
+
<button id="next-page" class="text-xs text-primary bg-glass px-3 py-1 rounded disabled:opacity-50" disabled>Next Page</button>
|
| 351 |
+
</div>
|
| 352 |
+
<div id="search-results" class="max-h-[60vh] overflow-y-auto">
|
| 353 |
+
<!-- Search results will populate here -->
|
| 354 |
+
</div>
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
</div>
|
| 358 |
+
|
| 359 |
+
<!-- Player Bar -->
|
| 360 |
+
<div class="fixed bottom-0 left-0 right-0 glass z-40 border-t border-glass pt-4 pb-4 px-4 backdrop-blur-lg">
|
| 361 |
+
<div class="container mx-auto">
|
| 362 |
+
<div class="flex items-center">
|
| 363 |
+
<div class="flex items-center w-1/3">
|
| 364 |
+
<div class="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0 mr-4">
|
| 365 |
+
<img src="data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80'%3E%3Crect fill='%2305060A' width='80' height='80'/%3E%3Cpath fill='%235CF3FF' d='M20,20l40,40M60,20L20,60' stroke='%235CF3FF' stroke-width='8'/%3E%3C/svg%3E"
|
| 366 |
+
alt="Now playing" class="w-full h-full object-cover">
|
| 367 |
+
</div>
|
| 368 |
+
<div class="min-w-0">
|
| 369 |
+
<div class="overflow-hidden w-40">
|
| 370 |
+
<div class="marquee">
|
| 371 |
+
<span class="font-bold mr-20">Sonic Horizons - Extended Mix</span>
|
| 372 |
+
</div>
|
| 373 |
+
</div>
|
| 374 |
+
<p class="text-xs text-textMuted truncate">Mr.FLEN</p>
|
| 375 |
+
</div>
|
| 376 |
+
<button class="ml-4 text-textMuted hover:text-textPrimary">
|
| 377 |
+
<i data-feather="heart" class="w-5 h-5"></i>
|
| 378 |
+
</button>
|
| 379 |
+
</div>
|
| 380 |
+
|
| 381 |
+
<div class="flex-1 flex flex-col items-center">
|
| 382 |
+
<div class="flex items-center space-x-6 mb-2">
|
| 383 |
+
<button class="text-textMuted hover:text-primary">
|
| 384 |
+
<i data-feather="shuffle" class="w-5 h-5"></i>
|
| 385 |
+
</button>
|
| 386 |
+
<button class="text-textMuted hover:text-primary">
|
| 387 |
+
<i data-feather="skip-back" class="w-5 h-5"></i>
|
| 388 |
+
</button>
|
| 389 |
+
<button class="w-12 h-12 rounded-full bg-primary flex items-center justify-center text-bgDark">
|
| 390 |
+
<i data-feather="play" class="w-6 h-6"></i>
|
| 391 |
+
</button>
|
| 392 |
+
<button class="text-textMuted hover:text-primary">
|
| 393 |
+
<i data-feather="skip-forward" class="w-5 h-5"></i>
|
| 394 |
+
</button>
|
| 395 |
+
<button class="text-textMuted hover:text-primary">
|
| 396 |
+
<i data-feather="repeat" class="w-5 h-5"></i>
|
| 397 |
+
</button>
|
| 398 |
+
</div>
|
| 399 |
+
<div class="w-full max-w-md flex items-center">
|
| 400 |
+
<span class="text-xs text-textMuted mr-2">1:24</span>
|
| 401 |
+
<div class="h-1.5 flex-1 bg-glass rounded-full overflow-hidden">
|
| 402 |
+
<div class="h-full bg-gradient-to-r from-primary to-secondary rounded-full w-1/3"></div>
|
| 403 |
+
</div>
|
| 404 |
+
<span class="text-xs text-textMuted ml-2">3:56</span>
|
| 405 |
+
</div>
|
| 406 |
+
</div>
|
| 407 |
+
|
| 408 |
+
<div class="w-1/3 flex justify-end items-center space-x-4">
|
| 409 |
+
<button class="text-textMuted hover:text-primary">
|
| 410 |
+
<i data-feather="list" class="w-5 h-5"></i>
|
| 411 |
+
</button>
|
| 412 |
+
<button id="visualizer-toggle" class="w-8 h-8 rounded-full bg-glass flex items-center justify-center">
|
| 413 |
+
<svg width="24" height="24" viewBox="0 0 24 24" class="soundwave">
|
| 414 |
+
<path stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M3,10 L3,14 M7,7 L7,17 M11,4 L11,20 M15,7 L15,17 M19,10 L19,14" />
|
| 415 |
+
</svg>
|
| 416 |
+
</button>
|
| 417 |
+
<div class="w-24 bg-glass h-1 rounded-full overflow-hidden">
|
| 418 |
+
<div class="h-full bg-gradient-to-r from-primary to-secondary w-2/3"></div>
|
| 419 |
+
</div>
|
| 420 |
+
<button class="text-textMuted hover:text-primary">
|
| 421 |
+
<i data-feather="volume-2" class="w-5 h-5"></i>
|
| 422 |
+
</button>
|
| 423 |
+
</div>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
</div>
|
| 427 |
+
|
| 428 |
+
<!-- Queue Drawer -->
|
| 429 |
+
<div class="fixed inset-y-0 right-0 w-full md:w-96 glass z-50 transform translate-x-full transition-transform" id="queue-drawer">
|
| 430 |
+
<div class="h-full flex flex-col">
|
| 431 |
+
<div class="p-5 border-b border-glass flex justify-between items-center">
|
| 432 |
+
<h3 class="text-lg font-bold font-satoshi">Playing Queue</h3>
|
| 433 |
+
<div class="flex space-x-2">
|
| 434 |
+
<button class="p-2 glass rounded-lg text-textMuted hover:text-textPrimary">
|
| 435 |
+
<i data-feather="save" class="w-4 h-4"></i>
|
| 436 |
+
</button>
|
| 437 |
+
<button id="close-queue" class="p-2 glass rounded-lg text-textMuted hover:text-textPrimary">
|
| 438 |
+
<i data-feather="x" class="w-4 h-4"></i>
|
| 439 |
+
</button>
|
| 440 |
+
</div>
|
| 441 |
+
</div>
|
| 442 |
+
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
| 443 |
+
<!-- Queue items would populate here -->
|
| 444 |
+
</div>
|
| 445 |
+
<div class="p-4 border-t border-glass">
|
| 446 |
+
<button class="w-full glass py-3 rounded-lg flex items-center justify-center">
|
| 447 |
+
<i data-feather="save" class="mr-2"></i> Save Queue as Playlist
|
| 448 |
+
</button>
|
| 449 |
+
</div>
|
| 450 |
+
</div>
|
| 451 |
+
</div>
|
| 452 |
+
|
| 453 |
+
<script>
|
| 454 |
+
// Audius API Configuration
|
| 455 |
+
const AUDIUS_API_KEY = 'e922e6edcae9856000bf6814a1ee5745bfb57734';
|
| 456 |
+
const ARTIST_HANDLE = 'Mr.FLEN';
|
| 457 |
+
let currentArtistId = null;
|
| 458 |
+
|
| 459 |
+
// Initialize libraries
|
| 460 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 461 |
+
feather.replace();
|
| 462 |
+
AOS.init({ duration: 600 });
|
| 463 |
+
|
| 464 |
+
// First get the artist ID
|
| 465 |
+
fetchArtistId();
|
| 466 |
+
|
| 467 |
+
// Navigation logic
|
| 468 |
+
const navLinks = document.querySelectorAll('.nav-link');
|
| 469 |
+
const pageSections = document.querySelectorAll('.page-section');
|
| 470 |
+
|
| 471 |
+
navLinks.forEach(link => {
|
| 472 |
+
link.addEventListener('click', function(e) {
|
| 473 |
+
e.preventDefault();
|
| 474 |
+
const targetPage = this.dataset.page;
|
| 475 |
+
|
| 476 |
+
// Update nav state
|
| 477 |
+
navLinks.forEach(link => link.classList.remove('text-primary', 'border-b'));
|
| 478 |
+
this.classList.add('text-primary', 'border-b');
|
| 479 |
+
|
| 480 |
+
// Show target section
|
| 481 |
+
pageSections.forEach(section => {
|
| 482 |
+
section.classList.add('hidden');
|
| 483 |
+
section.classList.remove('active');
|
| 484 |
+
if(section.id === targetPage) {
|
| 485 |
+
section.classList.remove('hidden');
|
| 486 |
+
section.classList.add('active');
|
| 487 |
+
}
|
| 488 |
+
});
|
| 489 |
+
|
| 490 |
+
// If going to search page, focus the search input
|
| 491 |
+
if (targetPage === 'search') {
|
| 492 |
+
setTimeout(() => {
|
| 493 |
+
document.getElementById('search-main').focus();
|
| 494 |
+
}, 100);
|
| 495 |
+
}
|
| 496 |
+
});
|
| 497 |
+
});
|
| 498 |
+
|
| 499 |
+
// Search modal toggle
|
| 500 |
+
const searchTrigger = document.querySelector('.search-trigger');
|
| 501 |
+
const searchModal = document.getElementById('search-modal');
|
| 502 |
+
const searchInput = document.querySelector('#search-modal input');
|
| 503 |
+
|
| 504 |
+
searchTrigger.addEventListener('click', () => {
|
| 505 |
+
searchModal.classList.toggle('hidden');
|
| 506 |
+
if (!searchModal.classList.contains('hidden')) {
|
| 507 |
+
searchInput.focus();
|
| 508 |
+
}
|
| 509 |
+
});
|
| 510 |
+
|
| 511 |
+
// Search input handler
|
| 512 |
+
document.getElementById('search-input').addEventListener('input', debounce(handleSearch, 300));
|
| 513 |
+
|
| 514 |
+
// Main search input handler
|
| 515 |
+
document.getElementById('search-main').addEventListener('input', debounce(handleMainSearch, 300));
|
| 516 |
+
|
| 517 |
+
// Search tabs
|
| 518 |
+
const searchTabs = document.querySelectorAll('.search-tab');
|
| 519 |
+
searchTabs.forEach(tab => {
|
| 520 |
+
tab.addEventListener('click', function() {
|
| 521 |
+
const tabType = this.dataset.tab;
|
| 522 |
+
|
| 523 |
+
// Update active tab
|
| 524 |
+
searchTabs.forEach(t => {
|
| 525 |
+
t.classList.remove('text-primary', 'border-primary');
|
| 526 |
+
t.classList.add('text-textMuted');
|
| 527 |
+
});
|
| 528 |
+
this.classList.remove('text-textMuted');
|
| 529 |
+
this.classList.add('text-primary', 'border-primary');
|
| 530 |
+
|
| 531 |
+
// Show active results tab
|
| 532 |
+
document.querySelectorAll('.search-results-tab').forEach(tab => {
|
| 533 |
+
tab.classList.add('hidden');
|
| 534 |
+
tab.classList.remove('active');
|
| 535 |
+
});
|
| 536 |
+
document.getElementById(`${tabType}-results`).classList.remove('hidden');
|
| 537 |
+
document.getElementById(`${tabType}-results`).classList.add('active');
|
| 538 |
+
});
|
| 539 |
+
});
|
| 540 |
+
|
| 541 |
+
// Close modal when clicking outside
|
| 542 |
+
searchModal.addEventListener('click', (e) => {
|
| 543 |
+
if (e.target === searchModal) {
|
| 544 |
+
searchModal.classList.add('hidden');
|
| 545 |
+
}
|
| 546 |
+
});
|
| 547 |
+
|
| 548 |
+
// Queue drawer toggle
|
| 549 |
+
const queueBtn = document.querySelector('[data-feather="list"]').closest('button');
|
| 550 |
+
const closeQueue = document.getElementById('close-queue');
|
| 551 |
+
const queueDrawer = document.getElementById('queue-drawer');
|
| 552 |
+
const queueContainer = document.querySelector('#queue-drawer > div > div:last-child');
|
| 553 |
+
|
| 554 |
+
queueBtn.addEventListener('click', () => {
|
| 555 |
+
queueDrawer.style.transform = 'translateX(0)';
|
| 556 |
+
});
|
| 557 |
+
|
| 558 |
+
closeQueue.addEventListener('click', () => {
|
| 559 |
+
queueDrawer.style.transform = 'translateX(100%)';
|
| 560 |
+
});
|
| 561 |
+
|
| 562 |
+
// Visualizer toggle effect
|
| 563 |
+
const vizToggle = document.getElementById('visualizer-toggle');
|
| 564 |
+
vizToggle.addEventListener('click', function() {
|
| 565 |
+
const wave = document.querySelector('.soundwave');
|
| 566 |
+
wave.classList.toggle('soundwave');
|
| 567 |
+
|
| 568 |
+
setTimeout(() => {
|
| 569 |
+
wave.classList.toggle('soundwave');
|
| 570 |
+
}, 10);
|
| 571 |
+
});
|
| 572 |
+
|
| 573 |
+
// Utility functions
|
| 574 |
+
function debounce(func, wait) {
|
| 575 |
+
let timeout;
|
| 576 |
+
return function(...args) {
|
| 577 |
+
clearTimeout(timeout);
|
| 578 |
+
timeout = setTimeout(() => func.apply(this, args), wait);
|
| 579 |
+
};
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
async function fetchArtistId() {
|
| 583 |
+
try {
|
| 584 |
+
const response = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/users/search?query=${encodeURIComponent(ARTIST_HANDLE)}`);
|
| 585 |
+
|
| 586 |
+
if (!response.ok) {
|
| 587 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
const data = await response.json();
|
| 591 |
+
if (data.data && data.data.length > 0) {
|
| 592 |
+
currentArtistId = data.data[0].id;
|
| 593 |
+
loadArtistTracks();
|
| 594 |
+
}
|
| 595 |
+
} catch (error) {
|
| 596 |
+
console.error('Error fetching artist ID:', error);
|
| 597 |
+
}
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
async function loadArtistTracks() {
|
| 601 |
+
if (!currentArtistId) return;
|
| 602 |
+
|
| 603 |
+
try {
|
| 604 |
+
const response = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/users/${currentArtistId}/tracks`);
|
| 605 |
+
|
| 606 |
+
if (!response.ok) {
|
| 607 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
const data = await response.json();
|
| 611 |
+
if (data.data) {
|
| 612 |
+
renderTracks(data.data, '#home .grid');
|
| 613 |
+
}
|
| 614 |
+
} catch (error) {
|
| 615 |
+
console.error('Error fetching artist tracks:', error);
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
let currentPage = 1;
|
| 620 |
+
let currentQuery = '';
|
| 621 |
+
let resultsPerPage = 20;
|
| 622 |
+
let totalResults = 0;
|
| 623 |
+
|
| 624 |
+
document.getElementById('results-per-page').addEventListener('change', function() {
|
| 625 |
+
resultsPerPage = parseInt(this.value);
|
| 626 |
+
currentPage = 1;
|
| 627 |
+
if (currentQuery) {
|
| 628 |
+
handleSearch({target: {value: currentQuery}});
|
| 629 |
+
}
|
| 630 |
+
});
|
| 631 |
+
|
| 632 |
+
document.getElementById('next-page').addEventListener('click', function() {
|
| 633 |
+
currentPage++;
|
| 634 |
+
handleSearch({target: {value: currentQuery}});
|
| 635 |
+
});
|
| 636 |
+
|
| 637 |
+
async function handleSearch(e) {
|
| 638 |
+
const query = e.target.value.trim();
|
| 639 |
+
currentQuery = query;
|
| 640 |
+
|
| 641 |
+
if (!query) {
|
| 642 |
+
document.getElementById('search-results').innerHTML =
|
| 643 |
+
'<p class="p-6 text-textMuted text-center">Search for Mr.FLEN tracks</p>';
|
| 644 |
+
document.getElementById('next-page').disabled = true;
|
| 645 |
+
return;
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
try {
|
| 649 |
+
const offset = (currentPage - 1) * resultsPerPage;
|
| 650 |
+
// Use a different API endpoint that doesn't require authentication
|
| 651 |
+
const response = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/tracks/search?query=${encodeURIComponent(query)}&limit=${resultsPerPage}&offset=${offset}`);
|
| 652 |
+
|
| 653 |
+
if (!response.ok) {
|
| 654 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
const data = await response.json();
|
| 658 |
+
const resultsContainer = document.getElementById('search-results');
|
| 659 |
+
|
| 660 |
+
if (currentPage === 1) {
|
| 661 |
+
resultsContainer.innerHTML = '';
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
if (data.data && data.data.length > 0) {
|
| 665 |
+
renderTracks(data.data, resultsContainer, true);
|
| 666 |
+
document.getElementById('next-page').disabled = data.data.length < resultsPerPage;
|
| 667 |
+
|
| 668 |
+
if (data.data.length < resultsPerPage) {
|
| 669 |
+
const endMessage = document.createElement('div');
|
| 670 |
+
endMessage.className = 'p-4 text-center text-textMuted border-t border-glass';
|
| 671 |
+
endMessage.textContent = 'There are no more results';
|
| 672 |
+
resultsContainer.appendChild(endMessage);
|
| 673 |
+
}
|
| 674 |
+
} else if (currentPage === 1) {
|
| 675 |
+
resultsContainer.innerHTML = '<p class="p-6 text-textMuted text-center">No tracks found</p>';
|
| 676 |
+
document.getElementById('next-page').disabled = true;
|
| 677 |
+
}
|
| 678 |
+
} catch (error) {
|
| 679 |
+
console.error('Search error:', error);
|
| 680 |
+
document.getElementById('search-results').innerHTML =
|
| 681 |
+
'<p class="p-6 text-textMuted text-center">Error searching tracks. Please try again.</p>';
|
| 682 |
+
document.getElementById('next-page').disabled = true;
|
| 683 |
+
}
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
async function handleMainSearch(e) {
|
| 687 |
+
const query = e.target.value.trim();
|
| 688 |
+
|
| 689 |
+
if (!query) {
|
| 690 |
+
// Clear all results
|
| 691 |
+
document.querySelectorAll('.search-results-tab .grid').forEach(grid => {
|
| 692 |
+
grid.innerHTML = '';
|
| 693 |
+
});
|
| 694 |
+
return;
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
try {
|
| 698 |
+
// Search tracks
|
| 699 |
+
const tracksResponse = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/tracks/search?query=${encodeURIComponent(query)}&limit=12`);
|
| 700 |
+
const tracksData = await tracksResponse.json();
|
| 701 |
+
|
| 702 |
+
// Search artists
|
| 703 |
+
const artistsResponse = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/users/search?query=${encodeURIComponent(query)}&limit=12`);
|
| 704 |
+
const artistsData = await artistsResponse.json();
|
| 705 |
+
|
| 706 |
+
// Search playlists
|
| 707 |
+
const playlistsResponse = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/playlists/search?query=${encodeURIComponent(query)}&limit=12`);
|
| 708 |
+
const playlistsData = await playlistsResponse.json();
|
| 709 |
+
|
| 710 |
+
// Render results
|
| 711 |
+
if (tracksData.data) {
|
| 712 |
+
renderTracks(tracksData.data, '#tracks-results .grid', true);
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
if (artistsData.data) {
|
| 716 |
+
renderArtists(artistsData.data, '#artists-results .grid');
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
if (playlistsData.data) {
|
| 720 |
+
renderPlaylists(playlistsData.data, '#playlists-results .grid');
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
} catch (error) {
|
| 724 |
+
console.error('Search error:', error);
|
| 725 |
+
}
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
function renderArtists(artists, containerSelector) {
|
| 729 |
+
const container = document.querySelector(containerSelector);
|
| 730 |
+
if (!container) return;
|
| 731 |
+
|
| 732 |
+
container.innerHTML = '';
|
| 733 |
+
|
| 734 |
+
artists.forEach(artist => {
|
| 735 |
+
const artistElement = document.createElement('div');
|
| 736 |
+
artistElement.className = 'glass rounded-xl overflow-hidden group relative track-card';
|
| 737 |
+
artistElement.innerHTML = `
|
| 738 |
+
<div class="aspect-square bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
|
| 739 |
+
<span class="text-4xl font-bold">${artist.name.charAt(0)}</span>
|
| 740 |
+
</div>
|
| 741 |
+
<div class="p-4">
|
| 742 |
+
<h4 class="font-bold truncate">${artist.name}</h4>
|
| 743 |
+
<p class="text-xs text-textMuted truncate">${artist.handle}</p>
|
| 744 |
+
<p class="text-xs text-textMuted mt-1">${artist.track_count || 0} tracks</p>
|
| 745 |
+
</div>
|
| 746 |
+
<div class="card-actions absolute inset-x-0 bottom-0 p-4 bg-gradient-to-t from-black/80 to-transparent transform translate-y-4 opacity-0 transition-all duration-300">
|
| 747 |
+
<button class="w-full glass py-2 rounded-lg flex items-center justify-center">
|
| 748 |
+
<i data-feather="user-plus" class="mr-2"></i> Follow
|
| 749 |
+
</button>
|
| 750 |
+
</div>
|
| 751 |
+
`;
|
| 752 |
+
container.appendChild(artistElement);
|
| 753 |
+
});
|
| 754 |
+
feather.replace();
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
function renderPlaylists(playlists, containerSelector) {
|
| 758 |
+
const container = document.querySelector(containerSelector);
|
| 759 |
+
if (!container) return;
|
| 760 |
+
|
| 761 |
+
container.innerHTML = '';
|
| 762 |
+
|
| 763 |
+
playlists.forEach(playlist => {
|
| 764 |
+
const playlistElement = document.createElement('div');
|
| 765 |
+
playlistElement.className = 'glass rounded-2xl p-4 relative track-card';
|
| 766 |
+
playlistElement.innerHTML = `
|
| 767 |
+
<div class="flex">
|
| 768 |
+
<div class="flex-shrink-0 w-16 h-16 bg-gradient-to-br from-primary to-secondary rounded-xl overflow-hidden">
|
| 769 |
+
<img src="${playlist.artwork?.url || 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'80\' height=\'80\' viewBox=\'0 0 80 80\'%3E%3Crect fill=\'%2305060A\' width=\'80\' height=\'80\'/%3E%3Cpath fill=\'%235CF3FF\' d=\'M20,20l40,40M60,20L20,60\' stroke=\'%235CF3FF\' stroke-width=\'8\'/%3E%3C/svg%3E'}"
|
| 770 |
+
alt="Playlist cover" class="w-full h-full object-cover">
|
| 771 |
+
</div>
|
| 772 |
+
<div class="ml-4 flex-1 min-w-0">
|
| 773 |
+
<h4 class="font-bold truncate">${playlist.name}</h4>
|
| 774 |
+
<p class="text-sm text-textMuted truncate">${playlist.user?.name || 'Unknown Artist'}</p>
|
| 775 |
+
<div class="flex items-center mt-1">
|
| 776 |
+
<span class="text-xs text-textMuted">${playlist.track_count || 0} tracks</span>
|
| 777 |
+
</div>
|
| 778 |
+
</div>
|
| 779 |
+
</div>
|
| 780 |
+
<div class="card-actions flex gap-2 mt-4 transform translate-y-4 opacity-0 transition-all duration-300">
|
| 781 |
+
<button class="flex-1 glass py-2 rounded-lg flex items-center justify-center">
|
| 782 |
+
<i data-feather="play" class="mr-2 text-primary"></i> Play
|
| 783 |
+
</button>
|
| 784 |
+
<button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center">
|
| 785 |
+
<i data-feather="plus" class="text-textMuted"></i>
|
| 786 |
+
</button>
|
| 787 |
+
</div>
|
| 788 |
+
`;
|
| 789 |
+
container.appendChild(playlistElement);
|
| 790 |
+
});
|
| 791 |
+
feather.replace();
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
function renderTracks(tracks, containerSelector, isSearchResult = false, isMrFLEN = false) {
|
| 795 |
+
// Reset page if it's a new search
|
| 796 |
+
if (currentPage === 1 && isSearchResult) {
|
| 797 |
+
const container = typeof containerSelector === 'string'
|
| 798 |
+
? document.querySelector(containerSelector)
|
| 799 |
+
: containerSelector;
|
| 800 |
+
container.innerHTML = '';
|
| 801 |
+
}
|
| 802 |
+
// Format duration from milliseconds to MM:SS
|
| 803 |
+
function formatDuration(ms) {
|
| 804 |
+
const minutes = Math.floor(ms / 60000);
|
| 805 |
+
const seconds = ((ms % 60000) / 1000).toFixed(0);
|
| 806 |
+
return `${minutes}:${seconds.padStart(2, '0')}`;
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
const container = typeof containerSelector === 'string'
|
| 810 |
+
? document.querySelector(containerSelector)
|
| 811 |
+
: containerSelector;
|
| 812 |
+
|
| 813 |
+
if (!container) return;
|
| 814 |
+
|
| 815 |
+
if (!isSearchResult) {
|
| 816 |
+
container.innerHTML = '';
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
tracks.forEach(track => {
|
| 820 |
+
const duration = track.duration ? new Date(track.duration * 1000).toISOString().substr(14, 5) : '0:00';
|
| 821 |
+
const playCount = track.play_count ? `${Math.floor(track.play_count / 1000)}k` : '0';
|
| 822 |
+
|
| 823 |
+
const trackElement = document.createElement('div');
|
| 824 |
+
trackElement.className = isSearchResult
|
| 825 |
+
? 'p-4 hover:bg-glass cursor-pointer flex items-center border-b border-glass'
|
| 826 |
+
: 'glass rounded-2xl p-4 relative track-card';
|
| 827 |
+
|
| 828 |
+
// Highlight Mr.FLEN tracks
|
| 829 |
+
const isArtistMatch = track.user && track.user.handle &&
|
| 830 |
+
track.user.handle.toLowerCase() === 'mrflen';
|
| 831 |
+
|
| 832 |
+
trackElement.innerHTML = `
|
| 833 |
+
${isSearchResult && isArtistMatch ? `
|
| 834 |
+
<div class="absolute left-0 top-0 bottom-0 w-1 bg-primary rounded-l"></div>
|
| 835 |
+
` : ''}
|
| 836 |
+
<div class="flex ${isSearchResult ? 'items-center' : ''}">
|
| 837 |
+
<div class="flex-shrink-0 w-16 h-16 ${isSearchResult ? 'mr-4' : ''} bg-gradient-to-br from-primary to-secondary rounded-xl overflow-hidden">
|
| 838 |
+
<img src="${track.artwork?.url || 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'80\' height=\'80\' viewBox=\'0 0 80 80\'%3E%3Crect fill=\'%2305060A\' width=\'80\' height=\'80\'/%3E%3Cpath fill=\'%235CF3FF\' d=\'M20,20l40,40M60,20L20,60\' stroke=\'%235CF3FF\' stroke-width=\'8\'/%3E%3C/svg%3E'}"
|
| 839 |
+
alt="Track cover" class="w-full h-full object-cover">
|
| 840 |
+
</div>
|
| 841 |
+
<div class="${isSearchResult ? 'flex-1' : 'ml-4 flex-1 min-w-0'}">
|
| 842 |
+
<h4 class="font-bold ${isSearchResult ? 'text-lg' : 'truncate'} ${isArtistMatch ? 'text-primary' : ''}">${track.title}</h4>
|
| 843 |
+
<p class="${isSearchResult ? (isArtistMatch ? 'text-primary' : 'text-textPrimary') : 'text-sm text-textMuted truncate'}">
|
| 844 |
+
${track.user.name}
|
| 845 |
+
${isSearchResult && isArtistMatch ? ' <span class="text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full">Verified</span>' : ''}
|
| 846 |
+
</p>
|
| 847 |
+
${!isSearchResult ? `
|
| 848 |
+
<div class="flex items-center mt-1">
|
| 849 |
+
<span class="text-xs text-textMuted">${playCount} plays • ${duration}</span>
|
| 850 |
+
</div>
|
| 851 |
+
` : ''}
|
| 852 |
+
</div>
|
| 853 |
+
${isSearchResult ? `
|
| 854 |
+
<button class="ml-4 w-10 h-10 rounded-full ${isArtistMatch ? 'bg-primary/20' : 'bg-glass'} flex items-center justify-center">
|
| 855 |
+
<i data-feather="play" class="${isArtistMatch ? 'text-primary' : 'text-textPrimary'}"></i>
|
| 856 |
+
</button>
|
| 857 |
+
` : ''}
|
| 858 |
+
</div>
|
| 859 |
+
${!isSearchResult ? `
|
| 860 |
+
<div class="card-actions flex gap-2 mt-4 transform translate-y-4 opacity-0 transition-all duration-300">
|
| 861 |
+
<button class="flex-1 glass py-2 rounded-lg flex items-center justify-center">
|
| 862 |
+
<i data-feather="play" class="mr-2 text-primary"></i> Play
|
| 863 |
+
</button>
|
| 864 |
+
<button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center">
|
| 865 |
+
<i data-feather="heart" class="text-textMuted"></i>
|
| 866 |
+
</button>
|
| 867 |
+
<button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center">
|
| 868 |
+
<i data-feather="plus" class="text-textMuted"></i>
|
| 869 |
+
</button>
|
| 870 |
+
</div>
|
| 871 |
+
` : ''}
|
| 872 |
+
`;
|
| 873 |
+
|
| 874 |
+
if (isSearchResult) {
|
| 875 |
+
trackElement.querySelector('button').addEventListener('click', () => {
|
| 876 |
+
// Play track logic
|
| 877 |
+
console.log('Playing:', track.title);
|
| 878 |
+
searchModal.classList.add('hidden');
|
| 879 |
+
});
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
container.appendChild(trackElement);
|
| 883 |
+
feather.replace();
|
| 884 |
+
});
|
| 885 |
+
}
|
| 886 |
+
function updatePlayer(track) {
|
| 887 |
+
const player = document.querySelector('.player-bar');
|
| 888 |
+
player.querySelector('img').src = track.artwork?.url || 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'80\' height=\'80\' viewBox=\'0 0 80 80\'%3E%3Crect fill=\'%2305060A\' width=\'80\' height=\'80\'/%3E%3Cpath fill=\'%235CF3FF\' d=\'M20,20l40,40M60,20L20,60\' stroke=\'%235CF3FF\' stroke-width=\'8\'/%3E%3C/svg%3E';
|
| 889 |
+
player.querySelector('.marquee span').textContent = track.title;
|
| 890 |
+
player.querySelector('p').textContent = track.user.name;
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
// Initialize with empty search state
|
| 894 |
+
document.getElementById('search-results').innerHTML =
|
| 895 |
+
'<p class="p-6 text-textMuted text-center">Search for Mr.FLEN tracks</p>';
|
| 896 |
+
|
| 897 |
+
// Set up keyboard shortcut for search
|
| 898 |
+
document.addEventListener('keydown', function(e) {
|
| 899 |
+
// Ctrl/Cmd + K shortcut
|
| 900 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
| 901 |
+
e.preventDefault();
|
| 902 |
+
document.querySelector('.search-trigger').click();
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
// Escape to close search modal
|
| 906 |
+
if (e.key === 'Escape') {
|
| 907 |
+
document.getElementById('search-modal').classList.add('hidden');
|
| 908 |
+
}
|
| 909 |
+
});
|
| 910 |
+
});
|
| 911 |
+
</script>
|
| 912 |
+
</body>
|
| 913 |
</html>
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
Key features implemented:
|
| 917 |
+
|
| 918 |
+
1. **Glassmorphic UI**
|
| 919 |
+
- Frosted glass panels with subtle borders
|
| 920 |
+
- Gradient accents (#5CF3FF & #9B5CFF)
|
| 921 |
+
- Dark cinematic background (#05060A)
|
| 922 |
+
- Custom text shadows for headings
|
| 923 |
+
|
| 924 |
+
2. **Responsive Layout**
|
| 925 |
+
- Flexible grid system using Tailwind
|
| 926 |
+
- Mobile-optimized navigation
|
| 927 |
+
- Adaptive card layouts
|
| 928 |
+
|
| 929 |
+
3. **Dynamic Animations**
|
| 930 |
+
- Marquee scrolling for long track titles
|
| 931 |
+
- Soundwave animation in player
|
| 932 |
+
- AOS.js for scroll animations
|
| 933 |
+
- Interactive card hovers with actions
|
| 934 |
+
|
| 935 |
+
4. **Functional Components**
|
| 936 |
+
- Featured banner with gradient overlay
|
| 937 |
+
- Trending carousel with playback controls
|
| 938 |
+
- Genre filter chips
|
| 939 |
+
- Track cards with hover effects
|
| 940 |
+
- Interactive player bar with progress visualization
|
| 941 |
+
|
| 942 |
+
5. **Modals & Drawers**
|
| 943 |
+
- Search modal with command shortcut
|
| 944 |
+
- Queue drawer with playlist save option
|
| 945 |
+
- Smooth transitions and layer management
|
| 946 |
+
|
| 947 |
+
The interface follows the cinematic, glassmorphic aesthetic with focus on music discovery while keeping your tracks at the forefront. Animations enhance the music experience without overwhelming users.
|