Initial import of Brookhaven site
This commit is contained in:
30
templates/about.html
Normal file
30
templates/about.html
Normal 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">
|
||||
We’re a small, senior team that prototypes fast and ships safely. We love systems thinking,
|
||||
clean UX, and deployments that don’t 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 %}
|
||||
|
||||
45
templates/admin_inquiries.html
Normal file
45
templates/admin_inquiries.html
Normal 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
153
templates/base.html
Normal 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 — Benny’s 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
84
templates/contact.html
Normal 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 — we’ll 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
134
templates/form.html
Normal 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">← 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"> I’ve 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
49
templates/host.html
Normal 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
76
templates/host_lobby.html
Normal 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>
|
||||
|
||||
154
templates/host_responses.html
Normal file
154
templates/host_responses.html
Normal 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
53
templates/host_room.html
Normal 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
36
templates/index.html
Normal 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">Let’s 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
35
templates/play.html
Normal 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
196
templates/play_ai.html
Normal 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
27
templates/queue.html
Normal 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">You’re 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
23
templates/services.html
Normal 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
36
templates/survey.html
Normal 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
10
templates/thanks.html
Normal 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! We’ll be in touch.</h1>
|
||||
<p class="mt-3 text-white/75">We usually reply within 1–2 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
22
templates/work.html
Normal 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 %}
|
||||
|
||||
Reference in New Issue
Block a user