WIP. A little custom music server
0
fork

Configure Feed

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

feat: add album grid

+111 -11
+31
web/src/components/album-card.tsx
··· 1 + import { Link } from "waku" 2 + 3 + type Props = { 4 + album: Readonly<{ 5 + id: string 6 + title: string 7 + artists: ReadonlyArray<{ 8 + id: string 9 + name: string, 10 + }> 11 + }> 12 + } 13 + 14 + const mockCover = "https://writteninmusic.com/wp-content/uploads/2025/09/TOP-Breach.jpg" 15 + export function AlbumCard(props: Props) { 16 + const { id, title, artists } = props.album; 17 + 18 + return ( 19 + <Link 20 + to={`/album/${id}`} 21 + className="space-y-px p-3 transition duration-100 hover:bg-black/10 h-fit rounded-lg"> 22 + <div className="aspect-square rounded-md overflow-hidden"> 23 + <img width={900} height={900} src={mockCover} className="object-cover w-full h-full" /> 24 + </div> 25 + <p className="text-md font-medium">{title}</p> 26 + <p className="text-sm opacity-70">{artists.at(0)?.name}</p> 27 + </Link> 28 + ) 29 + } 30 + 31 +
+11
web/src/lib/errors.ts
··· 1 + import { Data } from "effect"; 2 + 3 + export class FetchFailedError extends Data.TaggedError("FetchFailedError")<{ 4 + message: string; 5 + cause?: unknown; 6 + }> {} 7 + 8 + export class JsonParseError extends Data.TaggedError("JsonParseError")<{ 9 + message: string; 10 + cause?: unknown; 11 + }> {}
+1
web/src/pages.gen.ts
··· 13 13 // prettier-ignore 14 14 type Page = 15 15 | ({ path: '/' } & GetConfigResponse<typeof File_Index_getConfig>) 16 + | { path: '/album'; render: 'dynamic' } 16 17 | ({ path: '/album/[id]' } & GetConfigResponse<typeof File_AlbumId_getConfig>) 17 18 | ({ path: '/about' } & GetConfigResponse<typeof File_About_getConfig>); 18 19
+2 -11
web/src/pages/album/[id].tsx
··· 1 1 import { SongRow } from "@/components/song-row"; 2 - import { Console, Data, Effect, Schema } from "effect"; 3 - import { Struct } from "effect/Schema"; 2 + import { FetchFailedError, JsonParseError } from "@/lib/errors"; 3 + import { Console, Effect, Schema } from "effect"; 4 4 import { Link } from "waku"; 5 5 import { PageProps } from "waku/router"; 6 6 ··· 63 63 }; 64 64 } 65 65 66 - class FetchFailedError extends Data.TaggedError("FetchFailedError")<{ 67 - message: string; 68 - cause?: unknown; 69 - }> { } 70 - 71 - class JsonParseError extends Data.TaggedError("JsonParseError")<{ 72 - message: string; 73 - cause?: unknown; 74 - }> { } 75 66 76 67 const ArtistSchema = Schema.Struct({ 77 68 id: Schema.NonEmptyString,
+66
web/src/pages/album/index.tsx
··· 1 + import { AlbumCard } from "@/components/album-card"; 2 + import { FetchFailedError, JsonParseError } from "@/lib/errors"; 3 + import { Console, Effect, Schema } from "effect"; 4 + import { divide } from "effect/Duration"; 5 + 6 + 7 + const ArtistSchema = Schema.Struct({ 8 + id: Schema.NonEmptyString, 9 + name: Schema.NonEmptyString, 10 + }); 11 + 12 + const AlbumSchema = Schema.Struct({ 13 + id: Schema.NonEmptyString, 14 + title: Schema.NonEmptyString, 15 + cover: Schema.UndefinedOr(Schema.String), // TODO: enforce cover to be string, or atleast String | Null 16 + artists: Schema.Array(ArtistSchema) 17 + }); 18 + 19 + const getAlbums = () => Effect.gen(function*() { 20 + 21 + const request = yield* Effect.tryPromise({ 22 + try: () => fetch(`http://localhost:3003/albums`), 23 + catch: (err) => 24 + new FetchFailedError({ 25 + cause: err, 26 + message: "Failed to fetch album", 27 + }), 28 + }); 29 + 30 + const json = yield* Effect.tryPromise({ 31 + try: () => request.json().then((x) => x as unknown), 32 + catch: (err) => 33 + new JsonParseError({ 34 + cause: err, 35 + message: "Failed to parse json", 36 + }), 37 + }); 38 + yield* Console.dir(json, { depth: 3 }) 39 + 40 + const parsed = yield* Schema.decodeUnknown(Schema.Array(AlbumSchema))(json); 41 + return parsed 42 + //return Array.from({ length: 15 }).map(() => parsed).flat(); 43 + }) 44 + 45 + const AlbumPage = () => Effect.runPromise(Effect.gen(function*() { 46 + 47 + 48 + const albums = yield* getAlbums() 49 + 50 + return ( 51 + <div className="container"> 52 + <div className="grid lg:grid-cols-6 grid-cols-1"> 53 + 54 + 55 + {albums.map(album => <AlbumCard 56 + album={album} 57 + 58 + />)} 59 + 60 + </div> 61 + </div> 62 + ) 63 + })) 64 + 65 + 66 + export default AlbumPage;