Add Share to X button with proper tweet text

- Added "Share to X" button next to "Copy link"
- Tweet text: "I made [song] game boy version. Click to listen or generate your own, made in Wario Synth by @b1rdmania"
- Updated Twitter cards to summary_large_image
- Dynamic OG description includes song title

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
b1rdmania
2025-12-29 14:32:34 +00:00
parent 469212aa3c
commit eb3f61b63a
4 changed files with 60 additions and 11 deletions
+7 -6
View File
@@ -1149,12 +1149,13 @@
</div> </div>
</div> </div>
<div style="margin-top: 14px;"> <div style="margin-top: 14px; display: flex; gap: 12px; flex-wrap: wrap;">
<button id="copyLinkBtn" type="button" disabled>Share</button> <button id="copyLinkBtn" type="button" disabled>Copy link</button>
<div id="shareLinkBox" style="display: none; margin-top: 12px;"> <button id="shareToXBtn" type="button" disabled>Share to X</button>
<input type="text" id="shareLinkInput" readonly style="width: 100%; font-size: 8px; padding: 10px;" /> </div>
<div style="margin-top: 6px; font-size: 8px; color: var(--gb-dark);">Select and copy the link above</div> <div id="shareLinkBox" style="display: none; margin-top: 12px;">
</div> <input type="text" id="shareLinkInput" readonly style="width: 100%; font-size: 8px; padding: 10px;" />
<div style="margin-top: 6px; font-size: 8px; color: var(--gb-dark);">Select and copy the link above</div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -16,7 +16,7 @@
<meta property="og:url" content="https://motif-self.vercel.app/play" /> <meta property="og:url" content="https://motif-self.vercel.app/play" />
<!-- Twitter Card --> <!-- Twitter Card -->
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="WARIO SYNTH" /> <meta name="twitter:title" content="WARIO SYNTH" />
<meta name="twitter:description" content="Tap to play this Gameboy tune" /> <meta name="twitter:description" content="Tap to play this Gameboy tune" />
<meta name="twitter:image" content="https://motif-self.vercel.app/wariosynthlogo.png" /> <meta name="twitter:image" content="https://motif-self.vercel.app/wariosynthlogo.png" />
+5 -3
View File
@@ -220,8 +220,10 @@ app.get('/s/:code', async (req, res) => {
const imageUrl = origin ? `${origin}/wariosynthlogo.png` : '/wariosynthlogo.png'; const imageUrl = origin ? `${origin}/wariosynthlogo.png` : '/wariosynthlogo.png';
const sharedTitle = (payload.title || '').trim(); const sharedTitle = (payload.title || '').trim();
const ogTitle = sharedTitle || 'WARIO SYNTH'; const ogTitle = sharedTitle ? `${sharedTitle} - WARIO SYNTH` : 'WARIO SYNTH';
const ogDescription = 'I made this in Wario Synth, click to listen or generate your own'; const ogDescription = sharedTitle
? `I made ${sharedTitle} game boy version. Click to listen or generate your own, made in Wario Synth by @b1rdmania.`
: 'Turn any song into retro game console music. Made by @b1rdmania.';
const html = `<!doctype html> const html = `<!doctype html>
<html lang="en"> <html lang="en">
@@ -239,7 +241,7 @@ app.get('/s/:code', async (req, res) => {
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="${escapeHtml(shortUrl)}" /> <meta property="og:url" content="${escapeHtml(shortUrl)}" />
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${escapeHtml(ogTitle)}" /> <meta name="twitter:title" content="${escapeHtml(ogTitle)}" />
<meta name="twitter:description" content="${escapeHtml(ogDescription)}" /> <meta name="twitter:description" content="${escapeHtml(ogDescription)}" />
<meta name="twitter:image" content="${escapeHtml(imageUrl)}" /> <meta name="twitter:image" content="${escapeHtml(imageUrl)}" />
+47 -1
View File
@@ -58,6 +58,7 @@ class MotifApp {
private iosAudioState!: HTMLElement; private iosAudioState!: HTMLElement;
private copyLinkBtn!: HTMLButtonElement; private copyLinkBtn!: HTMLButtonElement;
private shareToXBtn!: HTMLButtonElement;
private shareLinkBox!: HTMLElement; private shareLinkBox!: HTMLElement;
private shareLinkInput!: HTMLInputElement; private shareLinkInput!: HTMLInputElement;
@@ -135,6 +136,7 @@ class MotifApp {
this.motifDuration = document.getElementById('motifDuration')!; this.motifDuration = document.getElementById('motifDuration')!;
this.copyLinkBtn = document.getElementById('copyLinkBtn') as HTMLButtonElement; this.copyLinkBtn = document.getElementById('copyLinkBtn') as HTMLButtonElement;
this.shareToXBtn = document.getElementById('shareToXBtn') as HTMLButtonElement;
this.shareLinkBox = document.getElementById('shareLinkBox') as HTMLElement; this.shareLinkBox = document.getElementById('shareLinkBox') as HTMLElement;
this.shareLinkInput = document.getElementById('shareLinkInput') as HTMLInputElement; this.shareLinkInput = document.getElementById('shareLinkInput') as HTMLInputElement;
@@ -184,6 +186,7 @@ class MotifApp {
this.motifProgressBar.addEventListener('change', seekHandler); this.motifProgressBar.addEventListener('change', seekHandler);
this.copyLinkBtn.addEventListener('click', () => void this.handleCopyLink()); this.copyLinkBtn.addEventListener('click', () => void this.handleCopyLink());
this.shareToXBtn.addEventListener('click', () => void this.handleShareToX());
// Embed snippet copy (may be disabled / not-live) // Embed snippet copy (may be disabled / not-live)
this.copyEmbedBtn?.addEventListener('click', () => void this.copyEmbedSnippet()); this.copyEmbedBtn?.addEventListener('click', () => void this.copyEmbedSnippet());
@@ -744,6 +747,47 @@ class MotifApp {
} }
} }
private async handleShareToX(): Promise<void> {
if (!this.hasGenerated) return;
const result = this.searchResults[this.selectedResultIndex];
if (!result?.midiUrl) return;
const title = this.cleanSongTitle(result.title || 'a song');
// Get share URL (try short link first)
let shareUrl: string | null = null;
try {
let payload: any = null;
if (result.source === 'bitmidi') {
const m = String(result.midiUrl).match(/\/uploads\/(\d+)\.mid/i);
if (m?.[1]) payload = { src: 'bitmidi', id: m[1], title: result.title };
}
if (!payload) payload = { u: result.midiUrl, title: result.title };
const resp = await fetch('/api/share', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (resp.ok) {
const data = await resp.json();
if (data?.url) shareUrl = `${window.location.origin}${data.url}`;
}
} catch {
// Fall through
}
if (!shareUrl) {
shareUrl = window.location.href;
}
// Compose tweet text
const tweetText = `I made ${title} game boy version. Click to listen or generate your own, made in Wario Synth by @b1rdmania`;
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(shareUrl)}`;
window.open(twitterUrl, '_blank', 'width=550,height=420');
}
private setState(state: 'idle' | 'results' | 'selected' | 'generated'): void { private setState(state: 'idle' | 'results' | 'selected' | 'generated'): void {
// results - keep visible in all states except idle so user can pick a different source // results - keep visible in all states except idle so user can pick a different source
const hasResults = state === 'results' || state === 'selected' || state === 'generated'; const hasResults = state === 'results' || state === 'selected' || state === 'generated';
@@ -754,9 +798,11 @@ class MotifApp {
const hasSelection = state === 'selected' || state === 'generated'; const hasSelection = state === 'selected' || state === 'generated';
this.playerSection.classList.toggle('visible', hasSelection); this.playerSection.classList.toggle('visible', hasSelection);
// share link only after generation // share buttons only after generation
this.copyLinkBtn.style.display = state === 'generated' ? 'inline-block' : 'none'; this.copyLinkBtn.style.display = state === 'generated' ? 'inline-block' : 'none';
this.copyLinkBtn.disabled = !(state === 'generated' && this.hasGenerated); this.copyLinkBtn.disabled = !(state === 'generated' && this.hasGenerated);
this.shareToXBtn.style.display = state === 'generated' ? 'inline-block' : 'none';
this.shareToXBtn.disabled = !(state === 'generated' && this.hasGenerated);
// Dim results when selected/generated, but keep them interactive // Dim results when selected/generated, but keep them interactive
this.resultsSection.classList.toggle('collapsed', state === 'selected' || state === 'generated'); this.resultsSection.classList.toggle('collapsed', state === 'selected' || state === 'generated');