Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,23 @@ StemDeck is free and **does not accept any money, sponsorship, or funding** fro

| Category | Name | What they do | Link |
|---|---|---|---|
| Artists & Creators | Joao Gaspar | Producer, film scorer, touring/session musician | [@jay_glaspar](https://www.instagram.com/jay_glaspar) |
| Artists & Creators | Killah Trakz | Industry Secret Villain and Media Influencer | [@killahtrakz](https://www.instagram.com/killahtrakz/) |
| Artists & Creators | More Notes Less Talk | Gear-focused creative project with a raw, tape-recorded identity | [@morenoteslesstalk](https://www.youtube.com/@morenoteslesstalk) |
| Artists & Creators | Adrianna Claro | Soul and Contemporary R&B Singer, Songwriter and Composer | [Spotify](https://open.spotify.com/album/10fZ48iJhIe3lWcXSMUlFG) · [Apple Music](https://music.apple.com/us/album/happy/1592026727?i=1592026728) |
| Artists & Creators | Analog4Lyfe | Analog gear specialist | [@analog4lyfe](https://www.instagram.com/analog4lyfe) |
| Artists & Creators | Dead röses | Cork-based punk rock band | [@dead_rosesband](https://www.instagram.com/dead_rosesband) |
| Artists & Creators | NIHIL | Modern alternative metal from Argentina. Fueled by emotionally charged vocals, anger and emptiness | [@somosnihil](https://www.instagram.com/somosnihil/) |
| Artists & Creators | Joao Gaspar | Producer, film scorer, touring/session musician | [@jay_glaspar](https://www.instagram.com/jay_glaspar) |
| Artists & Creators | Killah Trakz | Industry Secret Villain and Media Influencer | [@killahtrakz](https://www.instagram.com/killahtrakz/) · [Spotify](https://open.spotify.com/artist/6nePYOqT4E7D4C48twPtKk) · [Apple Music](https://music.apple.com/us/artist/killah-trakz/371296633) |
| Artists & Creators | More Notes Less Talk | Gear-focused creative project with a raw, tape-recorded identity | [@morenoteslesstalk](https://www.youtube.com/@morenoteslesstalk) |
| Artists & Creators | NIHIL | Modern alternative metal from Argentina. Fueled by emotionally charged vocals, anger and emptiness | [@somosnihil](https://www.instagram.com/somosnihil/) · [Spotify](https://open.spotify.com/artist/1OeKplJxFNM6JHrWeo2SaV) |
| Instrument Builders & Repair | Dlima Guitars | Custom guitars and basses | [@dlimaguitars](https://www.instagram.com/dlimaguitars) |
| Instrument Builders & Repair | Lisbon Guitar Works | Handmade guitars in Lisbon | [dlimaguitars.com](https://dlimaguitars.com) |
| Instrument Builders & Repair | Kris Luthier | Instrument repair and restoration | [@krisluthier](https://www.instagram.com/krisluthier) |
| Music Gear | Empress Effects | Boutique effects pedals | [empresseffects.com](https://empresseffects.com) |
| Music Gear | Thomann | Large music-equipment retailer | [@thomann.music](https://www.instagram.com/thomann.music) |
| Instrument Builders & Repair | Lisbon Guitar Works | Handmade guitars in Lisbon | [dlimaguitars.com](https://dlimaguitars.com) |
| Media & Community | Not Another Audio Podcast | Audio, music and tech industry podcast by working professionals | [Libsyn](https://directory.libsyn.com/shows/view/id/20f5a1e3-6fea-4d41-8cdf-451ce6fc6cda) |
| Media & Community | r/bass | Bass-player community | [r/Bass](https://www.reddit.com/r/Bass) |
| Media & Community | slashCAM | Camera, video, and post-production media | [@slashcam.de](https://www.instagram.com/slashcam.de) |
| Music & Karaoke Technology | Beltr | Local, subscription-free karaoke software | [beltr.app](https://beltr.app/) |
| Music & Karaoke Technology | Seratone | TV-based karaoke system | [seratone.audio](https://seratone.audio/) |
| Media & Community | slashCAM | Camera, video, and post-production media | [@slashcam.de](https://www.instagram.com/slashcam.de) |
| Media & Community | r/bass | Bass-player community | [r/Bass](https://www.reddit.com/r/Bass) |
| Media & Community | Not Another Audio Podcast | Audio, music and tech industry podcast by working professionals | [Libsyn](https://directory.libsyn.com/shows/view/id/20f5a1e3-6fea-4d41-8cdf-451ce6fc6cda) |
| Music Gear | Empress Effects | Boutique effects pedals | [empresseffects.com](https://empresseffects.com) |
| Music Gear | Thomann | Large music-equipment retailer | [@thomann.music](https://www.instagram.com/thomann.music) |
| Writers & Storytellers | Alexandre Borges | Portuguese writer, screenwriter, and cultural commentator | [Books & author profile](https://www.instagram.com/alexgram_b/) |


Expand Down
13 changes: 13 additions & 0 deletions static/css/daw.css
Original file line number Diff line number Diff line change
Expand Up @@ -4564,6 +4564,19 @@ input, textarea { font-family: inherit; }
}
.lib-friend:hover .lib-friend-ig,
.lib-friend:hover .lib-friend-link { opacity: 1; }
/* A card with several links: the card itself is not a link, each pill is. */
.lib-friend-multi:hover { background: var(--panel); border-color: var(--border); color: var(--fg-2); }
.lib-friend-links { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px; margin-top: auto; padding-top: 3px; }
.lib-friend-pill {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 7px; border-radius: 999px;
border: 1px solid var(--border-strong); background: var(--panel-2);
color: var(--fg-2); font-size: 9.5px; font-weight: 600; text-decoration: none;
transition: color var(--t-fast), border-color var(--t-fast);
}
.lib-friend-pill:hover { color: var(--fg); border-color: var(--accent); }
.lib-friend-pill .lib-friend-link,
.lib-friend-pill .lib-friend-ig { width: 10px; height: 10px; margin-top: 0; padding-top: 0; opacity: 1; }
.lib-friend-name {
width: 100%;
font-size: 11px;
Expand Down
Binary file added static/img/friends/adrianna-claro.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 47 additions & 7 deletions static/js/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,34 @@ const FRIEND_GROUPS = [
{
name: "Killah Trakz",
roleKey: "friends.role.killahTrakz",
url: "https://www.instagram.com/killahtrakz/",
links: [
{ label: "Instagram", url: "https://www.instagram.com/killahtrakz/" },
{ label: "Spotify", url: "https://open.spotify.com/artist/6nePYOqT4E7D4C48twPtKk" },
{ label: "Apple Music", url: "https://music.apple.com/us/artist/killah-trakz/371296633" },
],
logo: "/img/friends/killah-trakz.jpg",
avatar: true,
},
{
name: "NIHIL",
roleKey: "friends.role.nihil",
url: "https://www.instagram.com/somosnihil/",
links: [
{ label: "Instagram", url: "https://www.instagram.com/somosnihil/" },
{ label: "Spotify", url: "https://open.spotify.com/artist/1OeKplJxFNM6JHrWeo2SaV" },
],
logo: "/img/friends/nihil.jpg",
avatar: true,
},
{
name: "Adrianna Claro",
roleKey: "friends.role.adriannaClaro",
logo: "/img/friends/adrianna-claro.webp",
avatar: true,
links: [
{ label: "Spotify", url: "https://open.spotify.com/album/10fZ48iJhIe3lWcXSMUlFG" },
{ label: "Apple Music", url: "https://music.apple.com/us/album/happy/1592026727?i=1592026728" },
],
},
],
},
{
Expand Down Expand Up @@ -3523,13 +3540,23 @@ function friendGlyph(url) {
return svg;
}

// One partner card: image (or initial badge), name, role, link glyph.
function friendCard(f) {
/** A link that opens outside the app, as every partner link does. */
function friendLink(url) {
const a = document.createElement("a");
a.className = "lib-friend";
a.href = f.url;
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
return a;
}

// One partner card: image (or initial badge), name, role, link glyph. A card
// with one `url` is a link as a whole. A card with several `links` cannot be:
// a link inside a link is invalid HTML, and which one a click follows is up to
// the browser. So it is a plain card with one labelled link per destination.
function friendCard(f) {
const multi = Array.isArray(f.links) && f.links.length > 0;
const a = multi ? document.createElement("div") : friendLink(f.url);
a.className = multi ? "lib-friend lib-friend-multi" : "lib-friend";
a.title = f.name;

// A monogram badge (first initial) keeps the card on-brand when an entry has
Expand Down Expand Up @@ -3575,7 +3602,20 @@ function friendCard(f) {
a.appendChild(role);
}

a.appendChild(friendGlyph(f.url));
if (!multi) {
a.appendChild(friendGlyph(f.url));
return a;
}
const row = document.createElement("span");
row.className = "lib-friend-links";
for (const link of f.links) {
const l = friendLink(link.url);
l.className = "lib-friend-pill";
l.title = `${f.name}: ${link.label}`;
l.append(friendGlyph(link.url), link.label);
row.appendChild(l);
}
a.appendChild(row);
return a;
}

Expand Down
10 changes: 10 additions & 0 deletions static/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ const en = {
"friends.role.deadRoses": "Cork-based punk rock band",
"friends.role.killahTrakz": "Industry Secret Villain and Media Influencer",
"friends.role.nihil": "Modern alternative metal from Argentina. Fueled by emotionally charged vocals, anger and emptiness",
"friends.role.adriannaClaro": "Soul and Contemporary R&B Singer, Songwriter and Composer",
"friends.role.empressEffects": "Boutique effects pedals",
"friends.role.thomann": "Large music-equipment retailer",
"friends.role.beltr": "Local, subscription-free karaoke software",
Expand Down Expand Up @@ -1621,6 +1622,7 @@ const pl = {
"friends.role.deadRoses": "Zespół punkrockowy z Corku",
"friends.role.killahTrakz": "Sekretny złoczyńca branży i influencer medialny",
"friends.role.nihil": "Nowoczesny metal alternatywny z Argentyny. Napędzany emocjonalnym wokalem, gniewem i pustką",
"friends.role.adriannaClaro": "Wokalistka soul i współczesnego R&B, autorka piosenek i kompozytorka",
"friends.role.empressEffects": "Butikowe efekty gitarowe",
"friends.role.thomann": "Duży sklep ze sprzętem muzycznym",
"friends.role.beltr": "Lokalne karaoke bez abonamentu",
Expand Down Expand Up @@ -2500,6 +2502,7 @@ const ja = {
"friends.role.deadRoses": "コーク拠点のパンクロックバンド",
"friends.role.killahTrakz": "業界の隠れた悪役でメディアインフルエンサー",
"friends.role.nihil": "アルゼンチン発のモダンなオルタナティヴメタル。感情的なボーカル、怒り、そして虚無が原動力",
"friends.role.adriannaClaro": "ソウルとコンテンポラリーR&Bのシンガー、ソングライター、作曲家",
"friends.role.empressEffects": "ブティック系エフェクターペダル",
"friends.role.thomann": "大手の楽器機材販売店",
"friends.role.beltr": "サブスク不要、ローカルで動くカラオケソフト",
Expand Down Expand Up @@ -3355,6 +3358,7 @@ const zhHans = {
"friends.role.deadRoses": "来自科克的朋克摇滚乐队",
"friends.role.killahTrakz": "行业隐秘反派与媒体影响者",
"friends.role.nihil": "来自阿根廷的现代另类金属。以饱含情感的人声、愤怒与虚无为驱动",
"friends.role.adriannaClaro": "灵魂乐与当代 R&B 歌手、词曲作者和作曲家",
"friends.role.empressEffects": "精品效果器",
"friends.role.thomann": "大型乐器器材零售商",
"friends.role.beltr": "本地运行、无需订阅的卡拉OK软件",
Expand Down Expand Up @@ -4211,6 +4215,7 @@ const de = {
"friends.role.deadRoses": "Punkrock-Band aus Cork",
"friends.role.killahTrakz": "Heimlicher Bösewicht der Branche und Medien-Influencer",
"friends.role.nihil": "Moderner Alternative Metal aus Argentinien. Angetrieben von emotionsgeladenem Gesang, Wut und Leere",
"friends.role.adriannaClaro": "Soul- und Contemporary-R&B-Sängerin, Songwriterin und Komponistin",
"friends.role.empressEffects": "Boutique-Effektpedale",
"friends.role.thomann": "Großer Händler für Musik-Equipment",
"friends.role.beltr": "Lokale Karaoke-Software ohne Abo",
Expand Down Expand Up @@ -5077,6 +5082,7 @@ const pt = {
"friends.role.deadRoses": "Banda de punk rock de Cork",
"friends.role.killahTrakz": "Vilão secreto da indústria e influenciador digital",
"friends.role.nihil": "Metal alternativo moderno da Argentina. Movido por vocais carregados de emoção, raiva e vazio",
"friends.role.adriannaClaro": "Cantora, compositora e autora de soul e R&B contemporâneo",
"friends.role.empressEffects": "Pedais de efeito boutique",
"friends.role.thomann": "Grande varejista de equipamentos musicais",
"friends.role.beltr": "Software de karaokê local, sem assinatura",
Expand Down Expand Up @@ -5944,6 +5950,7 @@ const id = {
"friends.role.deadRoses": "Band punk rock asal Cork",
"friends.role.killahTrakz": "Penjahat Rahasia Industri dan Influencer Media",
"friends.role.nihil": "Metal alternatif modern dari Argentina. Digerakkan oleh vokal penuh emosi, amarah, dan kehampaan",
"friends.role.adriannaClaro": "Penyanyi, penulis lagu, dan komposer soul dan R&B kontemporer",
"friends.role.empressEffects": "Pedal efek butik",
"friends.role.thomann": "Peritel besar perangkat musik",
"friends.role.beltr": "Perangkat lunak karaoke lokal tanpa langganan",
Expand Down Expand Up @@ -6800,6 +6807,7 @@ const fr = {
"friends.role.deadRoses": "Groupe de punk rock basé à Cork",
"friends.role.killahTrakz": "Méchant secret de l'industrie et influenceur média",
"friends.role.nihil": "Metal alternatif moderne d'Argentine. Porté par un chant chargé d'émotion, la colère et le vide",
"friends.role.adriannaClaro": "Chanteuse, autrice et compositrice de soul et de R&B contemporain",
"friends.role.empressEffects": "Pédales d'effets boutique",
"friends.role.thomann": "Grand détaillant de matériel de musique",
"friends.role.beltr": "Logiciel de karaoké local, sans abonnement",
Expand Down Expand Up @@ -7840,6 +7848,7 @@ const es = {
"friends.role.deadRoses": "Banda de punk rock de Cork",
"friends.role.killahTrakz": "Villano secreto de la industria e influencer de medios",
"friends.role.nihil": "Metal alternativo moderno de Argentina. Impulsado por voces cargadas de emoción, rabia y vacío",
"friends.role.adriannaClaro": "Cantante, compositora y autora de soul y R&B contemporáneo",
"friends.role.empressEffects": "Pedales de efectos boutique",
"friends.role.thomann": "Gran tienda de equipo musical",
"friends.role.beltr": "Software de karaoke local y sin suscripción",
Expand Down Expand Up @@ -8728,6 +8737,7 @@ const ko = {
"friends.role.deadRoses": "코크를 기반으로 활동하는 펑크 록 밴드",
"friends.role.killahTrakz": "업계의 숨은 악당이자 미디어 인플루언서",
"friends.role.nihil": "아르헨티나에서 온 모던 얼터너티브 메탈. 감정적인 보컬과 분노, 공허가 원동력",
"friends.role.adriannaClaro": "소울과 컨템퍼러리 R&B 가수, 송라이터, 작곡가",
"friends.role.empressEffects": "부티크 이펙터 페달",
"friends.role.thomann": "대형 음악 장비 판매점",
"friends.role.beltr": "구독이 필요 없는 로컬 노래방 소프트웨어",
Expand Down
53 changes: 53 additions & 0 deletions tests/e2e/we-recommend.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// We Recommend: a partner card with one link is a link as a whole; a card with
// several (Adrianna Claro: Spotify and Apple Music) is a plain card with one
// labelled link per destination, since a link cannot hold other links.

import { test, expect } from "@playwright/test";

import { seedLibrary, stubUpdateCheck } from "./helpers.mjs";

async function openWeRecommend(page) {
await seedLibrary(page);
await stubUpdateCheck(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.locator("#friendsBtn").click();
}

test.describe("We Recommend", () => {
test("a card with two links offers both, each opening outside the app", async ({ page }) => {
await openWeRecommend(page);
const card = page.locator(".lib-friend").filter({ hasText: "Adrianna Claro" });
await expect(card).toHaveCount(1);
await expect(card).toBeVisible();
// Not a link itself, so the two below are the only things to click.
expect(await card.evaluate((el) => el.tagName)).toBe("DIV");
// Her photo loads, rather than falling back to the monogram.
await expect(card.locator("img.lib-friend-avatar")).toHaveAttribute("src", "/img/friends/adrianna-claro.webp");
await expect.poll(() => card.locator("img.lib-friend-avatar").evaluate((img) => img.naturalWidth)).toBeGreaterThan(0);

const spotify = card.locator(".lib-friend-pill", { hasText: "Spotify" });
const apple = card.locator(".lib-friend-pill", { hasText: "Apple Music" });
await expect(spotify).toHaveAttribute("href", "https://open.spotify.com/album/10fZ48iJhIe3lWcXSMUlFG");
await expect(apple).toHaveAttribute("href", "https://music.apple.com/us/album/happy/1592026727?i=1592026728");
for (const link of [spotify, apple]) {
await expect(link).toHaveAttribute("target", "_blank");
await expect(link).toHaveAttribute("rel", "noopener noreferrer");
}
});

test("a card with one link is still a link as a whole", async ({ page }) => {
await openWeRecommend(page);
const card = page.locator(".lib-friend").filter({ hasText: "Analog4Lyfe" });
expect(await card.evaluate((el) => el.tagName)).toBe("A");
await expect(card).toHaveAttribute("href", "https://www.instagram.com/analog4lyfe");
await expect(card.locator(".lib-friend-pill")).toHaveCount(0);
});

test("Killah Trakz and NIHIL offer Instagram and Spotify, and Killah Trakz Apple Music too", async ({ page }) => {
await openWeRecommend(page);
const pills = (name) => page.locator(".lib-friend").filter({ hasText: name }).locator(".lib-friend-pill");
await expect(pills("Killah Trakz")).toHaveText(["Instagram", "Spotify", "Apple Music"]);
await expect(pills("NIHIL")).toHaveText(["Instagram", "Spotify"]);
await expect(pills("NIHIL").filter({ hasText: "Spotify" })).toHaveAttribute("href", "https://open.spotify.com/artist/1OeKplJxFNM6JHrWeo2SaV");
});
});