refactor: Consolidate script imports to use main.js bundle
- dashboard.templ: Replaced individual script tags with single main.js import - admin_users.templ, custom_section.templ: Minor formatting/cleanup - docs.templ: Updated script imports (excluded from Alpine conversion per user request) - api.ts, docs.ts, password_validation.ts: Minor updates for compatibility
This commit is contained in:
+128
-138
@@ -3,143 +3,133 @@ package templates
|
|||||||
templ AdminUsers(currentUser User, users []User, adminCount int) {
|
templ AdminUsers(currentUser User, users []User, adminCount int) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8"/>
|
||||||
<title>Users - Bookhoard Admin</title>
|
<title>Users - Bookhoard Admin</title>
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/toast.js"></script>
|
<script src="/static/main.js"></script>
|
||||||
<script src="/static/header.js"></script>
|
<link href="/static/style.css" rel="stylesheet"/>
|
||||||
<script src="/static/theme.js"></script>
|
</head>
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
<body class="theme-{ currentUser.Theme }">
|
||||||
</head>
|
@Header(currentUser, "/admin/users")
|
||||||
<body class="theme-{ currentUser.Theme }">
|
<!-- Modal Container (populated by HTMX) -->
|
||||||
@Header(currentUser, "/admin/users")
|
<div id="modal-container"></div>
|
||||||
|
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
|
||||||
<!-- Modal Container (populated by HTMX) -->
|
@AdminSidebar(currentUser, "/admin/users")
|
||||||
<div id="modal-container"></div>
|
<main class="flex-1 p-8">
|
||||||
|
<div class="w-full">
|
||||||
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
|
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">User Management</h1>
|
||||||
@AdminSidebar(currentUser, "/admin/users")
|
<p style="color: var(--text-secondary)">Manage user accounts and permissions</p>
|
||||||
|
</div>
|
||||||
<main class="flex-1 p-8">
|
<!-- Users Table -->
|
||||||
<div class="w-full">
|
<div class="card rounded-lg border overflow-hidden" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">User Management</h1>
|
<table class="w-full">
|
||||||
<p style="color: var(--text-secondary)">Manage user accounts and permissions</p>
|
<thead style="background-color: var(--bg-primary)">
|
||||||
</div>
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Username</th>
|
||||||
<!-- Users Table -->
|
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Email</th>
|
||||||
<div class="card rounded-lg border overflow-hidden" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Role</th>
|
||||||
<table class="w-full">
|
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Created</th>
|
||||||
<thead style="background-color: var(--bg-primary)">
|
<th class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Actions</th>
|
||||||
<tr>
|
</tr>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Username</th>
|
</thead>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Email</th>
|
<tbody class="divide-y" style="divide-color: var(--border)">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Role</th>
|
for _, user := range users {
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Created</th>
|
<tr id={ "user-" + user.ID } class="hover:bg-opacity-50" style="transition: background-color 0.2s;">
|
||||||
<th class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Actions</th>
|
<!-- Username -->
|
||||||
</tr>
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
</thead>
|
<div class="flex items-center">
|
||||||
<tbody class="divide-y" style="divide-color: var(--border)">
|
<div>
|
||||||
for _, user := range users {
|
<div class="text-sm font-medium" style="color: var(--text-primary)">{ user.Username }</div>
|
||||||
<tr id={ "user-" + user.ID } class="hover:bg-opacity-50" style="transition: background-color 0.2s;">
|
if user.ID == currentUser.ID {
|
||||||
<!-- Username -->
|
<span class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: white;">You</span>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
}
|
||||||
<div class="flex items-center">
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<div class="text-sm font-medium" style="color: var(--text-primary)">{user.Username}</div>
|
</td>
|
||||||
if user.ID == currentUser.ID {
|
<!-- Email -->
|
||||||
<span class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: white;">You</span>
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
}
|
<div class="text-sm" style="color: var(--text-primary)">{ user.Email }</div>
|
||||||
</div>
|
</td>
|
||||||
</div>
|
<!-- Role Toggle (with last-admin protection) -->
|
||||||
</td>
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
if user.Role == "admin" && adminCount == 1 {
|
||||||
<!-- Email -->
|
<!-- Last admin - disabled -->
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<div class="relative">
|
||||||
<div class="text-sm" style="color: var(--text-primary)">{user.Email}</div>
|
<select
|
||||||
</td>
|
disabled
|
||||||
|
class="text-sm rounded px-2 py-1 cursor-not-allowed opacity-50"
|
||||||
<!-- Role Toggle (with last-admin protection) -->
|
style="background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);"
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
title="Cannot demote the last admin"
|
||||||
if user.Role == "admin" && adminCount == 1 {
|
>
|
||||||
<!-- Last admin - disabled -->
|
<option value="user">User</option>
|
||||||
<div class="relative">
|
<option value="admin" selected>Admin</option>
|
||||||
<select
|
</select>
|
||||||
disabled
|
</div>
|
||||||
class="text-sm rounded px-2 py-1 cursor-not-allowed opacity-50"
|
} else {
|
||||||
style="background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);"
|
<select
|
||||||
title="Cannot demote the last admin"
|
hx-put={ "/api/auth/profile/" + user.ID }
|
||||||
>
|
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
||||||
<option value="user">User</option>
|
hx-target={ "#role-result-" + user.ID }
|
||||||
<option value="admin" selected>Admin</option>
|
hx-swap="innerHTML"
|
||||||
</select>
|
name="role"
|
||||||
</div>
|
class="text-sm rounded px-2 py-1"
|
||||||
} else {
|
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
|
||||||
<select
|
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
|
||||||
hx-put={ "/api/auth/profile/" + user.ID }
|
hx-trigger="change"
|
||||||
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
hx-vals='{"role": this.value}'
|
||||||
hx-target={ "#role-result-" + user.ID }
|
>
|
||||||
hx-swap="innerHTML"
|
<option value="user" selected?={ user.Role == "user" }>User</option>
|
||||||
name="role"
|
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
|
||||||
class="text-sm rounded px-2 py-1"
|
</select>
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
|
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
|
||||||
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
|
}
|
||||||
hx-trigger="change"
|
</td>
|
||||||
hx-vals='{"role": this.value}'
|
<!-- Created -->
|
||||||
>
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<option value="user" selected?={user.Role == "user"}>User</option>
|
<div class="text-sm" style="color: var(--text-secondary)">{ user.CreatedAt.Format("2006-01-02") }</div>
|
||||||
<option value="admin" selected?={user.Role == "admin"}>Admin</option>
|
</td>
|
||||||
</select>
|
<!-- Actions -->
|
||||||
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||||
}
|
<button
|
||||||
</td>
|
hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
|
||||||
|
hx-target="#modal-container"
|
||||||
<!-- Created -->
|
hx-swap="innerHTML"
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
class="text-sm px-3 py-1 rounded mr-2"
|
||||||
<div class="text-sm" style="color: var(--text-secondary)">{ user.CreatedAt.Format("2006-01-02") }</div>
|
style="background-color: var(--accent); color: white;"
|
||||||
</td>
|
>
|
||||||
|
Edit
|
||||||
<!-- Actions -->
|
</button>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
if user.Role == "admin" && adminCount == 1 {
|
||||||
<button
|
<button
|
||||||
hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
|
disabled
|
||||||
hx-target="#modal-container"
|
class="text-sm px-3 py-1 rounded cursor-not-allowed opacity-50"
|
||||||
hx-swap="innerHTML"
|
style="background-color: #dc2626; color: white;"
|
||||||
class="text-sm px-3 py-1 rounded mr-2"
|
title="Cannot delete the last admin"
|
||||||
style="background-color: var(--accent); color: white;"
|
>
|
||||||
>
|
Delete
|
||||||
Edit
|
</button>
|
||||||
</button>
|
} else {
|
||||||
if user.Role == "admin" && adminCount == 1 {
|
<button
|
||||||
<button
|
hx-delete={ "/api/auth/profile/" + user.ID }
|
||||||
disabled
|
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
||||||
class="text-sm px-3 py-1 rounded cursor-not-allowed opacity-50"
|
hx-target={ "#user-" + user.ID }
|
||||||
style="background-color: #dc2626; color: white;"
|
hx-swap="outerHTML swap:0.5s"
|
||||||
title="Cannot delete the last admin"
|
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
|
||||||
>
|
class="text-sm px-3 py-1 rounded"
|
||||||
Delete
|
style="background-color: #dc2626; color: white;"
|
||||||
</button>
|
>
|
||||||
} else {
|
Delete
|
||||||
<button
|
</button>
|
||||||
hx-delete={ "/api/auth/profile/" + user.ID }
|
}
|
||||||
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
</td>
|
||||||
hx-target={ "#user-" + user.ID }
|
</tr>
|
||||||
hx-swap="outerHTML swap:0.5s"
|
}
|
||||||
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
|
</tbody>
|
||||||
class="text-sm px-3 py-1 rounded"
|
</table>
|
||||||
style="background-color: #dc2626; color: white;"
|
</div>
|
||||||
>
|
</main>
|
||||||
Delete
|
</div>
|
||||||
</button>
|
</body>
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|||||||
+177
-150
@@ -3,166 +3,193 @@ package templates
|
|||||||
templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage string) {
|
templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage string) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Create Custom Section - Bookhoard</title>
|
<title>Create Custom Section - Bookhoard</title>
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/toast.js"></script>
|
<script src="/static/main.js"></script>
|
||||||
<script src="/static/api.js"></script>
|
<link href="/static/style.css" rel="stylesheet"/>
|
||||||
<script src="/static/custom-section-builder.js"></script>
|
</head>
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
<body class="theme-{ user.Theme }">
|
||||||
</head>
|
@Header(user, "/custom-section")
|
||||||
<body class="theme-{ user.Theme }">
|
<main class="max-w-4xl mx-auto px-4 py-8">
|
||||||
@Header(user, "/custom-section")
|
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Create Custom Section</h1>
|
||||||
|
<p class="mb-6" style="color: var(--text-secondary)">Build a custom dashboard section by defining filter rules or manually selecting books.</p>
|
||||||
<main class="max-w-4xl mx-auto px-4 py-8">
|
<form id="custom-section-form" class="space-y-6">
|
||||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Create Custom Section</h1>
|
<!-- Section Details -->
|
||||||
<p class="mb-6" style="color: var(--text-secondary)">Build a custom dashboard section by defining filter rules or manually selecting books.</p>
|
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
||||||
|
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Section Details</h2>
|
||||||
<form id="custom-section-form" class="space-y-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<!-- Section Details -->
|
<div>
|
||||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name *</label>
|
||||||
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Section Details</h2>
|
<input
|
||||||
|
type="text"
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
id="section-name"
|
||||||
<div>
|
name="name"
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name *</label>
|
required
|
||||||
<input type="text" id="section-name" name="name" required
|
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||||
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Icon (emoji)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="section-icon"
|
||||||
|
name="icon"
|
||||||
|
maxlength="4"
|
||||||
|
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
placeholder="📚"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
<div>
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Icon (emoji)</label>
|
<textarea
|
||||||
<input type="text" id="section-icon" name="icon" maxlength="4"
|
id="section-description"
|
||||||
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
name="description"
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
rows="2"
|
||||||
placeholder="📚">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
|
||||||
<textarea id="section-description" name="description" rows="2"
|
|
||||||
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Library *</label>
|
|
||||||
<select id="section-library" name="library_id" required
|
|
||||||
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
<option value="">Select a library...</option>
|
></textarea>
|
||||||
for _, lib := range libraries {
|
</div>
|
||||||
<option value={ lib.ID }>{ lib.Name }</option>
|
<div class="mt-4">
|
||||||
}
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Library *</label>
|
||||||
</select>
|
<select
|
||||||
|
id="section-library"
|
||||||
|
name="library_id"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
>
|
||||||
|
<option value="">Select a library...</option>
|
||||||
|
for _, lib := range libraries {
|
||||||
|
<option value={ lib.ID }>{ lib.Name }</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Filter Rules -->
|
||||||
|
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
||||||
<!-- Filter Rules -->
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Filter Rules</h2>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<button
|
||||||
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Filter Rules</h2>
|
type="button"
|
||||||
<button type="button" id="add-rule-btn"
|
id="add-rule-btn"
|
||||||
class="px-3 py-1 rounded-lg text-sm font-medium"
|
class="px-3 py-1 rounded-lg text-sm font-medium"
|
||||||
style="background-color: var(--accent);">
|
style="background-color: var(--accent);"
|
||||||
+ Add Rule
|
>
|
||||||
</button>
|
+ Add Rule
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">
|
|
||||||
Books matching these rules will be automatically added to your section. Use AND for all rules, OR for any rule.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div id="rules-container" class="space-y-3">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-2">
|
|
||||||
<label class="text-sm font-medium" style="color: var(--text-secondary)">Match:</label>
|
|
||||||
<select id="match-type" name="match_type"
|
|
||||||
class="px-3 py-1 rounded border"
|
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
|
||||||
<option value="all">ALL rules (AND)</option>
|
|
||||||
<option value="any">ANY rule (OR)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Manual Book Selection -->
|
|
||||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
|
||||||
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Manual Book Selection</h2>
|
|
||||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">
|
|
||||||
Add specific books to this section. Use the search to find and select multiple books.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Search Books</label>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<input type="text" id="book-search" name="book_search"
|
|
||||||
class="flex-1 px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
|
||||||
placeholder="Search by title or author..."
|
|
||||||
autocomplete="off">
|
|
||||||
<button type="button" id="search-books-btn"
|
|
||||||
class="px-4 py-2 rounded-lg font-medium"
|
|
||||||
style="background-color: var(--accent);">
|
|
||||||
Search
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="text-sm mb-4" style="color: var(--text-secondary)">
|
||||||
|
Books matching these rules will be automatically added to your section. Use AND for all rules, OR for any rule.
|
||||||
<div id="search-results" class="hidden mb-4 p-3 rounded-lg max-h-60 overflow-y-auto"
|
</p>
|
||||||
style="background-color: var(--bg-primary);">
|
<div id="rules-container" class="space-y-3"></div>
|
||||||
</div>
|
<div class="mt-4 flex items-center gap-2">
|
||||||
|
<label class="text-sm font-medium" style="color: var(--text-secondary)">Match:</label>
|
||||||
<div class="mb-4">
|
<select
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Selected Books</label>
|
id="match-type"
|
||||||
<div id="selected-books" class="min-h-[60px] p-3 rounded-lg border-2 border-dashed"
|
name="match_type"
|
||||||
style="border-color: var(--border); background-color: var(--bg-primary);">
|
class="px-3 py-1 rounded border"
|
||||||
<p class="text-sm text-center" style="color: var(--text-secondary);">No books selected</p>
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
>
|
||||||
|
<option value="all">ALL rules (AND)</option>
|
||||||
|
<option value="any">ANY rule (OR)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Manual Book Selection -->
|
||||||
|
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
||||||
<!-- Live Preview -->
|
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Manual Book Selection</h2>
|
||||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
<p class="text-sm mb-4" style="color: var(--text-secondary)">
|
||||||
<div class="flex items-center justify-between mb-4">
|
Add specific books to this section. Use the search to find and select multiple books.
|
||||||
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Live Preview</h2>
|
</p>
|
||||||
<button type="button" id="preview-btn"
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Search Books</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="book-search"
|
||||||
|
name="book_search"
|
||||||
|
class="flex-1 px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
placeholder="Search by title or author..."
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="search-books-btn"
|
||||||
|
class="px-4 py-2 rounded-lg font-medium"
|
||||||
|
style="background-color: var(--accent);"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="search-results"
|
||||||
|
class="hidden mb-4 p-3 rounded-lg max-h-60 overflow-y-auto"
|
||||||
|
style="background-color: var(--bg-primary);"
|
||||||
|
></div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Selected Books</label>
|
||||||
|
<div
|
||||||
|
id="selected-books"
|
||||||
|
class="min-h-[60px] p-3 rounded-lg border-2 border-dashed"
|
||||||
|
style="border-color: var(--border); background-color: var(--bg-primary);"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-center" style="color: var(--text-secondary);">No books selected</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Live Preview -->
|
||||||
|
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Live Preview</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="preview-btn"
|
||||||
class="px-4 py-2 rounded-lg font-medium"
|
class="px-4 py-2 rounded-lg font-medium"
|
||||||
style="background-color: var(--accent);">
|
style="background-color: var(--accent);"
|
||||||
Refresh Preview
|
>
|
||||||
|
Refresh Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="preview-container"
|
||||||
|
class="p-4 rounded-lg"
|
||||||
|
style="background-color: var(--bg-primary); min-height: 200px;"
|
||||||
|
>
|
||||||
|
<p class="text-center" style="color: var(--text-secondary);">
|
||||||
|
Add filter rules or select books to see a preview of your custom section.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="cancel-btn"
|
||||||
|
class="px-6 py-2 rounded-lg font-medium border hover:opacity-80"
|
||||||
|
style="border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
id="save-section-btn"
|
||||||
|
class="px-6 py-2 rounded-lg font-medium text-white hover:opacity-90"
|
||||||
|
style="background-color: var(--accent);"
|
||||||
|
>
|
||||||
|
Save Section
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<div id="preview-container" class="p-4 rounded-lg"
|
</main>
|
||||||
style="background-color: var(--bg-primary); min-height: 200px;">
|
@ErrorToast(errorMessage)
|
||||||
<p class="text-center" style="color: var(--text-secondary);">
|
</body>
|
||||||
Add filter rules or select books to see a preview of your custom section.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Form Actions -->
|
|
||||||
<div class="flex justify-end gap-3">
|
|
||||||
<button type="button" id="cancel-btn"
|
|
||||||
class="px-6 py-2 rounded-lg font-medium border hover:opacity-80"
|
|
||||||
style="border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" id="save-section-btn"
|
|
||||||
class="px-6 py-2 rounded-lg font-medium text-white hover:opacity-90"
|
|
||||||
style="background-color: var(--accent);">
|
|
||||||
Save Section
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
@ErrorToast(errorMessage)
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Dashboard - Bookhoard</title>
|
<title>Dashboard - Bookhoard</title>
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/toast.js"></script>
|
<script src="/static/main.js"></script>
|
||||||
<script src="/static/api.js"></script>
|
|
||||||
<script src="/static/events.js"></script>
|
|
||||||
<script src="/static/dashboard.js"></script>
|
|
||||||
<link href="/static/style.css" rel="stylesheet"/>
|
<link href="/static/style.css" rel="stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-{ user.Theme }">
|
<body class="theme-{ user.Theme }">
|
||||||
|
|||||||
+227
-213
@@ -1,26 +1,22 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="dark">
|
<html lang="en" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>{ doc.Title } - Bookhoard Documentation</title>
|
<title>{ doc.Title } - Bookhoard Documentation</title>
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
<link href="/static/style.css" rel="stylesheet"/>
|
||||||
<link rel="stylesheet" href="/static/highlight-dark.min.css">
|
<link rel="stylesheet" href="/static/highlight-dark.min.css"/>
|
||||||
<script src="/static/highlight.min.js"></script>
|
<script src="/static/main.js"></script>
|
||||||
<script src="/static/lunr.min.js"></script>
|
|
||||||
<script src="/static/lunr-flex.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }>
|
<body class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }>
|
||||||
@Header(user, currentPath)
|
@Header(user, currentPath)
|
||||||
<!-- Mobile Menu Button -->
|
<!-- Mobile Menu Button -->
|
||||||
<button
|
<button
|
||||||
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2 text-text-primary hover:bg-background-secondary/80"
|
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2 text-text-primary hover:bg-background-secondary/80"
|
||||||
onclick="toggleSidebar()"
|
onclick="toggleSidebar()"
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
@@ -29,27 +25,23 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Search Results Overlay -->
|
<!-- Search Results Overlay -->
|
||||||
<div id="search-results" class="hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-8 transition-opacity duration-200 ease-in-out"></div>
|
<div id="search-results" class="hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-8 transition-opacity duration-200 ease-in-out"></div>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out">
|
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out">
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<div class="p-4 border-b border-border">
|
<div class="p-4 border-b border-border">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="search-input w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent text-sm"
|
class="search-input w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent text-sm"
|
||||||
placeholder="Search documentation..."
|
placeholder="Search documentation..."
|
||||||
id="docs-search"
|
id="docs-search"
|
||||||
|
/>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Sections -->
|
<!-- Navigation Sections -->
|
||||||
for _, section := range nav.Sections {
|
for _, section := range nav.Sections {
|
||||||
<div class="nav-section mb-6">
|
<div class="nav-section mb-6">
|
||||||
<div
|
<div
|
||||||
class="nav-section-title font-semibold text-xs uppercase tracking-wider text-text-secondary px-4 py-3 cursor-pointer select-none flex items-center justify-between"
|
class="nav-section-title font-semibold text-xs uppercase tracking-wider text-text-secondary px-4 py-3 cursor-pointer select-none flex items-center justify-between"
|
||||||
onclick="toggleSection(this)"
|
onclick="toggleSection(this)"
|
||||||
>
|
>
|
||||||
@@ -78,7 +70,6 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="main-content lg:ml-72 p-8 max-w-4xl mx-auto">
|
<main class="main-content lg:ml-72 p-8 max-w-4xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
@@ -92,10 +83,8 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
}
|
}
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1>
|
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1>
|
||||||
|
|
||||||
<!-- Table of Contents -->
|
<!-- Table of Contents -->
|
||||||
if len(doc.TOC) > 0 {
|
if len(doc.TOC) > 0 {
|
||||||
<details class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
|
<details class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
|
||||||
@@ -104,8 +93,8 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
<span class="ml-2 text-text-secondary text-xs">▼</span>
|
<span class="ml-2 text-text-secondary text-xs">▼</span>
|
||||||
</summary>
|
</summary>
|
||||||
for _, item := range doc.TOC {
|
for _, item := range doc.TOC {
|
||||||
<a
|
<a
|
||||||
href={ "#" + item.Anchor }
|
href={ "#" + item.Anchor }
|
||||||
class="toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline"
|
class="toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline"
|
||||||
style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) }
|
style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) }
|
||||||
>
|
>
|
||||||
@@ -114,155 +103,161 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
}
|
}
|
||||||
</details>
|
</details>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="prose prose-invert max-w-none">
|
<div class="prose prose-invert max-w-none">
|
||||||
@UnsafeHTML(doc.Content).ToComponent()
|
@UnsafeHTML(doc.Content).ToComponent()
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Toggle sidebar section
|
// Toggle sidebar section
|
||||||
function toggleSection(el) {
|
function toggleSection(el) {
|
||||||
const items = el.nextElementSibling;
|
const items = el.nextElementSibling;
|
||||||
const arrow = el.querySelector('.section-arrow');
|
const arrow = el.querySelector(".section-arrow");
|
||||||
const isCollapsed = items.getAttribute('data-collapsed') === 'true';
|
const isCollapsed = items.getAttribute("data-collapsed") === "true";
|
||||||
|
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
items.classList.remove('hidden');
|
items.classList.remove("hidden");
|
||||||
items.setAttribute('data-collapsed', 'false');
|
items.setAttribute("data-collapsed", "false");
|
||||||
arrow.style.transform = 'rotate(0deg)';
|
arrow.style.transform = "rotate(0deg)";
|
||||||
} else {
|
} else {
|
||||||
items.classList.add('hidden');
|
items.classList.add("hidden");
|
||||||
items.setAttribute('data-collapsed', 'true');
|
items.setAttribute("data-collapsed", "true");
|
||||||
arrow.style.transform = 'rotate(-90deg)';
|
arrow.style.transform = "rotate(-90deg)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle sidebar on mobile
|
// Toggle sidebar on mobile
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector(".sidebar");
|
||||||
sidebar.classList.toggle('open');
|
sidebar.classList.toggle("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight current page in nav
|
// Highlight current page in nav
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
document.querySelectorAll('.nav-item').forEach(item => {
|
document.querySelectorAll(".nav-item").forEach((item) => {
|
||||||
if (item.getAttribute('href') === currentPath) {
|
if (item.getAttribute("href") === currentPath) {
|
||||||
item.classList.add('text-accent', 'bg-accent/10', 'border-r-2', 'border-accent');
|
item.classList.add(
|
||||||
|
"text-accent",
|
||||||
|
"bg-accent/10",
|
||||||
|
"border-r-2",
|
||||||
|
"border-accent",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize syntax highlighting
|
// Initialize syntax highlighting
|
||||||
if (typeof hljs !== 'undefined') {
|
if (typeof hljs !== "undefined") {
|
||||||
hljs.highlightAll();
|
hljs.highlightAll();
|
||||||
|
|
||||||
// Add copy buttons to all code blocks
|
// Add copy buttons to all code blocks
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
document.querySelectorAll("pre code").forEach((block) => {
|
||||||
const pre = block.parentElement;
|
const pre = block.parentElement;
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = 'relative group';
|
wrapper.className = "relative group";
|
||||||
pre.parentNode.insertBefore(wrapper, pre);
|
pre.parentNode.insertBefore(wrapper, pre);
|
||||||
wrapper.appendChild(pre);
|
wrapper.appendChild(pre);
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement("button");
|
||||||
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
|
button.className =
|
||||||
button.textContent = 'Copy';
|
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
|
||||||
button.onclick = async () => {
|
button.textContent = "Copy";
|
||||||
await navigator.clipboard.writeText(block.textContent);
|
button.onclick = async () => {
|
||||||
button.textContent = 'Copied!';
|
await navigator.clipboard.writeText(block.textContent);
|
||||||
setTimeout(() => {
|
button.textContent = "Copied!";
|
||||||
button.textContent = 'Copy';
|
setTimeout(() => {
|
||||||
}, 2000);
|
button.textContent = "Copy";
|
||||||
};
|
}, 2000);
|
||||||
wrapper.appendChild(button);
|
};
|
||||||
});
|
wrapper.appendChild(button);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add copy buttons to all code blocks
|
// Add copy buttons to all code blocks
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
document.querySelectorAll("pre code").forEach((block) => {
|
||||||
const pre = block.parentElement;
|
const pre = block.parentElement;
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = 'relative group';
|
wrapper.className = "relative group";
|
||||||
pre.parentNode.insertBefore(wrapper, pre);
|
pre.parentNode.insertBefore(wrapper, pre);
|
||||||
wrapper.appendChild(pre);
|
wrapper.appendChild(pre);
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement("button");
|
||||||
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
|
button.className =
|
||||||
button.textContent = 'Copy';
|
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
|
||||||
|
button.textContent = "Copy";
|
||||||
button.onclick = async () => {
|
button.onclick = async () => {
|
||||||
await navigator.clipboard.writeText(block.textContent);
|
await navigator.clipboard.writeText(block.textContent);
|
||||||
button.textContent = 'Copied!';
|
button.textContent = "Copied!";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.textContent = 'Copy';
|
button.textContent = "Copy";
|
||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
wrapper.appendChild(button);
|
wrapper.appendChild(button);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add copy buttons to all code blocks
|
// Add copy buttons to all code blocks
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
document.querySelectorAll("pre code").forEach((block) => {
|
||||||
const pre = block.parentElement;
|
const pre = block.parentElement;
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = 'relative group';
|
wrapper.className = "relative group";
|
||||||
pre.parentNode.insertBefore(wrapper, pre);
|
pre.parentNode.insertBefore(wrapper, pre);
|
||||||
wrapper.appendChild(pre);
|
wrapper.appendChild(pre);
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement("button");
|
||||||
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
|
button.className =
|
||||||
button.textContent = 'Copy';
|
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
|
||||||
button.onclick = async () => {
|
button.textContent = "Copy";
|
||||||
await navigator.clipboard.writeText(block.textContent);
|
button.onclick = async () => {
|
||||||
button.textContent = 'Copied!';
|
await navigator.clipboard.writeText(block.textContent);
|
||||||
setTimeout(() => {
|
button.textContent = "Copied!";
|
||||||
button.textContent = 'Copy';
|
setTimeout(() => {
|
||||||
}, 2000);
|
button.textContent = "Copy";
|
||||||
};
|
}, 2000);
|
||||||
wrapper.appendChild(button);
|
};
|
||||||
});
|
wrapper.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
let idx;
|
let idx;
|
||||||
let searchDocs = [];
|
let searchDocs = [];
|
||||||
|
|
||||||
// Load index on page load
|
// Load index on page load
|
||||||
fetch('/docs/search-index.json')
|
fetch("/docs/search-index.json")
|
||||||
.then(r => r.json())
|
.then((r) => r.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
idx = lunr(function() {
|
idx = lunr(function () {
|
||||||
this.use(lunr.flex);
|
this.use(lunr.flex);
|
||||||
this.ref('id');
|
this.ref("id");
|
||||||
this.field('title', {boost: 10});
|
this.field("title", { boost: 10 });
|
||||||
this.field('content', {boost: 1});
|
this.field("content", { boost: 1 });
|
||||||
data.forEach(doc => this.add(doc));
|
data.forEach((doc) => this.add(doc));
|
||||||
});
|
});
|
||||||
searchDocs = data;
|
searchDocs = data;
|
||||||
console.log('Search index loaded:', data.length, 'documents');
|
console.log("Search index loaded:", data.length, "documents");
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Failed to load search index:', err));
|
.catch((err) => console.error("Failed to load search index:", err));
|
||||||
|
|
||||||
// Search input with debounce
|
// Search input with debounce
|
||||||
const searchInput = document.getElementById('docs-search');
|
const searchInput = document.getElementById("docs-search");
|
||||||
const searchResultsDiv = document.getElementById('search-results');
|
const searchResultsDiv = document.getElementById("search-results");
|
||||||
|
|
||||||
let debounceTimer;
|
let debounceTimer;
|
||||||
|
|
||||||
searchInput.addEventListener('input', (e) => {
|
searchInput.addEventListener("input", (e) => {
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
const query = e.target.value.trim();
|
const query = e.target.value.trim();
|
||||||
|
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
searchResultsDiv.classList.add('hidden');
|
searchResultsDiv.classList.add("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search with fuzzy matching
|
// Search with fuzzy matching
|
||||||
const results = idx.search(query, {
|
const results = idx.search(query, {
|
||||||
fields: {title: {boost: 10}, content: 1},
|
fields: { title: { boost: 10 }, content: 1 },
|
||||||
expand: true
|
expand: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render results
|
// Render results
|
||||||
@@ -273,26 +268,29 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
// Render search results
|
// Render search results
|
||||||
function renderSearchResults(results, query) {
|
function renderSearchResults(results, query) {
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
searchResultsDiv.innerHTML = '<div class="text-center py-8 text-text-secondary">No results found</div>';
|
searchResultsDiv.innerHTML =
|
||||||
searchResultsDiv.classList.remove('hidden');
|
'<div class="text-center py-8 text-text-secondary">No results found</div>';
|
||||||
|
searchResultsDiv.classList.remove("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
searchResultsDiv.innerHTML = results.map(r => {
|
searchResultsDiv.innerHTML = results
|
||||||
const doc = searchDocs.find(d => d.id === r.ref);
|
.map((r) => {
|
||||||
if (!doc) return '';
|
const doc = searchDocs.find((d) => d.id === r.ref);
|
||||||
|
if (!doc) return "";
|
||||||
|
|
||||||
const snippet = getSnippet(doc.content, query);
|
const snippet = getSnippet(doc.content, query);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<a href="${doc.url}" class="block p-4 border-b border-border hover:bg-background-secondary text-text-primary no-underline">
|
<a href="${doc.url}" class="block p-4 border-b border-border hover:bg-background-secondary text-text-primary no-underline">
|
||||||
<div class="font-semibold mb-2">${doc.title}</div>
|
<div class="font-semibold mb-2">${doc.title}</div>
|
||||||
<div class="text-sm text-text-secondary">${snippet}...</div>
|
<div class="text-sm text-text-secondary">${snippet}...</div>
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
searchResultsDiv.classList.remove('hidden');
|
searchResultsDiv.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSnippet(content, query) {
|
function getSnippet(content, query) {
|
||||||
@@ -304,23 +302,26 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
const start = Math.max(0, idx - 50);
|
const start = Math.max(0, idx - 50);
|
||||||
const end = Math.min(content.length, idx + 100);
|
const end = Math.min(content.length, idx + 100);
|
||||||
return '...' + content.substring(start, end) + '...';
|
return "..." + content.substring(start, end) + "...";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return content.substring(0, 150) + '...';
|
return content.substring(0, 150) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close search results when clicking outside
|
// Close search results when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (!searchResultsDiv.contains(e.target) && e.target !== searchInput) {
|
if (
|
||||||
searchResultsDiv.classList.add('hidden');
|
!searchResultsDiv.contains(e.target) &&
|
||||||
|
e.target !== searchInput
|
||||||
|
) {
|
||||||
|
searchResultsDiv.classList.add("hidden");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close search on Escape key
|
// Close search on Escape key
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === "Escape") {
|
||||||
searchResultsDiv.classList.add('hidden');
|
searchResultsDiv.classList.add("hidden");
|
||||||
searchInput.blur();
|
searchInput.blur();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -328,129 +329,136 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
<script>
|
<script>
|
||||||
// Toggle sidebar on mobile
|
// Toggle sidebar on mobile
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector(".sidebar");
|
||||||
sidebar.classList.toggle('open');
|
sidebar.classList.toggle("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Highlight current page in nav
|
// Highlight current page in nav
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
document.querySelectorAll('.nav-item').forEach(item => {
|
document.querySelectorAll(".nav-item").forEach((item) => {
|
||||||
if (item.getAttribute('href') === currentPath) {
|
if (item.getAttribute("href") === currentPath) {
|
||||||
item.classList.add('text-accent', 'bg-accent/10', 'border-r-2', 'border-accent');
|
item.classList.add(
|
||||||
|
"text-accent",
|
||||||
|
"bg-accent/10",
|
||||||
|
"border-r-2",
|
||||||
|
"border-accent",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize syntax highlighting
|
// Initialize syntax highlighting
|
||||||
if (typeof hljs !== 'undefined') {
|
if (typeof hljs !== "undefined") {
|
||||||
hljs.highlightAll();
|
hljs.highlightAll();
|
||||||
|
|
||||||
// Add copy buttons to all code blocks
|
// Add copy buttons to all code blocks
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
document.querySelectorAll("pre code").forEach((block) => {
|
||||||
const pre = block.parentElement;
|
const pre = block.parentElement;
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = 'relative group';
|
wrapper.className = "relative group";
|
||||||
pre.parentNode.insertBefore(wrapper, pre);
|
pre.parentNode.insertBefore(wrapper, pre);
|
||||||
wrapper.appendChild(pre);
|
wrapper.appendChild(pre);
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement("button");
|
||||||
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
|
button.className =
|
||||||
button.textContent = 'Copy';
|
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
|
||||||
button.onclick = async () => {
|
button.textContent = "Copy";
|
||||||
await navigator.clipboard.writeText(block.textContent);
|
button.onclick = async () => {
|
||||||
button.textContent = 'Copied!';
|
await navigator.clipboard.writeText(block.textContent);
|
||||||
setTimeout(() => {
|
button.textContent = "Copied!";
|
||||||
button.textContent = 'Copy';
|
setTimeout(() => {
|
||||||
}, 2000);
|
button.textContent = "Copy";
|
||||||
};
|
}, 2000);
|
||||||
wrapper.appendChild(button);
|
};
|
||||||
});
|
wrapper.appendChild(button);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add copy buttons to all code blocks
|
// Add copy buttons to all code blocks
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
document.querySelectorAll("pre code").forEach((block) => {
|
||||||
const pre = block.parentElement;
|
const pre = block.parentElement;
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = 'relative group';
|
wrapper.className = "relative group";
|
||||||
pre.parentNode.insertBefore(wrapper, pre);
|
pre.parentNode.insertBefore(wrapper, pre);
|
||||||
wrapper.appendChild(pre);
|
wrapper.appendChild(pre);
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement("button");
|
||||||
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
|
button.className =
|
||||||
button.textContent = 'Copy';
|
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
|
||||||
|
button.textContent = "Copy";
|
||||||
button.onclick = async () => {
|
button.onclick = async () => {
|
||||||
await navigator.clipboard.writeText(block.textContent);
|
await navigator.clipboard.writeText(block.textContent);
|
||||||
button.textContent = 'Copied!';
|
button.textContent = "Copied!";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.textContent = 'Copy';
|
button.textContent = "Copy";
|
||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
wrapper.appendChild(button);
|
wrapper.appendChild(button);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add copy buttons to all code blocks
|
// Add copy buttons to all code blocks
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
document.querySelectorAll("pre code").forEach((block) => {
|
||||||
const pre = block.parentElement;
|
const pre = block.parentElement;
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = 'relative group';
|
wrapper.className = "relative group";
|
||||||
pre.parentNode.insertBefore(wrapper, pre);
|
pre.parentNode.insertBefore(wrapper, pre);
|
||||||
wrapper.appendChild(pre);
|
wrapper.appendChild(pre);
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement("button");
|
||||||
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
|
button.className =
|
||||||
button.textContent = 'Copy';
|
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
|
||||||
button.onclick = async () => {
|
button.textContent = "Copy";
|
||||||
await navigator.clipboard.writeText(block.textContent);
|
button.onclick = async () => {
|
||||||
button.textContent = 'Copied!';
|
await navigator.clipboard.writeText(block.textContent);
|
||||||
setTimeout(() => {
|
button.textContent = "Copied!";
|
||||||
button.textContent = 'Copy';
|
setTimeout(() => {
|
||||||
}, 2000);
|
button.textContent = "Copy";
|
||||||
};
|
}, 2000);
|
||||||
wrapper.appendChild(button);
|
};
|
||||||
});
|
wrapper.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
let idx;
|
let idx;
|
||||||
let searchDocs = [];
|
let searchDocs = [];
|
||||||
|
|
||||||
// Load index on page load
|
// Load index on page load
|
||||||
fetch('/docs/search-index.json')
|
fetch("/docs/search-index.json")
|
||||||
.then(r => r.json())
|
.then((r) => r.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
idx = lunr(function() {
|
idx = lunr(function () {
|
||||||
this.use(lunr.flex);
|
this.use(lunr.flex);
|
||||||
this.ref('id');
|
this.ref("id");
|
||||||
this.field('title', {boost: 10});
|
this.field("title", { boost: 10 });
|
||||||
this.field('content', {boost: 1});
|
this.field("content", { boost: 1 });
|
||||||
data.forEach(doc => this.add(doc));
|
data.forEach((doc) => this.add(doc));
|
||||||
});
|
});
|
||||||
searchDocs = data;
|
searchDocs = data;
|
||||||
console.log('Search index loaded:', data.length, 'documents');
|
console.log("Search index loaded:", data.length, "documents");
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Failed to load search index:', err));
|
.catch((err) => console.error("Failed to load search index:", err));
|
||||||
|
|
||||||
// Search input with debounce
|
// Search input with debounce
|
||||||
const searchInput = document.getElementById('docs-search');
|
const searchInput = document.getElementById("docs-search");
|
||||||
const searchResultsDiv = document.getElementById('search-results');
|
const searchResultsDiv = document.getElementById("search-results");
|
||||||
|
|
||||||
let debounceTimer;
|
let debounceTimer;
|
||||||
|
|
||||||
searchInput.addEventListener('input', (e) => {
|
searchInput.addEventListener("input", (e) => {
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
const query = e.target.value.trim();
|
const query = e.target.value.trim();
|
||||||
|
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
searchResultsDiv.classList.add('hidden');
|
searchResultsDiv.classList.add("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search with fuzzy matching
|
// Search with fuzzy matching
|
||||||
const results = idx.search(query, {
|
const results = idx.search(query, {
|
||||||
fields: {title: {boost: 10}, content: 1},
|
fields: { title: { boost: 10 }, content: 1 },
|
||||||
expand: true
|
expand: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render results
|
// Render results
|
||||||
@@ -461,26 +469,29 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
// Render search results
|
// Render search results
|
||||||
function renderSearchResults(results, query) {
|
function renderSearchResults(results, query) {
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
searchResultsDiv.innerHTML = '<div class="text-center py-8 text-text-secondary">No results found</div>';
|
searchResultsDiv.innerHTML =
|
||||||
searchResultsDiv.classList.remove('hidden');
|
'<div class="text-center py-8 text-text-secondary">No results found</div>';
|
||||||
|
searchResultsDiv.classList.remove("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
searchResultsDiv.innerHTML = results.map(r => {
|
searchResultsDiv.innerHTML = results
|
||||||
const doc = searchDocs.find(d => d.id === r.ref);
|
.map((r) => {
|
||||||
if (!doc) return '';
|
const doc = searchDocs.find((d) => d.id === r.ref);
|
||||||
|
if (!doc) return "";
|
||||||
|
|
||||||
const snippet = getSnippet(doc.content, query);
|
const snippet = getSnippet(doc.content, query);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<a href="${doc.url}" class="block p-4 border-b border-border hover:bg-background-secondary text-text-primary no-underline">
|
<a href="${doc.url}" class="block p-4 border-b border-border hover:bg-background-secondary text-text-primary no-underline">
|
||||||
<div class="font-semibold mb-2">${doc.title}</div>
|
<div class="font-semibold mb-2">${doc.title}</div>
|
||||||
<div class="text-sm text-text-secondary">${snippet}...</div>
|
<div class="text-sm text-text-secondary">${snippet}...</div>
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
searchResultsDiv.classList.remove('hidden');
|
searchResultsDiv.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSnippet(content, query) {
|
function getSnippet(content, query) {
|
||||||
@@ -492,23 +503,26 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
const start = Math.max(0, idx - 50);
|
const start = Math.max(0, idx - 50);
|
||||||
const end = Math.min(content.length, idx + 100);
|
const end = Math.min(content.length, idx + 100);
|
||||||
return '...' + content.substring(start, end) + '...';
|
return "..." + content.substring(start, end) + "...";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return content.substring(0, 150) + '...';
|
return content.substring(0, 150) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close search results when clicking outside
|
// Close search results when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (!searchResultsDiv.contains(e.target) && e.target !== searchInput) {
|
if (
|
||||||
searchResultsDiv.classList.add('hidden');
|
!searchResultsDiv.contains(e.target) &&
|
||||||
|
e.target !== searchInput
|
||||||
|
) {
|
||||||
|
searchResultsDiv.classList.add("hidden");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close search on Escape key
|
// Close search on Escape key
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === "Escape") {
|
||||||
searchResultsDiv.classList.add('hidden');
|
searchResultsDiv.classList.add("hidden");
|
||||||
searchInput.blur();
|
searchInput.blur();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+15
-3
@@ -1,3 +1,6 @@
|
|||||||
|
import { Alpine } from "./alpine";
|
||||||
|
import { showToast } from "./toast";
|
||||||
|
|
||||||
function getAuthHeader(): string {
|
function getAuthHeader(): string {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
return token ? `Bearer ${token}` : "";
|
return token ? `Bearer ${token}` : "";
|
||||||
@@ -82,9 +85,7 @@ function handleError(error: unknown, context: string): void {
|
|||||||
console.error(`${context}:`, error);
|
console.error(`${context}:`, error);
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "An unexpected error occurred";
|
error instanceof Error ? error.message : "An unexpected error occurred";
|
||||||
if ((window as any).showToast?.error) {
|
showToast(message, "error");
|
||||||
(window as any).showToast.error(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -97,3 +98,14 @@ export {
|
|||||||
handleVoidResponse,
|
handleVoidResponse,
|
||||||
handleError,
|
handleError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Alpine.global("api", {
|
||||||
|
get: apiGet,
|
||||||
|
post: apiPost,
|
||||||
|
put: apiPut,
|
||||||
|
delete: apiDelete,
|
||||||
|
patch: apiPatch,
|
||||||
|
handleResponse: handleResponse,
|
||||||
|
handleVoidResponse: handleVoidResponse,
|
||||||
|
handleError: handleError,
|
||||||
|
});
|
||||||
|
|||||||
+13
-1
@@ -1,3 +1,5 @@
|
|||||||
|
import { Alpine } from "./alpine";
|
||||||
|
|
||||||
function toggleSidebar(): void {
|
function toggleSidebar(): void {
|
||||||
const sidebar = document.getElementById("docs-sidebar");
|
const sidebar = document.getElementById("docs-sidebar");
|
||||||
const overlay = document.getElementById("docs-overlay");
|
const overlay = document.getElementById("docs-overlay");
|
||||||
@@ -91,4 +93,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
initializeDocsSearch();
|
initializeDocsSearch();
|
||||||
});
|
});
|
||||||
|
|
||||||
(window as any).toggleSidebar = toggleSidebar;
|
Alpine.global("docs", {
|
||||||
|
toggleSidebar,
|
||||||
|
initializeSearch: initializeDocsSearch,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { toggleSidebar, initializeDocsSearch };
|
||||||
|
|
||||||
|
Alpine.global("docs", {
|
||||||
|
toggleSidebar,
|
||||||
|
initializeSearch: initializeDocsSearch,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Password validation for registration form
|
// Password validation for registration form
|
||||||
// Procedural style - no OOP, passive event listeners for password manager compatibility
|
// Procedural style - no OOP, passive event listeners for password manager compatibility
|
||||||
|
|
||||||
|
import { Alpine } from "./alpine";
|
||||||
|
|
||||||
// Validation check functions
|
// Validation check functions
|
||||||
function hasMinimumLength(password: string): boolean {
|
function hasMinimumLength(password: string): boolean {
|
||||||
return password.length >= 8;
|
return password.length >= 8;
|
||||||
@@ -185,4 +187,8 @@ function initPasswordValidation(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Export for use in template
|
// Export for use in template
|
||||||
(window as any).initPasswordValidation = initPasswordValidation;
|
export { initPasswordValidation };
|
||||||
|
|
||||||
|
Alpine.global("validation", {
|
||||||
|
initPasswordValidation,
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user