feat: artist pages
This commit is contained in:
parent
d39369bd99
commit
013daa1c82
17 changed files with 390 additions and 130 deletions
75
src/_data/artists.js
Normal file
75
src/_data/artists.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const SUPABASE_URL = process.env.SUPABASE_URL || 'YOUR_SUPABASE_URL'
|
||||
const SUPABASE_KEY = process.env.SUPABASE_KEY || 'YOUR_SUPABASE_KEY'
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' })
|
||||
const getCountryName = (countryCode) => regionNames.of(countryCode.trim()) || countryCode.trim()
|
||||
|
||||
const parseCountryField = (countryField) => {
|
||||
if (!countryField) return null
|
||||
|
||||
const delimiters = [',', '/', '&', 'and']
|
||||
let countries = [countryField]
|
||||
|
||||
delimiters.forEach(delimiter => {
|
||||
countries = countries.flatMap(country => country.split(delimiter))
|
||||
})
|
||||
|
||||
return countries.map(getCountryName).join(', ')
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
const fetchPaginatedData = async (table, selectFields) => {
|
||||
let data = []
|
||||
let page = 0
|
||||
let hasMoreRecords = true
|
||||
|
||||
while (hasMoreRecords) {
|
||||
const { data: pageData, error } = await supabase
|
||||
.from(table)
|
||||
.select(selectFields)
|
||||
.order('id', { ascending: true })
|
||||
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)
|
||||
|
||||
if (error) {
|
||||
console.error(`Error fetching ${table}:`, error)
|
||||
break
|
||||
}
|
||||
|
||||
data = data.concat(pageData)
|
||||
|
||||
if (pageData.length < PAGE_SIZE) {
|
||||
hasMoreRecords = false
|
||||
} else {
|
||||
page++
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export default async function () {
|
||||
const artists = await fetchPaginatedData('artists', 'mbid, name_string, image, genre, total_plays, country, description, favorite')
|
||||
const albums = await fetchPaginatedData('albums', 'mbid, name, release_year, artist_mbid, total_plays')
|
||||
|
||||
const albumsByArtist = albums.reduce((acc, album) => {
|
||||
if (!acc[album.artist_mbid]) acc[album.artist_mbid] = []
|
||||
acc[album.artist_mbid].push({
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
release_year: album.release_year,
|
||||
total_plays: album.total_plays > 0 ? album.total_plays : '-'
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
artists.forEach(artist => {
|
||||
artist.albums = albumsByArtist[artist.mbid]?.sort((a, b) => a['release_year'] - b['release_year']) || []
|
||||
artist.country = parseCountryField(artist.country)
|
||||
})
|
||||
|
||||
return artists
|
||||
}
|
|
@ -51,7 +51,7 @@ export default async function () {
|
|||
lastWatched: item['last_watched'],
|
||||
dateAdded: item['last_watched'],
|
||||
year: item['year'],
|
||||
url: `https://coryd.dev/movies/${item['tmdb_id']}`,
|
||||
url: `https://coryd.dev/watching/movies/${item['tmdb_id']}`,
|
||||
description: `${item['title']} (${item['year']})<br/>Watched at: ${DateTime.fromISO(item['last_watched'], { zone: 'utc' }).setZone('America/Los_Angeles').toFormat('MMMM d, yyyy, h:mma')}`,
|
||||
image: `https://coryd.dev/media/movies/poster-${item['tmdb_id']}.jpg`,
|
||||
backdrop: `https://coryd.dev/media/movies/backdrops/backdrop-${item['tmdb_id']}.jpg`,
|
||||
|
|
|
@ -1,10 +1,32 @@
|
|||
import { createClient } from '@supabase/supabase-js'
|
||||
import { DateTime } from 'luxon'
|
||||
import slugify from 'slugify'
|
||||
|
||||
const SUPABASE_URL = process.env.SUPABASE_URL
|
||||
const SUPABASE_KEY = process.env.SUPABASE_KEY
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
const slugifyString = (str) => slugify(str, {
|
||||
replacement: '-',
|
||||
remove: /[#,&,+()$~%.'":*?<>{}]/g,
|
||||
lower: true,
|
||||
})
|
||||
|
||||
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' })
|
||||
const getCountryName = (countryCode) => regionNames.of(countryCode.trim()) || countryCode.trim()
|
||||
const parseCountryField = (countryField) => {
|
||||
if (!countryField) return null
|
||||
|
||||
const delimiters = [',', '/', '&', 'and']
|
||||
let countries = [countryField]
|
||||
|
||||
delimiters.forEach(delimiter => {
|
||||
countries = countries.flatMap(country => country.split(delimiter))
|
||||
})
|
||||
|
||||
return countries.map(getCountryName).join(', ')
|
||||
}
|
||||
|
||||
const fetchDataForPeriod = async (startPeriod, fields, table) => {
|
||||
const PAGE_SIZE = 1000
|
||||
let rows = []
|
||||
|
@ -68,22 +90,22 @@ const aggregateData = (data, groupByField, groupByType) => {
|
|||
aggregation[key] = {
|
||||
title: item[groupByField],
|
||||
plays: 0,
|
||||
mbid: item['albums']?.mbid || '',
|
||||
url: item['albums']?.mbid ? `https://musicbrainz.org/release/${item['albums'].mbid}` : `https://musicbrainz.org/search?query=${encodeURIComponent(item['album_name'])}&type=release`,
|
||||
image: item['albums']?.image || '',
|
||||
mbid: item['albums']['mbid'],
|
||||
url: `https://coryd.dev/music/artists/${slugifyString(item['artist_name'])}-${slugifyString(parseCountryField(item['artists']['country']))}`,
|
||||
image: item['albums']?.['image'] || '',
|
||||
timestamp: item['listened_at'],
|
||||
type: groupByType,
|
||||
genre: item['artists']?.genre || 'Unknown'
|
||||
genre: item['artists']?.['genre'] || ''
|
||||
}
|
||||
} else {
|
||||
aggregation[key] = {
|
||||
title: item[groupByField],
|
||||
plays: 0,
|
||||
mbid: item[groupByType]?.mbid || '',
|
||||
url: item[groupByType]?.mbid ? `https://musicbrainz.org/${groupByType === 'albums' ? 'release' : 'artist'}/${item[groupByType].mbid}` : `https://musicbrainz.org/search?query=${encodeURIComponent(item[groupByField])}&type=${groupByType === 'albums' ? 'release' : 'artist'}`,
|
||||
mbid: item[groupByType]?.['mbid'] || '',
|
||||
url: `https://coryd.dev/music/artists/${slugifyString(item['artist_name'])}-${slugifyString(parseCountryField(item['artists']['country']))}`,
|
||||
image: item[groupByType]?.image || '',
|
||||
type: groupByType,
|
||||
genre: item['artists']?.genre || 'Unknown'
|
||||
genre: item['artists']?.['genre'] || ''
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
@ -129,7 +151,7 @@ export default async function() {
|
|||
album_name,
|
||||
album_key,
|
||||
listened_at,
|
||||
artists (mbid, image, genre),
|
||||
artists (mbid, image, genre, country),
|
||||
albums (mbid, image)
|
||||
`
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ export default async function () {
|
|||
|
||||
showEpisodesMap[showTmdbId].episodes.push({
|
||||
name: showTitle,
|
||||
url: `https://coryd.dev/shows/${showTmdbId}`,
|
||||
url: `https://coryd.dev/watching/shows/${showTmdbId}`,
|
||||
subtext: `${showTitle} • S${seasonNumber}E${episodeNumber}`,
|
||||
episode: episodeNumber,
|
||||
season: seasonNumber,
|
||||
|
@ -114,7 +114,7 @@ export default async function () {
|
|||
if (show.episodes.length > 1) {
|
||||
episodeData.push({
|
||||
name: show.title,
|
||||
url: `https://coryd.dev/shows/${show['tmdbId']}`,
|
||||
url: `https://coryd.dev/watching/shows/${show['tmdbId']}`,
|
||||
subtext: `S${startingSeason}E${startingEpisode} - S${endingSeason}E${endingEpisode}`,
|
||||
startingEpisode,
|
||||
startingSeason,
|
||||
|
@ -140,7 +140,7 @@ export default async function () {
|
|||
|
||||
const favoriteShows = shows.filter(show => show['favorite'])
|
||||
const collectedShows = shows.filter(show => show['collected'])
|
||||
const toWatch = shows.map(show => ({...show, url: `https://coryd.dev/shows/${show['tmdb_id']}`})).filter(show => !show.episodes.some(episode => episode.last_watched_at)).sort((a, b) => a['title'].localeCompare(b['title']))
|
||||
const toWatch = shows.map(show => ({...show, url: `https://coryd.dev/watching/shows/${show['tmdb_id']}`})).filter(show => !show.episodes.some(episode => episode.last_watched_at)).sort((a, b) => a['title'].localeCompare(b['title']))
|
||||
|
||||
return {
|
||||
shows,
|
||||
|
|
14
src/assets/scripts/text-toggle.js
Normal file
14
src/assets/scripts/text-toggle.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
window.onload = () => {
|
||||
const button = document.querySelector('[data-toggle-button]')
|
||||
const content = document.querySelector('[data-toggle-content]')
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
if (content.classList.contains('text-toggle-hidden')) {
|
||||
content.classList.remove('text-toggle-hidden')
|
||||
button.textContent = 'Show less'
|
||||
} else {
|
||||
content.classList.add('text-toggle-hidden')
|
||||
button.textContent = 'Show more'
|
||||
}
|
||||
});
|
||||
}
|
|
@ -210,19 +210,17 @@ hr {
|
|||
/* tables */
|
||||
table {
|
||||
display: block;
|
||||
overflow-x: scroll;
|
||||
overscroll-behavior: contain;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
max-width: fit-content;
|
||||
margin: 0 auto;
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--gray-light);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:is(th, td):not(:last-child) {
|
||||
|
@ -237,8 +235,13 @@ tr:not(:last-child) {
|
|||
th,
|
||||
td {
|
||||
padding: var(--sizing-sm);
|
||||
min-width: calc(var(--sizing-3xl) * 2);
|
||||
min-width: max-content;
|
||||
word-break: break-word;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td {
|
||||
min-width: calc(var(--sizing-3xl) * 2);
|
||||
}
|
||||
|
||||
th {
|
||||
|
@ -342,6 +345,7 @@ nav .active svg {
|
|||
.coffee svg { stroke: var(--brand-buy-me-a-coffee) !important; }
|
||||
.heart-handshake svg { stroke: var(--webrings) !important; }
|
||||
.rss svg { stroke: var(--brand-rss) !important; }
|
||||
.favorite svg { stroke: var(--favorite) !important }
|
||||
|
||||
/* layout */
|
||||
.default-wrapper {
|
||||
|
|
21
src/assets/styles/components/text-toggle.css
Normal file
21
src/assets/styles/components/text-toggle.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
[data-toggle-content].text-toggle-hidden {
|
||||
position: relative;
|
||||
height: calc(var(--sizing-3xl) * 5);
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--sizing-base);
|
||||
}
|
||||
|
||||
[data-toggle-content].text-toggle-hidden::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: inset 0 -100px 50px -60px var(--background-color);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
[data-toggle-button] {
|
||||
margin-bottom: var(--sizing-base) !important;
|
||||
}
|
|
@ -60,6 +60,7 @@
|
|||
--webrings: #da70d6;
|
||||
--moon: #6a5acd;
|
||||
--sun: #ffa500;
|
||||
--favorite: #ff69b4;
|
||||
|
||||
/* fonts */
|
||||
--font-mono: MonoLisa, Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, ui-monospace, monospace;
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
@import url('./pages/blogroll.css') layer(page);
|
||||
@import url('./pages/contact.css') layer(page);
|
||||
@import url('./pages/links.css') layer(page);
|
||||
@import url('./pages/music.css') layer(page);
|
||||
@import url('./pages/post.css') layer(page);
|
||||
@import url('./pages/watching.css') layer(page);
|
||||
@import url('./pages/webrings.css') layer(page);
|
||||
|
@ -37,5 +38,6 @@
|
|||
@import url('./components/paginator.css') layer(components);
|
||||
@import url('./components/progress-bar.css') layer(components);
|
||||
@import url('./components/share-button.css') layer(components);
|
||||
@import url('./components/text-toggle.css') layer(components);
|
||||
@import url('./components/theme-toggle.css') layer(components);
|
||||
@import url('./components/music-chart.css') layer(components);
|
57
src/assets/styles/pages/music.css
Normal file
57
src/assets/styles/pages/music.css
Normal file
|
@ -0,0 +1,57 @@
|
|||
.artist-focus {
|
||||
border-bottom: 0;
|
||||
|
||||
& img {
|
||||
border: 1px solid var(--accent-color);
|
||||
}
|
||||
|
||||
& .artist-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sizing-xs);
|
||||
margin-top: var(--sizing-base);
|
||||
margin-bottom: var(--sizing-base);
|
||||
|
||||
.artist-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sizing-xs);
|
||||
|
||||
& p {
|
||||
&.title {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
&.sub-meta {
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
}
|
||||
|
||||
&.title,
|
||||
&.sub-meta {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.sub-meta svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: var(--sizing-xs);
|
||||
}
|
||||
|
||||
&.favorite {
|
||||
color: var(--favorite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.artist-focus {
|
||||
& .artist-display {
|
||||
margin-top: 0;
|
||||
flex-direction: row;
|
||||
gap: var(--sizing-md);
|
||||
}
|
||||
}
|
||||
}
|
54
src/pages/main/music/artists/artist.html
Normal file
54
src/pages/main/music/artists/artist.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
layout: default
|
||||
pagination:
|
||||
data: artists
|
||||
size: 1
|
||||
alias: artist
|
||||
permalink: /music/artists/{{ artist.name_string | slugify | downcase }}-{{ artist.country | slugify | downcase}}/
|
||||
schema: artist
|
||||
---
|
||||
{%- capture alt -%}
|
||||
{{ artist.name_string }} • {{ artist.country }}
|
||||
{%- endcapture -%}
|
||||
{% capture js %}
|
||||
{% render "../../../../assets/scripts/text-toggle.js" %}
|
||||
{% endcapture %}
|
||||
<script>{{ js }}</script>
|
||||
<noscript><style>[data-toggle-content].text-toggle-hidden {height: unset !important;overflow: unset !important;margin-bottom: unset !important;}[data-toggle-content].text-toggle-hidden::after {display: none !important;}</style></noscript>
|
||||
<a class="back-link-header link-icon flex-centered" href="/music">{% tablericon "arrow-left" "Go back" %} Go back</a>
|
||||
<article class="artist-focus">
|
||||
<div class="artist-display">
|
||||
<img src="https://coryd.dev/.netlify/images/?url={{ artist.image }}&fm=webp&q=85&w=240&h=240&fit=cover" alt="{{ alt }}" loading="eager" decoding="async" width="240" height="240" />
|
||||
<div class="artist-meta">
|
||||
<p class="title"><strong>{{ artist.name_string }}</strong></p>
|
||||
{%- if artist.favorite -%}
|
||||
<p class="sub-meta favorite flex-centered">{% tablericon "heart" "Favorite" %} This is one of my favorite artists!</p>
|
||||
{%- endif -%}
|
||||
{%- if artist.total_plays > 0 -%}
|
||||
<p class="sub-meta"><strong class="highlight-text">{{ artist.total_plays }} plays</strong></p>
|
||||
{%- endif -%}
|
||||
<p class="sub-meta">{{ artist.genre }}</p>
|
||||
<p class="sub-meta">
|
||||
<a class="brain" href="https://musicbrainz.org/artist/{{ artist.mbid }}">{% tablericon "brain" "MusicBrainz" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{%- if artist.description -%}
|
||||
<div data-toggle-content class="text-toggle-hidden">{{ artist.description | markdown }}</div>
|
||||
<button data-toggle-button>Show more</button>
|
||||
{%- endif -%}
|
||||
<table>
|
||||
<tr>
|
||||
<th>Year</th>
|
||||
<th>Title</th>
|
||||
<th>Plays</th>
|
||||
</tr>
|
||||
{% for album in artist.albums %}
|
||||
<tr>
|
||||
<td>{{ album.release_year }}</td>
|
||||
<td>{{ album.name }}</td>
|
||||
<td>{{ album.total_plays }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</article>
|
|
@ -4,7 +4,7 @@ pagination:
|
|||
data: movies.movies
|
||||
size: 1
|
||||
alias: movie
|
||||
permalink: /movies/{{ movie.id }}/
|
||||
permalink: /watching/movies/{{ movie.id }}/
|
||||
schema: movie
|
||||
---
|
||||
{%- capture alt -%}
|
||||
|
|
|
@ -4,7 +4,7 @@ pagination:
|
|||
data: tv.shows
|
||||
size: 1
|
||||
alias: show
|
||||
permalink: /shows/{{ show.tmdb_id }}/
|
||||
permalink: /watching/shows/{{ show.tmdb_id }}/
|
||||
schema: show
|
||||
---
|
||||
{%- capture alt -%}
|
||||
|
|
Reference in a new issue