Initial Commit

This commit is contained in:
2025-11-27 00:00:50 +00:00
commit b7e68a9057
43 changed files with 3445 additions and 0 deletions

41
templates/base.html Normal file
View File

@@ -0,0 +1,41 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{% block title %}{{ brand }}{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
</head>
<body class="bg-neutral-950 text-neutral-100 antialiased">
<header class="border-b border-white/10 bg-black/60 backdrop-blur">
<div class="max-w-7xl mx-auto px-6 py-3 flex items-center gap-6">
<a class="font-bold" href="/">{{ brand }}</a>
<nav class="text-sm flex flex-wrap gap-4">
<a href="/board/">Intercom</a>
<a href="/quotes/">Quotes</a>
<a href="/memos/">Memos</a>
<a href="/notes/">Notes</a>
<a href="/journal/">Journal</a>
</nav>
<div class="ml-auto">
{% if cu %}
<form method="post" action="/auth/logout"><button class="btn">Log out</button></form>
{% else %}
<a class="btn" href="/auth/login">Log in</a>
{% endif %}
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-6 py-8">
{% with msgs = get_flashed_messages(with_categories=true) %}
{% for cat,msg in msgs %}
<div class="mb-4 rounded border px-3 py-2 {{ 'border-emerald-400 bg-emerald-500/10' if cat=='ok' else 'border-red-400 bg-red-500/10' }}">{{ msg }}</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>

58
templates/board.html Normal file
View File

@@ -0,0 +1,58 @@
{% extends 'base.html' %}{% block title %}Intercom — {{ brand }}{% endblock %}
{% block content %}
<section class="max-w-5xl mx-auto">
<div class="card glass p-4">
<textarea id="composer" rows="3" class="w-full" placeholder="Share an update…" {% if not can_post %}disabled{% endif %}></textarea>
<div class="mt-3 flex items-center justify-between">
<div class="text-xs text-white/60">{% if not can_post %}You dont have permission to post.{% endif %}</div>
<button id="sendBtn" class="btn bg-accent font-semibold" {% if not can_post %}disabled{% endif %}>Post</button>
</div>
</div>
<div class="mt-6 space-y-3" id="board">
{% for m in messages %}
<article class="card glass p-4">
<div class="flex items-start gap-3">
{% if m.avatar %}<img src="{{ m.avatar }}" class="size-9 rounded-lg">{% else %}<div class="size-9 rounded-lg bg-white/10 grid place-items-center">🟣</div>{% endif %}
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<div class="font-semibold">{{ m.username }}</div>
{% if m.timestamp %}<div class="text-xs text-white/50">{{ m.timestamp.replace('T',' ').replace('Z',' UTC') }}</div>{% endif %}
</div>
<div class="mt-1 whitespace-pre-wrap text-white/90">{{ m.content }}</div>
</div>
</div>
</article>
{% endfor %}
</div>
<div class="mt-4 text-center"><button id="refreshBtn" class="btn">Refresh</button></div>
</section>
<script>
const board=document.getElementById('board'), sendBtn=document.getElementById('sendBtn'), composer=document.getElementById('composer');
function esc(s){return (s||'').replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;');}
async function fetchMessages(){
const r=await fetch('/api/board/messages'); if(!r.ok) return;
const data=await r.json();
board.innerHTML=data.map(m=>`
<article class="card glass p-4">
<div class="flex items-start gap-3">
${m.avatar?`<img src="${m.avatar}" class="size-9 rounded-lg">`:`<div class="size-9 rounded-lg bg-white/10 grid place-items-center">🟣</div>`}
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<div class="font-semibold">${esc(m.username)}</div>
${m.timestamp?`<div class="text-xs text-white/50">${esc(m.timestamp.replace('T',' ').replace('Z',' UTC'))}</div>`:''}
</div>
<div class="mt-1 whitespace-pre-wrap text-white/90">${esc(m.content)}</div>
</div>
</div>
</article>`).join('');
}
async function postMessage(){
const content=(composer.value||'').trim(); if(!content) return;
const r=await fetch('/api/board/post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({content})});
if(r.ok){composer.value=''; fetchMessages()} else {const e=await r.json().catch(()=>({error:'Failed'})); alert(e.error||'Failed')}
}
document.getElementById('refreshBtn').addEventListener('click', fetchMessages);
sendBtn?.addEventListener('click', postMessage);
</script>
{% endblock %}

25
templates/home.html Normal file
View File

@@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% block title %}Home — {{ brand }}{% endblock %}
{% block content %}
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<a href="/board/" class="card glass p-6 block">
<h2 class="font-semibold text-lg">Discord Intercom</h2>
<p class="text-white/70 text-sm">Read channel, admins can post.</p>
</a>
<a href="/quotes/" class="card glass p-6 block">
<h2 class="font-semibold text-lg">Quotes</h2>
<p class="text-white/70 text-sm">Public estimator + admin panel.</p>
</a>
<a href="/memos/" class="card glass p-6 block">
<h2 class="font-semibold text-lg">Memos</h2>
<p class="text-white/70 text-sm">Reminders & quick tasks.</p>
</a>
<a href="/notes/" class="card glass p-6 block">
<h2 class="font-semibold text-lg">Notes</h2>
</a>
<a href="/journal/" class="card glass p-6 block">
<h2 class="font-semibold text-lg">Journal</h2>
</a>
</div>
{% endblock %}

19
templates/journal.html Normal file
View File

@@ -0,0 +1,19 @@
{% extends 'base.html' %}{% block title %}Journal — {{ brand }}{% endblock %}
{% block content %}
<form class="card glass p-4 max-w-2xl" method="post" action="/journal/add">
<label class="block mb-2">Title<input class="w-full" name="name" required></label>
<label>Entry<textarea class="w-full" name="entry" rows="6"></textarea></label>
<div class="mt-3 text-right"><button class="btn bg-accent font-semibold">Save</button></div>
</form>
<div class="grid md:grid-cols-2 gap-4 mt-6">
{% for j in journal %}
<article class="card glass p-4">
<div class="text-sm text-white/60">{{ (j.created_at|string)[:19].replace('T',' ') }}</div>
<h3 class="font-semibold">{{ j.name }}</h3>
<div class="mt-1 whitespace-pre-wrap text-white/90">{{ j.entry }}</div>
<form class="mt-3" method="post" action="/journal/{{j.slug}}/delete"><button class="btn text-xs">Delete</button></form>
</article>
{% endfor %}
</div>
{% endblock %}

21
templates/login.html Normal file
View File

@@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% block title %}Log in — {{ brand }}{% endblock %}
{% block content %}
<form class="max-w-md mx-auto card glass p-6" method="post" action="/auth/login">
<input type="hidden" name="next" value="{{ next or '/' }}">
<h1 class="text-xl font-semibold mb-4">Sign in</h1>
<label class="block mb-3">
<span class="text-sm text-white/70">Username or email</span>
<input class="w-full mt-1" name="username" required>
</label>
<label class="block mb-4">
<span class="text-sm text-white/70">Password</span>
<input class="w-full mt-1" type="password" name="password" required>
</label>
<div class="flex items-center gap-3">
<button class="btn bg-accent font-semibold" type="submit">Log in</button>
<a class="btn" href="/auth/discord">Sign in with Discord</a>
</div>
</form>
{% endblock %}

21
templates/memos.html Normal file
View File

@@ -0,0 +1,21 @@
{% extends 'base.html' %}{% block title %}Memos — {{ brand }}{% endblock %}
{% block content %}
<form class="card glass p-4 max-w-2xl" method="post" action="/memos/add">
<label class="block mb-2">Memo<textarea class="w-full" name="memo" rows="2" required></textarea></label>
<label>Remind at <input type="datetime-local" name="reminder_time"></label>
<div class="mt-3 text-right"><button class="btn bg-accent font-semibold">Add</button></div>
</form>
<div class="grid md:grid-cols-2 gap-4 mt-6">
{% for m in memos %}
<article class="card glass p-4">
<div class="text-sm text-white/60">{{ (m.reminder_time|string)[:16] if m.reminder_time else 'No reminder' }}</div>
<div class="mt-1 whitespace-pre-wrap">{{ m.memo }}</div>
<div class="mt-3 flex gap-2">
<form method="post" action="/memos/{{m.id}}/complete"><button class="btn text-xs">Complete</button></form>
<form method="post" action="/memos/{{m.id}}/delete"><button class="btn text-xs">Delete</button></form>
</div>
</article>
{% endfor %}
</div>
{% endblock %}

19
templates/notes.html Normal file
View File

@@ -0,0 +1,19 @@
{% extends 'base.html' %}{% block title %}Notes — {{ brand }}{% endblock %}
{% block content %}
<form class="card glass p-4 max-w-2xl" method="post" action="/notes/add">
<label class="block mb-2">Title<input class="w-full" name="name" required></label>
<label>Note<textarea class="w-full" name="note" rows="4"></textarea></label>
<div class="mt-3 text-right"><button class="btn bg-accent font-semibold">Save</button></div>
</form>
<div class="grid md:grid-cols-2 gap-4 mt-6">
{% for n in notes %}
<article class="card glass p-4">
<div class="text-sm text-white/60">{{ (n.created_at|string)[:19].replace('T',' ') }}</div>
<h3 class="font-semibold">{{ n.name }}</h3>
<div class="mt-1 whitespace-pre-wrap text-white/90">{{ n.note }}</div>
<form class="mt-3" method="post" action="/notes/{{n.slug}}/delete"><button class="btn text-xs">Delete</button></form>
</article>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends 'base.html' %}{% block title %}Thanks — {{ brand }}{% endblock %}
{% block content %}
<div class="max-w-md mx-auto card glass p-6">
<h1 class="text-xl font-semibold">Thanks!</h1>
<p class="text-white/70 mt-2">Well review and email you shortly.</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends 'base.html' %}{% block title %}Quotes Admin — {{ brand }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-4">
<h1 class="text-2xl font-bold">Quote Requests</h1>
<nav class="text-sm space-x-2">
{% for k,l in [('active','Active'),('open','Open'),('completed','Completed'),('deleted','Deleted'),('all','All')] %}
<a class="px-3 py-1 rounded border {{ 'bg-white/10' if show==k else 'border-white/10 hover:border-white/30' }}" href="?show={{k}}">{{l}}</a>
{% endfor %}
</nav>
</div>
<div 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">
<div>
<div class="text-xs text-white/60">#{{r.id}} • {{ (r.created_at|string)[:19].replace('T',' ') }}</div>
<h2 class="text-lg font-semibold">{{ r.name }}</h2>
<a class="text-sm underline" href="mailto:{{ r.email }}">{{ r.email }}</a>
</div>
<div class="flex gap-2">
{% set st = r.status or 'open' %}
<span class="px-2 py-0.5 rounded text-[11px] border {{ 'border-emerald-400 text-emerald-200' if st=='completed' else 'border-sky-400 text-sky-200' }}">{{ st }}</span>
<span class="px-2 py-0.5 rounded text-[11px] border">{{ r.timeline or '-' }}</span>
</div>
</header>
<dl class="grid grid-cols-2 gap-2 text-sm">
<div><dt class="text-white/60">Need</dt><dd class="font-medium">{{ r.need or '-' }}</dd></div>
<div><dt class="text-white/60">Scope</dt><dd class="font-medium">{{ r.scope_size or '-' }}</dd></div>
<div><dt class="text-white/60">Extras</dt><dd class="font-medium">{{ r.extras or '-' }}</dd></div>
<div><dt class="text-white/60">Budget</dt><dd class="font-medium">{{ r.budget_feel 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">{{ r.description or '—' }}</div>
<div class="mt-auto flex items-center justify-between gap-2">
<form method="post" action="/admin/quotes/{{r.id}}/complete?show={{show}}"><button class="btn bg-emerald-600/70 text-xs">Complete</button></form>
<form method="post" action="/admin/quotes/{{r.id}}/delete?show={{show}}"><button class="btn bg-red-600/70 text-xs">Delete</button></form>
</div>
</article>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends 'base.html' %}{% block title %}Get a Quick Estimate — {{ brand }}{% endblock %}
{% block content %}
<form class="max-w-3xl mx-auto space-y-6" action="/quotes/submit" method="post">
<div class="card glass p-6 grid sm:grid-cols-2 gap-4">
<div><label>Name*<input name="name" class="w-full mt-1" required></label></div>
<div><label>Email*<input name="email" type="email" class="w-full mt-1" required></label></div>
<div><label>Phone<input name="phone" class="w-full mt-1"></label></div>
<div><label>Company<input name="company" class="w-full mt-1"></label></div>
</div>
<div class="card glass p-6">
<h2 class="font-semibold mb-3">What do you need?</h2>
<div class="grid sm:grid-cols-2 gap-3">
{% for v,l in [('simple-site','Basic site'),('pro-site','Site with extras'),('online-form','Online form'),('sell-online','Sell online'),('fix-or-improve','Fix/Improve'),('it-help','IT help'),('custom-app','Custom tool'),('not-sure','Not sure')] %}
<label class="flex items-center gap-2"><input type="radio" name="need" value="{{v}}" required> {{l}}</label>
{% endfor %}
</div>
</div>
<div class="card glass p-6 grid sm:grid-cols-3 gap-3">
<label class="flex items-center gap-2"><input type="radio" name="scope_size" value="small" required> Small</label>
<label class="flex items-center gap-2"><input type="radio" name="scope_size" value="medium" required> Medium</label>
<label class="flex items-center gap-2"><input type="radio" name="scope_size" value="large" required> Large</label>
</div>
<div class="card glass p-6 grid sm:grid-cols-4 gap-3">
{% for v,l in [('flexible','Flexible'),('soon','Soon'),('rush','Rush'),('critical','Urgent')] %}
<label class="flex items-center gap-2"><input type="radio" name="timeline" value="{{v}}" required> {{l}}</label>
{% endfor %}
</div>
<div class="card glass p-6">
<h2 class="font-semibold mb-3">Extras</h2>
{% for v,l in [('content','Content help'),('branding','Branding'),('training','Training'),('care','Care plan')] %}
<label class="mr-4"><input type="checkbox" name="extras" value="{{v}}"> {{l}}</label>
{% endfor %}
</div>
<div class="card glass p-6">
<label>Budget comfort
<select class="w-full mt-1" name="budget_feel">
<option value="unsure">Im 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>
</label>
</div>
<div class="card glass p-6">
<label>Notes<textarea name="description" rows="4" class="w-full mt-1"></textarea></label>
</div>
<div class="text-right"><button class="btn bg-accent font-semibold">Get my estimate</button></div>
</form>
{% endblock %}