EmbedSnippet.tsx (7700B)
1 import { useCallback, useMemo, useState } from "react"; 2 import { PUBLIC_SITE_URL } from "../config/siteUrl"; 3 import { buildEmbedSearchParams } from "../lib/embedParams"; 4 import { EMBED_FONT_IDS, EMBED_PRESET_IDS, type EmbedFontId, type EmbedPresetId } from "../lib/embedTheme"; 5 6 function buildIframeSnippet(opts: { 7 autoplay: boolean; 8 compact: boolean; 9 startId: string; 10 showBrand: boolean; 11 preset: EmbedPresetId; 12 accent: string; 13 useAccent: boolean; 14 radius: string; 15 font: EmbedFontId; 16 }): string { 17 const radiusNum = opts.radius.trim() ? Number.parseInt(opts.radius, 10) : undefined; 18 const qs = buildEmbedSearchParams({ 19 autoplay: opts.autoplay, 20 theme: opts.compact ? "compact" : "default", 21 startId: opts.startId.trim() || null, 22 showBrand: opts.showBrand, 23 preset: opts.preset, 24 accent: opts.useAccent ? opts.accent : undefined, 25 radius: Number.isFinite(radiusNum) ? radiusNum : undefined, 26 font: opts.font, 27 }); 28 const src = `${PUBLIC_SITE_URL}/embed${qs}`; 29 return `<iframe 30 src="${src}" 31 title="MyMusics" 32 width="100%" 33 height="540" 34 style="max-width:380px;border:0;border-radius:12px" 35 loading="lazy" 36 allow="autoplay" 37 ></iframe>`; 38 } 39 40 type Props = { 41 className?: string; 42 }; 43 44 export function EmbedSnippet({ className }: Props = {}) { 45 const [copied, setCopied] = useState(false); 46 const [autoplay, setAutoplay] = useState(true); 47 const [compact, setCompact] = useState(false); 48 const [showBrand, setShowBrand] = useState(true); 49 const [startId, setStartId] = useState(""); 50 const [preset, setPreset] = useState<EmbedPresetId>("default"); 51 const [useAccent, setUseAccent] = useState(false); 52 const [accent, setAccent] = useState("#e64a19"); 53 const [radius, setRadius] = useState(""); 54 const [font, setFont] = useState<EmbedFontId>("sans"); 55 56 const embedOptions = useMemo( 57 () => ({ 58 autoplay, 59 compact, 60 startId, 61 showBrand, 62 preset, 63 accent, 64 useAccent, 65 radius, 66 font, 67 }), 68 [autoplay, compact, startId, showBrand, preset, accent, useAccent, radius, font], 69 ); 70 71 const code = useMemo(() => buildIframeSnippet(embedOptions), [embedOptions]); 72 73 const previewSrc = useMemo(() => { 74 const radiusNum = radius.trim() ? Number.parseInt(radius, 10) : undefined; 75 const qs = buildEmbedSearchParams({ 76 autoplay: false, 77 theme: compact ? "compact" : "default", 78 startId: startId.trim() || null, 79 showBrand, 80 preset, 81 accent: useAccent ? accent : undefined, 82 radius: Number.isFinite(radiusNum) ? radiusNum : undefined, 83 font, 84 }); 85 return `${PUBLIC_SITE_URL}/embed${qs}`; 86 }, [compact, startId, showBrand, preset, accent, useAccent, radius, font]); 87 88 const copy = useCallback(async () => { 89 try { 90 await navigator.clipboard.writeText(code); 91 setCopied(true); 92 window.setTimeout(() => setCopied(false), 2000); 93 } catch { 94 try { 95 const ta = document.createElement("textarea"); 96 ta.value = code; 97 ta.setAttribute("readonly", ""); 98 ta.style.position = "absolute"; 99 ta.style.left = "-9999px"; 100 document.body.appendChild(ta); 101 ta.select(); 102 document.execCommand("copy"); 103 document.body.removeChild(ta); 104 setCopied(true); 105 window.setTimeout(() => setCopied(false), 2000); 106 } catch { 107 setCopied(false); 108 } 109 } 110 }, [code]); 111 112 return ( 113 <section 114 className={["embed-snippet", "card", className].filter(Boolean).join(" ")} 115 aria-label="Embed this player" 116 > 117 <h2 className="embed-snippet-title">Embed on your site</h2> 118 <p className="embed-snippet-lead muted"> 119 Paste the HTML below on your page. Customize colors with presets and query params, or update 120 the theme at runtime via <code>postMessage</code>. See{" "} 121 <code>docs/EMBED-CUSTOMIZATION.md</code> for the full guide. 122 </p> 123 124 <div className="embed-snippet-options"> 125 <label className="check"> 126 <input type="checkbox" checked={autoplay} onChange={(e) => setAutoplay(e.target.checked)} /> 127 Autoplay / auto-advance 128 </label> 129 <label className="check"> 130 <input type="checkbox" checked={compact} onChange={(e) => setCompact(e.target.checked)} /> 131 Compact layout 132 </label> 133 <label className="check"> 134 <input type="checkbox" checked={showBrand} onChange={(e) => setShowBrand(e.target.checked)} /> 135 Show MyMusics logo 136 </label> 137 138 <div className="embed-snippet-theme-row"> 139 <label className="embed-snippet-field"> 140 <span>Preset</span> 141 <select 142 value={preset} 143 onChange={(e) => setPreset(e.target.value as EmbedPresetId)} 144 aria-label="Color preset" 145 > 146 {EMBED_PRESET_IDS.map((id) => ( 147 <option key={id} value={id}> 148 {id} 149 </option> 150 ))} 151 </select> 152 </label> 153 154 <label className="embed-snippet-field"> 155 <span>Font</span> 156 <select 157 value={font} 158 onChange={(e) => setFont(e.target.value as EmbedFontId)} 159 aria-label="Font stack" 160 > 161 {EMBED_FONT_IDS.map((id) => ( 162 <option key={id} value={id}> 163 {id} 164 </option> 165 ))} 166 </select> 167 </label> 168 169 <label className="embed-snippet-field"> 170 <span>Radius (px)</span> 171 <input 172 type="number" 173 min={0} 174 max={24} 175 placeholder="default" 176 value={radius} 177 onChange={(e) => setRadius(e.target.value)} 178 aria-label="Border radius in pixels" 179 /> 180 </label> 181 182 <label className="check embed-snippet-field"> 183 <span>Custom accent</span> 184 <span className="embed-snippet-accent-row"> 185 <input 186 type="checkbox" 187 checked={useAccent} 188 onChange={(e) => setUseAccent(e.target.checked)} 189 aria-label="Use custom accent color" 190 /> 191 <input 192 type="color" 193 value={accent} 194 disabled={!useAccent} 195 onChange={(e) => setAccent(e.target.value)} 196 aria-label="Accent color" 197 /> 198 </span> 199 </label> 200 </div> 201 202 <label className="embed-snippet-start"> 203 <span>Start track id (optional)</span> 204 <input 205 type="text" 206 value={startId} 207 onChange={(e) => setStartId(e.target.value)} 208 placeholder="e.g. 12345" 209 spellCheck={false} 210 /> 211 </label> 212 </div> 213 214 <div className="embed-snippet-preview-wrap"> 215 <iframe 216 className="embed-snippet-preview" 217 src={previewSrc} 218 title="MyMusics embed preview" 219 loading="lazy" 220 allow="autoplay" 221 /> 222 </div> 223 224 <textarea className="embed-snippet-code" readOnly rows={8} value={code} spellCheck={false} /> 225 <button type="button" className="btn primary embed-snippet-copy" onClick={() => void copy()}> 226 {copied ? "Copied!" : "Copy code"} 227 </button> 228 <p className="embed-snippet-lead muted"> 229 oEmbed:{" "} 230 <code> 231 {PUBLIC_SITE_URL}/api/oembed?url= 232 {encodeURIComponent(`${PUBLIC_SITE_URL}/embed${buildEmbedSearchParams({ preset, accent: useAccent ? accent : undefined, font })}`)} 233 </code> 234 </p> 235 </section> 236 ); 237 }