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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user