Initial import of Brookhaven site

This commit is contained in:
2025-11-18 20:28:52 +00:00
parent 7e27f39d01
commit 773a91dd2f
1900 changed files with 700543 additions and 0 deletions

30
templates/about.html Normal file
View File

@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}About — {{ brand }}{% endblock %}
{% block content %}
<section class="max-w-7xl mx-auto px-6 py-16">
<h1 class="text-3xl font-bold">About BrookHaven</h1>
<p class="mt-4 text-white/80 max-w-3xl">
Were a small, senior team that prototypes fast and ships safely. We love systems thinking,
clean UX, and deployments that dont wake you up at 3am.
</p>
<div class="mt-8 grid md:grid-cols-2 gap-6">
<div class="rounded-2xl bg-bh.card/70 border border-bh.ring p-6">
<h3 class="font-semibold">What we value</h3>
<ul class="mt-3 space-y-2 text-white/75 text-sm">
<li>• Production realism over slideware</li>
<li>• Data-in/data-out from day one</li>
<li>• Accessibility & performance as defaults</li>
</ul>
</div>
<div class="rounded-2xl bg-bh.card/70 border border-bh.ring p-6">
<h3 class="font-semibold">How we work</h3>
<ul class="mt-3 space-y-2 text-white/75 text-sm">
<li>• Short sprints to real demos</li>
<li>• Clear handoffs to internal teams</li>
<li>• Infra that fits your stack</li>
</ul>
</div>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}Admin — Inquiries{% endblock %}
{% block content %}
<section class="max-w-7xl mx-auto px-6 py-10">
<h1 class="text-2xl font-bold">Inquiries ({{ total }})</h1>
<div class="mt-4 overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="bg-bh.card/80 border border-bh.ring">
<tr>
<th class="p-2 text-left">ID</th>
<th class="p-2 text-left">Name</th>
<th class="p-2 text-left">Email</th>
<th class="p-2 text-left">NDA</th>
<th class="p-2 text-left">Message</th>
<th class="p-2 text-left">UA</th>
<th class="p-2 text-left">IP</th>
<th class="p-2 text-left">Created</th>
</tr>
</thead>
<tbody>
{% for r in rows %}
<tr class="border-b border-bh.ring/50">
<td class="p-2">{{ r.id }}</td>
<td class="p-2">{{ r.name }}</td>
<td class="p-2">{{ r.email }}</td>
<td class="p-2">{{ r.nda }}</td>
<td class="p-2">{{ r.message }}</td>
<td class="p-2">{{ r.meta.ua if r.meta else '' }}</td>
<td class="p-2">{{ r.meta.ip if r.meta else '' }}</td>
<td class="p-2 whitespace-nowrap">{{ r.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-4 flex items-center gap-3">
{% if page > 1 %}<a class="px-3 py-1 rounded bg-bh.card/80 border border-bh.ring" href="?page={{ page-1 }}">Prev</a>{% endif %}
<div class="opacity-75">Page {{ page }} / {{ pages }}</div>
{% if page < pages %}<a class="px-3 py-1 rounded bg-bh.card/80 border border-bh.ring" href="?page={{ page+1 }}">Next</a>{% endif %}
<a class="ml-auto px-3 py-1 rounded bg-bh-accent/90 text-black font-semibold" href="{{ url_for('admin_inquiries_csv') }}">Export CSV</a>
</div>
</section>
{% endblock %}

153
templates/base.html Normal file
View File

@@ -0,0 +1,153 @@
<!doctype html>
<html lang="en" class="h-full scroll-smooth">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}{{ brand }}{% endblock %}</title>
<meta name="theme-color" content="#0f172a" />
<link rel="icon" href="/static/favicon.ico" />
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
bh: {
bg: "#0b1220", card: "#0f1a2b", ring: "#1f2b40",
primary: "#0ea5e9", accent: "#34d399", glow: "#22d3ee",
}
},
boxShadow: { glow: "0 0 0 3px rgba(52, 211, 153, .25), 0 0 40px rgba(14,165,233,.15)" }
}
}
}
</script>
<style>
html, body { height: 100%; }
body { display:flex; flex-direction:column; }
main { flex:1; }
.bg-grid {
background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,.08) 1px, transparent 0);
background-size: 22px 22px;
}
.mask-fade {
-webkit-mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
}
</style>
{% block head %}{% endblock %}
</head>
<body class="min-h-full bg-bh-bg text-white selection:bg-bh-accent/30">
<!-- Header -->
<header class="sticky top-0 z-50 backdrop-blur bg-bh-bg/70 border-b border-bh-ring">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<!-- Brand -->
<a href="{{ url_for('home') }}" class="flex items-center gap-3 group">
<svg width="28" height="28" viewBox="0 0 32 32" class="shrink-0">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#34d399"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient></defs>
<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#g)"/>
<path d="M10 21V9h4.5a4 4 0 1 1 0 8H10zm4.2-6.5H13.6v3h.6a1.5 1.5 0 0 0 0-3Z" fill="#0b1220"/>
</svg>
<span class="font-semibold tracking-wide">BrookHaven <span class="text-bh-accent">by Benny's House</span></span>
</a>
<!-- Desktop nav -->
<nav class="hidden md:flex items-center gap-6 text-sm">
<a href="{{ url_for('services') }}" class="hover:text-bh-accent">Services</a>
<a href="{{ url_for('work') }}" class="hover:text-bh-accent">Work</a>
<a href="{{ url_for('about') }}" class="hover:text-bh-accent">About</a>
<a href="{{ url_for('contact') }}" class="hover:text-bh-accent">Contact</a>
<a href="{{ url_for('contact') }}" class="ml-2 inline-flex items-center rounded-lg bg-bh-accent/90 hover:bg-bh-accent text-black px-4 py-2 font-semibold shadow-glow">Start a project</a>
</nav>
<!-- Mobile toggle -->
<button
id="menuBtn"
class="md:hidden p-2 rounded hover:bg-white/5 focus:outline-none focus:ring-2 focus:ring-bh-accent/60"
aria-controls="mobileNav"
aria-label="Toggle menu"
aria-expanded="false">
<!-- hamburger -->
<svg id="iconOpen" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" class=""><path d="M3 6h16M3 11h16M3 16h16"/></svg>
<!-- close (hidden by default) -->
<svg id="iconClose" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" class="hidden"><path d="M4 4l14 14M18 4L4 18"/></svg>
</button>
</div>
<!-- Mobile overlay (hidden by default) -->
<div id="mobileOverlay" class="hidden md:hidden fixed inset-0 bg-black/40"></div>
<!-- Mobile panel (hidden by default) -->
<div
id="mobileNav"
class="hidden md:hidden border-t border-bh-ring bg-bh-bg/95 backdrop-blur fixed inset-x-0 top-16 z-50 will-change-transform">
<nav class="px-4 py-4 grid gap-2 text-sm">
<a class="py-2 hover:text-bh-accent" href="{{ url_for('services') }}">Services</a>
<a class="py-2 hover:text-bh-accent" href="{{ url_for('work') }}">Work</a>
<a class="py-2 hover:text-bh-accent" href="{{ url_for('about') }}">About</a>
<a class="py-2 hover:text-bh-accent" href="{{ url_for('contact') }}">Contact</a>
<a class="mt-2 inline-flex items-center justify-center rounded-lg bg-bh-accent/90 hover:bg-bh-accent text-black px-4 py-2 font-semibold" href="{{ url_for('contact') }}">Start a project</a>
</nav>
</div>
</header>
<main>{% block content %}{% endblock %}</main>
<!-- Footer -->
<footer class="border-t border-bh-ring">
<div class="max-w-7xl mx-auto px-6 py-8 text-sm text-white/60 flex flex-col sm:flex-row gap-2 sm:items-center sm:justify-between">
<div>© <span id="year"></span> BrookHaven Technologies — Bennys House</div>
<div class="opacity-80">Canyon · Amarillo · Borger · Remote</div>
</div>
<div class="max-w-7xl mx-auto px-6 pb-8 text-sm flex flex-wrap gap-4 items-center justify-between">
<div class="opacity-80">{{ CONTACT.name }} — {{ CONTACT.title }}</div>
<div class="flex flex-wrap gap-4">
<a href="mailto:{{ CONTACT.email }}" class="underline">{{ CONTACT.email }}</a>
<span class="opacity-60">·</span>
<a href="tel:{{ CONTACT.phone|replace(' ', '') }}" class="underline">{{ CONTACT.phone }}</a>
{% if CONTACT.cal %}<span class="opacity-60">·</span><a class="underline" href="{{ CONTACT.cal }}" target="_blank" rel="noopener">Book a call</a>{% endif %}
</div>
</div>
</footer>
<script>
// Year
document.getElementById('year').textContent = new Date().getFullYear();
// Mobile menu logic (no Alpine)
const btn = document.getElementById('menuBtn');
const nav = document.getElementById('mobileNav');
const overlay = document.getElementById('mobileOverlay');
const iconOpen = document.getElementById('iconOpen');
const iconClose = document.getElementById('iconClose');
let isOpen = false;
function setOpen(open) {
isOpen = open;
[nav, overlay].forEach(el => el.classList.toggle('hidden', !open));
iconOpen.classList.toggle('hidden', open);
iconClose.classList.toggle('hidden', !open);
btn.setAttribute('aria-expanded', String(open));
// Prevent background scroll when open
document.documentElement.classList.toggle('overflow-hidden', open);
}
btn?.addEventListener('click', () => setOpen(!isOpen));
overlay?.addEventListener('click', () => setOpen(false));
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isOpen) setOpen(false);
});
// Close menu if resized to desktop
window.addEventListener('resize', () => {
if (window.matchMedia('(min-width: 768px)').matches && isOpen) setOpen(false);
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>

84
templates/contact.html Normal file
View File

@@ -0,0 +1,84 @@
{% extends "base.html" %}
{% block title %}Contact — {{ brand }}{% endblock %}
{% block content %}
<section class="max-w-7xl mx-auto px-6 py-16">
{% with msgs = get_flashed_messages(with_categories=true) %}
{% if msgs %}
{% for cat,msg in msgs %}
<div class="mb-4 rounded border p-3 {{ 'border-red-700 bg-red-900/40 text-red-200' if cat=='error' else 'border-emerald-700 bg-emerald-900/30 text-emerald-100' }}">{{ msg }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="grid lg:grid-cols-2 gap-8">
<!-- Direct Contact Card -->
<div class="rounded-2xl bg-bh.card/70 border border-bh.ring p-6">
<h1 class="text-3xl font-bold">Get in touch</h1>
<p class="mt-2 text-white/75">Prefer email or a quick call? Reach me directly.</p>
<div class="mt-5 rounded-xl bg-white/5 border border-white/10 p-4">
<div class="text-xl font-semibold">{{ CONTACT.name }}</div>
<div class="text-white/70">{{ CONTACT.title }}</div>
<div class="mt-3 grid gap-2 text-sm">
<div class="flex items-center gap-2">
<span class="opacity-70">Email:</span>
<a id="emailLink" href="mailto:{{ CONTACT.email }}" class="underline">{{ CONTACT.email }}</a>
<button class="ml-2 px-2 py-0.5 rounded bg-bh-accent/20 hover:bg-bh-accent/30 text-bh-accent text-xs" onclick="copyTxt('{{ CONTACT.email }}')">Copy</button>
</div>
<div class="flex items-center gap-2">
<span class="opacity-70">Phone:</span>
<a id="phoneLink" href="tel:{{ CONTACT.phone|replace(' ', '') }}" class="underline">{{ CONTACT.phone }}</a>
<button class="ml-2 px-2 py-0.5 rounded bg-bh-accent/20 hover:bg-bh-accent/30 text-bh-accent text-xs" onclick="copyTxt('{{ CONTACT.phone }}')">Copy</button>
</div>
<div class="flex items-center gap-2">
<span class="opacity-70">Hours:</span>
<span>{{ CONTACT.hours }}</span>
</div>
<div class="flex items-center gap-2">
<span class="opacity-70">Location:</span>
<span>{{ CONTACT.city }}</span>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
{% if CONTACT.cal %}
<a href="{{ CONTACT.cal }}" target="_blank" class="inline-flex items-center rounded-lg bg-bh-accent/90 hover:bg-bh-accent text-black px-4 py-2 font-semibold shadow-glow">Book a call</a>
{% endif %}
{% if CONTACT.link %}
<a href="{{ CONTACT.link }}" target="_blank" class="inline-flex items-center rounded-lg border border-bh.ring hover:border-bh-accent/60 px-4 py-2">LinkedIn</a>
{% endif %}
<a href="{{ url_for('contact_vcf') }}" class="inline-flex items-center rounded-lg border border-bh.ring hover:border-bh-accent/60 px-4 py-2">Download vCard</a>
</div>
<!-- New “Get a Quote” button -->
<div class="mt-5">
<a href="https://netdeploy.net"
target="_blank"
class="inline-flex items-center justify-center rounded-lg w-full bg-bh-accent/90 hover:bg-bh-accent text-black font-semibold py-3 shadow-glow transition-all duration-200">
Get a Quote
</a>
</div>
</div>
<p class="mt-6 text-white/70 text-sm">Or use the form — well reply with a short plan and timeline.</p>
</div>
<!-- Inquiry Form (Link to netdeploy.net) -->
</div>
</section>
{% block scripts %}
<script>
function copyTxt(txt){
navigator.clipboard.writeText(txt).then(()=> {
const el = document.createElement('div');
el.textContent = 'Copied!';
el.className = 'fixed bottom-4 right-4 px-3 py-2 rounded bg-bh-accent text-black text-sm shadow-glow';
document.body.appendChild(el);
setTimeout(()=> el.remove(), 900);
});
}
</script>
{% endblock %}
{% endblock %}

134
templates/form.html Normal file
View File

@@ -0,0 +1,134 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>United Supermarkets · Cyber Sale Survey</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-950 text-white min-h-screen">
<header class="max-w-3xl mx-auto px-4 py-6">
<a href="/" class="text-slate-300 underline">&larr; Back</a>
<h1 class="mt-2 text-3xl font-extrabold">Quick Survey</h1>
<p class="text-slate-300 mt-1">Help us improve <span class="font-semibold">E-Commerce & Digital Coupons</span> at United.</p>
</header>
<main class="max-w-3xl mx-auto px-4">
<form method="post" class="bg-slate-900/60 border border-slate-800 rounded-2xl p-5 grid gap-6">
<!-- Contact (optional) -->
<div class="grid sm:grid-cols-2 gap-4">
<label class="grid gap-1">
<span class="text-sm text-slate-300">Name (optional)</span>
<input name="name" class="text-black p-2 rounded" placeholder="Your name" />
</label>
<label class="grid gap-1">
<span class="text-sm text-slate-300">Email (optional for follow-ups)</span>
<input type="email" name="email" class="text-black p-2 rounded" placeholder="you@example.com" />
</label>
</div>
<label class="inline-flex items-center gap-2 text-sm text-slate-300">
<input type="checkbox" name="email_consent" value="yes" class="h-4 w-4" />
I agree United may contact me about e-commerce and coupons.
</label>
<hr class="border-slate-800">
<!-- Shopping frequency -->
<div class="grid gap-2">
<div class="text-sm font-semibold">How often do you shop for groceries online with United?</div>
<div class="grid sm:grid-cols-4 gap-2">
<label class="flex items-center gap-2"><input type="radio" name="shop_online_freq" value="never" required> Never</label>
<label class="flex items-center gap-2"><input type="radio" name="shop_online_freq" value="rarely"> Rarely</label>
<label class="flex items-center gap-2"><input type="radio" name="shop_online_freq" value="monthly"> Monthly</label>
<label class="flex items-center gap-2"><input type="radio" name="shop_online_freq" value="weekly+"> Weekly+</label>
</div>
</div>
<!-- Fulfillment preference -->
<div class="grid gap-2">
<div class="text-sm font-semibold">Preferred way to get your order?</div>
<div class="grid sm:grid-cols-3 gap-2">
<label class="flex items-center gap-2"><input type="radio" name="fulfillment_preference" value="pickup" required> Curbside Pickup</label>
<label class="flex items-center gap-2"><input type="radio" name="fulfillment_preference" value="delivery"> Delivery</label>
<label class="flex items-center gap-2"><input type="radio" name="fulfillment_preference" value="in-store"> In-store Shopping</label>
</div>
</div>
<!-- Digital coupons -->
<div class="grid gap-2">
<div class="text-sm font-semibold">Do you use digital coupons?</div>
<div class="grid sm:grid-cols-4 gap-2">
<label class="flex items-center gap-2"><input type="radio" name="digital_coupons_use" value="often" required> Often</label>
<label class="flex items-center gap-2"><input type="radio" name="digital_coupons_use" value="sometimes"> Sometimes</label>
<label class="flex items-center gap-2"><input type="radio" name="digital_coupons_use" value="tried"> Ive tried them</label>
<label class="flex items-center gap-2"><input type="radio" name="digital_coupons_use" value="never"> Never</label>
</div>
</div>
<!-- Preference: Digital vs Paper -->
<div class="grid gap-2">
<div class="text-sm font-semibold">Coupons preference</div>
<div class="grid sm:grid-cols-4 gap-2">
<label class="flex items-center gap-2"><input type="radio" name="coupons_preference" value="digital" required> Digital</label>
<label class="flex items-center gap-2"><input type="radio" name="coupons_preference" value="paper"> Paper</label>
<label class="flex items-center gap-2"><input type="radio" name="coupons_preference" value="both"> Both</label>
<label class="flex items-center gap-2"><input type="radio" name="coupons_preference" value="none"> None</label>
</div>
</div>
<!-- What matters most -->
<div class="grid gap-2">
<div class="text-sm font-semibold">What matters most when ordering online? <span class="opacity-70">(pick all that apply)</span></div>
<div class="grid sm:grid-cols-3 gap-2">
<label class="flex items-center gap-2"><input type="checkbox" name="what_matters" value="price"> Low Prices</label>
<label class="flex items-center gap-2"><input type="checkbox" name="what_matters" value="fees"> Low Fees</label>
<label class="flex items-center gap-2"><input type="checkbox" name="what_matters" value="timeslots"> Convenient Time Slots</label>
<label class="flex items-center gap-2"><input type="checkbox" name="what_matters" value="speed"> Fast Pickup/Delivery</label>
<label class="flex items-center gap-2"><input type="checkbox" name="what_matters" value="substitutions"> Good Substitutions</label>
<label class="flex items-center gap-2"><input type="checkbox" name="what_matters" value="coupons"> Easy Digital Coupons</label>
</div>
</div>
<!-- Barriers -->
<div class="grid gap-2">
<div class="text-sm font-semibold">What keeps you from ordering online more often? <span class="opacity-70">(pick all that apply)</span></div>
<div class="grid sm:grid-cols-3 gap-2">
<label class="flex items-center gap-2"><input type="checkbox" name="barriers" value="fees"> Fees</label>
<label class="flex items-center gap-2"><input type="checkbox" name="barriers" value="pricing"> Prices</label>
<label class="flex items-center gap-2"><input type="checkbox" name="barriers" value="substitutions"> Substitutions quality</label>
<label class="flex items-center gap-2"><input type="checkbox" name="barriers" value="availability"> Item availability</label>
<label class="flex items-center gap-2"><input type="checkbox" name="barriers" value="timeslots"> Time slot availability</label>
<label class="flex items-center gap-2"><input type="checkbox" name="barriers" value="app"> App/website usability</label>
</div>
</div>
<!-- Device -->
<div class="grid gap-2">
<div class="text-sm font-semibold">What device do you usually use?</div>
<div class="grid sm:grid-cols-3 gap-2">
<label class="flex items-center gap-2"><input type="radio" name="device" value="phone" required> Phone</label>
<label class="flex items-center gap-2"><input type="radio" name="device" value="tablet"> Tablet</label>
<label class="flex items-center gap-2"><input type="radio" name="device" value="desktop"> Desktop/Laptop</label>
</div>
</div>
<!-- Free text -->
<div class="grid gap-1">
<label class="text-sm font-semibold">Anything we could improve for online shopping?</label>
<textarea name="feedback" rows="3" class="text-black p-2 rounded" placeholder="Tell us what would make United online shopping even better."></textarea>
</div>
<!-- Submit -->
<div class="flex items-center justify-between gap-3">
<p class="text-xs text-slate-400">Note: This is a quick promo survey; answers help us improve E-Commerce and coupons. </p>
<button class="bg-emerald-500 hover:bg-emerald-600 px-6 py-3 rounded-lg font-semibold text-black">Start the Game</button>
</div>
</form>
</main>
<footer class="max-w-3xl mx-auto px-4 py-8 text-center text-slate-400 text-xs">
© United Supermarkets — Cyber Sale Tailgate
</footer>
</body>
</html>

49
templates/host.html Normal file
View File

@@ -0,0 +1,49 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
</head>
<body class="bg-slate-900 text-white">
<div class="max-w-3xl mx-auto p-6">
<h1 class="text-3xl font-bold mb-4">Tapdown Showdown Room <span id="room"></span></h1>
<div class="mb-4 flex gap-2">
<button id="startBtn" class="px-4 py-2 bg-emerald-500 rounded">Start Match</button>
</div>
<div class="relative h-40 rounded bg-slate-800 overflow-hidden">
<div class="absolute inset-y-0 left-1/2 w-0.5 bg-slate-600"></div>
<div id="puck" class="absolute top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white shadow"></div>
<div class="absolute inset-y-0 left-0 w-1 bg-red-500"></div>
<div class="absolute inset-y-0 right-0 w-1 bg-blue-500"></div>
</div>
<p id="status" class="mt-4 text-lg"></p>
</div>
<script>
const room = "{{ room }}";
document.getElementById("room").textContent = room;
const socket = io("/game");
socket.emit("join_room", { room, player_id: "host", side: null });
document.getElementById("startBtn").onclick = () => {
socket.emit("start", { room });
document.getElementById("status").textContent = "Fight!";
};
socket.on("state", ({ puck }) => {
const box = document.querySelector(".relative.h-40");
const puckEl = document.getElementById("puck");
const w = box.clientWidth;
const x = (puck / 100) * (w/2 - 10); // map -100..100 to pixels
puckEl.style.left = `calc(50% + ${x}px)`;
});
socket.on("match_end", ({ winner }) => {
document.getElementById("status").textContent = winner.toUpperCase() + " WINS!";
});
</script>
</body>
</html>

76
templates/host_lobby.html Normal file
View File

@@ -0,0 +1,76 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
</head>
<body class="bg-slate-900 text-white p-6">
<h1 class="text-2xl font-bold mb-4">Lobby</h1>
<div class="grid md:grid-cols-2 gap-6">
<div>
<h2 class="font-semibold mb-2">Waiting Players</h2>
<ul id="waiting" class="space-y-2"></ul>
</div>
<div>
<h2 class="font-semibold mb-2">Create Room</h2>
<div class="flex gap-2 items-center mb-3">
<input id="code" class="text-black p-2 rounded" placeholder="Room code (e.g., 4321)">
<input id="left" class="text-black p-2 rounded" placeholder="Left PID">
<input id="right" class="text-black p-2 rounded" placeholder="Right PID">
<button id="create" class="bg-emerald-500 px-3 py-2 rounded">Assign</button>
</div>
<h2 class="font-semibold mb-2">Rooms</h2>
<ul id="rooms" class="space-y-2"></ul>
</div>
</div>
<script>
const socket = io("/game");
socket.emit("lobby_subscribe");
const waitingEl = document.getElementById("waiting");
const roomsEl = document.getElementById("rooms");
socket.on("waiting_list", ({players}) => {
waitingEl.innerHTML = "";
players.forEach(pid => {
const li = document.createElement("li");
li.className = "bg-slate-800 rounded px-3 py-2 cursor-pointer";
li.textContent = pid;
li.onclick = () => {
const left = document.getElementById("left");
const right = document.getElementById("right");
if(!left.value) left.value = pid;
else if(!right.value) right.value = pid;
else left.value = pid;
};
waitingEl.appendChild(li);
});
});
socket.on("rooms_list", ({rooms}) => {
roomsEl.innerHTML = "";
Object.entries(rooms).forEach(([code, info]) => {
const li = document.createElement("li");
li.className = "bg-slate-800 rounded px-3 py-2 flex justify-between";
li.innerHTML = `<div><b>${code}</b> — L:${info.left ?? "-"} R:${info.right ?? "-"} ${info.running ? "🟢" : "⚪"}</div>
<a class="underline" href="/host/${code}" target="_blank">Open Host</a>`;
roomsEl.appendChild(li);
});
});
document.getElementById("create").onclick = () => {
const code = document.getElementById("code").value.trim() || (Math.random()*1e4|0).toString().padStart(4,"0");
const left = document.getElementById("left").value.trim();
const right = document.getElementById("right").value.trim();
if(!left || !right) return alert("Pick two players.");
socket.emit("assign_to_room", { code, left, right });
document.getElementById("code").value = "";
document.getElementById("left").value = "";
document.getElementById("right").value = "";
};
</script>
</body>
</html>

View File

@@ -0,0 +1,154 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Survey Responses</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.nice-scrollbar::-webkit-scrollbar{height:8px;width:8px}
.nice-scrollbar::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:9999px}
</style>
</head>
<body class="min-h-screen bg-slate-950 text-slate-100">
<!-- gradient header -->
<div class="bg-gradient-to-b from-slate-900 via-slate-900/70 to-transparent">
<header class="mx-auto max-w-7xl px-6 pt-8 pb-6">
<h1 class="text-2xl md:text-3xl font-bold tracking-tight">Survey Responses</h1>
<p class="mt-1 text-sm text-slate-400">Review and export all captured entries.</p>
<div class="mt-4 flex flex-wrap items-center gap-2 text-sm text-slate-300">
<span>Total <span class="font-semibold text-white">{{ total }}</span></span>
<span class="opacity-50"></span>
{% set pages = (total // per_page) + (1 if total % per_page else 0) %}
<span>Page <span class="font-semibold text-white">{{ page }}</span> / {{ pages }}</span>
</div>
</header>
</div>
<main class="mx-auto max-w-7xl px-6 pb-12">
<!-- sticky tools -->
<div class="sticky top-0 z-20 -mx-6 px-6 py-3 border-y border-slate-800/60 backdrop-blur bg-slate-950/65">
<div class="flex items-center gap-2">
<input id="search" type="search" placeholder="Search name, persona, device, email…"
class="w-72 max-w-[70vw] rounded-lg bg-slate-900/80 border border-slate-800 px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-emerald-500" />
<a href="/host/responses.csv"
class="ml-auto inline-flex items-center gap-2 rounded-lg bg-emerald-500 text-black font-semibold px-3 py-2 text-sm hover:bg-emerald-400 transition">
Export CSV
</a>
</div>
</div>
<!-- table card -->
<div class="mt-4 overflow-hidden rounded-xl border border-slate-800 shadow-[0_0_0_1px_rgba(255,255,255,0.03)_inset]">
<div class="overflow-x-auto nice-scrollbar">
<table class="min-w-[1200px] w-full text-sm">
<thead class="bg-slate-900/90">
<tr class="text-slate-300">
<th class="p-3 text-left font-semibold">ID</th>
<th class="p-3 text-left font-semibold">PID</th>
<th class="p-3 text-left font-semibold">Name</th>
<th class="p-3 text-left font-semibold">Persona</th>
<th class="p-3 text-left font-semibold">Online Freq</th>
<th class="p-3 text-left font-semibold">Fulfillment</th>
<th class="p-3 text-left font-semibold">Coupons Use</th>
<th class="p-3 text-left font-semibold">Pref</th>
<th class="p-3 text-left font-semibold">Matters</th>
<th class="p-3 text-left font-semibold">Barriers</th>
<th class="p-3 text-left font-semibold">Device</th>
<th class="p-3 text-left font-semibold">Email</th>
<th class="p-3 text-left font-semibold whitespace-nowrap">Created</th>
</tr>
</thead>
<tbody id="rows" class="[&_tr:nth-child(even)]:bg-slate-900/30">
{% for r in rows %}
{% set a = r.answers or {} %}
<tr class="border-t border-slate-800 hover:bg-slate-900/60 transition">
<td class="p-3 text-slate-300">{{ r.id }}</td>
<td class="p-3 text-slate-300">{{ r.pid }}</td>
<td class="p-3">
<div class="font-medium">{{ r.name }}</div>
{% if a.device %}<div class="text-xs text-slate-400">{{ a.device }}</div>{% endif %}
</td>
<td class="p-3">
{% set persona = r.persona or '-' %}
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold
{% if persona == 'Sharpshooter' %} bg-emerald-500/15 text-emerald-300 border border-emerald-600/40
{% elif persona == 'Speed Demon' %} bg-fuchsia-500/15 text-fuchsia-300 border border-fuchsia-600/40
{% elif persona == 'Tank' %} bg-sky-500/15 text-sky-300 border border-sky-600/40
{% else %} bg-slate-700/40 text-slate-200 border border-slate-600/40 {% endif %}">
{{ persona }}
</span>
</td>
<td class="p-3 text-slate-200">{{ a.shop_online_freq or '-' }}</td>
<td class="p-3 text-slate-200">{{ a.fulfillment_preference or '-' }}</td>
<td class="p-3 text-slate-200">{{ a.digital_coupons_use or '-' }}</td>
<td class="p-3 text-slate-200">{{ a.coupons_preference or '-' }}</td>
<td class="p-3">
{% set wm = (a.what_matters or []) %}
{% if wm %}
<div class="flex flex-wrap gap-1">
{% for item in wm %}
<span class="rounded-full bg-slate-800 px-2 py-0.5 text-xs text-slate-300">{{ item }}</span>
{% endfor %}
</div>
{% else %}-{% endif %}
</td>
<td class="p-3">
{% set br = (a.barriers or []) %}
{% if br %}
<div class="flex flex-wrap gap-1">
{% for item in br %}
<span class="rounded-full bg-slate-800 px-2 py-0.5 text-xs text-slate-300">{{ item }}</span>
{% endfor %}
</div>
{% else %}-{% endif %}
</td>
<td class="p-3 text-slate-200">{{ a.device or '-' }}</td>
<td class="p-3">
{% if a.email_consent and a.email %}
<a class="underline decoration-slate-600 hover:decoration-emerald-400" href="mailto:{{
a.email }}">{{ a.email }}</a>
{% else %}-{% endif %}
</td>
<td class="p-3 whitespace-nowrap text-slate-300">
{% if r.created_at %}{{ r.created_at.strftime('%Y-%m-%d %H:%M') }}{% else %}-{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- pagination -->
<div class="flex items-center gap-2 p-3 bg-slate-900/70 border-t border-slate-800">
{% if page > 1 %}
<a class="px-3 py-1.5 bg-slate-800 rounded-lg hover:bg-slate-700 transition" href="?page={{ page - 1 }}">Prev</a>
{% else %}
<span class="px-3 py-1.5 bg-slate-900 rounded-lg opacity-40">Prev</span>
{% endif %}
<span class="text-sm text-slate-300">Page <span class="text-white font-semibold">{{ page }}</span> / {{ pages }}</span>
{% if page < pages %}
<a class="px-3 py-1.5 bg-slate-800 rounded-lg hover:bg-slate-700 transition" href="?page={{ page + 1 }}">Next</a>
{% else %}
<span class="px-3 py-1.5 bg-slate-900 rounded-lg opacity-40">Next</span>
{% endif %}
<div class="ml-auto text-xs text-slate-400">Showing {{ rows|length }} of {{ total }}</div>
</div>
</div>
</main>
<script>
// simple client-side filter
const q = document.getElementById('search');
const rows = Array.from(document.querySelectorAll('#rows tr'));
q?.addEventListener('input', () => {
const needle = q.value.trim().toLowerCase();
rows.forEach(tr => {
if (!needle) { tr.style.display = ''; return; }
tr.style.display = tr.innerText.toLowerCase().includes(needle) ? '' : 'none';
});
});
</script>
</body>
</html>

53
templates/host_room.html Normal file
View File

@@ -0,0 +1,53 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
</head>
<body class="bg-slate-900 text-white p-6">
<h1 class="text-2xl font-bold mb-3">Room <span id="code">{{ code }}</span></h1>
<div id="info" class="opacity-80 mb-3"></div>
<button id="start" class="bg-emerald-500 px-4 py-2 rounded mb-4">Start Match</button>
<div class="relative h-40 rounded bg-slate-800 overflow-hidden">
<div class="absolute inset-y-0 left-1/2 w-0.5 bg-slate-600"></div>
<div id="puck" class="absolute top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white shadow"></div>
<div class="absolute inset-y-0 left-0 w-1 bg-red-500"></div>
<div class="absolute inset-y-0 right-0 w-1 bg-blue-500"></div>
</div>
<p id="status" class="mt-3 text-lg"></p>
<script>
const code = "{{ code }}";
const socket = io("/game", { transports: ["websocket"] });
socket.emit("host_subscribe", { code });
document.getElementById("start").onclick = () => socket.emit("start_match", { code });
socket.on("room_state", ({exists, left, right, running, puck}) => {
if(!exists) return;
document.getElementById("info").textContent = `Left: ${left ?? "-"} | Right: ${right ?? "-"} | ${running ? "LIVE" : "Idle"}`;
if(typeof puck === "number") updatePuck(puck);
});
socket.on("state", ({code: c, puck}) => {
if(c !== code) return;
updatePuck(puck);
});
socket.on("match_end", ({code: c, winner}) => {
if(c !== code) return;
document.getElementById("status").textContent = winner.toUpperCase() + " WINS!";
});
function updatePuck(p) {
const box = document.querySelector(".relative.h-40");
const puckEl = document.getElementById("puck");
const w = box.clientWidth;
const x = (p / 100) * (w/2 - 10);
puckEl.style.left = `calc(50% + ${x}px)`;
}
</script>
</body>
</html>

36
templates/index.html Normal file
View File

@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}{{ brand }} — {{ tagline }}{% endblock %}
{% block content %}
<section class="relative overflow-hidden">
<div class="absolute inset-0 bg-grid mask-fade pointer-events-none"></div>
<div class="max-w-7xl mx-auto px-6 py-20 sm:py-28">
<div class="max-w-3xl">
<h1 class="text-4xl sm:text-6xl font-extrabold leading-tight">
{{ tagline.split('.')[0] }}.<br><span class="text-bh-accent">{{ tagline.split('.')[1].strip() }}</span>
</h1>
<p class="mt-5 text-lg text-white/80 max-w-2xl">
BrookHaven is here to be your all-in-one solution to modern data-management.
<br>
<br>
Whether you are looking to connect with your customers using a technical interface, or require solutions for the team that helps you keep those customers, we offer both customer-facing technology, as well as backend, administrative technology for the working man.
</p>
<div class="mt-8 flex flex-wrap gap-3">
<a href="{{ url_for('contact') }}" class="inline-flex items-center rounded-lg bg-bh-accent/90 hover:bg-bh-accent text-black px-6 py-3 font-semibold shadow-glow">Lets build</a>
<a href="{{ url_for('work') }}" class="inline-flex items-center rounded-lg border border-bh.ring hover:border-bh-accent/60 px-6 py-3">See our work</a>
</div>
</div>
</div>
<div class="pointer-events-none absolute -top-24 -right-24 h-72 w-72 rounded-full blur-3xl opacity-20" style="background: radial-gradient(closest-side, #22d3ee, transparent)"></div>
</section>
<section id="services" class="max-w-7xl mx-auto px-6 py-12 sm:py-20">
<div class="grid md:grid-cols-3 gap-6">
<div class="rounded-2xl bg-bh.card/70 border border-bh.ring p-6"><h3 class="text-xl font-semibold">Rapid Prototypes</h3><p class="mt-2 text-white/70">Clickable by Friday. Usable by Sunday.</p></div>
<div class="rounded-2xl bg-bh.card/70 border border-bh.ring p-6"><h3 class="text-xl font-semibold">Enterprise Rails</h3><p class="mt-2 text-white/70">Nginx, Gunicorn, Flask/FastAPI, SQL with guardrails.</p></div>
<div class="rounded-2xl bg-bh.card/70 border border-bh.ring p-6"><h3 class="text-xl font-semibold">Event Tech</h3><p class="mt-2 text-white/70">Offline-friendly kiosks, QR onboarding, live games.</p></div>
</div>
</section>
{% endblock %}

35
templates/play.html Normal file
View File

@@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
</head>
<body class="bg-slate-900 text-white min-h-screen grid place-items-center">
<div class="grid gap-4 text-center">
<h2 class="text-2xl font-bold">Room {{ code }}</h2>
<button id="tap" class="text-2xl py-16 px-24 rounded bg-indigo-600 active:scale-95">TAP</button>
</div>
<script>
const code = "{{ code }}";
const pid = "{{ pid }}";
const socket = io("/game", { transports: ["websocket"] });
// join the room (server will verify if you're left/right)
socket.emit("player_join_room", { code, pid });
const tapBtn = document.getElementById("tap");
tapBtn.addEventListener("pointerdown", sendTap);
function sendTap(e){ e && e.preventDefault(); socket.emit("tap", { code, pid }); }
// optional: hold-to-repeat
tapBtn.addEventListener("pointerdown", () => {
const t = setInterval(() => socket.emit("tap", { code, pid }), 100);
const stop = () => { clearInterval(t); window.removeEventListener("pointerup", stop); tapBtn.removeEventListener("pointerleave", stop); };
window.addEventListener("pointerup", stop, { once: true });
tapBtn.addEventListener("pointerleave", stop, { once: true });
});
</script>
</body>
</html>

196
templates/play_ai.html Normal file
View File

@@ -0,0 +1,196 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tapdown vs CPU — Cyber Sale Edition</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-950 text-white min-h-screen flex flex-col">
<!-- Top bar -->
<header class="max-w-5xl mx-auto w-full px-4 py-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-sm text-slate-400">Player:</span>
<span class="font-semibold">{{ name }}</span>
<span class="mx-2 opacity-50">·</span>
<span class="text-sm text-slate-400">Persona:</span>
<span class="font-semibold">{{ persona }}</span>
</div>
<div class="flex items-center gap-2">
<label class="text-sm opacity-80">Difficulty</label>
<select id="difficulty" class="text-black p-1 rounded">
<option value="easy">Easy</option>
<option value="normal" selected>Normal</option>
<option value="hard">Hard</option>
<option value="insane">Insane</option>
</select>
</div>
</header>
<!-- Arena -->
<main class="max-w-5xl mx-auto w-full px-4 flex-1">
<div class="mb-3 text-center text-slate-300 text-sm">
Beat the <b>Super Saver</b> bot — then browse Cyber Sale deals!
</div>
<div class="relative h-44 rounded-2xl bg-slate-900/60 border border-slate-800 overflow-hidden">
<div class="absolute inset-y-0 left-1/2 w-0.5 bg-slate-700"></div>
<div class="absolute inset-y-0 left-0 w-1 bg-emerald-500"></div>
<div class="absolute inset-y-0 right-0 w-1 bg-cyan-500"></div>
<div id="puck" class="absolute top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white shadow"></div>
</div>
<p id="status" class="mt-3 text-center text-lg"></p>
<div class="grid grid-cols-2 gap-4 mt-6">
<button id="tapLeft" class="text-2xl py-12 rounded-xl bg-emerald-600 active:scale-95 font-semibold">YOU TAP</button>
<button id="tapRight" class="text-2xl py-12 rounded-xl bg-cyan-700 opacity-60 cursor-not-allowed font-semibold" disabled>CPU</button>
</div>
<div class="mt-6 flex items-center justify-center gap-3">
<button id="startBtn" class="bg-emerald-500 hover:bg-emerald-600 px-5 py-2 rounded-lg font-semibold">Start</button>
<button id="resetBtn" class="bg-slate-800 hover:bg-slate-700 px-5 py-2 rounded-lg font-semibold">Reset</button>
<a id="shopBtn" href="/"
class="bg-slate-900 border border-slate-700 px-5 py-2 rounded-lg font-semibold">Back</a>
</div>
<!-- Cyber Sale CTA under the arena -->
<div class="mt-8 text-center">
<a href="{{ url_for('index') }}" class="text-slate-300 underline decoration-dotted">Cyber Sale details & QR on the promo page</a>
</div>
</main>
<footer class="max-w-5xl mx-auto w-full px-4 py-6 text-center text-slate-400 text-xs">
Tip: Holding the button auto-taps. Bursts & fatigue make the bot feel human.
</footer>
<script>
/* --- Pull persona from template to lightly tweak the feel --- */
const PERSONA = "{{ persona }}";
/* --- Physics constants --- */
const EDGE = 100.0;
const BASE_IMPULSE = 7.0;
let FRICTION = 0.92;
const TICK_HZ = 60;
/* Persona perk (very light-touch) */
let leftImpulseMult = 1.0;
if (PERSONA === "Speed Demon") leftImpulseMult = 1.02;
if (PERSONA === "Tank") FRICTION = 0.91;
/* --- Game state --- */
let puck = 0.0, vel = 0.0, running = false;
let lastTapLeft = 0, lastTapRight = 0;
let MIN_TAP_MS = 85;
/* --- CPU model --- */
const DIFF = {
easy: { baseHz: 4.8, burstHz: 7.2, burstProb: 0.20, whiff: 0.08, friction: 0.92 },
normal: { baseHz: 6.6, burstHz: 9.2, burstProb: 0.28, whiff: 0.05, friction: 0.92 },
hard: { baseHz: 8.2, burstHz: 11.5, burstProb: 0.35, whiff: 0.035, friction: 0.91 },
insane: { baseHz: 9.8, burstHz: 13.5, burstProb: 0.45, whiff: 0.02, friction: 0.905 },
};
let cpuCfg = DIFF.normal;
let cpuBurstUntil = 0;
let cpuNextTapAt = 0;
/* --- UI elements --- */
const statusEl = document.getElementById("status");
const puckEl = document.getElementById("puck");
const leftBtn = document.getElementById("tapLeft");
const startBtn = document.getElementById("startBtn");
const resetBtn = document.getElementById("resetBtn");
const diffSel = document.getElementById("difficulty");
/* --- Helpers --- */
function flash(msg){ statusEl.textContent = msg; setTimeout(()=>statusEl.textContent="", 1200); }
function setDifficulty(key){
cpuCfg = DIFF[key] || DIFF.normal;
FRICTION = cpuCfg.friction;
}
diffSel.onchange = () => { setDifficulty(diffSel.value); flash(`Difficulty: ${diffSel.value.toUpperCase()}`); };
/* --- Controls --- */
leftBtn.addEventListener("pointerdown", (e) => {
e.preventDefault(); tap("left");
const t = setInterval(()=> tap("left"), 100);
const stop = ()=>{ clearInterval(t); window.removeEventListener("pointerup", stop); leftBtn.removeEventListener("pointerleave", stop); };
window.addEventListener("pointerup", stop, { once: true });
leftBtn.addEventListener("pointerleave", stop, { once: true });
});
startBtn.onclick = startMatch;
resetBtn.onclick = resetMatch;
/* --- Game functions --- */
function startMatch(){
resetMatch();
running = true;
scheduleCpuTap(performance.now());
flash("Fight!");
}
function resetMatch(){
running = false; vel = 0; puck = 0; updatePuck(puck);
statusEl.textContent = "";
}
function tap(side){
if(!running) return;
const now = performance.now();
if(side === "left"){
if(now - lastTapLeft < MIN_TAP_MS) return;
lastTapLeft = now;
vel -= BASE_IMPULSE * leftImpulseMult;
} else {
if(now - lastTapRight < MIN_TAP_MS) return;
lastTapRight = now;
vel += BASE_IMPULSE;
}
}
/* --- CPU scheduling --- */
function scheduleCpuTap(now){
if(now > cpuBurstUntil && Math.random() < cpuCfg.burstProb){
cpuBurstUntil = now + (300 + Math.random()*400);
}
const inBurst = now < cpuBurstUntil;
const hz = inBurst ? cpuCfg.burstHz : cpuCfg.baseHz;
const intervalMs = 1000 * ( -Math.log(1 - Math.random()) / hz );
cpuNextTapAt = now + intervalMs;
}
function maybeCpuTap(now){
if(now < cpuNextTapAt) return;
if(Math.random() > cpuCfg.whiff) tap("right");
scheduleCpuTap(now);
}
/* --- Loop --- */
function tick(){
const dt = 1.0 / TICK_HZ;
if(running){
vel *= FRICTION;
puck += vel * dt * 6.0;
const now = performance.now();
maybeCpuTap(now);
if(puck <= -EDGE || puck >= EDGE){
running = false;
statusEl.textContent = puck <= -EDGE ? "YOU WIN! 🥳" : "CPU WINS! 🤖";
}
}
updatePuck(puck);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
function updatePuck(p){
const track = document.querySelector(".relative.h-44");
const w = track.clientWidth;
const x = (p / 100) * (w/2 - 10);
puckEl.style.left = `calc(50% + ${x}px)`;
}
/* init */
setDifficulty("normal");
</script>
</body>
</html>

27
templates/queue.html Normal file
View File

@@ -0,0 +1,27 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
</head>
<body class="bg-slate-900 text-white min-h-screen grid place-items-center">
<div class="text-center space-y-3">
<h2 class="text-3xl font-bold">Youre in the Queue</h2>
<p>ID: <b id="pid">{{ pid }}</b> — Persona: <b>{{ persona }}</b></p>
<p>Hang tight—host will assign your match.</p>
</div>
<script>
const pid = "{{ pid }}";
const socket = io("/game", { transports: ["websocket"] });
socket.emit("queue_subscribe", { pid });
// Host will notify your personal channel
socket.on("assigned_room", ({ code, side }) => {
// Optional: you can show side here; server still verifies it
window.location.href = `/play/${code}`;
});
</script>
</body>
</html>

23
templates/services.html Normal file
View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}Services — {{ brand }}{% endblock %}
{% block content %}
<section class="max-w-7xl mx-auto px-6 py-16">
<h1 class="text-3xl font-bold">Services</h1>
<div class="mt-6 grid lg:grid-cols-3 gap-6">
{% for s in [
{"t":"Prototyping", "d":"Clickable by Friday; prove the bet fast."},
{"t":"Web Platforms", "d":"Flask/FastAPI, Tailwind, SQL with CI/CD."},
{"t":"Event Activations", "d":"Kiosks, QR onboarding, local-first modes."},
{"t":"Data & Analytics", "d":"Capture, export, and visualize with ease."},
{"t":"Integrations", "d":"Payments, auth, coupon APIs, catalog sync."},
{"t":"Ops & Hardening", "d":"Nginx, Gunicorn, Docker, logging, SSO."},
] %}
<div class="rounded-2xl bg-bh.card/70 border border-bh.ring p-6">
<h3 class="text-xl font-semibold">{{ s.t }}</h3>
<p class="mt-2 text-white/75">{{ s.d }}</p>
</div>
{% endfor %}
</div>
</section>
{% endblock %}

36
templates/survey.html Normal file
View File

@@ -0,0 +1,36 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-900 text-white">
<form method="post" class="max-w-xl mx-auto p-6 grid gap-4">
<h1 class="text-2xl font-bold">30-Second Survey</h1>
<label class="grid gap-2">
Favorite tailgate drink?
<select name="drink" class="text-black p-2 rounded">
<option value="water">Water</option>
<option value="soda">Soda</option>
<option value="energy">Energy drink</option>
</select>
</label>
<label class="grid gap-2">
Food vibe?
<select name="food" class="text-black p-2 rounded">
<option value="bbq">BBQ</option>
<option value="veggies">Veggies</option>
</select>
</label>
<label class="grid gap-2">
Your style?
<select name="vibe" class="text-black p-2 rounded">
<option value="precision">Precision</option>
<option value="chaos">Chaos</option>
</select>
</label>
<button class="bg-emerald-500 py-2 rounded text-lg">Submit</button>
</form>
</body>
</html>

10
templates/thanks.html Normal file
View File

@@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}Thanks — {{ brand }}{% endblock %}
{% block content %}
<section class="max-w-7xl mx-auto px-6 py-24 text-center">
<h1 class="text-3xl font-bold">Thanks! Well be in touch.</h1>
<p class="mt-3 text-white/75">We usually reply within 12 business days.</p>
<a href="{{ url_for('home') }}" class="mt-6 inline-flex items-center rounded-lg bg-bh-accent/90 hover:bg-bh-accent text-black px-5 py-2 font-semibold">Back to home</a>
</section>
{% endblock %}

22
templates/work.html Normal file
View File

@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}Work — {{ brand }}{% endblock %}
{% block content %}
<section class="max-w-7xl mx-auto px-6 py-16">
<h1 class="text-3xl font-bold">Selected Work</h1>
<div class="mt-8 grid lg:grid-cols-2 gap-8">
{% for c in cases %}
<article class="rounded-2xl bg-bh.card/70 border border-bh.ring overflow-hidden">
<img src="{{ c.image }}" alt="{{ c.title }}" class="w-full aspect-video object-cover" onerror="this.style.display='none'">
<div class="p-6">
<h3 class="text-xl font-semibold">{{ c.title }}</h3>
<p class="mt-2 text-white/80">{{ c.desc }}</p>
<ul class="mt-3 space-y-1 text-white/70 text-sm">
{% for b in c.bullets %}<li>• {{ b }}</li>{% endfor %}
</ul>
</div>
</article>
{% endfor %}
</div>
</section>
{% endblock %}