feat(go-crm): industrial terminal-core UI redesign

Complete visual overhaul of the go-crm web interface:

- New shared layout system (internal/templates/ui.go) with dark zinc
  industrial theme, JetBrains Mono typography, grid/noise textures
- Redesigned all pages: Dashboard, Login/Signup, Clients, Customers,
  Services, Scheduling, Payments, Questions, Answers, Leads,
  Review Queue, Report, Keyword Mapping
- Tailwind CSS via CDN with custom color palette (amber/emerald/rose/sky)
- HTMX-powered interactions with CSS swap animations
- Status pills, KPI cards, data tables, empty states, inline forms
- Mobile-responsive sidebar with collapsible navigation
- All existing tests updated and passing
- Zero new build dependencies — works with existing go run/air workflow
This commit is contained in:
2026-05-23 18:00:32 -03:00
parent 744868caa1
commit 9c3bfd131d
16 changed files with 1901 additions and 791 deletions

View File

@@ -2,9 +2,12 @@ package handlers
import (
"database/sql"
"fmt"
"net/http"
"time"
"go-crm/internal/templates"
"github.com/go-chi/chi/v5"
"golang.org/x/crypto/bcrypt"
)
@@ -13,24 +16,38 @@ import (
var DB *sql.DB
func (a *App) SignupPage(w http.ResponseWriter, r *http.Request) {
content := fmt.Sprintf(`
<div class="min-h-[60vh] flex items-center justify-center">
<div class="w-full max-w-sm">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-amber-400/10 border border-amber-400/20 mb-4">
<svg class="w-6 h-6 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
</div>
<h1 class="text-2xl font-bold text-zinc-100 tracking-tight">go-crm</h1>
<p class="text-sm text-zinc-500 mt-1">Create your account</p>
</div>
<div class="card-industrial p-6">
<form method="POST" action="/auth/signup" class="space-y-4">
<div>
<label class="block text-[10px] font-mono uppercase tracking-wider text-zinc-500 mb-1.5">Email</label>
<input type="email" name="email" required class="input-industrial" placeholder="you@company.com">
</div>
<div>
<label class="block text-[10px] font-mono uppercase tracking-wider text-zinc-500 mb-1.5">Name</label>
<input type="text" name="name" required class="input-industrial" placeholder="Your name">
</div>
<div>
<label class="block text-[10px] font-mono uppercase tracking-wider text-zinc-500 mb-1.5">Password</label>
<input type="password" name="password" required class="input-industrial" placeholder="Min 8 characters">
</div>
<button type="submit" class="btn-primary w-full justify-center mt-2">Create Account</button>
</form>
</div>
<p class="text-center text-xs text-zinc-500 mt-6">Already have an account? <a href="/auth/login" class="text-amber-400 hover:text-amber-300 transition-colors">Sign in</a></p>
</div>
</div>`)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sign Up</title>
</head>
<body>
<h1>Sign Up</h1>
<form method="POST" action="/auth/signup">
<input type="email" name="email" placeholder="Email" required>
<input type="text" name="name" placeholder="Name" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Sign Up</button>
</form>
<p>Already have an account? <a href="/auth/login">Login</a></p>
</body>
</html>`))
templates.WriteHTMLPage(w, "Sign Up", "", content)
}
func (a *App) Signup(w http.ResponseWriter, r *http.Request) {
@@ -50,7 +67,6 @@ func (a *App) Signup(w http.ResponseWriter, r *http.Request) {
return
}
// Use a transaction so account + client + session are atomic.
tx, err := a.DB.Begin()
if err != nil {
http.Error(w, "Failed to start transaction", http.StatusInternalServerError)
@@ -101,23 +117,34 @@ func (a *App) Signup(w http.ResponseWriter, r *http.Request) {
}
func (a *App) LoginPage(w http.ResponseWriter, r *http.Request) {
content := fmt.Sprintf(`
<div class="min-h-[60vh] flex items-center justify-center">
<div class="w-full max-w-sm">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-amber-400/10 border border-amber-400/20 mb-4">
<svg class="w-6 h-6 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
</div>
<h1 class="text-2xl font-bold text-zinc-100 tracking-tight">go-crm</h1>
<p class="text-sm text-zinc-500 mt-1">Sign in to your workspace</p>
</div>
<div class="card-industrial p-6">
<form method="POST" action="/auth/login" class="space-y-4">
<div>
<label class="block text-[10px] font-mono uppercase tracking-wider text-zinc-500 mb-1.5">Email</label>
<input type="email" name="email" required class="input-industrial" placeholder="you@company.com">
</div>
<div>
<label class="block text-[10px] font-mono uppercase tracking-wider text-zinc-500 mb-1.5">Password</label>
<input type="password" name="password" required class="input-industrial" placeholder="Enter your password">
</div>
<button type="submit" class="btn-primary w-full justify-center mt-2">Sign In</button>
</form>
</div>
<p class="text-center text-xs text-zinc-500 mt-6">Don't have an account? <a href="/auth/signup" class="text-amber-400 hover:text-amber-300 transition-colors">Create one</a></p>
</div>
</div>`)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form method="POST" action="/auth/login">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
<p>Don't have an account? <a href="/auth/signup">Sign Up</a></p>
</body>
</html>`))
templates.WriteHTMLPage(w, "Sign In", "", content)
}
func (a *App) Login(w http.ResponseWriter, r *http.Request) {
@@ -173,14 +200,32 @@ func (a *App) AccountPage(w http.ResponseWriter, r *http.Request) {
return
}
content := fmt.Sprintf(`
%s
<div class="max-w-lg animate-slide-up">
<div class="card-industrial p-6">
<form method="POST" action="/auth/account" class="space-y-4">
<div>
<label class="block text-[10px] font-mono uppercase tracking-wider text-zinc-500 mb-1.5">Name</label>
<input type="text" name="name" value="%s" class="input-industrial">
</div>
<div>
<label class="block text-[10px] font-mono uppercase tracking-wider text-zinc-500 mb-1.5">Email</label>
<input type="email" value="%s" disabled class="input-industrial opacity-50 cursor-not-allowed">
</div>
<div>
<label class="block text-[10px] font-mono uppercase tracking-wider text-zinc-500 mb-1.5">New Password</label>
<input type="password" name="password" class="input-industrial" placeholder="Leave blank to keep current">
</div>
<div class="pt-2">
<button type="submit" class="btn-primary">Update Account</button>
</div>
</form>
</div>
</div>`, templates.PageHeader("Account Settings", "Manage your profile and security"), htmlEscape(name), htmlEscape(email))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`<!DOCTYPE html><html><head><title>Account</title></head><body><h1>Account Settings</h1><form method="POST" action="/auth/account">
<p>Name: <input type="text" name="name" value="` + name + `"></p>
<p>Email: <input type="email" value="` + email + `" disabled></p>
<p>New Password: <input type="password" name="password" placeholder="Leave blank to keep current"></p>
<button type="submit">Update</button>
</form>
<a href="/">Back to Dashboard</a></body></html>`))
templates.WriteHTMLPage(w, "Account", "", content)
}
func (a *App) UpdateAccount(w http.ResponseWriter, r *http.Request) {