Initial import of NetDeploy project
This commit is contained in:
45
templates/about.html
Normal file
45
templates/about.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="py-16 md:py-24">
|
||||
<div class="max-w-4xl mx-auto px-6 space-y-8">
|
||||
<header>
|
||||
<h1 class="font-bebas text-5xl">About Benny’s House</h1>
|
||||
<p class="mt-4 text-white/80 max-w-prose">Benny’s House is an Amarillo‑homed IT & Web services studio serving the Texas Panhandle and the greater Texas area. We build and run practical systems for small businesses and individuals, and we can operate at enterprise scale when needed.</p>
|
||||
</header>
|
||||
|
||||
|
||||
<section class="glass rounded-2xl p-6 space-y-4">
|
||||
<h2 class="text-xl font-semibold">What we believe</h2>
|
||||
<ul class="card-list !pl-0 list-none space-y-2">
|
||||
<li class="dot">Local‑first when it makes sense, cloud when it earns the right.</li>
|
||||
<li class="dot">Simple, reliable stacks beat flashy complexity.</li>
|
||||
<li class="dot">Own your infra where possible; avoid lock‑in.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="grid md:grid-cols-2 gap-6">
|
||||
<article class="card">
|
||||
<h3 class="card-title">Service Area</h3>
|
||||
<p class="card-body">Based in Amarillo, we happily travel anywhere in the Texas Panhandle — and consult across the greater Texas region. Remote support available.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h3 class="card-title">Stack & Focus</h3>
|
||||
<p class="card-body">Proxmox • Windows Server/AD • Linux • Flask/Node • Tailwind • MariaDB • WireGuard • Nginx. We ship secure, maintainable, no‑nonsense solutions.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="glass rounded-2xl p-6">
|
||||
<h2 class="text-xl font-semibold">Who we help</h2>
|
||||
<p class="card-body">Local shops, offices, and individuals who need dependable systems and clean websites. Need enterprise rigor? We can engage via BrookHaven for larger programs.</p>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="pt-2">
|
||||
<a href="/contact" class="btn-primary">Start a project</a>
|
||||
<a href="/services" class="btn-ghost ml-2">Explore services</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
147
templates/admin.html
Normal file
147
templates/admin.html
Normal file
@@ -0,0 +1,147 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="max-w-7xl mx-auto py-10">
|
||||
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<h1 class="text-3xl font-bold">Quote Requests</h1>
|
||||
<div class="flex items-center gap-3">
|
||||
<input id="q" placeholder="Search name/email/notes…" class="w-64" oninput="filterCards(this.value)" />
|
||||
<nav class="text-sm space-x-2">
|
||||
{% for key,label in [('active','Active'),('open','Open'),('completed','Completed'),('deleted','Deleted'),('all','All')] %}
|
||||
<a class="px-3 py-1 rounded border {{ 'bg-white/10' if show==key else 'border-white/10 hover:border-white/30' }}"
|
||||
href="{{ url_for('admin', show=key) }}">{{ label }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="cardGrid" class="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{% for r in rows %}
|
||||
<article class="card glass p-5 flex flex-col gap-4 {{ 'opacity-60' if r.deleted_at }}">
|
||||
<header class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs text-white/60">#{{ r.id }} • {{ (r.created_at or '')[:19].replace('T',' ') }}</div>
|
||||
<h2 class="text-lg font-semibold">{{ r.name }}</h2>
|
||||
<a class="text-sm underline text-white/80" href="mailto:{{ r.email }}">{{ r.email }}</a>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1 justify-end">
|
||||
{% set tl = (r.urgency or '-') %}
|
||||
{% set st = r.status if r.status else 'open' %}
|
||||
<span class="px-2 py-0.5 rounded text-[11px] border
|
||||
{% if st == 'completed' %} border-emerald-400 text-emerald-200
|
||||
{% else %} border-sky-400 text-sky-200 {% endif %}">{{ st }}</span>
|
||||
<span class="px-2 py-0.5 rounded text-[11px] border
|
||||
{% if tl in ['critical','rush'] %} border-red-400 text-red-200
|
||||
{% elif tl == 'soon' %} border-yellow-400 text-yellow-200
|
||||
{% else %} border-emerald-400 text-emerald-200 {% endif %}">{{ tl }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
|
||||
<div>
|
||||
<dt class="text-white/60">What they need</dt>
|
||||
<dd class="font-medium">{{ r.project_type or '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-white/60">Scope</dt>
|
||||
<dd class="font-medium">{{ r.complexity or '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-white/60">Extras</dt>
|
||||
<dd class="font-medium">{{ r.features or '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-white/60">Budget</dt>
|
||||
<dd class="font-medium">{{ r.budget_range or '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-white/60">Est. Hours</dt>
|
||||
<dd class="font-medium">{{ r.est_hours }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-white/60">Est. Cost</dt>
|
||||
<dd class="font-medium">${{ '%.2f'|format(r.est_cost or 0) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="text-sm bg-black/30 rounded border border-white/10 p-3 max-h-28 overflow-auto">
|
||||
<div class="text-white/60 text-xs mb-1">Client notes</div>
|
||||
<div class="line-clamp-3">{{ (r.description or '—') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex items-center justify-between gap-2">
|
||||
<button class="btn text-xs" type="button"
|
||||
data-notes="{{ (r.description or '') | e }}"
|
||||
data-json='{{ (r.json_payload or "{}") | e }}'
|
||||
data-meta='{{ (r.name ~ " • " ~ (r.email or "") ~ " • #" ~ r.id) | e }}'
|
||||
onclick="openNotes(this)">View notes</button>
|
||||
|
||||
<div class="flex gap-2">
|
||||
{% if not r.deleted_at and (r.status or 'open') != 'completed' %}
|
||||
<form method="post" action="{{ url_for('mark_complete', rid=r.id, show=show) }}">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf }}">
|
||||
<button class="btn bg-emerald-600/70 text-xs" onclick="return confirm('Mark #{{r.id}} as completed?')">Complete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not r.deleted_at %}
|
||||
<form method="post" action="{{ url_for('delete_request', rid=r.id, show=show) }}">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf }}">
|
||||
<button class="btn bg-red-600/70 text-xs" onclick="return confirm('Move #{{r.id}} to Deleted?')">Delete</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-xs text-white/50">Deleted</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Modal for notes -->
|
||||
<dialog id="notesModal" class="backdrop:bg-black/70 rounded-xl w-[min(90vw,760px)]">
|
||||
<form method="dialog" class="glass card p-0 overflow-hidden">
|
||||
<header class="flex items-center justify-between px-5 py-3 border-b border-white/10">
|
||||
<h2 id="modalTitle" class="font-semibold">Notes</h2>
|
||||
<button class="btn" value="close">Close</button>
|
||||
</header>
|
||||
<div class="p-5 space-y-5">
|
||||
<section>
|
||||
<h3 class="text-sm font-semibold text-white/70 mb-2">Client notes</h3>
|
||||
<pre id="notesText" class="whitespace-pre-wrap text-sm bg-black/40 p-3 rounded border border-white/10"></pre>
|
||||
</section>
|
||||
<section>
|
||||
<h3 class="text-sm font-semibold text-white/70 mb-2">Raw submission (JSON)</h3>
|
||||
<pre id="jsonText" class="overflow-auto text-xs bg-black/40 p-3 rounded border border-white/10 max-h-64"></pre>
|
||||
</section>
|
||||
</div>
|
||||
<footer class="px-5 py-3 border-t border-white/10 text-right">
|
||||
<button class="btn bg-accent font-semibold" value="close">Done</button>
|
||||
</footer>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function openNotes(btn) {
|
||||
const modal = document.getElementById('notesModal');
|
||||
const notes = btn.getAttribute('data-notes') || '';
|
||||
const raw = btn.getAttribute('data-json') || '{}';
|
||||
const meta = btn.getAttribute('data-meta') || 'Notes';
|
||||
document.getElementById('modalTitle').textContent = meta;
|
||||
document.getElementById('notesText').textContent = notes.trim() || '—';
|
||||
try { document.getElementById('jsonText').textContent = JSON.stringify(JSON.parse(raw), null, 2) }
|
||||
catch { document.getElementById('jsonText').textContent = raw; }
|
||||
modal.showModal();
|
||||
}
|
||||
|
||||
function filterCards(q) {
|
||||
q = (q || '').toLowerCase();
|
||||
document.querySelectorAll('#cardGrid article').forEach(card => {
|
||||
card.style.display = card.innerText.toLowerCase().includes(q) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
54
templates/base.html
Normal file
54
templates/base.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="h-full bg-zinc-950">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ title or "Quote Estimator" }}</title>
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<!-- Tailwind CDN for speed -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
:root { --accent: 255, 225, 87; } /* warm gold */
|
||||
.glass { background: rgba(255,255,255,0.05); backdrop-filter: blur(8px); }
|
||||
.card { border: 1px solid rgba(255,255,255,0.08); border-radius: 1rem; }
|
||||
.btn { display:inline-flex; align-items:center; gap:.5rem; border:1px solid rgba(255,255,255,.15); padding:.75rem 1rem; border-radius:.75rem; }
|
||||
.btn:hover { border-color: rgba(255,255,255,.35); transform: translateY(-1px); }
|
||||
.accent { color: rgb(var(--accent)); }
|
||||
.bg-accent { background-color: rgb(var(--accent)); color:#111; }
|
||||
input, select, textarea { background:#0b0b0b; border:1px solid rgba(255,255,255,.12); border-radius:.75rem; padding:.65rem .8rem; }
|
||||
label { color: #e5e5e5; font-weight: 500; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-full text-zinc-100">
|
||||
<header class="sticky top-0 z-40 glass border-b border-white/10">
|
||||
<div class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-3">
|
||||
<div class="size-8 rounded bg-accent grid place-items-center font-black">BH</div>
|
||||
<span class="font-bold tracking-wide">Benny’s House — NetDeploy</span>
|
||||
</a>
|
||||
<nav class="text-sm">
|
||||
<a class="hover:underline" href="/">Request a Quote</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-5xl mx-auto px-6 py-10">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="space-y-2 mb-6">
|
||||
{% for category, msg in messages %}
|
||||
<div class="p-3 rounded border {{ 'border-red-400 text-red-300' if category=='error' else 'border-green-400 text-green-300' }}">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="py-10 border-t border-white/10 text-sm text-white/60">
|
||||
<div class="max-w-5xl mx-auto px-6">
|
||||
<p>© {{ 2025 }} Benny’s House LLC. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
41
templates/contact.html
Normal file
41
templates/contact.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="py-16 md:py-24">
|
||||
<div class="max-w-4xl mx-auto px-6 grid md:grid-cols-2 gap-10 items-start">
|
||||
<div>
|
||||
<h1 class="font-bebas text-5xl">Contact</h1>
|
||||
<div class="mt-6 glass rounded-2xl p-6 space-y-2">
|
||||
<div class="text-xl font-semibold">{{ contact.name }}</div>
|
||||
<div class="text-white/70">{{ contact.title }}</div>
|
||||
<div class="text-white/80">{{ contact.city }}</div>
|
||||
<div class="text-white/80">Hours: {{ contact.hours }}</div>
|
||||
<div class="pt-4 flex flex-col gap-2 text-sm">
|
||||
<a class="link" href="mailto:{{ contact.email }}">{{ contact.email }}</a>
|
||||
<a class="link" href="tel:{{ contact.phone }}">{{ contact.phone }}</a>
|
||||
<a class="link" href="{{ contact.cal }}">Book a call</a>
|
||||
<a class="link" href="/benjamin.vcf">Download vCard</a>
|
||||
<a class="link" href="{{ contact.link }}">LinkedIn</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form action="https://formspree.io/f/your-id" method="POST" class="glass rounded-2xl p-6 space-y-4">
|
||||
<div>
|
||||
<label class="label">Name</label>
|
||||
<input required name="name" class="input" placeholder="Jane Doe" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Email</label>
|
||||
<input required name="email" type="email" class="input" placeholder="you@company.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">What do you need?</label>
|
||||
<textarea required name="message" rows="6" class="textarea" placeholder="Briefly describe your project..."></textarea>
|
||||
</div>
|
||||
<button class="btn-primary w-full">Send</button>
|
||||
<p class="text-xs text-white/50">By sending, you consent to be contacted about this request.</p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
162
templates/index.html
Normal file
162
templates/index.html
Normal file
@@ -0,0 +1,162 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="max-w-3xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="card glass p-6 mb-8">
|
||||
<h1 class="text-2xl font-bold">Get a Quick Project Estimate</h1>
|
||||
<p class="text-white/70 mt-2">
|
||||
Answer a few simple questions—no tech talk required. We’ll review and email you a tailored quote.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form action="{{ url_for('submit') }}" method="post" class="space-y-6">
|
||||
|
||||
<!-- Your info -->
|
||||
<div class="card glass p-6">
|
||||
<h2 class="font-semibold text-lg mb-3">About you</h2>
|
||||
<div class="grid sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="name">Your name *</label>
|
||||
<input id="name" name="name" required class="w-full mt-1" placeholder="Jane Doe">
|
||||
</div>
|
||||
<div>
|
||||
<label for="email">Email *</label>
|
||||
<input id="email" name="email" type="email" required class="w-full mt-1" placeholder="you@example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label for="phone">Phone (optional)</label>
|
||||
<input id="phone" name="phone" class="w-full mt-1" placeholder="(optional)">
|
||||
</div>
|
||||
<div>
|
||||
<label for="company">Business or organization (optional)</label>
|
||||
<input id="company" name="company" class="w-full mt-1" placeholder="(optional)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- What do you need -->
|
||||
<div class="card glass p-6">
|
||||
<h2 class="font-semibold text-lg mb-3">What do you need?</h2>
|
||||
<p class="text-white/60 text-sm mb-4">Pick the one that fits best. We’ll handle the details later.</p>
|
||||
<div class="grid sm:grid-cols-2 gap-3">
|
||||
{% for val, label, hint in [
|
||||
('simple-site', 'A basic website', 'Home, About, Contact'),
|
||||
('pro-site', 'A website with extras', 'Portfolio, blog, more pages'),
|
||||
('online-form', 'An online form', 'Collect info, send to email/Sheet'),
|
||||
('sell-online', 'Sell online', 'Checkout / payments'),
|
||||
('fix-or-improve', 'Fix or improve something', 'Speed, bugs, cleanup'),
|
||||
('it-help', 'IT setup/help', 'Email, domains, backups, networks'),
|
||||
('custom-app', 'A custom tool/app', 'Dashboards, portals, automations'),
|
||||
('not-sure', 'Not sure yet', 'I need guidance')
|
||||
] %}
|
||||
<label class="flex items-start gap-3 p-3 rounded border border-white/10 hover:border-white/30 cursor-pointer">
|
||||
<input class="mt-1" type="radio" name="need" value="{{ val }}" required>
|
||||
<span>
|
||||
<span class="font-medium">{{ label }}</span><br>
|
||||
<span class="text-white/60 text-sm">{{ hint }}</span>
|
||||
</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scope -->
|
||||
<div class="card glass p-6">
|
||||
<h2 class="font-semibold text-lg mb-3">How big is this?</h2>
|
||||
<p class="text-white/60 text-sm mb-4">A rough guess is perfect.</p>
|
||||
<div class="grid sm:grid-cols-3 gap-3">
|
||||
{% for val, label, hint in [
|
||||
('small', 'Small', '~1–3 key pages or a simple task'),
|
||||
('medium', 'Medium', '~4–8 pages or a few features'),
|
||||
('large', 'Large', 'Many pages/features or complex work')
|
||||
] %}
|
||||
<label class="flex items-start gap-3 p-3 rounded border border-white/10 hover:border-white/30 cursor-pointer">
|
||||
<input class="mt-1" type="radio" name="scope_size" value="{{ val }}" required>
|
||||
<span>
|
||||
<span class="font-medium">{{ label }}</span><br>
|
||||
<span class="text-white/60 text-sm">{{ hint }}</span>
|
||||
</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="card glass p-6">
|
||||
<h2 class="font-semibold text-lg mb-3">When do you need it?</h2>
|
||||
<div class="grid sm:grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{% for val, label in [
|
||||
('flexible', 'Flexible'),
|
||||
('soon', 'Soon (2–4 weeks)'),
|
||||
('rush', 'Rush (under 2 weeks)'),
|
||||
('critical', 'Urgent (ASAP)')
|
||||
] %}
|
||||
<label class="flex items-center gap-2 p-3 rounded border border-white/10 hover:border-white/30 cursor-pointer">
|
||||
<input type="radio" name="timeline" value="{{ val }}" required>
|
||||
<span>{{ label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Helpful extras -->
|
||||
<div class="card glass p-6">
|
||||
<h2 class="font-semibold text-lg mb-3">Would any of these help?</h2>
|
||||
<p class="text-white/60 text-sm mb-4">Optional add-ons to make life easier.</p>
|
||||
<div class="grid sm:grid-cols-2 gap-2">
|
||||
{% for val, label in [
|
||||
('content', 'Write or improve the words'),
|
||||
('branding', 'Light logo/branding help'),
|
||||
('training', 'Walkthrough & training'),
|
||||
('care', 'Ongoing care & updates')
|
||||
] %}
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" name="extras" value="{{ val }}">
|
||||
<span>{{ label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Budget comfort -->
|
||||
<div class="card glass p-6">
|
||||
<h2 class="font-semibold text-lg mb-3">Budget comfort zone</h2>
|
||||
<p class="text-white/60 text-sm mb-4">This helps us suggest the right approach. Totally fine if you’re unsure.</p>
|
||||
<select class="w-full" name="budget_feel">
|
||||
<option value="unsure" selected>I’m not sure yet</option>
|
||||
<option value="under-2k">Under $2k</option>
|
||||
<option value="2k-5k">$2k – $5k</option>
|
||||
<option value="5k-10k">$5k – $10k</option>
|
||||
<option value="10k-plus">$10k+</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="card glass p-6">
|
||||
<h2 class="font-semibold text-lg mb-3">Anything else?</h2>
|
||||
<textarea name="description" rows="4" class="w-full" placeholder="Links to examples you like, goals, must-haves, nice-to-haves…"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-white/50 text-xs">We’ll never share your info. You’ll get a personal email from us with next steps.</p>
|
||||
<button class="btn bg-accent font-semibold" type="submit">Get my estimate</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// tiny enhancement: if user picks “sell online”, preselect “medium” scope for them
|
||||
const needRadios = document.querySelectorAll('input[name="need"]');
|
||||
const scopeRadios = document.querySelectorAll('input[name="scope_size"]');
|
||||
needRadios.forEach(r => {
|
||||
r.addEventListener('change', () => {
|
||||
if (r.value === 'sell-online' && ![...scopeRadios].some(s=>s.checked)) {
|
||||
[...scopeRadios].find(s => s.value==='medium').checked = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
42
templates/login.html
Normal file
42
templates/login.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="min-h-[60vh] grid place-items-center">
|
||||
<div class="w-full max-w-md card glass p-8">
|
||||
<h1 class="text-2xl font-bold mb-2">Admin Sign In</h1>
|
||||
<p class="text-white/70 mb-6 text-sm">Enter your credentials to access the dashboard.</p>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="space-y-2 mb-4">
|
||||
{% for category, msg in messages %}
|
||||
<div class="p-3 rounded border {{ 'border-red-400 text-red-300' if category=='error' else 'border-green-400 text-green-300' }}">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post" action="{{ url_for('admin_login') }}" class="space-y-4">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf }}">
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
|
||||
<div>
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" required type="text" class="w-full mt-1" placeholder="admin" autocomplete="username">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" required type="password" class="w-full mt-1" placeholder="••••••••" autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<label class="flex items-center gap-2 text-sm text-white/80">
|
||||
<input type="checkbox" name="remember">
|
||||
<span>Remember me for 30 days</span>
|
||||
</label>
|
||||
|
||||
<button class="btn bg-accent font-semibold w-full" type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
41
templates/new_request_email.html
Normal file
41
templates/new_request_email.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!-- Email to Admin: New Request Summary -->
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial; color:#0f172a; background:#f8fafc; padding:24px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="640" cellpadding="0" cellspacing="0" style="background:#ffffff; border:1px solid #e5e7eb; border-radius:12px;">
|
||||
<tr>
|
||||
<td style="padding:24px; border-bottom:1px solid #e5e7eb;">
|
||||
<h1 style="margin:0; font-size:20px;">New Quote Request</h1>
|
||||
<p style="margin:4px 0 0; color:#6b7280; font-size:14px;">{{ payload.name }} ({{ payload.email }})</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:24px;">
|
||||
<h2 style="font-size:16px; margin:0 0 8px;">Project Summary</h2>
|
||||
<ul style="margin:0 0 16px; padding-left:18px; color:#374151; font-size:14px;">
|
||||
<li><strong>Type:</strong> {{ payload.project_type }}</li>
|
||||
<li><strong>Complexity:</strong> {{ payload.complexity }}</li>
|
||||
<li><strong>Urgency:</strong> {{ payload.urgency }}</li>
|
||||
<li><strong>Features:</strong> {{ payload.features|join(', ') if payload.features else 'None' }}</li>
|
||||
<li><strong>Budget:</strong> {{ payload.budget_range or 'n/a' }}</li>
|
||||
</ul>
|
||||
|
||||
<h3 style="font-size:14px; margin:0 0 6px; color:#111827;">Description</h3>
|
||||
<p style="margin:0 0 16px; color:#374151; white-space:pre-line;">{{ payload.description or '—' }}</p>
|
||||
|
||||
<h3 style="font-size:14px; margin:0 0 6px; color:#111827;">Auto-Estimate</h3>
|
||||
<p style="margin:0; color:#111827; font-size:14px;">
|
||||
~{{ est_hours }} hours @ ${{ '%.2f'|format(hourly_rate) }}/hr → <strong>${{ '%.2f'|format(est_cost) }}</strong>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:16px 24px; background:#f8fafc; border-top:1px solid #e5e7eb; color:#6b7280; font-size:12px;">
|
||||
<a href="{{ base_url }}/admin?p={{ config('ADMIN_DASH_PASSWORD', 'changeme') if false else '' }}" style="color:#2563eb;text-decoration:underline;">Open Admin</a>
|
||||
• <a href="{{ base_url }}/preview-client-email" style="color:#2563eb;text-decoration:underline;">Preview Client Email Template</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
55
templates/quote_email.html
Normal file
55
templates/quote_email.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!-- Email to Client: Professional Quote Template -->
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial; color:#0f172a; background:#f5f5f5; padding:24px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="640" cellpadding="0" cellspacing="0" style="background:#ffffff; border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;">
|
||||
<tr>
|
||||
<td style="background:#111827; color:#fff; padding:24px;">
|
||||
<h1 style="margin:0; font-size:20px;">Your Project Quote</h1>
|
||||
<p style="margin:6px 0 0; color:#e5e7eb; font-size:14px;">{{ company_name }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:24px;">
|
||||
<p style="margin:0 0 12px;">Hi {{ client_name }},</p>
|
||||
<p style="margin:0 0 12px;">Thanks for reaching out! Here’s our estimate for <strong>{{ project_title }}</strong>.</p>
|
||||
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb; border-radius:8px; margin:16px 0;">
|
||||
<tr>
|
||||
<td style="padding:12px; font-size:14px; border-bottom:1px solid #e5e7eb;"><strong>Overview</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px; color:#374151; font-size:14px;">{{ proposal_summary }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px; font-size:14px; border-top:1px solid #e5e7eb;">
|
||||
Estimated Hours: <strong>{{ est_hours }}</strong> | Hourly Rate: <strong>${{ '%.2f'|format(hourly_rate|float) }}</strong> →
|
||||
Estimated Cost: <strong>${{ '%.2f'|format(est_cost|float) }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style="margin:0 0 12px; color:#374151; font-size:14px;">
|
||||
This quote is valid until <strong>{{ valid_until }}</strong>. If everything looks good, the next step is to reply to this email or visit the link below so we can finalize scope and timeline.
|
||||
</p>
|
||||
|
||||
{% if next_steps_url %}
|
||||
<p style="margin:16px 0;">
|
||||
<a href="{{ next_steps_url }}" style="display:inline-block; background:#fde047; color:#111827; padding:10px 16px; border-radius:8px; text-decoration:none; font-weight:600;">Proceed</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p style="margin:12px 0 0; color:#6b7280; font-size:12px;">
|
||||
Questions? Reach us at <a href="mailto:{{ contact_email }}" style="color:#2563eb;">{{ contact_email }}</a>.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="background:#111827; color:#9ca3af; padding:16px; font-size:12px;">
|
||||
© {{ 2025 }} {{ company_name }}. All rights reserved.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
67
templates/services.html
Normal file
67
templates/services.html
Normal file
@@ -0,0 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="py-16 md:py-24">
|
||||
<div class="max-w-6xl mx-auto px-6">
|
||||
<header class="max-w-3xl">
|
||||
<h1 class="font-bebas text-5xl">Services</h1>
|
||||
<p class="mt-4 text-white/80">From local sysadmin to modern web, we tailor the stack to your constraints and budget — including refurb‑powered discounts to keep costs sensible.</p>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="mt-10 grid md:grid-cols-2 gap-6">
|
||||
<article class="card">
|
||||
<h3 class="card-title">Local System Administration</h3>
|
||||
<p class="card-body">On‑site/remote support for small businesses and individuals. Proxmox clusters, Windows Server/Active Directory, Linux, backups, VLANs, DNS, VPN, and monitoring.</p>
|
||||
<ul class="card-list">
|
||||
<li>User & identity management (AD)</li>
|
||||
<li>Secure file servers (Samba/NFS)</li>
|
||||
<li>Automated backups & recovery drills</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="card">
|
||||
<h3 class="card-title">Web Development & Hosting</h3>
|
||||
<p class="card-body">Flask/Node apps or static sites with Tailwind front‑ends. SSL, backups, staging, wildcard subdomains — and Stripe/domain upsells when you’re ready.</p>
|
||||
<ul class="card-list">
|
||||
<li>New builds or refactors</li>
|
||||
<li>Observability baked in</li>
|
||||
<li>Fast, accessible UI</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="card">
|
||||
<h3 class="card-title">App Deployment & Integrations</h3>
|
||||
<p class="card-body">Want a custom Obsidian server for your office? Need a self‑hosted wiki, dashboard, or existing tool stood up securely? We handle the infra, auth, and logistics.</p>
|
||||
<ul class="card-list">
|
||||
<li>Install, configure, and harden</li>
|
||||
<li>Single‑sign‑on options</li>
|
||||
<li>Backups & updates scheduled</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="card">
|
||||
<h3 class="card-title">Refurb & Recycle Discounts</h3>
|
||||
<p class="card-body">Lower your quote by letting us refurb quality used hardware into dependable local servers. Burn‑in tests and warranty options included.</p>
|
||||
<ul class="card-list">
|
||||
<li>Cost‑effective nodes</li>
|
||||
<li>Green and budget‑friendly</li>
|
||||
<li>On‑site support options</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="card md:col-span-2">
|
||||
<h3 class="card-title">Enterprise via BrookHaven</h3>
|
||||
<p class="card-body">If you need larger‑scale rollouts, compliance, or cross‑site coordination, we can deliver through our enterprise channel (BrookHaven) while keeping execution pragmatic.</p>
|
||||
<div class="mt-4">
|
||||
<a href="/contact" class="btn-primary">Tell us what you need</a>
|
||||
<a href="/about" class="btn-ghost ml-2">Learn about us</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/thanks.html
Normal file
8
templates/thanks.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="card glass p-10 text-center grid place-items-center">
|
||||
<div class="size-14 rounded-full bg-accent grid place-items-center text-black font-black text-2xl mb-4">✓</div>
|
||||
<h1 class="text-2xl font-bold mb-2">Thanks! We received your request.</h1>
|
||||
<p class="text-white/70">We’ll review your requirements and email you a quote soon.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user