From e0036dd0a2a142a320730c9502958e7adaaf2a40 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Mon, 23 Mar 2026 00:26:49 +0000 Subject: [PATCH] feat(pkgs): add Popfeed card, migrate NoiseImage to @ewanc26/ui, add release script --- packages/atproto/package.json | 2 +- packages/atproto/src/fetch.ts | 59 +++++- packages/atproto/src/index.ts | 6 +- packages/atproto/src/types.ts | 43 ++++ packages/ui/package.json | 3 +- .../layout/main/card/MusicStatusCard.svelte | 18 +- .../layout/main/card/PopfeedCard.svelte | 122 ++++++++++++ .../layout/main/card/TangledRepoCard.svelte | 4 +- .../lib/components/layout/main/card/index.ts | 2 + .../src/lib/components/ui/NoiseImage.svelte | 50 +++++ packages/ui/src/lib/components/ui/index.ts | 1 + packages/ui/src/lib/index.ts | 4 +- pnpm-lock.yaml | 5 + release.sh | 186 ++++++++++++++++++ 14 files changed, 487 insertions(+), 18 deletions(-) create mode 100644 packages/ui/src/lib/components/layout/main/card/PopfeedCard.svelte create mode 100644 packages/ui/src/lib/components/ui/NoiseImage.svelte create mode 100644 release.sh diff --git a/packages/atproto/package.json b/packages/atproto/package.json index de8bf6b..20078f3 100644 --- a/packages/atproto/package.json +++ b/packages/atproto/package.json @@ -1,6 +1,6 @@ { "name": "@ewanc26/atproto", - "version": "0.2.5", + "version": "0.2.7", "description": "AT Protocol service layer extracted from ewancroft.uk", "author": "Ewan Croft", "license": "AGPL-3.0-only", diff --git a/packages/atproto/src/fetch.ts b/packages/atproto/src/fetch.ts index 8432150..b0c786a 100644 --- a/packages/atproto/src/fetch.ts +++ b/packages/atproto/src/fetch.ts @@ -9,7 +9,10 @@ import type { MusicStatusData, KibunStatusData, TangledRepo, - TangledReposData + TangledReposData, + PopfeedReview, + PopfeedCreativeWorkType, + PopfeedMainCreditRole } from './types.js'; export async function fetchProfile(did: string, fetchFn?: typeof fetch): Promise { @@ -306,6 +309,60 @@ export async function fetchKibunStatus( } } +export async function fetchRecentPopfeedReviews( + did: string, + limit = 5, + fetchFn?: typeof fetch +): Promise { + const cacheKey = `popfeed-reviews:${did}:${limit}`; + const cached = cache.get(cacheKey); + if (cached) return cached; + + try { + const records = await withFallback( + did, + async (agent) => { + const response = await agent.com.atproto.repo.listRecords({ + repo: did, + collection: 'social.popfeed.feed.review', + limit + }); + return response.data.records; + }, + true, + fetchFn + ); + + if (!records?.length) return []; + + const data: PopfeedReview[] = records.map((record) => { + const value = record.value as any; + const rkey = record.uri.split('/').pop() ?? record.uri; + return { + rkey, + uri: record.uri, + title: value.title, + creativeWorkType: value.creativeWorkType as PopfeedCreativeWorkType, + rating: value.rating, + text: value.text, + posterUrl: value.posterUrl, + mainCredit: value.mainCredit, + mainCreditRole: value.mainCreditRole as PopfeedMainCreditRole | undefined, + genres: value.genres, + tags: value.tags, + createdAt: value.createdAt, + containsSpoilers: value.containsSpoilers, + isRevisit: value.isRevisit + }; + }); + + cache.set(cacheKey, data); + return data; + } catch { + return []; + } +} + export async function fetchTangledRepos( did: string, fetchFn?: typeof fetch diff --git a/packages/atproto/src/index.ts b/packages/atproto/src/index.ts index 55dc1cd..d223ba7 100644 --- a/packages/atproto/src/index.ts +++ b/packages/atproto/src/index.ts @@ -4,6 +4,9 @@ // now accept `did: string` as their first argument. export type { + PopfeedReview, + PopfeedCreativeWorkType, + PopfeedMainCreditRole, ProfileData, StatusData, SiteInfoData, @@ -43,7 +46,8 @@ export { fetchLinks, fetchMusicStatus, fetchKibunStatus, - fetchTangledRepos + fetchTangledRepos, + fetchRecentPopfeedReviews } from './fetch.js'; export { diff --git a/packages/atproto/src/types.ts b/packages/atproto/src/types.ts index 978efba..f1c9d01 100644 --- a/packages/atproto/src/types.ts +++ b/packages/atproto/src/types.ts @@ -279,3 +279,46 @@ export interface StandardSiteDocument { export interface StandardSiteDocumentsData { documents: StandardSiteDocument[]; } + +export type PopfeedCreativeWorkType = + | 'movie' + | 'tv_show' + | 'video_game' + | 'album' + | 'book' + | 'book_series' + | 'episode' + | 'ep' + | 'tv_season' + | 'tv_episode' + | 'track'; + +export type PopfeedMainCreditRole = + | 'director' + | 'author' + | 'artist' + | 'showrunner' + | 'lead_actor' + | 'creator' + | 'studio' + | 'publisher' + | 'developer' + | 'performer' + | 'network'; + +export interface PopfeedReview { + rkey: string; + uri: string; + title?: string; + creativeWorkType: PopfeedCreativeWorkType; + rating: number; + text?: string; + posterUrl?: string; + mainCredit?: string; + mainCreditRole?: PopfeedMainCreditRole; + genres?: string[]; + tags?: string[]; + createdAt: string; + containsSpoilers?: boolean; + isRevisit?: boolean; +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 4bf4d81..83cc8cf 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@ewanc26/ui", - "version": "0.2.1", + "version": "0.3.8", "description": "Svelte UI component library extracted from ewancroft.uk — pluggable layout, UI primitives, stores, and SEO components.", "author": "Ewan Croft", "license": "AGPL-3.0-only", @@ -48,6 +48,7 @@ "check": "svelte-check --tsconfig ./tsconfig.json" }, "dependencies": { + "@ewanc26/noise-avatar": "workspace:*", "@lucide/svelte": "^0.577.0" }, "optionalDependencies": { diff --git a/packages/ui/src/lib/components/layout/main/card/MusicStatusCard.svelte b/packages/ui/src/lib/components/layout/main/card/MusicStatusCard.svelte index eb85b33..9b33cab 100644 --- a/packages/ui/src/lib/components/layout/main/card/MusicStatusCard.svelte +++ b/packages/ui/src/lib/components/layout/main/card/MusicStatusCard.svelte @@ -1,14 +1,13 @@ + +
+ {#if !reviews} + + {#snippet skeleton()} +
+
+ {#each Array(3) as _} +
+
+
+
+
+
+
+
+
+
+
+ {/each} +
+ {/snippet} +
+ {:else if reviews.length > 0} + + {#snippet children()} +

Recent Reviews

+
+ {#each reviews as review (review.rkey)} + + {#snippet children()} + +
+ +
+ + +
+ {#if review.creativeWorkType} +
+ + {TYPE_LABELS[review.creativeWorkType] ?? review.creativeWorkType} + +
+ {/if} + {#if review.title} +

+ {review.title} +

+ {/if} + {#if review.mainCredit} +

+ {review.mainCredit} +

+ {/if} +
+ +
+
+ + +
+
+ {/snippet} +
+ {/each} +
+ {/snippet} +
+ {/if} +
diff --git a/packages/ui/src/lib/components/layout/main/card/TangledRepoCard.svelte b/packages/ui/src/lib/components/layout/main/card/TangledRepoCard.svelte index e4dafb2..301dde4 100644 --- a/packages/ui/src/lib/components/layout/main/card/TangledRepoCard.svelte +++ b/packages/ui/src/lib/components/layout/main/card/TangledRepoCard.svelte @@ -1,5 +1,5 @@ + +{#if src && !failed} + (failed = true)} + /> +{:else} + +{/if} diff --git a/packages/ui/src/lib/components/ui/index.ts b/packages/ui/src/lib/components/ui/index.ts index 384c280..13345e9 100644 --- a/packages/ui/src/lib/components/ui/index.ts +++ b/packages/ui/src/lib/components/ui/index.ts @@ -1,3 +1,4 @@ +export { default as NoiseImage } from './NoiseImage.svelte'; export { default as Card } from './Card.svelte'; export { default as InternalCard } from './InternalCard.svelte'; export { default as Dropdown } from './Dropdown.svelte'; diff --git a/packages/ui/src/lib/index.ts b/packages/ui/src/lib/index.ts index 672e1cd..6ff552b 100644 --- a/packages/ui/src/lib/index.ts +++ b/packages/ui/src/lib/index.ts @@ -24,11 +24,11 @@ export { default as WolfToggle } from './components/layout/WolfToggle.svelte'; export { DynamicLinks, ScrollToTop, TangledRepos } from './components/layout/main/index.js'; // ─── Cards ──────────────────────────────────────────────────────────────────── -export { LinkCard, ProfileCard, PostCard, BlueskyPostCard, TangledRepoCard, MusicStatusCard, KibunStatusCard, FeedCard } from './components/layout/main/card/index.js'; +export { PopfeedCard, LinkCard, ProfileCard, PostCard, BlueskyPostCard, TangledRepoCard, MusicStatusCard, KibunStatusCard, FeedCard } from './components/layout/main/card/index.js'; export type { FeedItem } from './components/layout/main/card/index.js'; // ─── SEO ────────────────────────────────────────────────────────────────────── export { MetaTags } from './components/seo/index.js'; // ─── UI primitives ──────────────────────────────────────────────────────────── -export { Card, InternalCard, Dropdown, Pagination, SearchBar, Tabs, PostsGroupedView, DocumentCard, BlogPostCard } from './components/ui/index.js'; +export { NoiseImage, Card, InternalCard, Dropdown, Pagination, SearchBar, Tabs, PostsGroupedView, DocumentCard, BlogPostCard } from './components/ui/index.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad2de7d..e253953 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -377,6 +377,9 @@ importers: packages/ui: dependencies: + '@ewanc26/noise-avatar': + specifier: workspace:* + version: link:../noise-avatar '@lucide/svelte': specifier: ^0.577.0 version: 0.577.0(svelte@5.53.11) @@ -413,6 +416,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/wafrn-theme: {} + packages: '@atproto-labs/did-resolver@0.2.6': diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..3b5b480 --- /dev/null +++ b/release.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +# release.sh — build, version-bump, publish one or more packages, then optionally +# update downstream consumers. +# +# Usage: +# ./release.sh [options] [package-name ...] +# +# Options: +# -b, --bump Version bump type (default: patch) +# -d, --downstream [dir...] Paths to run `pnpm install` in after publish +# -w, --wait Registry propagation wait (default: 10) +# -n, --dry-run Print what would happen without doing it +# -h, --help Show this help +# +# Package names can be short ("ui", "atproto") or scoped ("@ewanc26/ui"). +# They are matched against the `name` field in each packages/*/package.json. +# +# Examples: +# ./release.sh ui +# ./release.sh --bump minor ui atproto +# ./release.sh --bump patch ui --downstream ../website + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGES_DIR="$SCRIPT_DIR/packages" + +# ── Defaults ────────────────────────────────────────────────────────────────── +BUMP="patch" +DOWNSTREAM=() +WAIT=10 +DRY_RUN=false +TARGETS=() + +# ── Colours ─────────────────────────────────────────────────────────────────── +bold="\033[1m" +green="\033[32m" +yellow="\033[33m" +red="\033[31m" +reset="\033[0m" + +info() { echo -e "${bold} →${reset} $*"; } +success() { echo -e "${green} ✓${reset} $*"; } +warn() { echo -e "${yellow} ⚠${reset} $*"; } +die() { echo -e "${red} ✗${reset} $*" >&2; exit 1; } +dryrun() { echo -e "${yellow} [dry-run]${reset} $*"; } + +# ── Argument parsing ────────────────────────────────────────────────────────── +while [[ $# -gt 0 ]]; do + case "$1" in + -b|--bump) + BUMP="$2"; shift 2 + [[ "$BUMP" =~ ^(patch|minor|major)$ ]] || die "Invalid bump type: $BUMP" + ;; + -d|--downstream) + shift + while [[ $# -gt 0 && ! "$1" =~ ^- ]]; do + DOWNSTREAM+=("$1"); shift + done + ;; + -w|--wait) + WAIT="$2"; shift 2 + ;; + -n|--dry-run) + DRY_RUN=true; shift + ;; + -h|--help) + sed -n '3,20p' "$0" | sed 's/^# \?//' + exit 0 + ;; + -*) + die "Unknown option: $1" + ;; + *) + TARGETS+=("$1"); shift + ;; + esac +done + +[[ ${#TARGETS[@]} -eq 0 ]] && die "No packages specified. Run with --help for usage." + +# ── Resolve package directories ─────────────────────────────────────────────── +resolve_package() { + local target="$1" + # Strip leading @scope/ if present, try both directory name and package.json name + for pkg_dir in "$PACKAGES_DIR"/*/; do + [[ -f "$pkg_dir/package.json" ]] || continue + local name dir_name + name="$(node -p "require('$pkg_dir/package.json').name" 2>/dev/null || true)" + dir_name="$(basename "$pkg_dir")" + if [[ "$name" == "$target" || "$dir_name" == "$target" || "$name" == "@ewanc26/$target" ]]; then + echo "$pkg_dir" + return + fi + done + die "Could not find package matching: $target" +} + +# ── Main loop ───────────────────────────────────────────────────────────────── +RELEASED=() + +for target in "${TARGETS[@]}"; do + pkg_dir="$(resolve_package "$target")" + pkg_name="$(node -p "require('$pkg_dir/package.json').name")" + current_ver="$(node -p "require('$pkg_dir/package.json').version")" + + echo "" + info "Package: ${bold}$pkg_name${reset} (${pkg_dir##"$SCRIPT_DIR/"})" + info "Current: $current_ver" + info "Bump: $BUMP" + + if $DRY_RUN; then + dryrun "Would run: cd $pkg_dir && npm version $BUMP --no-git-tag-version" + dryrun "Would run: pnpm --filter $pkg_name build" + dryrun "Would run: pnpm --filter $pkg_name publish --no-git-checks" + RELEASED+=("$pkg_name") + continue + fi + + # Bump version + new_ver="$(cd "$pkg_dir" && npm version "$BUMP" --no-git-tag-version | tr -d 'v')" + success "Bumped to $new_ver" + + # Build + info "Building..." + pnpm --filter "$pkg_name" build + success "Built" + + # Publish + info "Publishing..." + pnpm --filter "$pkg_name" publish --no-git-checks + success "Published $pkg_name@$new_ver" + + RELEASED+=("$pkg_name@$new_ver") +done + +# ── Downstream updates ──────────────────────────────────────────────────────── +if [[ ${#DOWNSTREAM[@]} -gt 0 ]]; then + if $DRY_RUN; then + dryrun "Would wait ${WAIT}s for registry propagation" + for dir in "${DOWNSTREAM[@]}"; do + dryrun "Would run: cd $dir && pnpm install --registry https://registry.npmjs.org" + done + else + echo "" + for dir in "${DOWNSTREAM[@]}"; do + abs_dir="$(cd "$SCRIPT_DIR" && cd "$dir" && pwd)" + pkg_name_there="$(node -p "require('$abs_dir/package.json').name" 2>/dev/null || echo "$dir")" + info "Updating downstream: ${bold}$pkg_name_there${reset}" + + # For each released package, update the constraint in package.json if pinned + for released in "${RELEASED[@]}"; do + pkg="${released%@*}" # e.g. @ewanc26/ui + ver="${released##*@}" # e.g. 0.3.4 + escaped_pkg="$(echo "$pkg" | sed 's/[\/&]/\\&/g')" + sed -i '' "s/\"$escaped_pkg\": \"\^[0-9]*\.[0-9]*\.[0-9]*\"/\"$escaped_pkg\": \"^$ver\"/" \ + "$abs_dir/package.json" 2>/dev/null || true + done + + # Retry install until the registry has propagated the new versions + _attempt=0 + _max=10 + while true; do + _attempt=$((_attempt + 1)) + info "Install attempt $_attempt/$_max..." + if (cd "$abs_dir" && pnpm install --registry https://registry.npmjs.org 2>&1); then + success "Updated $pkg_name_there" + break + fi + if [[ $_attempt -ge $_max ]]; then + die "Failed to install after $_max attempts. Registry may still be propagating — try again in a moment." + fi + warn "Not yet available, waiting ${WAIT}s..." + sleep "$WAIT" + done + done + fi +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +echo "" +echo -e "${bold}Released:${reset}" +for r in "${RELEASED[@]}"; do + echo -e " ${green}✓${reset} $r" +done +echo "" -- 2.51.2