feat: create admin dashboard with user preferences and library settings
- Add admin dashboard at /admin route consolidating all user settings - Move user preferences (username, email, password, theme) from separate page - Add ebook library preferences section with folder management - Add scan settings (frequency and auto-scan toggle) with database persistence - Create database migration for scan_frequency_minutes and auto_scan_enabled columns - Add API endpoints for scan settings management: - PUT /api/library/scan-settings - Update scan preferences - GET /api/library/scan-settings - Get current scan settings - Update dashboard navigation to link to admin dashboard - Remove old preferences.html template (functionality moved to admin) - Create Bruno API testing files for library endpoints - Add real-time folder loading and management in admin interface - Implement scan settings persistence and retrieval from database
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
-- 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;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
meta {
|
||||||
|
name: Get Scan Settings
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/api/library/scan-settings
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docs {
|
||||||
|
## Get Scan Settings
|
||||||
|
|
||||||
|
Retrieves the user's current ebook scanning settings.
|
||||||
|
|
||||||
|
**Method:** GET
|
||||||
|
|
||||||
|
**Endpoint:** /api/library/scan-settings
|
||||||
|
|
||||||
|
**Authentication:** Required
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- `scan_frequency_minutes` (integer): Minutes between automatic scans
|
||||||
|
- `auto_scan_enabled` (boolean): Whether automatic scanning is enabled
|
||||||
|
|
||||||
|
**Status Codes:**
|
||||||
|
- 200: Success
|
||||||
|
- 401: Unauthorized
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
meta {
|
||||||
|
name: Update Scan Settings
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
put {
|
||||||
|
url: {{base_url}}/api/library/scan-settings
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
{
|
||||||
|
"scan_frequency_minutes": 60,
|
||||||
|
"auto_scan_enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docs {
|
||||||
|
## Update Scan Settings
|
||||||
|
|
||||||
|
Updates the user's ebook scanning settings.
|
||||||
|
|
||||||
|
**Method:** PUT
|
||||||
|
|
||||||
|
**Endpoint:** /api/library/scan-settings
|
||||||
|
|
||||||
|
**Authentication:** Required
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
- `scan_frequency_minutes` (integer, required): Minutes between automatic scans (15-1440)
|
||||||
|
- `auto_scan_enabled` (boolean, required): Whether automatic scanning is enabled
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- `message` (string): Success message
|
||||||
|
|
||||||
|
**Status Codes:**
|
||||||
|
- 200: Success
|
||||||
|
- 400: Invalid settings
|
||||||
|
- 401: Unauthorized
|
||||||
|
}
|
||||||
@@ -55,11 +55,13 @@ type UserEbookFolders struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Users struct {
|
type Users struct {
|
||||||
ID pgtype.UUID `db:"id" json:"id"`
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
Email string `db:"email" json:"email"`
|
Email string `db:"email" json:"email"`
|
||||||
Username string `db:"username" json:"username"`
|
Username string `db:"username" json:"username"`
|
||||||
PasswordHash string `db:"password_hash" json:"password_hash"`
|
PasswordHash string `db:"password_hash" json:"password_hash"`
|
||||||
Theme pgtype.Text `db:"theme" json:"theme"`
|
Theme pgtype.Text `db:"theme" json:"theme"`
|
||||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||||
|
ScanFrequencyMinutes pgtype.Int4 `db:"scan_frequency_minutes" json:"scan_frequency_minutes"`
|
||||||
|
AutoScanEnabled pgtype.Bool `db:"auto_scan_enabled" json:"auto_scan_enabled"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Querier interface {
|
|||||||
GetEbookRating(ctx context.Context, arg GetEbookRatingParams) (EbookRatings, error)
|
GetEbookRating(ctx context.Context, arg GetEbookRatingParams) (EbookRatings, error)
|
||||||
GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]GetEbookRatingsRow, error)
|
GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]GetEbookRatingsRow, error)
|
||||||
GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error)
|
GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error)
|
||||||
|
GetScanSettings(ctx context.Context, id pgtype.UUID) (GetScanSettingsRow, error)
|
||||||
GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error)
|
GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error)
|
||||||
GetUserByEmail(ctx context.Context, email string) (Users, error)
|
GetUserByEmail(ctx context.Context, email string) (Users, error)
|
||||||
GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error)
|
GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error)
|
||||||
@@ -38,6 +39,7 @@ type Querier interface {
|
|||||||
UpdateEmail(ctx context.Context, arg UpdateEmailParams) error
|
UpdateEmail(ctx context.Context, arg UpdateEmailParams) error
|
||||||
UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error
|
UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error
|
||||||
UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error)
|
UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error)
|
||||||
|
UpdateScanSettings(ctx context.Context, arg UpdateScanSettingsParams) error
|
||||||
UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error
|
UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error
|
||||||
UpdateUsername(ctx context.Context, arg UpdateUsernameParams) error
|
UpdateUsername(ctx context.Context, arg UpdateUsernameParams) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ func (q *Queries) CreateEbookRating(ctx context.Context, arg CreateEbookRatingPa
|
|||||||
const CreateUser = `-- name: CreateUser :one
|
const CreateUser = `-- name: CreateUser :one
|
||||||
INSERT INTO users (email, username, password_hash, theme)
|
INSERT INTO users (email, username, password_hash, theme)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING id, email, username, password_hash, theme, created_at, updated_at
|
RETURNING id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateUserParams struct {
|
type CreateUserParams struct {
|
||||||
@@ -157,6 +157,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (Users,
|
|||||||
&i.Theme,
|
&i.Theme,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.ScanFrequencyMinutes,
|
||||||
|
&i.AutoScanEnabled,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -373,6 +375,22 @@ func (q *Queries) GetReadingProgress(ctx context.Context, arg GetReadingProgress
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetScanSettings = `-- name: GetScanSettings :one
|
||||||
|
SELECT scan_frequency_minutes, auto_scan_enabled FROM users WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetScanSettingsRow struct {
|
||||||
|
ScanFrequencyMinutes pgtype.Int4 `db:"scan_frequency_minutes" json:"scan_frequency_minutes"`
|
||||||
|
AutoScanEnabled pgtype.Bool `db:"auto_scan_enabled" json:"auto_scan_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetScanSettings(ctx context.Context, id pgtype.UUID) (GetScanSettingsRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetScanSettings, id)
|
||||||
|
var i GetScanSettingsRow
|
||||||
|
err := row.Scan(&i.ScanFrequencyMinutes, &i.AutoScanEnabled)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetUser = `-- name: GetUser :one
|
const GetUser = `-- name: GetUser :one
|
||||||
SELECT id, email, username, theme, created_at, updated_at FROM users WHERE id = $1
|
SELECT id, email, username, theme, created_at, updated_at FROM users WHERE id = $1
|
||||||
`
|
`
|
||||||
@@ -401,7 +419,7 @@ func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetUserByEmail = `-- name: GetUserByEmail :one
|
const GetUserByEmail = `-- name: GetUserByEmail :one
|
||||||
SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE email = $1
|
SELECT id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled FROM users WHERE email = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, error) {
|
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, error) {
|
||||||
@@ -415,12 +433,14 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, erro
|
|||||||
&i.Theme,
|
&i.Theme,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.ScanFrequencyMinutes,
|
||||||
|
&i.AutoScanEnabled,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
|
const GetUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
|
||||||
SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE email = $1 OR username = $1
|
SELECT id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled FROM users WHERE email = $1 OR username = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error) {
|
func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error) {
|
||||||
@@ -434,12 +454,14 @@ func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (U
|
|||||||
&i.Theme,
|
&i.Theme,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.ScanFrequencyMinutes,
|
||||||
|
&i.AutoScanEnabled,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserByUsername = `-- name: GetUserByUsername :one
|
const GetUserByUsername = `-- name: GetUserByUsername :one
|
||||||
SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE username = $1
|
SELECT id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled FROM users WHERE username = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users, error) {
|
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users, error) {
|
||||||
@@ -453,6 +475,8 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users
|
|||||||
&i.Theme,
|
&i.Theme,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.ScanFrequencyMinutes,
|
||||||
|
&i.AutoScanEnabled,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -753,6 +777,21 @@ func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingPr
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UpdateScanSettings = `-- name: UpdateScanSettings :exec
|
||||||
|
UPDATE users SET scan_frequency_minutes = $2, auto_scan_enabled = $3, updated_at = NOW() WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateScanSettingsParams struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
ScanFrequencyMinutes pgtype.Int4 `db:"scan_frequency_minutes" json:"scan_frequency_minutes"`
|
||||||
|
AutoScanEnabled pgtype.Bool `db:"auto_scan_enabled" json:"auto_scan_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateScanSettings(ctx context.Context, arg UpdateScanSettingsParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, UpdateScanSettings, arg.ID, arg.ScanFrequencyMinutes, arg.AutoScanEnabled)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const UpdateUserTheme = `-- name: UpdateUserTheme :exec
|
const UpdateUserTheme = `-- name: UpdateUserTheme :exec
|
||||||
UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1
|
UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1;
|
|||||||
-- name: DeleteUser :exec
|
-- name: DeleteUser :exec
|
||||||
DELETE FROM users WHERE id = $1;
|
DELETE FROM users WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: UpdateScanSettings :exec
|
||||||
|
UPDATE users SET scan_frequency_minutes = $2, auto_scan_enabled = $3, updated_at = NOW() WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetScanSettings :one
|
||||||
|
SELECT scan_frequency_minutes, auto_scan_enabled FROM users WHERE id = $1;
|
||||||
|
|
||||||
-- name: CreateEbookRating :one
|
-- name: CreateEbookRating :one
|
||||||
INSERT INTO ebook_ratings (ebook_id, user_id, rating)
|
INSERT INTO ebook_ratings (ebook_id, user_id, rating)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
|
|||||||
@@ -588,6 +588,62 @@ func (h *AuthHandler) DeleteAccount(c echo.Context) error {
|
|||||||
return c.JSON(http.StatusOK, map[string]string{"message": "account deleted successfully"})
|
return c.JSON(http.StatusOK, map[string]string{"message": "account deleted successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateScanSettingsRequest struct {
|
||||||
|
ScanFrequencyMinutes int32 `json:"scan_frequency_minutes" validate:"required,min=15,max=1440"`
|
||||||
|
AutoScanEnabled bool `json:"auto_scan_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateScanSettings handles PUT /api/library/scan-settings
|
||||||
|
func (h *AuthHandler) UpdateScanSettings(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateScanSettingsRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||||
|
}
|
||||||
|
if err := c.Validate(&req); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.db.UpdateScanSettings(c.Request().Context(), database.UpdateScanSettingsParams{
|
||||||
|
ID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
ScanFrequencyMinutes: pgtype.Int4{Int32: req.ScanFrequencyMinutes, Valid: true},
|
||||||
|
AutoScanEnabled: pgtype.Bool{Bool: req.AutoScanEnabled, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"message": "scan settings updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScanSettings handles GET /api/library/scan-settings
|
||||||
|
func (h *AuthHandler) GetScanSettings(c echo.Context) error {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := h.db.GetScanSettings(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
// If no settings found, return defaults
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"scan_frequency_minutes": 60,
|
||||||
|
"auto_scan_enabled": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"scan_frequency_minutes": settings.ScanFrequencyMinutes.Int32,
|
||||||
|
"auto_scan_enabled": settings.AutoScanEnabled.Bool,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) generateJWT(userID string) (string, error) {
|
func (h *AuthHandler) generateJWT(userID string) (string, error) {
|
||||||
claims := jwtgo.MapClaims{
|
claims := jwtgo.MapClaims{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 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;
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
{{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)">Dashboard</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-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<a href="/" class="btn-secondary px-4 py-2 rounded-lg font-medium">
|
||||||
|
← Back to Library
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-3xl font-bold" style="color: var(--text-primary)">Admin Dashboard</h2>
|
||||||
|
<p style="color: var(--text-secondary)">Manage your account settings and library preferences</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Sections -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<!-- User Preferences -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<h3 class="text-2xl font-semibold" style="color: var(--text-primary)">User Preferences</h3>
|
||||||
|
|
||||||
|
<!-- Update Username -->
|
||||||
|
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||||
|
<h4 class="text-lg font-medium mb-4" 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">
|
||||||
|
<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="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||||
|
<h4 class="text-lg font-medium mb-4" 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">
|
||||||
|
<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 class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||||
|
<h4 class="text-lg font-medium mb-4" 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-3">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Theme Settings -->
|
||||||
|
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||||
|
<h4 class="text-lg font-medium mb-4" 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">
|
||||||
|
<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 Theme</button>
|
||||||
|
</form>
|
||||||
|
<div id="theme-result" class="mt-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Library Preferences -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<h3 class="text-2xl font-semibold" style="color: var(--text-primary)">Library Preferences</h3>
|
||||||
|
|
||||||
|
<!-- Ebook Folders Management -->
|
||||||
|
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||||
|
<h4 class="text-lg font-medium mb-4" style="color: var(--text-primary)">Ebook Folders</h4>
|
||||||
|
<p style="color: var(--text-secondary)" class="mb-4 text-sm">Manage the folders where Bookmann scans for ebooks</p>
|
||||||
|
|
||||||
|
<!-- Add New Folder -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<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-2">
|
||||||
|
<input type="text" name="folder_path" placeholder="/path/to/ebooks" class="flex-1 px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
||||||
|
<button type="submit" class="btn-primary px-3 py-2 rounded text-sm">Add Folder</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Folders List -->
|
||||||
|
<div id="folders-list" class="space-y-2">
|
||||||
|
<!-- Folders will be loaded here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="folders-result" class="mt-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scan Settings -->
|
||||||
|
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||||
|
<h4 class="text-lg font-medium mb-4" style="color: var(--text-primary)">Scan Settings</h4>
|
||||||
|
|
||||||
|
<!-- Auto Scan Toggle -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="flex items-center space-x-3">
|
||||||
|
<input type="checkbox" id="auto-scan-toggle" class="rounded" onchange="toggleAutoScan()">
|
||||||
|
<span style="color: var(--text-primary)">Enable automatic scanning</span>
|
||||||
|
</label>
|
||||||
|
<p style="color: var(--text-secondary)" class="text-sm mt-1">Automatically scan folders for new ebooks</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scan Frequency -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Scan Frequency</label>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual Scan Button -->
|
||||||
|
<div>
|
||||||
|
<button onclick="manualScan()" class="btn-secondary px-4 py-2 rounded">Scan Now</button>
|
||||||
|
<p style="color: var(--text-secondary)" class="text-sm mt-1">Manually scan all configured folders</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="scan-result" class="mt-2"></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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Load user data on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadTheme();
|
||||||
|
loadUserTheme();
|
||||||
|
loadFolders();
|
||||||
|
loadScanSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme management
|
||||||
|
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(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folder management
|
||||||
|
function loadFolders() {
|
||||||
|
fetch('/api/auth/ebook-folders', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
renderFolders(data);
|
||||||
|
}).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 = '<p style="color: var(--text-secondary)" class="text-sm italic">No folders configured</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.forEach(folder => {
|
||||||
|
const folderDiv = document.createElement('div');
|
||||||
|
folderDiv.className = 'flex items-center justify-between p-2 border rounded' style="border-color: var(--border); background-color: var(--bg-primary)";
|
||||||
|
folderDiv.innerHTML = `
|
||||||
|
<span style="color: var(--text-primary)" class="text-sm font-mono">${folder.folder_path}</span>
|
||||||
|
<button onclick="removeFolder('${folder.folder_path}')" class="text-red-500 hover:text-red-700 text-sm">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
container.appendChild(folderDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFolder(folderPath) {
|
||||||
|
if (confirm(`Remove folder: ${folderPath}?`)) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan settings (placeholder for now)
|
||||||
|
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() {
|
||||||
|
// TODO: Implement auto scan toggle
|
||||||
|
console.log('Auto scan toggled');
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
// Show success message briefly
|
||||||
|
console.log('Scan settings updated:', data.message);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error updating scan settings:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function manualScan() {
|
||||||
|
// Get folder paths from the current folders list
|
||||||
|
const folderElements = document.querySelectorAll('#folders-list .font-mono');
|
||||||
|
const folderPaths = Array.from(folderElements).map(el => el.textContent.trim());
|
||||||
|
|
||||||
|
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'}</div>`;
|
||||||
|
}).catch(err => {
|
||||||
|
document.getElementById('scan-result').innerHTML = `<div style="color: #ef4444">Scan failed</div>`;
|
||||||
|
console.error('Scan error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account deletion
|
||||||
|
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.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-4">
|
<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="/" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Dashboard</a>
|
||||||
<a href="/preferences" class="px-3 py-2 text-sm hover:opacity-80" style="color: var(--text-secondary)">Preferences</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>
|
<span style="color: var(--text-secondary)">Welcome, {{.User.Username}}!</span>
|
||||||
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
|
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
{{template "base.html" .}}
|
|
||||||
|
|
||||||
{{define "title"}}Preferences - 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>
|
|
||||||
<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-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
||||||
<!-- Header Section -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<button onclick="window.history.back()" class="btn-secondary px-4 py-2 rounded-lg font-medium">
|
|
||||||
← Back to Dashboard
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-3xl font-bold" style="color: var(--text-primary)">Account Preferences</h2>
|
|
||||||
<p style="color: var(--text-secondary)">Manage your account settings and preferences</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preferences Sections -->
|
|
||||||
<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">
|
|
||||||
<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-6 py-2 rounded">Update Username</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">
|
|
||||||
<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-6 py-2 rounded">Update Email</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-3">
|
|
||||||
<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-6 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">
|
|
||||||
<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-6 py-2 rounded">Update Theme</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-6 text-red-500">Danger Zone</h3>
|
|
||||||
<div class="border-t border-red-500 pt-6">
|
|
||||||
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Delete Account</h4>
|
|
||||||
<p style="color: var(--text-secondary)" class="mb-4">
|
|
||||||
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-6 py-2 rounded font-medium">
|
|
||||||
Delete Account
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function logout() {
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
localStorage.removeItem('user');
|
|
||||||
window.location.href = '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
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)',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.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 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(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load theme on page load
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
loadTheme();
|
|
||||||
loadUserTheme();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{{end}}
|
|
||||||
Reference in New Issue
Block a user