|
57 | 57 | .btn-outline:hover{background:var(--bg3);color:var(--cream);border-color:var(--rust)} |
58 | 58 | .hero-note{font-family:var(--font-mono);font-size:.7rem;color:var(--cream-muted);letter-spacing:.5px;margin-top:.8rem} |
59 | 59 |
|
| 60 | +/* ── Hero generator ───────────────────────────────────────────────── */ |
| 61 | +.hero-gen{padding-top:3rem;padding-bottom:3rem} |
| 62 | +.hero-gen h1{margin-bottom:1rem} |
| 63 | +.hero-gen .sub{margin-bottom:1.8rem} |
| 64 | +.gen-form{display:flex;gap:.5rem;max-width:640px;margin:0 auto 1rem;flex-wrap:wrap} |
| 65 | +.gen-input{flex:1 1 260px;min-width:0;padding:.95rem 1.1rem;background:var(--bg2);border:1px solid var(--bg3);color:var(--cream);font-family:var(--font-serif);font-size:1rem;border-radius:2px;outline:none;transition:border-color .15s} |
| 66 | +.gen-input:focus{border-color:var(--rust)} |
| 67 | +.gen-input::placeholder{color:var(--cream-muted);font-style:italic} |
| 68 | +.gen-btn{padding:.95rem 1.6rem;background:var(--rust);color:var(--cream);border:none;font-family:var(--font-mono);font-size:.78rem;letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;border-radius:2px;transition:background .15s;white-space:nowrap} |
| 69 | +.gen-btn:hover{background:var(--rust-light)} |
| 70 | +.gen-btn:disabled{opacity:.6;cursor:wait} |
| 71 | +.gen-chips{display:flex;flex-wrap:wrap;gap:.5rem;justify-content:center;align-items:center;margin-bottom:1.5rem;font-family:var(--font-mono);font-size:.75rem} |
| 72 | +.gen-chips-prefix{color:var(--cream-muted);letter-spacing:1px;text-transform:uppercase;font-size:.68rem} |
| 73 | +.gen-chip{background:transparent;border:1px solid var(--bg3);color:var(--cream-dim);padding:.4rem .75rem;font-family:var(--font-mono);font-size:.75rem;cursor:pointer;border-radius:2px;transition:all .15s} |
| 74 | +.gen-chip:hover{border-color:var(--rust);color:var(--rust-light)} |
| 75 | +.gen-result{max-width:780px;margin:1.5rem auto 0;text-align:left;min-height:0} |
| 76 | +.gen-result:empty{display:none} |
| 77 | +.gen-thinking{background:var(--bg2);border:1px solid var(--bg3);padding:1.5rem;text-align:center;font-family:var(--font-mono);font-size:.82rem;color:var(--cream-dim);border-radius:2px;animation:gen-pulse 1.6s ease-in-out infinite} |
| 78 | +.gen-thinking strong{color:var(--rust-light);font-weight:500} |
| 79 | +@keyframes gen-pulse{0%,100%{opacity:.7}50%{opacity:1}} |
| 80 | +.gen-card{background:var(--bg2);border:1px solid var(--bg3);padding:1.8rem;border-radius:2px} |
| 81 | +.gen-card-head{border-bottom:1px solid var(--bg3);padding-bottom:1rem;margin-bottom:1rem} |
| 82 | +.gen-card-title{font-family:var(--font-serif);font-size:1.3rem;color:var(--cream);margin-bottom:.3rem;line-height:1.3} |
| 83 | +.gen-card-audience{font-size:.85rem;color:var(--cream-dim);font-style:italic;line-height:1.5} |
| 84 | +.gen-tools{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:.6rem;margin-bottom:1.2rem} |
| 85 | +.gen-tool{background:var(--bg);border:1px solid var(--bg3);padding:.75rem .9rem;border-radius:2px} |
| 86 | +.gen-tool-name{font-family:var(--font-mono);font-size:.82rem;color:var(--leather-light);margin-bottom:.2rem;letter-spacing:.5px} |
| 87 | +.gen-tool-replaces{font-size:.72rem;color:var(--cream-muted);font-family:var(--font-mono)} |
| 88 | +.gen-savings{display:flex;justify-content:space-between;align-items:center;padding-top:1rem;border-top:1px solid var(--bg3);font-family:var(--font-mono);font-size:.8rem;flex-wrap:wrap;gap:.8rem} |
| 89 | +.gen-savings-amount{color:var(--gold);font-size:1.1rem;font-weight:600} |
| 90 | +.gen-savings-label{color:var(--cream-muted);letter-spacing:1px;text-transform:uppercase;font-size:.68rem} |
| 91 | +.gen-cta{display:inline-block;padding:.75rem 1.3rem;background:var(--rust);color:var(--cream);font-family:var(--font-mono);font-size:.75rem;letter-spacing:1.5px;text-transform:uppercase;text-decoration:none;border-radius:2px;transition:background .15s} |
| 92 | +.gen-cta:hover{background:var(--rust-light);color:var(--cream)} |
| 93 | +.gen-error{background:var(--bg2);border:1px solid var(--bg3);padding:1rem 1.2rem;font-family:var(--font-mono);font-size:.78rem;color:var(--cream-dim);border-radius:2px;text-align:center} |
| 94 | +.gen-error a{color:var(--rust-light)} |
| 95 | + |
60 | 96 | /* ── App screenshot (mocked) ──────────────────────────────────────── */ |
61 | 97 | .shot-wrap{max-width:960px;margin:2rem auto 3rem;padding:0 2rem} |
62 | 98 | .shot{background:var(--bg2);border:1px solid var(--bg3);box-shadow:0 12px 40px rgba(0,0,0,.35);position:relative} |
|
151 | 187 | .hero{padding-top:2.5rem} |
152 | 188 | .hero-actions{flex-direction:column;align-items:stretch;max-width:300px;margin:0 auto 1rem} |
153 | 189 | .nav-links{gap:.8rem;font-size:.75rem} |
| 190 | + .gen-form{flex-direction:column;max-width:360px} |
| 191 | + .gen-input,.gen-btn{width:100%} |
| 192 | + .gen-tools{grid-template-columns:1fr} |
| 193 | + .hero-gen h1{font-size:1.7rem} |
154 | 194 | } |
155 | 195 | </style> |
156 | 196 | </head> |
|
174 | 214 | </nav> |
175 | 215 |
|
176 | 216 | <!-- ── Hero ──────────────────────────────────────────────────────── --> |
177 | | -<section class="hero"> |
178 | | -<div class="hero-eyebrow">For small business owners</div> |
179 | | -<h1>You didn't start your business to pay six SaaS bills.</h1> |
180 | | -<p class="sub">Stockyard is one app on your computer that replaces your scheduling tool, your invoice tool, your member management, and your email blasts. Your data lives in a file you own. Works offline. No per-seat fees. Starts at $49/month for cloud backup, or $299 once if you want to keep it all local.</p> |
181 | | -<div class="hero-actions"> |
182 | | -<a href="/desktop/" class="btn">See pricing</a> |
183 | | -<a href="#what" class="btn btn-outline">How it works</a> |
| 217 | +<section class="hero hero-gen"> |
| 218 | +<div class="hero-eyebrow">Describe your business</div> |
| 219 | +<h1>Type what you do. Get a tech stack you own.</h1> |
| 220 | +<p class="sub">Stockyard builds you a personalized toolkit from 164 self-hosted tools. Scheduling, invoicing, customer records, email — composed to fit your specific business, running on your own computer. Your data in a file you own.</p> |
| 221 | +<form class="gen-form" id="gen-form" autocomplete="off"> |
| 222 | +<input class="gen-input" id="gen-input" type="text" placeholder="mobile dog groomer in Portland" maxlength="500" aria-label="Describe your business" /> |
| 223 | +<button class="gen-btn" id="gen-btn" type="submit">Build toolkit</button> |
| 224 | +</form> |
| 225 | +<div class="gen-chips" id="gen-chips"> |
| 226 | +<span class="gen-chips-prefix">try:</span> |
| 227 | +<button class="gen-chip" data-chip="yoga studio">yoga studio</button> |
| 228 | +<button class="gen-chip" data-chip="tattoo shop">tattoo shop</button> |
| 229 | +<button class="gen-chip" data-chip="minecraft server">minecraft server</button> |
| 230 | +<button class="gen-chip" data-chip="therapist">therapist</button> |
| 231 | +<button class="gen-chip" data-chip="accounting firm">accounting firm</button> |
184 | 232 | </div> |
185 | | -<div class="hero-note">Mac, Windows, Linux. Your data stays on your computer.</div> |
| 233 | +<div class="gen-result" id="gen-result" aria-live="polite"></div> |
| 234 | +<div class="hero-note">Starts at $49/month with cloud backup, or $299 once for local. Mac, Windows, Linux.</div> |
186 | 235 | </section> |
187 | 236 |
|
188 | 237 | <!-- ── Mocked app screenshot ────────────────────────────────────── --> |
@@ -464,5 +513,160 @@ <h2>If Stockyard disappears tomorrow, your software keeps working.</h2> |
464 | 513 | <p class="fine">hello@stockyard.dev · Built by a solo developer in Minnesota · © 2026</p> |
465 | 514 | </footer> |
466 | 515 |
|
| 516 | +<script> |
| 517 | +(function(){ |
| 518 | + var form = document.getElementById('gen-form'); |
| 519 | + var input = document.getElementById('gen-input'); |
| 520 | + var btn = document.getElementById('gen-btn'); |
| 521 | + var result = document.getElementById('gen-result'); |
| 522 | + if (!form || !input || !btn || !result) return; |
| 523 | + |
| 524 | + function escapeHtml(s){ |
| 525 | + return String(s == null ? '' : s).replace(/[&<>"']/g, function(c){ |
| 526 | + return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; |
| 527 | + }); |
| 528 | + } |
| 529 | + |
| 530 | + function articleFor(word){ |
| 531 | + var first = (word || '').trim().charAt(0).toLowerCase(); |
| 532 | + return 'aeiou'.indexOf(first) !== -1 ? 'an' : 'a'; |
| 533 | + } |
| 534 | + |
| 535 | + function renderThinking(desc){ |
| 536 | + result.innerHTML = |
| 537 | + '<div class="gen-thinking">Building a toolkit for ' + articleFor(desc) + ' <strong>' + |
| 538 | + escapeHtml(desc) + '</strong>…</div>'; |
| 539 | + } |
| 540 | + |
| 541 | + function renderError(msg, showCategoryLink){ |
| 542 | + var link = showCategoryLink |
| 543 | + ? ' <a href="/for/">Browse toolkits by category →</a>' |
| 544 | + : ''; |
| 545 | + result.innerHTML = '<div class="gen-error">' + escapeHtml(msg) + link + '</div>'; |
| 546 | + } |
| 547 | + |
| 548 | + function renderResult(data, userInput){ |
| 549 | + var tools = (data && data.tools) || []; |
| 550 | + if (tools.length === 0){ |
| 551 | + renderError("We couldn't build a toolkit for that. Try describing it differently, or", true); |
| 552 | + return; |
| 553 | + } |
| 554 | + var savings = data.savings_per_year || 0; |
| 555 | + var replaces = data.total_replaces_cost || 0; |
| 556 | + var title = data.title || 'Your Toolkit'; |
| 557 | + var audience = data.audience || ''; |
| 558 | + var cards = tools.map(function(t){ |
| 559 | + var replacesLine = t.replaces |
| 560 | + ? 'replaces ' + escapeHtml(t.replaces) + |
| 561 | + (t.replaces_cost ? ' ($' + t.replaces_cost + '/mo)' : '') |
| 562 | + : ''; |
| 563 | + return '<div class="gen-tool">' + |
| 564 | + '<div class="gen-tool-name">' + escapeHtml(t.label || t.slug) + '</div>' + |
| 565 | + (replacesLine ? '<div class="gen-tool-replaces">' + replacesLine + '</div>' : '') + |
| 566 | + '</div>'; |
| 567 | + }).join(''); |
| 568 | + var savingsBlock = savings > 0 |
| 569 | + ? '<div class="gen-savings">' + |
| 570 | + '<div>' + |
| 571 | + '<div class="gen-savings-label">Replaces</div>' + |
| 572 | + '<div class="gen-savings-amount">$' + savings.toLocaleString() + '/yr</div>' + |
| 573 | + '</div>' + |
| 574 | + '<a class="gen-cta" href="/desktop/">Install Stockyard →</a>' + |
| 575 | + '</div>' |
| 576 | + : '<div class="gen-savings">' + |
| 577 | + '<div></div>' + |
| 578 | + '<a class="gen-cta" href="/desktop/">Install Stockyard →</a>' + |
| 579 | + '</div>'; |
| 580 | + result.innerHTML = |
| 581 | + '<div class="gen-card">' + |
| 582 | + '<div class="gen-card-head">' + |
| 583 | + '<div class="gen-card-title">' + escapeHtml(title) + '</div>' + |
| 584 | + (audience ? '<div class="gen-card-audience">' + escapeHtml(audience) + '</div>' : '') + |
| 585 | + '</div>' + |
| 586 | + '<div class="gen-tools">' + cards + '</div>' + |
| 587 | + savingsBlock + |
| 588 | + '</div>'; |
| 589 | + } |
| 590 | + |
| 591 | + var inFlight = false; |
| 592 | + |
| 593 | + function submit(desc){ |
| 594 | + desc = (desc || '').trim(); |
| 595 | + if (desc.length < 3){ |
| 596 | + renderError('Please describe your business in a few words (at least 3 characters).', false); |
| 597 | + return; |
| 598 | + } |
| 599 | + if (desc.length > 500){ |
| 600 | + renderError('That description is too long — please shorten to under 500 characters.', false); |
| 601 | + return; |
| 602 | + } |
| 603 | + if (inFlight) return; |
| 604 | + inFlight = true; |
| 605 | + btn.disabled = true; |
| 606 | + renderThinking(desc); |
| 607 | + |
| 608 | + // Soft client timeout — 45s ceiling. Server has its own 60s for the |
| 609 | + // LLM call; this gives the user a clear stopping point if the |
| 610 | + // network itself hangs. |
| 611 | + var controller = (typeof AbortController !== 'undefined') ? new AbortController() : null; |
| 612 | + var timeoutId = setTimeout(function(){ |
| 613 | + if (controller) controller.abort(); |
| 614 | + }, 45000); |
| 615 | + |
| 616 | + fetch('/api/recommend', { |
| 617 | + method: 'POST', |
| 618 | + headers: {'Content-Type': 'application/json'}, |
| 619 | + body: JSON.stringify({description: desc}), |
| 620 | + signal: controller ? controller.signal : undefined |
| 621 | + }).then(function(r){ |
| 622 | + if (!r.ok){ |
| 623 | + if (r.status === 429){ |
| 624 | + throw new Error('rate_limited'); |
| 625 | + } |
| 626 | + throw new Error('server_' + r.status); |
| 627 | + } |
| 628 | + return r.json(); |
| 629 | + }).then(function(data){ |
| 630 | + renderResult(data, desc); |
| 631 | + if (typeof gtag === 'function'){ |
| 632 | + gtag('event', 'hero_toolkit_generated', { |
| 633 | + layer: data.cached ? 'cached' : 'fresh', |
| 634 | + tool_count: (data.tools || []).length |
| 635 | + }); |
| 636 | + } |
| 637 | + }).catch(function(err){ |
| 638 | + if (err && err.name === 'AbortError'){ |
| 639 | + renderError("That took longer than expected. Please try again, or", true); |
| 640 | + } else if (err && err.message === 'rate_limited'){ |
| 641 | + renderError("We're getting a lot of traffic — please try again in a minute, or", true); |
| 642 | + } else { |
| 643 | + renderError("Something went wrong building that toolkit. Try again, or", true); |
| 644 | + } |
| 645 | + }).then(function(){ |
| 646 | + clearTimeout(timeoutId); |
| 647 | + inFlight = false; |
| 648 | + btn.disabled = false; |
| 649 | + }); |
| 650 | + } |
| 651 | + |
| 652 | + form.addEventListener('submit', function(e){ |
| 653 | + e.preventDefault(); |
| 654 | + submit(input.value); |
| 655 | + }); |
| 656 | + |
| 657 | + // Chip click → fill input and submit |
| 658 | + var chips = document.querySelectorAll('.gen-chip'); |
| 659 | + for (var i = 0; i < chips.length; i++){ |
| 660 | + chips[i].addEventListener('click', function(e){ |
| 661 | + e.preventDefault(); |
| 662 | + var chip = e.currentTarget.getAttribute('data-chip'); |
| 663 | + if (!chip) return; |
| 664 | + input.value = chip; |
| 665 | + submit(chip); |
| 666 | + }); |
| 667 | + } |
| 668 | +})(); |
| 669 | +</script> |
| 670 | + |
467 | 671 | </body> |
468 | 672 | </html> |
0 commit comments