mymusics

retro MySpace-style music player
Log | Files | Refs | README

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 }