perfume-api / index.html
scento-team's picture
Rename to Scento Perfume API
25a0cbb verified
Raw History Blame Contribute Delete
8.67 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scento Perfume API</title>
<style>
:root{--bg:#faf7f2;--ink:#1d1a16;--mute:#6f675c;--line:#e4ddd0;--card:#fff;--acc:#8a6a2f}
@media (prefers-color-scheme:dark){:root{--bg:#14120f;--ink:#f1ece3;--mute:#a39a8b;--line:#2e2a23;--card:#1b1814;--acc:#d4b06a}}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
main{max-width:720px;margin:0 auto;padding:40px 16px 56px}
h1{font:500 clamp(28px,6vw,40px)/1.1 Georgia,"Times New Roman",serif;margin:0 0 10px;letter-spacing:-.01em}
.lede{color:var(--mute);margin:0 0 28px}
.lede a,.foot a,.tail a,.found a{color:var(--acc)}
nav{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-bottom:20px}
nav button{background:none;border:0;border-bottom:2px solid transparent;padding:10px 14px;font:inherit;color:var(--mute);cursor:pointer;margin-bottom:-1px}
nav button[aria-selected=true]{color:var(--ink);border-color:var(--acc)}
nav button:focus-visible,input:focus-visible,.go:focus-visible{outline:2px solid var(--acc);outline-offset:2px}
form{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:20px}
input{flex:1 1 180px;min-width:0;padding:11px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--ink);font:inherit}
.go{padding:11px 18px;border:0;border-radius:8px;background:var(--ink);color:var(--bg);font:inherit;cursor:pointer}
.out{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:20px 22px;min-height:120px}
.out h2{font:500 24px/1.2 Georgia,serif;margin:0 0 6px}
.out .l{color:var(--mute);font-style:italic;margin:0 0 14px}
.out dl{margin:0;display:grid;grid-template-columns:auto 1fr;gap:6px 16px}
.out dt{color:var(--mute);font-size:13px;text-transform:uppercase;letter-spacing:.06em;padding-top:2px}
.out dd{margin:0}
table{width:100%;border-collapse:collapse;font-size:14px;margin:6px 0 14px}
th,td{text-align:left;padding:7px 8px;border-bottom:1px solid var(--line);vertical-align:top}
th{color:var(--mute);font-weight:500}
.tail{margin-top:16px;font-size:14px;color:var(--mute)}
.tail a{margin-right:12px}
.chips{display:flex;gap:6px;flex-wrap:wrap;margin:-8px 0 20px}
.chips button{border:1px solid var(--line);background:none;color:var(--mute);border-radius:99px;padding:3px 11px;font:inherit;font-size:13px;cursor:pointer}
.foot{margin-top:28px;font-size:14px;color:var(--mute)}
code{background:var(--card);border:1px solid var(--line);border-radius:4px;padding:1px 5px;font-size:13px}
</style>
</head>
<body>
<main>
<h1>Scento Perfume API</h1>
<p class="lede">Scento Perfume API, built on Scento's verified perfume database, for people and AI assistants: 1,400+ designer and niche perfumes, 400+ raw materials and 140+ houses, every answer tied to a page on <a href="https://www.scento.com">scento.com</a>. No key, no account. This demo calls the public REST API from your browser.</p>
<nav role="tablist">
<button role="tab" aria-selected="true" data-t="p">Perfume portrait</button>
<button role="tab" aria-selected="false" data-t="c">Compare two</button>
<button role="tab" aria-selected="false" data-t="m">Raw material</button>
</nav>
<section id="p"><form><input name="a" value="Oud Wood Tom Ford" aria-label="Perfume" autocomplete="off"><button class="go">Draw the portrait</button></form>
<div class="chips" data-for="p"><button>Delina</button><button>Baccarat Rouge 540</button><button>Eau Sauvage Dior</button></div></section>
<section id="c" hidden><form><input name="a" value="Eau Sauvage Dior" aria-label="First perfume" autocomplete="off"><input name="b" value="Oud Wood Tom Ford" aria-label="Second perfume" autocomplete="off"><button class="go">Compare</button></form></section>
<section id="m" hidden><form><input name="a" value="vetiver" aria-label="Raw material" autocomplete="off"><button class="go">Look it up</button></form>
<div class="chips" data-for="m"><button>oud</button><button>iris</button><button>ambroxan</button></div></section>
<div class="out" id="out" aria-live="polite"></div>
<p class="foot">Use it from code: <code>https://www.scento.com/api/perfume/v1</code> 路 MCP <code>https://www.scento.com/mcp</code> 路 <a href="https://www.scento.com/developers/perfume-api">REST docs</a> 路 <a href="https://www.scento.com/developers/perfume-mcp-server">MCP docs</a> 路 <a href="https://www.scento.com/api/perfume/v1/openapi.json">OpenAPI</a> 路 <a href="https://github.com/Scento-com/perfume-mcp-server">GitHub</a></p>
</main>
<script>
const API="https://www.scento.com/api/perfume/v1";
const out=document.getElementById("out");
const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
const safeUrl=u=>/^https:\/\/www\.scento\.com\//.test(u)?u:"https://www.scento.com";
async function get(path){
try{
const r=await fetch(API+path);
const j=await r.json();
if(!r.ok) return {err:(j.error&&j.error.message)||"No match in the Scento Perfume API."};
if(j.detail&&j.detail.found===false) return {err:j.lede};
return {j};
}catch(e){return {err:"The Scento Perfume API could not be reached just now. Please try again in a moment."}}
}
function tail(j){
const l=[];
if(j.scento_page) l.push(`<a href="${safeUrl(j.scento_page.url)}">${esc(j.scento_page.label)}</a>`);
(j.further_reading||[]).slice(0,4).forEach(f=>l.push(`<a href="${safeUrl(f.url)}">${esc(f.label)}</a>`));
return `<div class="tail">${l.join("")}<br>${esc(j.cite||"Scento Perfume API, scento.com")}</div>`;
}
const row=(k,v)=>v&&(Array.isArray(v)?v.length:true)?`<dt>${k}</dt><dd>${esc(Array.isArray(v)?v.join(", "):v)}</dd>`:"";
async function portrait(q){
let r=await get("/perfumes/"+encodeURIComponent(q));
if(r.err) return `<p>${esc(r.err)}</p>`;
const j=r.j,d=j.detail,p=d.pyramid||{};
return `<h2>${esc(d.name)} 路 ${esc(d.house)}</h2><p class="l">${esc(j.lede)}</p><dl>`+
(d.pyramid?row("Top",p.top)+row("Heart",p.heart)+row("Base",p.base):row("Notes",d.notes))+
row("Character",d.character)+row("Family",d.family)+row("Concentration",d.concentration)+row("Year",d.year&&String(d.year))+row("Perfumer",d.perfumer)+`</dl>`+tail(j);
}
async function contrast(a,b){
const r=await get("/contrast?perfumes="+encodeURIComponent(a+","+b));
if(r.err) return `<p>${esc(r.err)}</p>`;
const j=r.j,d=j.detail;
if(!d.perfumes) return `<p>${esc(j.lede)}</p>`;
const rows=d.axes.map(x=>`<tr><th>${esc(x.axis)}</th>${x.values.map(v=>`<td>${esc(v)}</td>`).join("")}</tr>`).join("");
return `<p class="l">${esc(j.lede)}</p><table><thead><tr><th></th>${d.perfumes.map(p=>`<th>${esc(p.name)}</th>`).join("")}</tr></thead><tbody>${rows}</tbody></table><p><b>Shared materials:</b> ${esc((d.shared_materials||[]).join(", ")||"none listed")}</p>`+tail(j);
}
async function material(q){
const r=await get("/materials/"+encodeURIComponent(q));
if(r.err) return `<p>${esc(r.err)}</p>`;
const j=r.j,d=j.detail;
const found=(d.showcased_in||[]).slice(0,6).map(s=>`<a href="${safeUrl(s.page)}">${esc(s.name)} (${esc(s.house)})</a>`).join(", ");
return `<h2>${esc(d.name)}</h2><p class="l">${esc(j.lede)}</p><dl>${row("Smells like",d.smells_like)}${row("Origin",d.origin)}${row("Source",d.natural_or_synthetic)}${row("Scento family",d.scento_family)}${row("Pairs well with",d.pairs_well_with)}${found?`<dt>Found in</dt><dd class="found">${found}</dd>`:""}</dl>`+tail(j);
}
async function run(t,f){
out.innerHTML="<p class='l'>Reading the Scento Perfume API...</p>";
const v=n=>f.elements[n]&&f.elements[n].value.trim();
if(!v("a")||(t==="c"&&!v("b"))){out.innerHTML="<p>Type a name to begin.</p>";return}
out.innerHTML=t==="p"?await portrait(v("a")):t==="c"?await contrast(v("a"),v("b")):await material(v("a"));
}
document.querySelectorAll("section").forEach(s=>s.querySelector("form").addEventListener("submit",e=>{e.preventDefault();run(s.id,e.target)}));
document.querySelectorAll(".chips").forEach(c=>c.addEventListener("click",e=>{if(e.target.tagName!=="BUTTON")return;const s=document.getElementById(c.dataset.for);s.querySelector("input").value=e.target.textContent;run(s.id,s.querySelector("form"))}));
document.querySelectorAll("nav button").forEach(b=>b.addEventListener("click",()=>{
document.querySelectorAll("nav button").forEach(x=>x.setAttribute("aria-selected",x===b));
document.querySelectorAll("section").forEach(s=>s.hidden=s.id!==b.dataset.t);
run(b.dataset.t,document.querySelector("#"+b.dataset.t+" form"));
}));
run("p",document.querySelector("#p form"));
</script>
</body>
</html>