Migrate to templ: Convert all HTML templates to type-safe templ components

- Add templ dependency for type-safe HTML rendering
- Convert index.html, login.html, register.html, dashboard.html, admin.html, admin_profile.html, admin_library.html to .templ files
- Generate Go code with templ generate for compile-time safety
- Update server routes to use templ components with proper rendering
- Refactor JavaScript in dashboard to avoid backtick parsing conflicts
- Delete old HTML template files
- Clean up project by removing unnecessary files and binaries
This commit is contained in:
2026-01-23 16:33:08 -05:00
parent fcc9b0f0b3
commit 5f5870b95d
20 changed files with 1371 additions and 1648 deletions
@@ -1,3 +0,0 @@
-- Add scan settings to users table
ALTER TABLE users ADD COLUMN scan_frequency_minutes INTEGER DEFAULT 60;
ALTER TABLE users ADD COLUMN auto_scan_enabled BOOLEAN DEFAULT true;
+2 -1
View File
@@ -4,6 +4,7 @@ go 1.25
require ( require (
github.com/ArcadiaLin/go-epub v0.1.1 github.com/ArcadiaLin/go-epub v0.1.1
github.com/a-h/templ v0.3.977
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/go-playground/validator/v10 v10.30.1 github.com/go-playground/validator/v10 v10.30.1
github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt v3.2.2+incompatible
@@ -23,7 +24,7 @@ require (
github.com/labstack/gommon v0.4.0 // indirect github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.47.0 // indirect
+8 -4
View File
@@ -1,5 +1,7 @@
github.com/ArcadiaLin/go-epub v0.1.1 h1:13roe62tarrZ1Y1QTxE+Bzd/NKlChhRzegLVmU5Hgws= github.com/ArcadiaLin/go-epub v0.1.1 h1:13roe62tarrZ1Y1QTxE+Bzd/NKlChhRzegLVmU5Hgws=
github.com/ArcadiaLin/go-epub v0.1.1/go.mod h1:GY09AG6jnEbsYytkw6VeICLOt25F1GQDGXjYS7cxNhU= github.com/ArcadiaLin/go-epub v0.1.1/go.mod h1:GY09AG6jnEbsYytkw6VeICLOt25F1GQDGXjYS7cxNhU=
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -17,6 +19,8 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -38,15 +42,15 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+4 -2
View File
@@ -141,8 +141,9 @@ func (h *AuthHandler) Register(c echo.Context) error {
<script> <script>
localStorage.setItem('token', '%s'); localStorage.setItem('token', '%s');
localStorage.setItem('user', JSON.stringify(%s)); localStorage.setItem('user', JSON.stringify(%s));
document.cookie = 'token=%s; path=/; max-age=86400';
window.location.href = '/'; window.location.href = '/';
</script>`, token, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username)) </script>`, token, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username), token)
return c.HTML(http.StatusCreated, html) return c.HTML(http.StatusCreated, html)
} }
@@ -237,8 +238,9 @@ func (h *AuthHandler) Login(c echo.Context) error {
<script> <script>
localStorage.setItem('token', '%s'); localStorage.setItem('token', '%s');
localStorage.setItem('user', JSON.stringify(%s)); localStorage.setItem('user', JSON.stringify(%s));
document.cookie = 'token=%s; path=/; max-age=86400';
window.location.href = '/'; window.location.href = '/';
</script>`, token, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username)) </script>`, token, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username), token)
return c.HTML(http.StatusOK, html) return c.HTML(http.StatusOK, html)
} }
-143
View File
@@ -1,143 +0,0 @@
{{template "base.html" .}}
{{define "title"}}Admin Dashboard - Bookmann{{end}}
{{define "content"}}
<!-- Navigation Header -->
<nav class="border-b" style="border-color: var(--border); background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold" style="color: var(--text-primary)">📚 Bookmann</h1>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Library</a>
<span style="color: var(--text-secondary)">Welcome, {{.User.Username}}!</span>
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
Logout
</button>
</div>
</div>
</div>
</nav>
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
<!-- Sidebar -->
<aside class="w-64 border-r" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="p-6">
<h2 class="text-lg font-semibold mb-6" style="color: var(--text-primary)">Admin Panel</h2>
<nav class="space-y-2">
<a href="/admin" class="block px-4 py-2 rounded-lg {{if eq .CurrentPage "dashboard"}}bg-accent text-bg-primary{{else}}hover:opacity-80{{end}}" style="color: var(--text-primary)">
🏠 Dashboard
</a>
<a href="/admin/profile" class="block px-4 py-2 rounded-lg {{if eq .CurrentPage "profile"}}bg-accent text-bg-primary{{else}}hover:opacity-80{{end}}" style="color: var(--text-primary)">
👤 Profile Settings
</a>
<a href="/admin/library" class="block px-4 py-2 rounded-lg {{if eq .CurrentPage "library"}}bg-accent text-bg-primary{{else}}hover:opacity-80{{end}}" style="color: var(--text-primary)">
📚 Library Management
</a>
<a href="/admin/scanner" class="block px-4 py-2 rounded-lg {{if eq .CurrentPage "scanner"}}bg-accent text-bg-primary{{else}}hover:opacity-80{{end}}" style="color: var(--text-primary)">
🔍 Scanner Settings
</a>
</nav>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 p-8">
<div class="max-w-4xl">
{{if eq .CurrentPage "dashboard"}}
<!-- Dashboard Overview -->
<div class="mb-8">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Dashboard</h1>
<p style="color: var(--text-secondary)">Overview of your Bookmann library and settings</p>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex items-center space-x-3">
<div class="text-3xl">📖</div>
<div>
<h3 class="font-semibold" style="color: var(--text-primary)">Library</h3>
<p style="color: var(--text-secondary)" class="text-sm">Manage your ebook collection</p>
</div>
</div>
<a href="/" class="mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded">View Library</a>
</div>
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex items-center space-x-3">
<div class="text-3xl">⚙️</div>
<div>
<h3 class="font-semibold" style="color: var(--text-primary)">Settings</h3>
<p style="color: var(--text-secondary)" class="text-sm">Configure your preferences</p>
</div>
</div>
<a href="/admin/profile" class="mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded">Manage Settings</a>
</div>
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex items-center space-x-3">
<div class="text-3xl">🔍</div>
<div>
<h3 class="font-semibold" style="color: var(--text-primary)">Scanner</h3>
<p style="color: var(--text-secondary)" class="text-sm">Library scanning options</p>
</div>
</div>
<a href="/admin/scanner" class="mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded">Configure Scanner</a>
</div>
</div>
<!-- Quick Actions -->
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Quick Actions</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button onclick="quickScan()" class="btn-primary p-4 rounded-lg text-left">
<div class="font-medium">Scan Library</div>
<div style="color: var(--text-secondary)" class="text-sm">Find new ebooks in your folders</div>
</button>
<a href="/admin/library" class="btn-secondary p-4 rounded-lg text-left block">
<div class="font-medium">Manage Folders</div>
<div style="color: var(--text-secondary)" class="text-sm">Add or remove scan directories</div>
</a>
</div>
</div>
{{end}}
</div>
</main>
</div>
<script>
// Quick scan from dashboard
function quickScan() {
fetch('/api/scanner/scan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
folder_paths: [] // Will scan all configured folders
})
}).then(res => res.json()).then(data => {
alert(data.message || 'Scan completed successfully!');
}).catch(err => {
console.error('Scan error:', err);
alert('Scan failed. Please check your folder configuration.');
});
}
// Logout
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
// Load theme on page load
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
});
</script>
{{end}}
+175
View File
@@ -0,0 +1,175 @@
package templates
templ Admin(user User, currentPage string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Admin Dashboard - Bookmann</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
}
.btn-primary:hover {
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night">
<nav class="border-b" style="border-color: var(--border); background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold" style="color: var(--text-primary)">📚 Bookmann</h1>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Library</a>
<span style="color: var(--text-secondary)">Welcome, { user.Username }!</span>
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
Logout
</button>
</div>
</div>
</div>
</nav>
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
<aside class="w-64 border-r" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="p-6">
<h2 class="text-lg font-semibold mb-6" style="color: var(--text-primary)">Admin Panel</h2>
<nav class="space-y-2">
<a href="/admin" class="block px-4 py-2 rounded-lg bg-accent text-bg-primary" style="color: var(--text-primary)">
🏠 Dashboard
</a>
<a href="/admin/profile" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
👤 Profile Settings
</a>
<a href="/admin/library" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
📚 Library Management
</a>
<a href="/admin/scanner" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
🔍 Scanner Settings
</a>
</nav>
</div>
</aside>
<main class="flex-1 p-8">
<div class="max-w-4xl">
if currentPage == "dashboard" {
<div class="mb-8">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Dashboard</h1>
<p style="color: var(--text-secondary)">Overview of your Bookmann library and settings</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex items-center space-x-3">
<div class="text-3xl">📖</div>
<div>
<h3 class="font-semibold" style="color: var(--text-primary)">Library</h3>
<p style="color: var(--text-secondary)" class="text-sm">Manage your ebook collection</p>
</div>
</div>
<a href="/" class="mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded">View Library</a>
</div>
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex items-center space-x-3">
<div class="text-3xl">⚙️</div>
<div>
<h3 class="font-semibold" style="color: var(--text-primary)">Settings</h3>
<p style="color: var(--text-secondary)" class="text-sm">Configure your preferences</p>
</div>
</div>
<a href="/admin/profile" class="mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded">Manage Settings</a>
</div>
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex items-center space-x-3">
<div class="text-3xl">🔍</div>
<div>
<h3 class="font-semibold" style="color: var(--text-primary)">Scanner</h3>
<p style="color: var(--text-secondary)" class="text-sm">Library scanning options</p>
</div>
</div>
<a href="/admin/scanner" class="mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded">Configure Scanner</a>
</div>
</div>
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Quick Actions</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button onclick="quickScan()" class="btn-primary p-4 rounded-lg text-left">
<div class="font-medium">Scan Library</div>
<div style="color: var(--text-secondary)" class="text-sm">Find new ebooks in your folders</div>
</button>
<a href="/admin/library" class="btn-secondary p-4 rounded-lg text-left block">
<div class="font-medium">Manage Folders</div>
<div style="color: var(--text-secondary)" class="text-sm">Add or remove scan directories</div>
</a>
</div>
</div>
}
</div>
</main>
</div>
<script>
function quickScan() {
fetch('/api/scanner/scan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
folder_paths: []
})
}).then(res => res.json()).then(data => {
alert(data.message || 'Scan completed successfully!');
}).catch(err => {
console.error('Scan error:', err);
alert('Scan failed. Please check your folder configuration.');
});
}
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
});
</script>
</body>
</html>
}
-303
View File
@@ -1,303 +0,0 @@
{{template "base.html" .}}
{{define "title"}}Library Management - Bookmann{{end}}
{{define "content"}}
<!-- Navigation Header -->
<nav class="border-b" style="border-color: var(--border); background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold" style="color: var(--text-primary)">📚 Bookmann</h1>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Library</a>
<span style="color: var(--text-secondary)">Welcome, {{.User.Username}}!</span>
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
Logout
</button>
</div>
</div>
</div>
</nav>
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
<!-- Sidebar -->
<aside class="w-64 border-r" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="p-6">
<h2 class="text-lg font-semibold mb-6" style="color: var(--text-primary)">Admin Panel</h2>
<nav class="space-y-2">
<a href="/admin" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
🏠 Dashboard
</a>
<a href="/admin/profile" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
👤 Profile Settings
</a>
<a href="/admin/library" class="block px-4 py-2 rounded-lg bg-accent text-bg-primary" style="color: var(--text-primary)">
📚 Library Management
</a>
</nav>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 p-8">
<div class="max-w-4xl">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center space-x-4 mb-4">
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium">
← Back to Dashboard
</a>
</div>
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Library Management</h1>
<p style="color: var(--text-secondary)">Configure your ebook folders and scanning preferences</p>
</div>
<!-- Library Management Sections -->
<div class="space-y-8">
<!-- Ebook Folders Management -->
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">📁 Ebook Folders</h3>
<p style="color: var(--text-secondary)" class="mb-6">Manage the directories where Bookmann scans for ebooks. Supported formats: EPUB, PDF, MOBI, AZW3, FB2, TXT</p>
<!-- Add New Folder -->
<div class="mb-6">
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Add Folder</h4>
<form hx-post="/api/auth/ebook-folders" hx-target="#folders-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3">
<input type="text" name="folder_path" placeholder="/home/user/ebooks" class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
<button type="submit" class="btn-primary px-6 py-2 rounded">Add Folder</button>
</form>
<div id="folders-result" class="mt-2"></div>
</div>
<!-- Current Folders List -->
<div>
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Configured Folders</h4>
<div id="folders-list" class="space-y-3">
<!-- Folders will be loaded here -->
</div>
</div>
</div>
<!-- Scan Settings -->
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">🔍 Scan Settings</h3>
<p style="color: var(--text-secondary)" class="mb-6">Configure how and when Bookmann scans your folders for new ebooks</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Auto Scan Toggle -->
<div>
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Automatic Scanning</h4>
<label class="flex items-center space-x-3 p-3 border rounded" style="border-color: var(--border); background-color: var(--bg-primary)">
<input type="checkbox" id="auto-scan-toggle" class="rounded" onchange="toggleAutoScan()">
<div>
<span style="color: var(--text-primary)">Enable automatic scanning</span>
<p style="color: var(--text-secondary)" class="text-sm">Automatically scan folders for new ebooks</p>
</div>
</label>
</div>
<!-- Scan Frequency -->
<div>
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Scan Frequency</h4>
<select id="scan-frequency" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" onchange="updateScanSettings()">
<option value="15">Every 15 minutes</option>
<option value="30">Every 30 minutes</option>
<option value="60">Every hour</option>
<option value="240">Every 4 hours</option>
<option value="720">Every 12 hours</option>
<option value="1440">Daily</option>
</select>
<p style="color: var(--text-secondary)" class="text-sm mt-1">How often to scan when automatic scanning is enabled</p>
</div>
</div>
<!-- Manual Scan -->
<div class="mt-6 pt-6 border-t" style="border-color: var(--border)">
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Manual Scan</h4>
<div class="flex items-center space-x-4">
<button onclick="manualScan()" class="btn-secondary px-6 py-2 rounded">Scan Now</button>
<span style="color: var(--text-secondary)" class="text-sm">Manually scan all configured folders immediately</span>
</div>
<div id="scan-result" class="mt-2"></div>
</div>
</div>
<!-- Library Statistics -->
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">📊 Library Statistics</h3>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="text-center p-4 border rounded" style="border-color: var(--border); background-color: var(--bg-primary)">
<div class="text-2xl font-bold" style="color: var(--text-primary)" id="folders-count">-</div>
<div style="color: var(--text-secondary)">Folders</div>
</div>
<div class="text-center p-4 border rounded" style="border-color: var(--border); background-color: var(--bg-primary)">
<div class="text-2xl font-bold" style="color: var(--text-primary)" id="books-count">-</div>
<div style="color: var(--text-secondary)">Books</div>
</div>
<div class="text-center p-4 border rounded" style="border-color: var(--border); background-color: var(--bg-primary)">
<div class="text-2xl font-bold" style="color: var(--text-primary)" id="last-scan">-</div>
<div style="color: var(--text-secondary)">Last Scan</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
// Load data on page load
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
loadFolders();
loadScanSettings();
loadStats();
});
// Folder management
function loadFolders() {
fetch('/api/auth/ebook-folders', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
}).then(res => res.json()).then(data => {
renderFolders(data);
updateFolderCount(data.length);
}).catch(err => console.error('Error loading folders:', err));
}
function renderFolders(folders) {
const container = document.getElementById('folders-list');
container.innerHTML = '';
if (folders.length === 0) {
container.innerHTML = `
<div class="text-center py-8" style="color: var(--text-secondary)">
<div class="text-4xl mb-3">📁</div>
<p>No folders configured yet</p>
<p class="text-sm">Add a folder above to start building your library</p>
</div>
`;
return;
}
folders.forEach(folder => {
const folderDiv = document.createElement('div');
folderDiv.className = 'flex items-center justify-between p-4 border rounded' style="border-color: var(--border); background-color: var(--bg-primary)";
folderDiv.innerHTML = `
<div class="flex-1">
<div style="color: var(--text-primary)" class="font-medium font-mono text-sm break-all">${folder.folder_path}</div>
<div style="color: var(--text-secondary)" class="text-xs mt-1">Added ${new Date(folder.created_at).toLocaleDateString()}</div>
</div>
<button onclick="removeFolder('${folder.folder_path}')" class="ml-4 text-red-500 hover:text-red-700 px-3 py-1 border border-red-500 rounded text-sm hover:bg-red-50">
Remove
</button>
`;
container.appendChild(folderDiv);
});
}
function removeFolder(folderPath) {
if (confirm(`Remove folder: ${folderPath}?\n\nThis will not delete any files, but ebooks in this folder will no longer be automatically discovered.`)) {
fetch(`/api/auth/ebook-folders/${encodeURIComponent(folderPath)}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
}).then(() => {
loadFolders();
}).catch(err => console.error('Error removing folder:', err));
}
}
function updateFolderCount(count) {
document.getElementById('folders-count').textContent = count;
}
// Scan settings
function loadScanSettings() {
fetch('/api/library/scan-settings', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
}).then(res => res.json()).then(data => {
document.getElementById('auto-scan-toggle').checked = data.auto_scan_enabled;
document.getElementById('scan-frequency').value = data.scan_frequency_minutes;
}).catch(err => {
console.error('Error loading scan settings:', err);
// Fall back to defaults
document.getElementById('auto-scan-toggle').checked = true;
document.getElementById('scan-frequency').value = '60';
});
}
function toggleAutoScan() {
updateScanSettings();
}
function updateScanSettings() {
const scanFrequency = parseInt(document.getElementById('scan-frequency').value);
const autoScanEnabled = document.getElementById('auto-scan-toggle').checked;
fetch('/api/library/scan-settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
scan_frequency_minutes: scanFrequency,
auto_scan_enabled: autoScanEnabled
})
}).catch(err => {
console.error('Error updating scan settings:', err);
});
}
function manualScan() {
// Get folder paths from the current folders list
fetch('/api/auth/ebook-folders', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
}).then(res => res.json()).then(folders => {
const folderPaths = folders.map(f => f.folder_path);
return fetch('/api/scanner/scan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
folder_paths: folderPaths
})
});
}).then(res => res.json()).then(data => {
document.getElementById('scan-result').innerHTML = `<div style="color: var(--accent)">✓ ${data.message || 'Scan completed successfully'}</div>`;
loadStats(); // Refresh stats after scan
}).catch(err => {
document.getElementById('scan-result').innerHTML = `<div style="color: #ef4444">✗ Scan failed</div>`;
console.error('Scan error:', err);
});
}
// Statistics
function loadStats() {
// TODO: Implement actual stats loading
// For now, just update book count when we have folder info
fetch('/api/ebooks?limit=1', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
}).then(() => {
// We could get total count from a header or separate endpoint
// For now, just show placeholder
document.getElementById('books-count').textContent = '?';
document.getElementById('last-scan').textContent = 'Never';
}).catch(() => {
document.getElementById('books-count').textContent = '0';
document.getElementById('last-scan').textContent = 'Never';
});
}
// Logout
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
</script>
{{end}}
+186
View File
@@ -0,0 +1,186 @@
package templates
templ AdminLibrary(user User) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Library Management - Bookmann</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
}
.btn-primary:hover {
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night">
<nav class="border-b" style="border-color: var(--border); background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold" style="color: var(--text-primary)">📚 Bookmann</h1>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Library</a>
<span style="color: var(--text-secondary)">Welcome, { user.Username }!</span>
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
Logout
</button>
</div>
</div>
</div>
</nav>
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
<aside class="w-64 border-r" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="p-6">
<h2 class="text-lg font-semibold mb-6" style="color: var(--text-primary)">Admin Panel</h2>
<nav class="space-y-2">
<a href="/admin" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
🏠 Dashboard
</a>
<a href="/admin/profile" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
👤 Profile Settings
</a>
<a href="/admin/library" class="block px-4 py-2 rounded-lg bg-accent text-bg-primary" style="color: var(--text-primary)">
📚 Library Management
</a>
</nav>
</div>
</aside>
<main class="flex-1 p-8">
<div class="max-w-4xl">
<div class="mb-8">
<div class="flex items-center space-x-4 mb-4">
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium">
Back to Dashboard
</a>
</div>
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Library Management</h1>
<p style="color: var(--text-secondary)">Configure library scanning settings and manage ebook folders</p>
</div>
<div class="space-y-8">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Scan Settings</h3>
<div class="mb-6">
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Scan Frequency</h4>
<form hx-put="/api/library/scan-settings" hx-target="#scan-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
<select name="scan_frequency_minutes" class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
<option value="15">Every 15 minutes</option>
<option value="30">Every 30 minutes</option>
<option value="60">Every hour</option>
<option value="120">Every 2 hours</option>
<option value="240">Every 4 hours</option>
</select>
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
</form>
<div id="scan-result" class="mt-2"></div>
</div>
<div>
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Auto Scan</h4>
<form hx-put="/api/library/scan-settings" hx-target="#auto-scan-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex items-center space-x-3">
<input type="checkbox" name="auto_scan_enabled" id="auto-scan" class="w-4 h-4" style="background-color: var(--bg-primary); border-color: var(--border)">
<label for="auto-scan" style="color: var(--text-secondary)">Enable automatic library scanning</label>
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
</form>
<div id="auto-scan-result" class="mt-2"></div>
</div>
</div>
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Ebook Folders</h3>
<p style="color: var(--text-secondary)" class="mb-4">Manage folders that Bookmann scans for ebooks</p>
<div id="folders-list" class="space-y-3 mb-6">
<!-- Folders will be loaded here -->
</div>
<form hx-post="/api/auth/ebook-folders" hx-target="#add-folder-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3">
<input type="text" name="folder_path" placeholder="/path/to/ebooks" class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
<button type="submit" class="btn-primary px-4 py-2 rounded">Add Folder</button>
</form>
<div id="add-folder-result" class="mt-2"></div>
</div>
</div>
</div>
</main>
</div>
<script>
function loadFolders() {
fetch('/api/auth/ebook-folders', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => response.json())
.then(folders => {
const container = document.getElementById('folders-list');
container.innerHTML = folders.map(folder =>
'<div class="flex items-center justify-between p-3 border rounded" style="background-color: var(--bg-primary); border-color: var(--border)">' +
'<span style="color: var(--text-primary)">' + folder.folder_path + '</span>' +
'<button onclick="removeFolder(\'' + folder.id + '\')" class="text-red-500 hover:text-red-700">Remove</button>' +
'</div>'
).join('');
})
.catch(error => console.error('Error loading folders:', error));
}
function removeFolder(folderId) {
if (confirm('Remove this folder from scanning?')) {
fetch('/api/auth/ebook-folders/' + folderId, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (response.ok) loadFolders();
else alert('Error removing folder');
})
.catch(error => console.error('Error removing folder:', error));
}
}
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
loadFolders();
});
</script>
</body>
</html>
}
-198
View File
@@ -1,198 +0,0 @@
{{template "base.html" .}}
{{define "title"}}Profile Settings - Bookmann{{end}}
{{define "content"}}
<!-- Navigation Header -->
<nav class="border-b" style="border-color: var(--border); background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold" style="color: var(--text-primary)">📚 Bookmann</h1>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Library</a>
<span style="color: var(--text-secondary)">Welcome, {{.User.Username}}!</span>
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
Logout
</button>
</div>
</div>
</div>
</nav>
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
<!-- Sidebar -->
<aside class="w-64 border-r" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="p-6">
<h2 class="text-lg font-semibold mb-6" style="color: var(--text-primary)">Admin Panel</h2>
<nav class="space-y-2">
<a href="/admin" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
🏠 Dashboard
</a>
<a href="/admin/profile" class="block px-4 py-2 rounded-lg bg-accent text-bg-primary" style="color: var(--text-primary)">
👤 Profile Settings
</a>
<a href="/admin/library" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
📚 Library Management
</a>
</nav>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 p-8">
<div class="max-w-4xl">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center space-x-4 mb-4">
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium">
← Back to Dashboard
</a>
</div>
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Profile Settings</h1>
<p style="color: var(--text-secondary)">Manage your account information and preferences</p>
</div>
<!-- Profile Settings -->
<div class="space-y-8">
<!-- Account Information -->
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Account Information</h3>
<!-- Update Username -->
<div class="mb-6">
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Username</h4>
<form hx-put="/api/user/username" hx-target="#username-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
<input type="text" name="username" value="{{.User.Username}}" class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
</form>
<div id="username-result" class="mt-2"></div>
</div>
<!-- Update Email -->
<div class="mb-6">
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Email Address</h4>
<form hx-put="/api/user/email" hx-target="#email-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
<input type="email" name="email" value="{{.User.Email}}" class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
</form>
<div id="email-result" class="mt-2"></div>
</div>
<!-- Update Password -->
<div>
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Change Password</h4>
<form hx-put="/api/user/password" hx-target="#password-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="space-y-4 max-w-md">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Current Password</label>
<input type="password" name="current_password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">New Password</label>
<input type="password" name="new_password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required minlength="6">
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm New Password</label>
<input type="password" name="confirm_password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required minlength="6">
</div>
<button type="submit" class="btn-primary px-4 py-2 rounded">Update Password</button>
</form>
<div id="password-result" class="mt-2"></div>
</div>
</div>
<!-- Appearance Settings -->
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Appearance</h3>
<div>
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Theme</h4>
<form hx-put="/api/auth/theme" hx-target="#theme-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
<select name="theme" id="theme-select" class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
<option value="tokyo-night">Tokyo Night</option>
<option value="dracula">Dracula</option>
<option value="nord">Nord</option>
<option value="solarized-dark">Solarized Dark</option>
<option value="monokai">Monokai</option>
<option value="one-dark-pro">One Dark Pro</option>
<option value="material-dark">Material Dark</option>
<option value="catppuccin-mocha">Catppuccin Mocha</option>
<option value="catppuccin-macchiato">Catppuccin Macchiato</option>
<option value="catppuccin-frappe">Catppuccin Frappé</option>
<option value="catppuccin-latte">Catppuccin Latte</option>
</select>
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
</form>
<div id="theme-result" class="mt-2"></div>
</div>
</div>
<!-- Danger Zone -->
<div class="card p-6 rounded-lg border border-red-500" style="background-color: var(--bg-secondary);">
<h3 class="text-xl font-semibold mb-4 text-red-500">Danger Zone</h3>
<div class="border-t border-red-500 pt-4">
<h4 class="text-lg font-medium mb-2" style="color: var(--text-primary)">Delete Account</h4>
<p style="color: var(--text-secondary)" class="mb-4 text-sm">
Once you delete your account, there is no going back. Please be certain.
</p>
<button onclick="deleteAccount()" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded font-medium">
Delete Account
</button>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
// Load user theme on page load
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
loadUserTheme();
});
function loadUserTheme() {
const token = localStorage.getItem('token');
if (token) {
fetch('/api/auth/profile', {
headers: { 'Authorization': 'Bearer ' + token }
}).then(res => res.json()).then(data => {
if (data.theme) {
document.getElementById('theme-select').value = data.theme;
applyTheme(data.theme);
}
}).catch(() => {});
}
}
function deleteAccount() {
if (confirm('Are you absolutely sure you want to delete your account? This action cannot be undone and will permanently delete all your ebooks and data.')) {
fetch('/api/user/account', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
}).then(response => {
if (response.ok) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
} else {
alert('Error deleting account. Please try again.');
}
}).catch(error => {
console.error('Error deleting account:', error);
alert('Error deleting account. Please try again.');
});
}
}
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
</script>
{{end}}
+152
View File
@@ -0,0 +1,152 @@
package templates
templ AdminProfile(user User) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Profile Settings - Bookmann</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
}
.btn-primary:hover {
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night">
<nav class="border-b" style="border-color: var(--border); background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold" style="color: var(--text-primary)">📚 Bookmann</h1>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Library</a>
<span style="color: var(--text-secondary)">Welcome, { user.Username }!</span>
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
Logout
</button>
</div>
</div>
</div>
</nav>
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
<aside class="w-64 border-r" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="p-6">
<h2 class="text-lg font-semibold mb-6" style="color: var(--text-primary)">Admin Panel</h2>
<nav class="space-y-2">
<a href="/admin" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
🏠 Dashboard
</a>
<a href="/admin/profile" class="block px-4 py-2 rounded-lg bg-accent text-bg-primary" style="color: var(--text-primary)">
👤 Profile Settings
</a>
<a href="/admin/library" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
📚 Library Management
</a>
</nav>
</div>
</aside>
<main class="flex-1 p-8">
<div class="max-w-4xl">
<div class="mb-8">
<div class="flex items-center space-x-4 mb-4">
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium">
Back to Dashboard
</a>
</div>
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Profile Settings</h1>
<p style="color: var(--text-secondary)">Manage your account information and preferences</p>
</div>
<div class="space-y-8">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Account Information</h3>
<div class="mb-6">
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Username</h4>
<form hx-put="/api/user/username" hx-target="#username-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
<input type="text" name="username" value="{ user.Username }" class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
</form>
<div id="username-result" class="mt-2"></div>
</div>
<div class="mb-6">
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Email Address</h4>
<form hx-put="/api/user/email" hx-target="#email-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
<input type="email" name="email" value="{ user.Email }" class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
</form>
<div id="email-result" class="mt-2"></div>
</div>
<div>
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Change Password</h4>
<form hx-put="/api/user/password" hx-target="#password-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="space-y-4 max-w-md">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Current Password</label>
<input type="password" name="current_password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">New Password</label>
<input type="password" name="new_password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required minlength="6">
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm New Password</label>
<input type="password" name="confirm_password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required minlength="6">
</div>
<button type="submit" class="btn-primary px-4 py-2 rounded">Update Password</button>
</form>
<div id="password-result" class="mt-2"></div>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
});
</script>
</body>
</html>
}
-137
View File
@@ -1,137 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{block "title" .}}Bookmann{{end}}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-dracula {
--bg-primary: #282a36;
--bg-secondary: #21222c;
--text-primary: #f8f8f2;
--text-secondary: #6272a4;
--accent: #bd93f9;
--border: #44475a;
}
.theme-nord {
--bg-primary: #2e3440;
--bg-secondary: #3b4252;
--text-primary: #eceff4;
--text-secondary: #5e81ac;
--accent: #88c0d0;
--border: #4c566a;
}
.theme-solarized-dark {
--bg-primary: #002b36;
--bg-secondary: #073642;
--text-primary: #93a1a1;
--text-secondary: #586e75;
--accent: #2aa198;
--border: #586e75;
}
.theme-monokai {
--bg-primary: #272822;
--bg-secondary: #3e3d32;
--text-primary: #f8f8f2;
--text-secondary: #75715e;
--accent: #a6e22e;
--border: #49483e;
}
.theme-one-dark-pro {
--bg-primary: #282c34;
--bg-secondary: #21252b;
--text-primary: #abb2bf;
--text-secondary: #5c6370;
--accent: #61dafb;
--border: #3e4451;
}
.theme-material-dark {
--bg-primary: #263238;
--bg-secondary: #37474f;
--text-primary: #eeffff;
--text-secondary: #546e7a;
--accent: #80cbc4;
--border: #455a64;
}
.theme-catppuccin-mocha {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--text-primary: #cdd6f4;
--text-secondary: #bac2de;
--accent: #f38ba8;
--border: #313244;
}
.theme-catppuccin-macchiato {
--bg-primary: #24273a;
--bg-secondary: #1e2030;
--text-primary: #cad3f5;
--text-secondary: #b8c0e0;
--accent: #f0c6c6;
--border: #363a4f;
}
.theme-catppuccin-frappe {
--bg-primary: #303446;
--bg-secondary: #292c3c;
--text-primary: #c6d0f5;
--text-secondary: #b5bfe2;
--accent: #f2d5cf;
--border: #414559;
}
.theme-catppuccin-latte {
--bg-primary: #eff1f5;
--bg-secondary: #e6e9ef;
--text-primary: #4c4f69;
--text-secondary: #5c5f77;
--accent: #d20f39;
--border: #bcc0cc;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
}
.btn-primary:hover {
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night min-h-screen">
{{block "content" .}}{{end}}
<script>
function applyTheme(theme) {
document.body.className = `theme-${theme} min-h-screen`;
localStorage.setItem('theme', theme);
}
function loadTheme() {
const theme = localStorage.getItem('theme') || 'tokyo-night';
applyTheme(theme);
}
document.addEventListener('DOMContentLoaded', loadTheme);
</script>
</body>
</html>
-446
View File
@@ -1,446 +0,0 @@
{{template "base.html" .}}
{{define "title"}}Dashboard - Bookmann{{end}}
{{define "content"}}
<!-- Navigation Header -->
<nav class="border-b" style="border-color: var(--border); background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold" style="color: var(--text-primary)">📚 Bookmann</h1>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Dashboard</a>
<a href="/admin" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Admin</a>
<span style="color: var(--text-secondary)">Welcome, {{.User.Username}}!</span>
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
Logout
</button>
</div>
</div>
</div>
</nav>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header Section -->
<div class="mb-8">
<div class="flex justify-between items-center">
<div>
<h2 class="text-3xl font-bold" style="color: var(--text-primary)">Your Ebook Library</h2>
<p style="color: var(--text-secondary)">Manage your ebook collection and reading progress</p>
</div>
<button onclick="showCreateForm()" class="btn-primary px-6 py-3 rounded-lg font-medium">
+ Add New Ebook
</button>
</div>
</div>
<!-- Search and Filter -->
<div class="mb-6">
<div class="flex flex-col sm:flex-row gap-4">
<input type="text" id="search-input" placeholder="Search ebooks..." class="flex-1 px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onkeyup="filterEbooks()">
<select id="sort-select" class="px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onchange="loadEbooks()">
<option value="created_at DESC">Newest First</option>
<option value="title ASC">Title A-Z</option>
<option value="author ASC">Author A-Z</option>
<option value="created_at ASC">Oldest First</option>
</select>
</div>
</div>
<!-- Create/Edit Form (Hidden by default) -->
<div id="ebook-form" class="card p-6 rounded-lg border mb-6" style="display: none;">
<h3 id="form-title" class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Add New Ebook</h3>
<form id="ebook-form-element" hx-post="/api/ebooks" hx-target="#form-result" hx-swap="innerHTML" hx-on:htmx:after-request="handleFormResponse()" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Title *</label>
<input type="text" name="title" id="form-title-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Author</label>
<input type="text" name="author" id="form-author-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">ISBN</label>
<input type="text" name="isbn" id="form-isbn-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Series</label>
<input type="text" name="series" id="form-series-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Series Number</label>
<input type="number" name="series_number" id="form-series-number-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Publisher</label>
<input type="text" name="publisher" id="form-publisher-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Date Published</label>
<input type="date" name="date_published" id="form-date-published-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">ASIN</label>
<input type="text" name="asin" id="form-asin-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Tags</label>
<input type="text" name="tags" id="form-tags-input" placeholder="fiction, adventure, ..." class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Contributors</label>
<input type="text" name="contributors" id="form-contributors-input" placeholder="Editor Name, Illustrator Name" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Description</label>
<textarea name="description" id="form-description-input" rows="3" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"></textarea>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">File Path *</label>
<input type="text" name="file_path" id="form-file-path-input" placeholder="/uploads/book.epub" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">File Size (bytes) *</label>
<input type="number" name="file_size" id="form-file-size-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">MIME Type *</label>
<input type="text" name="mime_type" id="form-mime-type-input" placeholder="application/epub+zip" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Cover Image Path</label>
<input type="text" name="cover_image_path" id="form-cover-image-path-input" placeholder="/uploads/cover.jpg" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button type="button" onclick="hideCreateForm()" class="px-4 py-2 border rounded" style="border-color: var(--border); color: var(--text-secondary)">Cancel</button>
<button type="submit" class="btn-primary px-6 py-2 rounded">Save Ebook</button>
</div>
</form>
<div id="form-result" class="mt-4"></div>
</div>
<!-- Ebooks Grid -->
<div id="ebooks-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Ebooks will be loaded here -->
</div>
<!-- Loading indicator -->
<div id="loading" class="text-center py-8" style="color: var(--text-secondary)">
Loading your ebooks...
</div>
<!-- Empty state -->
<div id="empty-state" class="text-center py-12" style="display: none;">
<div class="text-6xl mb-4">📚</div>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No ebooks yet</h3>
<p style="color: var(--text-secondary)">Start building your library by adding your first ebook</p>
<button onclick="showCreateForm()" class="btn-primary px-6 py-3 rounded-lg font-medium mt-4">
Add Your First Ebook
</button>
</div>
<!-- Pagination -->
<div id="pagination" class="mt-8 flex justify-center space-x-2" style="display: none;">
<button id="prev-btn" onclick="changePage(-1)" class="px-4 py-2 border rounded disabled:opacity-50" style="border-color: var(--border); color: var(--text-secondary)">Previous</button>
<span id="page-info" style="color: var(--text-secondary)"></span>
<button id="next-btn" onclick="changePage(1)" class="px-4 py-2 border rounded disabled:opacity-50" style="border-color: var(--border); color: var(--text-secondary)">Next</button>
</div>
</div>
<!-- Ebook Detail Modal -->
<div id="ebook-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style="display: none; z-index: 1000;">
<div class="card max-w-2xl w-full mx-4 rounded-lg border max-h-screen overflow-y-auto" style="background-color: var(--bg-secondary);">
<div class="p-6">
<div class="flex justify-between items-start mb-4">
<h3 id="modal-title" class="text-xl font-semibold" style="color: var(--text-primary)"></h3>
<button onclick="closeModal()" class="text-xl" style="color: var(--text-secondary)">&times;</button>
</div>
<div id="modal-content">
<!-- Content will be loaded here -->
</div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentSort = 'created_at DESC';
let allEbooks = [];
function showCreateForm() {
document.getElementById('ebook-form').style.display = 'block';
document.getElementById('form-title').textContent = 'Add New Ebook';
document.getElementById('ebook-form-element').setAttribute('hx-post', '/api/ebooks');
clearForm();
document.getElementById('form-title-input').focus();
}
function showEditForm(ebook) {
document.getElementById('ebook-form').style.display = 'block';
document.getElementById('form-title').textContent = 'Edit Ebook';
document.getElementById('ebook-form-element').setAttribute('hx-put', `/api/ebooks/${ebook.id}`);
populateForm(ebook);
document.getElementById('form-title-input').focus();
}
function hideCreateForm() {
document.getElementById('ebook-form').style.display = 'none';
clearForm();
}
function clearForm() {
document.getElementById('ebook-form-element').reset();
}
function populateForm(ebook) {
document.getElementById('form-title-input').value = ebook.title || '';
document.getElementById('form-author-input').value = ebook.author || '';
document.getElementById('form-isbn-input').value = ebook.isbn || '';
document.getElementById('form-series-input').value = ebook.series || '';
document.getElementById('form-series-number-input').value = ebook.series_number || '';
document.getElementById('form-publisher-input').value = ebook.publisher || '';
document.getElementById('form-date-published-input').value = ebook.date_published || '';
document.getElementById('form-asin-input').value = ebook.asin || '';
document.getElementById('form-tags-input').value = ebook.tags || '';
document.getElementById('form-contributors-input').value = ebook.contributors || '';
document.getElementById('form-description-input').value = ebook.description || '';
document.getElementById('form-file-path-input').value = ebook.file_path || '';
document.getElementById('form-file-size-input').value = ebook.file_size || '';
document.getElementById('form-mime-type-input').value = ebook.mime_type || '';
document.getElementById('form-cover-image-path-input').value = ebook.cover_image_path || '';
}
function handleFormResponse() {
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
hideCreateForm();
loadEbooks();
}
}
function loadEbooks() {
const loading = document.getElementById('loading');
const container = document.getElementById('ebooks-container');
const emptyState = document.getElementById('empty-state');
loading.style.display = 'block';
container.innerHTML = '';
const sort = document.getElementById('sort-select').value;
currentSort = sort;
fetch(`/api/ebooks?limit=12&offset=${(currentPage - 1) * 12}`, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
loading.style.display = 'none';
allEbooks = data;
if (data.length === 0) {
emptyState.style.display = 'block';
document.getElementById('pagination').style.display = 'none';
return;
}
emptyState.style.display = 'none';
renderEbooks(data);
})
.catch(error => {
loading.style.display = 'none';
console.error('Error loading ebooks:', error);
container.innerHTML = '<div class="col-span-full text-center py-8" style="color: var(--text-secondary)">Error loading ebooks. Please try again.</div>';
});
}
function renderEbooks(ebooks) {
const container = document.getElementById('ebooks-container');
container.innerHTML = '';
ebooks.forEach(ebook => {
const card = createEbookCard(ebook);
container.appendChild(card);
});
}
function createEbookCard(ebook) {
const card = document.createElement('div');
card.className = 'card p-4 rounded-lg border hover:shadow-lg transition-shadow';
card.style.cssText = `background-color: var(--bg-secondary); border-color: var(--border);`;
const coverUrl = ebook.cover_image_path ? ebook.cover_image_path : '/static/placeholder-book.svg';
card.innerHTML = `
<div class="flex flex-col h-full">
<div class="flex-1">
<div class="aspect-w-3 aspect-h-4 mb-3 overflow-hidden rounded">
<img src="${coverUrl}" alt="Cover" class="w-full h-48 object-cover rounded" onerror="this.src='/static/placeholder-book.svg'">
</div>
<h3 class="font-semibold text-lg mb-1 line-clamp-2" style="color: var(--text-primary)">${ebook.title}</h3>
${ebook.author ? `<p class="text-sm mb-2" style="color: var(--text-secondary)">by ${ebook.author}</p>` : ''}
${ebook.series ? `<p class="text-sm mb-2" style="color: var(--text-secondary)">${ebook.series}${ebook.series_number ? ' #' + ebook.series_number : ''}</p>` : ''}
<div class="flex flex-wrap gap-1 mb-3">
${ebook.tags ? ebook.tags.split(',').slice(0, 3).map(tag => `<span class="px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">${tag.trim()}</span>`).join('') : ''}
</div>
</div>
<div class="flex justify-between items-center mt-4">
<div class="flex space-x-2">
<button onclick="viewEbook('${ebook.id}')" class="px-3 py-1 text-sm border rounded hover:opacity-80" style="border-color: var(--border); color: var(--text-secondary)">View</button>
<button onclick="showEditForm(${JSON.stringify(ebook).replace(/"/g, '&quot;')})" class="px-3 py-1 text-sm border rounded hover:opacity-80" style="border-color: var(--border); color: var(--text-secondary)">Edit</button>
</div>
<button onclick="deleteEbook('${ebook.id}')" class="px-3 py-1 text-sm text-red-500 hover:text-red-700">
Delete
</button>
</div>
</div>
`;
return card;
}
function viewEbook(id) {
fetch(`/api/ebooks/${id}`, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(ebook => {
showEbookModal(ebook);
})
.catch(error => {
console.error('Error loading ebook:', error);
});
}
function showEbookModal(ebook) {
const modal = document.getElementById('ebook-modal');
const title = document.getElementById('modal-title');
const content = document.getElementById('modal-content');
title.textContent = ebook.title;
content.innerHTML = `
<div class="space-y-4">
<div class="flex space-x-4">
<img src="${ebook.cover_image_path || '/static/placeholder-book.svg'}" alt="Cover" class="w-32 h-40 object-cover rounded" onerror="this.src='/static/placeholder-book.svg'">
<div class="flex-1">
<h4 class="font-semibold mb-2" style="color: var(--text-primary)">${ebook.title}</h4>
${ebook.author ? `<p style="color: var(--text-secondary)">Author: ${ebook.author}</p>` : ''}
${ebook.publisher ? `<p style="color: var(--text-secondary)">Publisher: ${ebook.publisher}</p>` : ''}
${ebook.date_published ? `<p style="color: var(--text-secondary)">Published: ${new Date(ebook.date_published).toLocaleDateString()}</p>` : ''}
${ebook.isbn ? `<p style="color: var(--text-secondary)">ISBN: ${ebook.isbn}</p>` : ''}
${ebook.asin ? `<p style="color: var(--text-secondary)">ASIN: ${ebook.asin}</p>` : ''}
${ebook.series ? `<p style="color: var(--text-secondary)">Series: ${ebook.series}${ebook.series_number ? ' #' + ebook.series_number : ''}</p>` : ''}
</div>
</div>
${ebook.description ? `<div><h5 class="font-semibold mb-2" style="color: var(--text-primary)">Description</h5><p style="color: var(--text-secondary)">${ebook.description}</p></div>` : ''}
${ebook.tags ? `<div><h5 class="font-semibold mb-2" style="color: var(--text-primary)">Tags</h5><div class="flex flex-wrap gap-1">${ebook.tags.split(',').map(tag => `<span class="px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">${tag.trim()}</span>`).join('')}</div></div>` : ''}
${ebook.contributors ? `<div><h5 class="font-semibold mb-2" style="color: var(--text-primary)">Contributors</h5><p style="color: var(--text-secondary)">${ebook.contributors}</p></div>` : ''}
<div class="border-t pt-4" style="border-color: var(--border)">
<h5 class="font-semibold mb-2" style="color: var(--text-primary)">File Information</h5>
<p style="color: var(--text-secondary)">Size: ${formatFileSize(ebook.file_size)} | Type: ${ebook.mime_type}</p>
<p style="color: var(--text-secondary)">Path: ${ebook.file_path}</p>
</div>
</div>
`;
modal.style.display = 'flex';
}
function closeModal() {
document.getElementById('ebook-modal').style.display = 'none';
}
function deleteEbook(id) {
if (confirm('Are you sure you want to delete this ebook?')) {
fetch(`/api/ebooks/${id}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.then(response => {
if (response.ok) {
loadEbooks();
} else {
alert('Error deleting ebook');
}
})
.catch(error => {
console.error('Error deleting ebook:', error);
alert('Error deleting ebook');
});
}
}
function filterEbooks() {
const searchTerm = document.getElementById('search-input').value.toLowerCase();
const filteredEbooks = allEbooks.filter(ebook =>
ebook.title.toLowerCase().includes(searchTerm) ||
(ebook.author && ebook.author.toLowerCase().includes(searchTerm)) ||
(ebook.tags && ebook.tags.toLowerCase().includes(searchTerm))
);
renderEbooks(filteredEbooks);
}
function changePage(direction) {
currentPage += direction;
if (currentPage < 1) currentPage = 1;
loadEbooks();
}
function formatFileSize(bytes) {
if (!bytes) return 'Unknown';
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
function changeTheme() {
const theme = document.getElementById('theme-select').value;
applyTheme(theme);
fetch('/api/auth/theme', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({ theme })
}).catch(err => console.log('Theme save failed', err));
}
function loadUserTheme() {
const token = localStorage.getItem('token');
if (token) {
fetch('/api/auth/profile', {
headers: { 'Authorization': 'Bearer ' + token }
}).then(res => res.json()).then(data => {
if (data.theme) applyTheme(data.theme);
}).catch(() => {});
}
}
// Load ebooks on page load
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
loadUserTheme();
document.getElementById('theme-select').value = localStorage.getItem('theme') || 'tokyo-night';
loadEbooks();
});
</script>
{{end}}
+163
View File
@@ -0,0 +1,163 @@
package templates
templ Dashboard(user User) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Bookmann</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
}
.btn-primary:hover {
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night">
<!-- Navigation Header -->
<nav class="border-b" style="border-color: var(--border); background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold" style="color: var(--text-primary)">📚 Bookmann</h1>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Dashboard</a>
<a href="/admin" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Admin</a>
<span style="color: var(--text-secondary)">Welcome, { user.Username }!</span>
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
Logout
</button>
</div>
</div>
</div>
</nav>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header Section -->
<div class="mb-8">
<div class="flex justify-between items-center">
<div>
<h2 class="text-3xl font-bold" style="color: var(--text-primary)">Your Ebook Library</h2>
<p style="color: var(--text-secondary)">Manage your ebook collection and reading progress</p>
</div>
<button onclick="showCreateForm()" class="btn-primary px-6 py-3 rounded-lg font-medium">
+ Add New Ebook
</button>
</div>
</div>
<!-- Search and Filter -->
<div class="mb-6">
<div class="flex flex-col sm:flex-row gap-4">
<input type="text" id="search-input" placeholder="Search ebooks..." class="flex-1 px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onkeyup="filterEbooks()">
<select id="sort-select" class="px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onchange="loadEbooks()">
<option value="created_at DESC">Newest First</option>
<option value="title ASC">Title A-Z</option>
<option value="author ASC">Author A-Z</option>
<option value="created_at ASC">Oldest First</option>
</select>
</div>
</div>
<!-- Ebooks Grid -->
<div id="ebooks-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Ebooks will be loaded here -->
</div>
<!-- Loading indicator -->
<div id="loading" class="text-center py-8" style="color: var(--text-secondary)">
Loading your ebooks...
</div>
</div>
<script>
function loadEbooks() {
const loading = document.getElementById('loading');
const container = document.getElementById('ebooks-container');
loading.style.display = 'block';
container.innerHTML = '';
const sort = document.getElementById('sort-select').value;
fetch(`/api/ebooks?limit=12&offset=0&sort=${sort}`, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
loading.style.display = 'none';
renderEbooks(data);
})
.catch(error => {
loading.style.display = 'none';
console.error('Error loading ebooks:', error);
container.innerHTML = '<div class="col-span-full text-center py-8" style="color: var(--text-secondary)">Error loading ebooks. Please try again.</div>';
});
}
function renderEbooks(ebooks) {
const container = document.getElementById('ebooks-container');
container.innerHTML = '';
ebooks.forEach(ebook => {
const card = document.createElement('div');
card.className = 'card p-4 rounded-lg border hover:shadow-lg transition-shadow';
card.style.cssText = 'background-color: var(--bg-secondary); border-color: var(--border);';
const coverUrl = ebook.cover_image_path ? ebook.cover_image_path : '/static/placeholder-book.svg';
card.innerHTML = '<div class="flex flex-col h-full"><div class="flex-1"><div class="aspect-w-3 aspect-h-4 mb-3 overflow-hidden rounded"><img src="' + coverUrl + '" alt="Cover" class="w-full h-48 object-cover rounded" onerror="this.src=\'/static/placeholder-book.svg\'"></div><h3 class="font-semibold text-lg mb-1 line-clamp-2" style="color: var(--text-primary)">' + ebook.title + '</h3>' + (ebook.author ? '<p class="text-sm mb-2" style="color: var(--text-secondary)">by ' + ebook.author + '</p>' : '') + '<div class="flex justify-between items-center mt-4"><div class="flex space-x-2"><button onclick="viewEbook(\'' + ebook.id + '\')" class="px-3 py-1 text-sm border rounded hover:opacity-80" style="border-color: var(--border); color: var(--text-secondary)">View</button></div></div></div>';
container.appendChild(card);
});
}
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
// Load ebooks on page load
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
loadEbooks();
});
</script>
</body>
</html>
}
-313
View File
@@ -1,313 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bookmann - Home</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-dracula {
--bg-primary: #282a36;
--bg-secondary: #21222c;
--text-primary: #f8f8f2;
--text-secondary: #6272a4;
--accent: #bd93f9;
--border: #44475a;
}
.theme-nord {
--bg-primary: #2e3440;
--bg-secondary: #3b4252;
--text-primary: #eceff4;
--text-secondary: #5e81ac;
--accent: #88c0d0;
--border: #4c566a;
}
.theme-solarized-dark {
--bg-primary: #002b36;
--bg-secondary: #073642;
--text-primary: #93a1a1;
--text-secondary: #586e75;
--accent: #2aa198;
--border: #586e75;
}
.theme-monokai {
--bg-primary: #272822;
--bg-secondary: #3e3d32;
--text-primary: #f8f8f2;
--text-secondary: #75715e;
--accent: #a6e22e;
--border: #49483e;
}
.theme-one-dark-pro {
--bg-primary: #282c34;
--bg-secondary: #21252b;
--text-primary: #abb2bf;
--text-secondary: #5c6370;
--accent: #61dafb;
--border: #3e4451;
}
.theme-material-dark {
--bg-primary: #263238;
--bg-secondary: #37474f;
--text-primary: #eeffff;
--text-secondary: #546e7a;
--accent: #80cbc4;
--border: #455a64;
}
.theme-catppuccin-mocha {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--text-primary: #cdd6f4;
--text-secondary: #bac2de;
--accent: #f38ba8;
--border: #313244;
}
.theme-catppuccin-macchiato {
--bg-primary: #24273a;
--bg-secondary: #1e2030;
--text-primary: #cad3f5;
--text-secondary: #b8c0e0;
--accent: #f0c6c6;
--border: #363a4f;
}
.theme-catppuccin-frappe {
--bg-primary: #303446;
--bg-secondary: #292c3c;
--text-primary: #c6d0f5;
--text-secondary: #b5bfe2;
--accent: #f2d5cf;
--border: #414559;
}
.theme-catppuccin-latte {
--bg-primary: #eff1f5;
--bg-secondary: #e6e9ef;
--text-primary: #4c4f69;
--text-secondary: #5c5f77;
--accent: #d20f39;
--border: #bcc0cc;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
}
.btn-primary:hover {
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night">
<!-- Hero Section -->
<div class="relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-r from-blue-600 to-purple-700 opacity-10"></div>
<div class="relative max-w-7xl mx-auto px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
<div class="flex justify-between items-start mb-8">
<div></div>
<select id="theme-select" class="px-3 py-2 border rounded text-sm" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onchange="changeTheme()">
<option value="tokyo-night">Tokyo Night</option>
<option value="dracula">Dracula</option>
<option value="nord">Nord</option>
<option value="solarized-dark">Solarized Dark</option>
<option value="monokai">Monokai</option>
<option value="one-dark-pro">One Dark Pro</option>
<option value="material-dark">Material Dark</option>
<option value="catppuccin-mocha">Catppuccin Mocha</option>
<option value="catppuccin-macchiato">Catppuccin Macchiato</option>
<option value="catppuccin-frappe">Catppuccin Frappé</option>
<option value="catppuccin-latte">Catppuccin Latte</option>
</select>
</div>
<div class="text-center">
<h1 class="text-4xl sm:text-6xl lg:text-7xl font-extrabold" style="color: var(--text-primary)">
📚 Bookmann
</h1>
<p class="mt-6 max-w-2xl mx-auto text-xl" style="color: var(--text-secondary)">
Your personal ebook management system. Track reading progress, organize your library, and enjoy beautiful themes.
</p>
<div class="mt-10">
<a href="#auth" class="btn-primary px-8 py-3 rounded-lg font-medium text-lg">
Get Started
</a>
</div>
</div>
</div>
</div>
<!-- Features Section -->
<div class="py-16" style="background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-3xl font-extrabold" style="color: var(--text-primary)">Why Bookmann?</h2>
<p class="mt-4 text-lg" style="color: var(--text-secondary)">Discover the features that make ebook management effortless</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="card p-6 rounded-xl border">
<div class="text-center">
<div class="mx-auto w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center mb-4">
<span class="text-white text-2xl">📖</span>
</div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Reading Progress</h3>
<p style="color: var(--text-secondary)">Track your reading progress across all your ebooks with detailed statistics.</p>
</div>
</div>
<div class="card p-6 rounded-xl border">
<div class="text-center">
<div class="mx-auto w-12 h-12 bg-purple-500 rounded-lg flex items-center justify-center mb-4">
<span class="text-white text-2xl">🎨</span>
</div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Beautiful Themes</h3>
<p style="color: var(--text-secondary)">Choose from 11 stunning themes including Tokyo Night, Dracula, and Catppuccin variants.</p>
</div>
</div>
<div class="card p-6 rounded-xl border">
<div class="text-center">
<div class="mx-auto w-12 h-12 bg-green-500 rounded-lg flex items-center justify-center mb-4">
<span class="text-white text-2xl"></span>
</div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Fast & Modern</h3>
<p style="color: var(--text-secondary)">Built with Go and HTMX for lightning-fast performance and smooth interactions.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Auth Section -->
<div id="auth" class="py-16">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-3xl font-extrabold" style="color: var(--text-primary)">Join Bookmann Today</h2>
<p class="mt-4 text-lg" style="color: var(--text-secondary)">Create your account or sign in to start managing your ebook library</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="card p-8 rounded-xl border">
<h3 class="text-2xl font-semibold mb-6 text-center" style="color: var(--text-primary)">Login</h3>
<form hx-post="/api/auth/login" hx-target="#auth-result" hx-swap="innerHTML">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email or Username</label>
<input type="text" name="login" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label>
<input type="password" name="password" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<button type="submit" class="w-full btn-primary py-2 rounded-lg font-medium">
Sign In
</button>
</div>
</form>
</div>
<div class="card p-8 rounded-xl border">
<h3 class="text-2xl font-semibold mb-6 text-center" style="color: var(--text-primary)">Create Account</h3>
<form hx-post="/api/auth/register" hx-target="#auth-result" hx-swap="innerHTML">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email</label>
<input type="email" name="email" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Username</label>
<input type="text" name="username" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label>
<input type="password" name="password" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<button type="submit" class="w-full btn-primary py-2 rounded-lg font-medium">
Sign Up
</button>
</div>
</form>
</div>
</div>
<div id="auth-result" class="mt-8 text-center"></div>
</div>
</div>
<script>
function applyTheme(theme) {
document.body.className = `theme-${theme}`;
localStorage.setItem('theme', theme);
}
function loadTheme() {
const theme = localStorage.getItem('theme') || 'tokyo-night';
applyTheme(theme);
}
function changeTheme() {
const theme = document.getElementById('theme-select').value;
applyTheme(theme);
// Save to server if logged in
fetch('/api/auth/theme', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({ theme })
}).catch(err => console.log('Theme save failed', err));
}
function loadUserTheme() {
// If logged in, load from profile
const token = localStorage.getItem('token');
if (token) {
fetch('/api/auth/profile', {
headers: { 'Authorization': 'Bearer ' + token }
}).then(res => res.json()).then(data => {
if (data.theme) applyTheme(data.theme);
}).catch(() => {});
}
}
document.addEventListener('DOMContentLoaded', () => {
loadTheme();
loadUserTheme();
document.getElementById('theme-select').value = localStorage.getItem('theme') || 'tokyo-night';
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
});
</script>
</body>
</html>
+317
View File
@@ -0,0 +1,317 @@
package templates
templ Index() {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bookmann - Home</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-dracula {
--bg-primary: #282a36;
--bg-secondary: #21222c;
--text-primary: #f8f8f2;
--text-secondary: #6272a4;
--accent: #bd93f9;
--border: #44475a;
}
.theme-nord {
--bg-primary: #2e3440;
--bg-secondary: #3b4252;
--text-primary: #eceff4;
--text-secondary: #5e81ac;
--accent: #88c0d0;
--border: #4c566a;
}
.theme-solarized-dark {
--bg-primary: #002b36;
--bg-secondary: #073642;
--text-primary: #93a1a1;
--text-secondary: #586e75;
--accent: #2aa198;
--border: #586e75;
}
.theme-monokai {
--bg-primary: #272822;
--bg-secondary: #3e3d32;
--text-primary: #f8f8f2;
--text-secondary: #75715e;
--accent: #a6e22e;
--border: #49483e;
}
.theme-one-dark-pro {
--bg-primary: #282c34;
--bg-secondary: #21252b;
--text-primary: #abb2bf;
--text-secondary: #5c6370;
--accent: #61dafb;
--border: #3e4451;
}
.theme-material-dark {
--bg-primary: #263238;
--bg-secondary: #37474f;
--text-primary: #eeffff;
--text-secondary: #546e7a;
--accent: #80cbc4;
--border: #455a64;
}
.theme-catppuccin-mocha {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--text-primary: #cdd6f4;
--text-secondary: #bac2de;
--accent: #f38ba8;
--border: #313244;
}
.theme-catppuccin-macchiato {
--bg-primary: #24273a;
--bg-secondary: #1e2030;
--text-primary: #cad3f5;
--text-secondary: #b8c0e0;
--accent: #f0c6c6;
--border: #363a4f;
}
.theme-catppuccin-frappe {
--bg-primary: #303446;
--bg-secondary: #292c3c;
--text-primary: #c6d0f5;
--text-secondary: #b5bfe2;
--accent: #f2d5cf;
--border: #414559;
}
.theme-catppuccin-latte {
--bg-primary: #eff1f5;
--bg-secondary: #e6e9ef;
--text-primary: #4c4f69;
--text-secondary: #5c5f77;
--accent: #d20f39;
--border: #bcc0cc;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
}
.btn-primary:hover {
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night">
<!-- Hero Section -->
<div class="relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-r from-blue-600 to-purple-700 opacity-10"></div>
<div class="relative max-w-7xl mx-auto px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
<div class="flex justify-between items-start mb-8">
<div></div>
<select id="theme-select" class="px-3 py-2 border rounded text-sm" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onchange="changeTheme()">
<option value="tokyo-night">Tokyo Night</option>
<option value="dracula">Dracula</option>
<option value="nord">Nord</option>
<option value="solarized-dark">Solarized Dark</option>
<option value="monokai">Monokai</option>
<option value="one-dark-pro">One Dark Pro</option>
<option value="material-dark">Material Dark</option>
<option value="catppuccin-mocha">Catppuccin Mocha</option>
<option value="catppuccin-macchiato">Catppuccin Macchiato</option>
<option value="catppuccin-frappe">Catppuccin Frappé</option>
<option value="catppuccin-latte">Catppuccin Latte</option>
</select>
</div>
<div class="text-center">
<h1 class="text-4xl sm:text-6xl lg:text-7xl font-extrabold" style="color: var(--text-primary)">
📚 Bookmann
</h1>
<p class="mt-6 max-w-2xl mx-auto text-xl" style="color: var(--text-secondary)">
Your personal ebook management system. Track reading progress, organize your library, and enjoy beautiful themes.
</p>
<div class="mt-10">
<a href="#auth" class="btn-primary px-8 py-3 rounded-lg font-medium text-lg">
Get Started
</a>
</div>
</div>
</div>
</div>
<!-- Features Section -->
<div class="py-16" style="background-color: var(--bg-secondary)">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-3xl font-extrabold" style="color: var(--text-primary)">Why Bookmann?</h2>
<p class="mt-4 text-lg" style="color: var(--text-secondary)">Discover the features that make ebook management effortless</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="card p-6 rounded-xl border">
<div class="text-center">
<div class="mx-auto w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center mb-4">
<span class="text-white text-2xl">📖</span>
</div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Reading Progress</h3>
<p style="color: var(--text-secondary)">Track your reading progress across all your ebooks with detailed statistics.</p>
</div>
</div>
<div class="card p-6 rounded-xl border">
<div class="text-center">
<div class="mx-auto w-12 h-12 bg-purple-500 rounded-lg flex items-center justify-center mb-4">
<span class="text-white text-2xl">🎨</span>
</div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Beautiful Themes</h3>
<p style="color: var(--text-secondary)">Choose from 11 stunning themes including Tokyo Night, Dracula, and Catppuccin variants.</p>
</div>
</div>
<div class="card p-6 rounded-xl border">
<div class="text-center">
<div class="mx-auto w-12 h-12 bg-green-500 rounded-lg flex items-center justify-center mb-4">
<span class="text-white text-2xl"></span>
</div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Fast & Modern</h3>
<p style="color: var(--text-secondary)">Built with Go and HTMX for lightning-fast performance and smooth interactions.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Auth Section -->
<div id="auth" class="py-16">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-3xl font-extrabold" style="color: var(--text-primary)">Join Bookmann Today</h2>
<p class="mt-4 text-lg" style="color: var(--text-secondary)">Create your account or sign in to start managing your ebook library</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="card p-8 rounded-xl border">
<h3 class="text-2xl font-semibold mb-6 text-center" style="color: var(--text-primary)">Login</h3>
<form hx-post="/api/auth/login" hx-target="#auth-result" hx-swap="innerHTML">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email or Username</label>
<input type="text" name="login" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label>
<input type="password" name="password" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<button type="submit" class="w-full btn-primary py-2 rounded-lg font-medium">
Sign In
</button>
</div>
</form>
</div>
<div class="card p-8 rounded-xl border">
<h3 class="text-2xl font-semibold mb-6 text-center" style="color: var(--text-primary)">Create Account</h3>
<form hx-post="/api/auth/register" hx-target="#auth-result" hx-swap="innerHTML">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email</label>
<input type="email" name="email" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Username</label>
<input type="text" name="username" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label>
<input type="password" name="password" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<button type="submit" class="w-full btn-primary py-2 rounded-lg font-medium">
Sign Up
</button>
</div>
</form>
</div>
</div>
<div id="auth-result" class="mt-8 text-center"></div>
</div>
</div>
<script>
function applyTheme(theme) {
document.body.className = `theme-${theme}`;
localStorage.setItem('theme', theme);
}
function loadTheme() {
const theme = localStorage.getItem('theme') || 'tokyo-night';
applyTheme(theme);
}
function changeTheme() {
const theme = document.getElementById('theme-select').value;
applyTheme(theme);
// Save to server if logged in
fetch('/api/auth/theme', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({ theme })
}).catch(err => console.log('Theme save failed', err));
}
function loadUserTheme() {
// If logged in, load from profile
const token = localStorage.getItem('token');
if (token) {
fetch('/api/auth/profile', {
headers: { 'Authorization': 'Bearer ' + token }
}).then(res => res.json()).then(data => {
if (data.theme) applyTheme(data.theme);
}).catch(() => {});
}
}
document.addEventListener('DOMContentLoaded', () => {
loadTheme();
loadUserTheme();
document.getElementById('theme-select').value = localStorage.getItem('theme') || 'tokyo-night';
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
});
</script>
</body>
</html>
}
-65
View File
@@ -1,65 +0,0 @@
{{template "base.html" .}}
{{define "title"}}Login{{end}}
{{define "content"}}
<div class="container mx-auto px-4 py-8 max-w-md">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold">Login</h1>
<select id="theme-select" class="px-3 py-2 border rounded" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onchange="changeTheme()">
<option value="tokyo-night">Tokyo Night</option>
<option value="dracula">Dracula</option>
<option value="nord">Nord</option>
<option value="solarized-dark">Solarized Dark</option>
<option value="monokai">Monokai</option>
<option value="one-dark-pro">One Dark Pro</option>
<option value="material-dark">Material Dark</option>
<option value="catppuccin-mocha">Catppuccin Mocha</option>
<option value="catppuccin-macchiato">Catppuccin Macchiato</option>
<option value="catppuccin-frappe">Catppuccin Frappé</option>
<option value="catppuccin-latte">Catppuccin Latte</option>
</select>
</div>
<form hx-post="/api/auth/login" hx-target="#result" hx-swap="innerHTML" class="card p-6 rounded-lg shadow-md border">
<div class="mb-4">
<label class="block mb-2" style="color: var(--text-secondary)">Email or Username</label>
<input type="text" name="login" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div class="mb-4">
<label class="block mb-2" style="color: var(--text-secondary)">Password</label>
<input type="password" name="password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<button type="submit" class="btn-primary w-full py-2 rounded">
Login
</button>
</form>
<div id="result" class="mt-4 text-center"></div>
<p class="text-center mt-4">
<a href="/register" class="text-blue-500 hover:underline">Don't have an account? Register</a>
</p>
</div>
<script>
function changeTheme() {
const theme = document.getElementById('theme-select').value;
applyTheme(theme);
const token = localStorage.getItem('token');
if (token) {
fetch('/api/auth/theme', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ theme })
}).catch(err => console.log('Theme save failed', err));
}
}
document.addEventListener('DOMContentLoaded', () => {
loadTheme();
document.getElementById('theme-select').value = localStorage.getItem('theme') || 'tokyo-night';
});
</script>
{{end}}
+196
View File
@@ -0,0 +1,196 @@
package templates
templ Login() {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-dracula {
--bg-primary: #282a36;
--bg-secondary: #21222c;
--text-primary: #f8f8f2;
--text-secondary: #6272a4;
--accent: #bd93f9;
--border: #44475a;
}
.theme-nord {
--bg-primary: #2e3440;
--bg-secondary: #3b4252;
--text-primary: #eceff4;
--text-secondary: #5e81ac;
--accent: #88c0d0;
--border: #4c566a;
}
.theme-solarized-dark {
--bg-primary: #002b36;
--bg-secondary: #073642;
--text-primary: #93a1a1;
--text-secondary: #586e75;
--accent: #2aa198;
--border: #586e75;
}
.theme-monokai {
--bg-primary: #272822;
--bg-secondary: #3e3d32;
--text-primary: #f8f8f2;
--text-secondary: #75715e;
--accent: #a6e22e;
--border: #49483e;
}
.theme-one-dark-pro {
--bg-primary: #282c34;
--bg-secondary: #21252b;
--text-primary: #abb2bf;
--text-secondary: #5c6370;
--accent: #61dafb;
--border: #3e4451;
}
.theme-material-dark {
--bg-primary: #263238;
--bg-secondary: #37474f;
--text-primary: #eeffff;
--text-secondary: #546e7a;
--accent: #80cbc4;
--border: #455a64;
}
.theme-catppuccin-mocha {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--text-primary: #cdd6f4;
--text-secondary: #bac2de;
--accent: #f38ba8;
--border: #313244;
}
.theme-catppuccin-macchiato {
--bg-primary: #24273a;
--bg-secondary: #1e2030;
--text-primary: #cad3f5;
--text-secondary: #b8c0e0;
--accent: #f0c6c6;
--border: #363a4f;
}
.theme-catppuccin-frappe {
--bg-primary: #303446;
--bg-secondary: #292c3c;
--text-primary: #c6d0f5;
--text-secondary: #b5bfe2;
--accent: #f2d5cf;
--border: #414559;
}
.theme-catppuccin-latte {
--bg-primary: #eff1f5;
--bg-secondary: #e6e9ef;
--text-primary: #4c4f69;
--text-secondary: #5c5f77;
--accent: #d20f39;
--border: #bcc0cc;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
}
.btn-primary:hover {
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-md">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold">Login to Bookmann</h1>
<select id="theme-select" class="px-3 py-2 border rounded" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onchange="changeTheme()">
<option value="tokyo-night">Tokyo Night</option>
<option value="dracula">Dracula</option>
<option value="nord">Nord</option>
<option value="solarized-dark">Solarized Dark</option>
<option value="monokai">Monokai</option>
<option value="one-dark-pro">One Dark Pro</option>
<option value="material-dark">Material Dark</option>
<option value="catppuccin-mocha">Catppuccin Mocha</option>
<option value="catppuccin-macchiato">Catppuccin Macchiato</option>
<option value="catppuccin-frappe">Catppuccin Frappé</option>
<option value="catppuccin-latte">Catppuccin Latte</option>
</select>
</div>
<form hx-post="/api/auth/login" hx-target="#result" hx-swap="innerHTML" class="card p-6 rounded-lg shadow-md border">
<div class="mb-4">
<label class="block mb-2" style="color: var(--text-secondary)">Email or Username</label>
<input type="text" name="login" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<div class="mb-4">
<label class="block mb-2" style="color: var(--text-secondary)">Password</label>
<input type="password" name="password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
</div>
<button type="submit" class="btn-primary w-full py-2 rounded">
Sign In
</button>
</form>
<div id="result" class="mt-4 text-center"></div>
<p class="text-center mt-4">
<a href="/register" class="text-blue-500 hover:underline">Don't have an account? Sign Up</a>
</p>
</div>
<script>
function applyTheme(theme) {
document.body.className = `theme-${theme} min-h-screen`;
localStorage.setItem('theme', theme);
}
function loadTheme() {
const theme = localStorage.getItem('theme') || 'tokyo-night';
applyTheme(theme);
}
function changeTheme() {
const theme = document.getElementById('theme-select').value;
applyTheme(theme);
const token = localStorage.getItem('token');
if (token) {
fetch('/api/auth/theme', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ theme })
}).catch(err => console.log('Theme save failed', err));
}
}
document.addEventListener('DOMContentLoaded', () => {
loadTheme();
document.getElementById('theme-select').value = localStorage.getItem('theme') || 'tokyo-night';
});
</script>
</body>
</html>
}
-33
View File
@@ -1,33 +0,0 @@
{{template "base.html" .}}
{{define "title"}}Register{{end}}
{{define "content"}}
<div class="container mx-auto px-4 py-8 max-w-md">
<h1 class="text-3xl font-bold text-center mb-8">Register</h1>
<form hx-post="/api/auth/register" hx-target="#result" hx-swap="innerHTML" class="bg-white p-6 rounded-lg shadow-md">
<div class="mb-4">
<label class="block text-gray-700 mb-2">Email</label>
<input type="email" name="email" class="w-full px-3 py-2 border rounded" required>
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2">Username</label>
<input type="text" name="username" class="w-full px-3 py-2 border rounded" required>
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2">Password</label>
<input type="password" name="password" class="w-full px-3 py-2 border rounded" required>
</div>
<button type="submit" class="w-full bg-green-500 text-white py-2 rounded hover:bg-green-600">
Register
</button>
</form>
<div id="result" class="mt-4 text-center"></div>
<p class="text-center mt-4">
<a href="/login" class="text-blue-500 hover:underline">Already have an account? Login</a>
</p>
</div>
{{end}}
+156
View File
@@ -0,0 +1,156 @@
package templates
templ Register() {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #a9b1d6;
--text-secondary: #565f89;
--accent: #7aa2f7;
--border: #414868;
}
.theme-dracula {
--bg-primary: #282a36;
--bg-secondary: #21222c;
--text-primary: #f8f8f2;
--text-secondary: #6272a4;
--accent: #bd93f9;
--border: #44475a;
}
.theme-nord {
--bg-primary: #2e3440;
--bg-secondary: #3b4252;
--text-primary: #eceff4;
--text-secondary: #5e81ac;
--accent: #88c0d0;
--border: #4c566a;
}
.theme-solarized-dark {
--bg-primary: #002b36;
--bg-secondary: #073642;
--text-primary: #93a1a1;
--text-secondary: #586e75;
--accent: #2aa198;
--border: #586e75;
}
.theme-monokai {
--bg-primary: #272822;
--bg-secondary: #3e3d32;
--text-primary: #f8f8f2;
--text-secondary: #75715e;
--accent: #a6e22e;
--border: #49483e;
}
.theme-one-dark-pro {
--bg-primary: #282c34;
--bg-secondary: #21252b;
--text-primary: #abb2bf;
--text-secondary: #5c6370;
--accent: #61dafb;
--border: #3e4451;
}
.theme-material-dark {
--bg-primary: #263238;
--bg-secondary: #37474f;
--text-primary: #eeffff;
--text-secondary: #546e7a;
--accent: #80cbc4;
--border: #455a64;
}
.theme-catppuccin-mocha {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--text-primary: #cdd6f4;
--text-secondary: #bac2de;
--accent: #f38ba8;
--border: #313244;
}
.theme-catppuccin-macchiato {
--bg-primary: #24273a;
--bg-secondary: #1e2030;
--text-primary: #cad3f5;
--text-secondary: #b8c0e0;
--accent: #f0c6c6;
--border: #363a4f;
}
.theme-catppuccin-frappe {
--bg-primary: #303446;
--bg-secondary: #292c3c;
--text-primary: #c6d0f5;
--text-secondary: #b5bfe2;
--accent: #f2d5cf;
--border: #414559;
}
.theme-catppuccin-latte {
--bg-primary: #eff1f5;
--bg-secondary: #e6e9ef;
--text-primary: #4c4f69;
--text-secondary: #5c5f77;
--accent: #d20f39;
--border: #bcc0cc;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
}
.btn-primary:hover {
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-md">
<h1 class="text-3xl font-bold text-center mb-8">Register for Bookmann</h1>
<form hx-post="/api/auth/register" hx-target="#result" hx-swap="innerHTML" class="bg-white p-6 rounded-lg shadow-md">
<div class="mb-4">
<label class="block text-gray-700 mb-2">Email</label>
<input type="email" name="email" class="w-full px-3 py-2 border rounded" required>
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2">Username</label>
<input type="text" name="username" class="w-full px-3 py-2 border rounded" required>
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2">Password</label>
<input type="password" name="password" class="w-full px-3 py-2 border rounded" required>
</div>
<button type="submit" class="w-full bg-green-500 text-white py-2 rounded hover:bg-green-600">
Register
</button>
</form>
<div id="result" class="mt-4 text-center"></div>
<p class="text-center mt-4">
<a href="/login" class="text-blue-500 hover:underline">Already have an account? Login</a>
</p>
</div>
</body>
</html>
}
+12
View File
@@ -0,0 +1,12 @@
package templates
type User struct {
ID string
Username string
Email string
}
type PageData struct {
User User
CurrentPage string
}