A website inspired by Last.fm that will keep track of your listening statistics
lastfm music statistics
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add pages on frontend, and add artist list item component.

oscar345 891a3c20 7da85e14

+178 -43
+1 -1
internal/web/router/router.go
··· 87 87 return 88 88 } 89 89 90 - s.inertia.Render(w, r, "library/Artists", inertia.Props{ 90 + s.inertia.Render(w, r, "library/artists/Index", inertia.Props{ 91 91 "artists": inertia.Always(responses.Paginate(page, enum.Map(artists, responses.NewArtistFromModel))), 92 92 }) 93 93 })
+31
pkg/inertia/prop.go
··· 17 17 DeferredKey string 18 18 Default bool 19 19 isError bool 20 + Scroll ScrollMetadata 21 + } 22 + 23 + type ScrollMetadata struct { 24 + Name string 25 + PreviousPage int 26 + NextPage int 27 + CurrentPage int 20 28 } 21 29 22 30 func (p *Prop) ShouldDefer() bool { ··· 79 87 return Prop{Value: func() (any, error) { return value, nil }, Always: true} 80 88 } 81 89 90 + func Scroll(value any, current, previous, next int, opts ...PropOption) Prop { 91 + prop := Prop{ 92 + Value: func() (any, error) { return value, nil }, 93 + Default: true, 94 + Scroll: ScrollMetadata{ 95 + CurrentPage: current, 96 + NextPage: next, 97 + PreviousPage: previous, 98 + }} 99 + 100 + for _, opt := range opts { 101 + opt(&prop) 102 + } 103 + 104 + return prop 105 + } 106 + 82 107 func WithDeferredKey(key string) PropOption { 83 108 return func(p *Prop) { 84 109 p.DeferredKey = key 110 + } 111 + } 112 + 113 + func WithPageName(name string) PropOption { 114 + return func(p *Prop) { 115 + p.Scroll.Name = name 85 116 } 86 117 } 87 118
+18
pkg/pagination/pagination.go
··· 6 6 Size int 7 7 } 8 8 9 + // If there is a previous page, returns the page number, otherwise returns 0 10 + func (p *Page) GetPrevious() int { 11 + if p.Page == 1 { 12 + return 0 13 + } 14 + 15 + return p.Page - 1 16 + } 17 + 18 + // If there is a next page, returns the page number, otherwise returns 0 19 + func (p *Page) GetNext() int { 20 + if (p.Page+1)*p.Size > p.Total { 21 + return 0 22 + } 23 + 24 + return p.Page + 1 25 + } 26 + 9 27 func New(filter Filter, total int) Page { 10 28 return Page{ 11 29 Total: total,
+55
web/components/catalog/ArtistListItem.svelte
··· 1 + <script lang="ts"> 2 + import { GET_ArtistsByID } from "$routes"; 3 + import { type Artist } from "$schemas/responses"; 4 + import { Link } from "@inertiajs/svelte"; 5 + 6 + type Props = { 7 + artist: Artist; 8 + leading?: "image" | "none"; 9 + trailing?: "scrobbles" | "none"; 10 + }; 11 + 12 + let { artist, leading = "image", trailing = "scrobbles" }: Props = $props(); 13 + </script> 14 + 15 + <article> 16 + {#if leading === "image" && artist.image_url !== undefined} 17 + <img src={artist.image_url} alt={artist.name} /> 18 + {/if} 19 + 20 + <div class="text"> 21 + <Link href={GET_ArtistsByID(artist.mbid)}> 22 + {artist.name} 23 + </Link> 24 + </div> 25 + 26 + {#if trailing === "scrobbles"} 27 + <div class="scrobbles"> 28 + {artist.count} 29 + </div> 30 + {/if} 31 + </article> 32 + 33 + <style> 34 + article { 35 + display: grid; 36 + grid-template-columns: var(--spacing-10) 1fr max-content; 37 + gap: var(--spacing-2); 38 + } 39 + 40 + article > .text { 41 + grid-column-start: 1; 42 + grid-column-end: 4; 43 + display: flex; 44 + flex-direction: column; 45 + justify-content: space-around; 46 + } 47 + 48 + article:has(> img) > .text { 49 + grid-column-start: 2; 50 + } 51 + 52 + article:has(> .scrobbles) > .text { 53 + grid-column-end: 3; 54 + } 55 + </style>
+1
web/components/layouts/library/Layout.svelte
··· 21 21 .view { 22 22 display: grid; 23 23 grid-template-columns: 1fr 3fr; 24 + column-gap: var(--spacing-8); 24 25 } 25 26 </style>
+6 -14
web/styles/app.css
··· 5 5 @import "tailwindcss/utilities.css" layer(utilities); 6 6 7 7 @import "./colors.css" layer(theme); 8 - 9 - body { 10 - background-color: var(--color-base-100); 11 - } 12 - 13 - @source not inline('container'); 8 + @import "./typography.css" layer(components); 14 9 15 - .container { 10 + @utility container { 16 11 padding-inline: var(--spacing-4); 17 - 18 - @media (width >= 48rem) { 19 - padding-inline: var(--spacing-6); 20 - } 12 + margin-inline: auto; 13 + } 21 14 22 - @media (width >= 64rem) { 23 - padding-inline: var(--spacing-8); 24 - } 15 + body { 16 + background-color: var(--color-base-100); 25 17 } 26 18 27 19 @theme {
+34
web/styles/typography.css
··· 1 + .h1 { 2 + font-size: var(--text-2xl); 3 + line-height: var(--leading-normal); 4 + font-weight: var(--font-weight-medium); 5 + color: var(--color-content-100); 6 + } 7 + 8 + .h2 { 9 + font-size: var(--text-lg); 10 + line-height: var(--leading-snug); 11 + font-weight: var(--font-weight-medium); 12 + color: var(--color-content-100); 13 + } 14 + 15 + .h3 { 16 + font-size: var(--text-sm); 17 + line-height: var(--leading-tight); 18 + font-weight: var(--font-weight-semibold); 19 + color: var(--color-content-100); 20 + text-transform: uppercase; 21 + } 22 + 23 + .subtitle { 24 + font-size: var(--text-sm); 25 + line-height: var(--text-sm--line-height); 26 + font-weight: var(--font-weight-normal); 27 + color: var(--color-content-300); 28 + } 29 + 30 + /* Automatically add spacing between hgroup children, unless the hgroup has the class no-hgroup */ 31 + hgroup:not(.no-hgroup) { 32 + display: flex; 33 + flex-direction: column; 34 + }
-7
web/tsconfig.json
··· 16 16 }, 17 17 }, 18 18 "exclude": ["./node_modules/**"], 19 - "include": [ 20 - "lib", 21 - "lib/**/*.ts", 22 - "lib/.gen/**/*.ts", 23 - "**/*.svelte", 24 - "esbuild.config.ts", 25 - ], 26 19 }
-21
web/views/library/Artists.svelte
··· 1 - <script lang="ts" module> 2 - import { default as Base } from "$components/layouts/Layout.svelte"; 3 - import { default as Web } from "$components/layouts/web/Layout.svelte"; 4 - import Layout from "$components/layouts/library/Layout.svelte"; 5 - 6 - export const layout = [Base, Web, Layout]; 7 - </script> 8 - 9 - <script lang="ts"> 10 - import type { Page, Artist } from "$schemas/responses"; 11 - 12 - type Props = { 13 - artists: Page<Artist>; 14 - }; 15 - 16 - let { artists }: Props = $props(); 17 - </script> 18 - 19 - <header> 20 - <h1>Artists</h1> 21 - </header>
+32
web/views/library/artists/Index.svelte
··· 1 + <script lang="ts" module> 2 + import { default as Base } from "$components/layouts/Layout.svelte"; 3 + import { default as Web } from "$components/layouts/web/Layout.svelte"; 4 + import Layout from "$components/layouts/library/Layout.svelte"; 5 + 6 + export const layout = [Base, Web, Layout]; 7 + </script> 8 + 9 + <script lang="ts"> 10 + import type { Page, Artist } from "$schemas/responses"; 11 + import ArtistListItem from "$components/catalog/ArtistListItem.svelte"; 12 + 13 + type Props = { 14 + artists: Page<Artist>; 15 + }; 16 + 17 + let { artists }: Props = $props(); 18 + </script> 19 + 20 + <div class="content"> 21 + <header class="header"> 22 + <hgroup> 23 + <h1 class="h1">Artists</h1> 24 + </hgroup> 25 + </header> 26 + 27 + <section> 28 + {#each artists.items as item} 29 + <ArtistListItem artist={item} /> 30 + {/each} 31 + </section> 32 + </div>
web/views/mixtapes/Form.svelte

This is a binary file and will not be displayed.

web/views/mixtapes/Index.svelte

This is a binary file and will not be displayed.

web/views/mixtapes/Show.svelte

This is a binary file and will not be displayed.