Introduces WebSocket-based real-time updates for the shopping list. Changes to items (add, mark, delete) are now instantly reflected across all connected user sessions without requiring a page refresh. This commit: - Extends the WebSocket ConnectionManager to broadcast item updates. - Modifies item manipulation endpoints (add, mark, delete) to trigger broadcasts. - Updates the frontend to listen for update broadcasts and refresh the list. - Updates README.md to reflect the new real-time update feature.
1235 lines
54 KiB
HTML
1235 lines
54 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Geteilte Einkaufsliste</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<link rel="icon" type="image/png" href="/favicon.png">
|
|
<style>
|
|
:root {
|
|
--background-gradient-start: #ece9e6;
|
|
--background-gradient-end: #ffffff;
|
|
--text-color: #212529;
|
|
--card-background-color: #ffffff;
|
|
--card-shadow-color: rgba(0, 0, 0, 0.05);
|
|
--input-background-color: #ffffff;
|
|
--input-text-color: #212529;
|
|
--status-bar-background-color: #f8f9fa;
|
|
--status-bar-border-color: #dee2e6;
|
|
--list-group-item-background-color: #ffffff;
|
|
}
|
|
|
|
[data-theme="dark"] {
|
|
--background-gradient-start: #212529;
|
|
--background-gradient-end: #343a40;
|
|
--text-color: #f8f9fa;
|
|
--card-background-color: #343a40;
|
|
--card-shadow-color: rgba(255, 255, 255, 0.05);
|
|
--input-background-color: #495057;
|
|
--input-text-color: #f8f9fa;
|
|
--status-bar-background-color: #343a40;
|
|
--status-bar-border-color: #495057;
|
|
--list-group-item-background-color: #343a40;
|
|
}
|
|
|
|
[data-theme="dark"] .text-dark {
|
|
color: var(--text-color) !important;
|
|
}
|
|
|
|
[data-theme="dark"] .modal-content {
|
|
background-color: var(--card-background-color);
|
|
}
|
|
|
|
[data-theme="dark"] .form-label,
|
|
[data-theme="dark"] .card-title,
|
|
[data-theme="dark"] #create-user-status,
|
|
[data-theme="dark"] #change-password-status,
|
|
[data-theme="dark"] #set-deletion-password-status {
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .btn-primary {
|
|
background-color: #0d6efd;
|
|
border-color: #0d6efd;
|
|
}
|
|
|
|
[data-theme="dark"] .btn-outline-primary {
|
|
color: #0d6efd;
|
|
border-color: #0d6efd;
|
|
}
|
|
|
|
[data-theme="dark"] .btn-outline-primary:hover {
|
|
background-color: #0d6efd;
|
|
color: #fff;
|
|
}
|
|
|
|
[data-theme="dark"] .btn-secondary {
|
|
background-color: #6c757d;
|
|
border-color: #6c757d;
|
|
}
|
|
|
|
[data-theme="dark"] .btn-outline-secondary {
|
|
color: #6c757d;
|
|
border-color: #6c757d;
|
|
}
|
|
|
|
[data-theme="dark"] .btn-outline-secondary:hover {
|
|
background-color: #6c757d;
|
|
color: #fff;
|
|
}
|
|
|
|
[data-theme="dark"] #admin-panel-title,
|
|
[data-theme="dark"] #logout-btn,
|
|
[data-theme="dark"] #item-name,
|
|
[data-theme="dark"] #info-text-existing,
|
|
[data-theme="dark"] #info-text-new {
|
|
color: var(--text-color) !important;
|
|
}
|
|
|
|
[data-theme="dark"] #is-admin-label {
|
|
color: var(--text-color) !important;
|
|
}
|
|
|
|
[data-theme="dark"] #item-name::placeholder,
|
|
[data-theme="dark"] #deletion-password::placeholder {
|
|
color: var(--text-color) !important;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
[data-theme="dark"] .form-check-input {
|
|
background-color: var(--input-background-color);
|
|
border-color: var(--status-bar-border-color);
|
|
}
|
|
|
|
[data-theme="dark"] #password-set-message p {
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] hr {
|
|
border: 0;
|
|
border-top: 1px solid var(--status-bar-border-color);
|
|
opacity: 1;
|
|
}
|
|
|
|
[data-theme="dark"] #toggle-deletion-password svg {
|
|
fill: #fff;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(to right, var(--background-gradient-start), var(--background-gradient-end));
|
|
color: var(--text-color);
|
|
}
|
|
.container {
|
|
max-width: 800px;
|
|
}
|
|
.input-card-sticky {
|
|
position: -webkit-sticky;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1020;
|
|
}
|
|
#page-title {
|
|
font-weight: 300;
|
|
}
|
|
.list-group-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background-color: var(--list-group-item-background-color);
|
|
color: var(--text-color);
|
|
}
|
|
.delete-btn {
|
|
cursor: pointer;
|
|
color: #dc3545;
|
|
font-weight: bold;
|
|
}
|
|
#status-bar {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background-color: var(--status-bar-background-color);
|
|
padding: 5px;
|
|
text-align: center;
|
|
font-size: 12px;
|
|
border-top: 1px solid var(--status-bar-border-color);
|
|
}
|
|
#suggestion-info {
|
|
font-size: 0.9rem;
|
|
color: #6c757d;
|
|
margin-top: 10px;
|
|
}
|
|
#suggestion-box {
|
|
margin-top: 5px;
|
|
}
|
|
.suggestion-btn {
|
|
margin-right: 5px;
|
|
margin-bottom: 5px;
|
|
}
|
|
#login-view, #app-view {
|
|
display: none; /* Hidden by default */
|
|
}
|
|
.card {
|
|
background-color: var(--card-background-color);
|
|
box-shadow: 0 0.125rem 0.25rem var(--card-shadow-color);
|
|
}
|
|
.form-control {
|
|
background-color: var(--input-background-color);
|
|
color: var(--input-text-color);
|
|
}
|
|
.form-control:focus {
|
|
background-color: var(--input-background-color);
|
|
color: var(--input-text-color);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="pb-5">
|
|
<script>
|
|
(function() {
|
|
const theme = localStorage.getItem('theme');
|
|
if (theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
} else {
|
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<!-- Login View -->
|
|
<div id="login-view" class="container mt-5">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-6">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<h3 id="login-title" class="card-title text-center"></h3>
|
|
<form id="login-form">
|
|
<div class="mb-3">
|
|
<label id="login-username-label" for="username" class="form-label"></label>
|
|
<input type="text" class="form-control" id="username" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label id="login-password-label" for="password" class="form-label"></label>
|
|
<input type="password" class="form-control" id="password" required>
|
|
</div>
|
|
<div class="d-grid">
|
|
<button id="login-button" type="submit" class="btn btn-primary"></button>
|
|
</div>
|
|
<div id="login-error" class="text-danger mt-2"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main App View -->
|
|
<div id="app-view" class="container mt-5">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h1 id="page-title" class="text-center mb-4"></h1>
|
|
<div class="d-flex align-items-center">
|
|
<button id="delete-marked-btn" class="btn btn-sm btn-outline-danger me-2" style="display: none;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
|
</svg>
|
|
</button>
|
|
<button id="theme-switcher" class="btn btn-sm btn-outline-secondary me-2">☀️</button>
|
|
<button id="logout-btn" class="btn btn-sm btn-outline-secondary">Logout</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="online-users-container" class="mb-3" style="display: none;">
|
|
<strong>Online:</strong> <span id="online-users-list"></span>
|
|
</div>
|
|
|
|
<div id="user-view-content">
|
|
<div class="card shadow-sm mb-4 input-card-sticky">
|
|
<div class="card-body">
|
|
<form id="add-item-form" class="input-group">
|
|
<input type="text" id="item-name" class="form-control" autocomplete="off" required>
|
|
<button type="submit" id="add-button" class="btn btn-primary"></button>
|
|
<button type="button" id="add-one-button" class="btn btn-secondary"></button>
|
|
</form>
|
|
<div id="suggestion-info">
|
|
<div class="d-flex align-items-center mb-1">
|
|
<button id="example-button-existing" type="button" class="btn btn-primary btn-sm me-2" disabled></button>
|
|
<span id="info-text-existing"></span>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<button id="example-button-new" type="button" class="btn btn-outline-primary btn-sm me-2" disabled></button>
|
|
<span id="info-text-new"></span>
|
|
</div>
|
|
</div>
|
|
<div id="suggestion-box"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2 mb-3">
|
|
<div class="col">
|
|
<div class="d-grid">
|
|
<button id="notify-btn" class="btn btn-success"></button>
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="d-grid">
|
|
<button id="select-all-btn" class="btn btn-primary">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-square" viewBox="0 0 16 16">
|
|
<path d="M3 14.5A1.5 1.5 0 0 1 1.5 13V3A1.5 1.5 0 0 1 3 1.5h8A1.5 1.5 0 0 1 12.5 3v1.5a.5.5 0 0 1-1 0V3a.5.5 0 0 0-.5-.5H3a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V10a.5.5 0 0 1 1 0v3a1.5 1.5 0 0 1-1.5 1.5H3z"/>
|
|
<path d="m8.354 10.354 7-7a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ul id="item-list" class="list-group shadow-sm"></ul>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Admin Section -->
|
|
<div id="admin-section" class="card shadow-sm mt-3" style="display: none;">
|
|
<div id="admin-panel-title" class="card-header"></div>
|
|
<div class="card-body">
|
|
|
|
<h5 id="create-user-title" class="card-title"></h5>
|
|
<form id="create-user-form">
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label id="username-label" for="new-username" class="form-label"></label>
|
|
<input type="text" id="new-username" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label id="password-label" for="new-password" class="form-label"></label>
|
|
<input type="password" id="new-password" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-4 mb-3 d-flex align-items-end">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="new-user-is-admin">
|
|
<label id="is-admin-label" class="form-check-label" for="new-user-is-admin"></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button id="create-user-button" type="submit" class="btn btn-success"></button>
|
|
<div id="create-user-status" class="mt-2"></div>
|
|
</form>
|
|
|
|
<hr class="my-4">
|
|
|
|
<h5 id="change-password-title" class="card-title"></h5>
|
|
<form id="change-password-form">
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label id="current-password-label" for="current-password" class="form-label"></label>
|
|
<input type="password" id="current-password" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label id="new-password-label" for="new-password-change" class="form-label"></label>
|
|
<input type="password" id="new-password-change" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label id="confirm-password-label" for="confirm-password" class="form-label"></label>
|
|
<input type="password" id="confirm-password" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<button id="change-password-button" type="submit" class="btn btn-primary"></button>
|
|
<div id="change-password-status" class="mt-2"></div>
|
|
</form>
|
|
|
|
<hr class="my-4">
|
|
|
|
<h5 id="manage-users-title" class="card-title"></h5>
|
|
<ul id="user-list" class="list-group mb-3">
|
|
<!-- Users will be populated by JavaScript -->
|
|
</ul>
|
|
|
|
<hr class="my-4">
|
|
|
|
<h5 id="set-deletion-password-title" class="card-title"></h5>
|
|
<div id="password-set-message" style="display: none;">
|
|
<p>Passwort ist gesetzt.</p>
|
|
<button id="change-password-btn" class="btn btn-primary">Passwort ändern</button>
|
|
<button id="delete-password-btn" class="btn btn-danger">Passwort löschen</button>
|
|
</div>
|
|
<form id="set-deletion-password-form">
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<div class="input-group">
|
|
<input type="password" id="deletion-password" class="form-control" required>
|
|
<button class="btn btn-outline-secondary" type="button" id="toggle-deletion-password">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
|
|
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.12 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
|
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
|
</svg>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash" viewBox="0 0 16 16" style="display: none;">
|
|
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.94 5.94 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.707z"/>
|
|
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.288.822.822.073.073.026.026a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829l.822.822zm-2.943-1.288.822.822.073.073.026.026a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829l.822.822zm-2.943-1.288.822.822.073.073.026.026a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829l.822.822z"/>
|
|
<path d="M.5 8a.5.5 0 0 1 .5-.5h14a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button id="set-deletion-password-button" type="submit" class="btn btn-primary"></button>
|
|
<div id="set-deletion-password-status" class="mt-2"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<div id="status-bar"></div>
|
|
|
|
<!-- Deletion Password Modal -->
|
|
<div class="modal fade" id="delete-password-modal" tabindex="-1" aria-labelledby="delete-password-modal-label" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="delete-password-modal-label"></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="delete-password-form">
|
|
<div class="mb-3">
|
|
<label id="delete-password-label" for="delete-password-input" class="form-label"></label>
|
|
<input type="password" class="form-control" id="delete-password-input" required>
|
|
</div>
|
|
<div id="delete-password-error" class="text-danger"></div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"></button>
|
|
<button type="button" id="confirm-delete-btn" class="btn btn-danger"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Views
|
|
const loginView = document.getElementById('login-view');
|
|
const appView = document.getElementById('app-view');
|
|
const userViewContent = document.getElementById('user-view-content');
|
|
|
|
// Login elements
|
|
const loginForm = document.getElementById('login-form');
|
|
const usernameInput = document.getElementById('username');
|
|
const passwordInput = document.getElementById('password');
|
|
const loginError = document.getElementById('login-error');
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
|
|
const adminSection = document.getElementById('admin-section');
|
|
const createUserForm = document.getElementById('create-user-form');
|
|
const newUsernameInput = document.getElementById('new-username');
|
|
const newPasswordInput = document.getElementById('new-password');
|
|
const newUserIsAdminCheckbox = document.getElementById('new-user-is-admin');
|
|
const createUserStatus = document.getElementById('create-user-status');
|
|
|
|
// App elements
|
|
const pageTitle = document.getElementById('page-title');
|
|
const itemList = document.getElementById('item-list');
|
|
const addItemForm = document.getElementById('add-item-form');
|
|
const itemNameInput = document.getElementById('item-name');
|
|
const addButton = document.getElementById('add-button');
|
|
const addOneButton = document.getElementById('add-one-button');
|
|
const suggestionInfo = document.getElementById('suggestion-info');
|
|
const suggestionBox = document.getElementById('suggestion-box');
|
|
const notifyBtn = document.getElementById('notify-btn');
|
|
const exampleButtonExisting = document.getElementById('example-button-existing');
|
|
const infoTextExisting = document.getElementById('info-text-existing');
|
|
const exampleButtonNew = document.getElementById('example-button-new');
|
|
const infoTextNew = document.getElementById('info-text-new');
|
|
const statusBar = document.getElementById('status-bar');
|
|
|
|
const onlineUsersContainer = document.getElementById('online-users-container');
|
|
const onlineUsersList = document.getElementById('online-users-list');
|
|
|
|
const deleteMarkedBtn = document.getElementById('delete-marked-btn');
|
|
const deletePasswordModal = new bootstrap.Modal(document.getElementById('delete-password-modal'));
|
|
const deletePasswordForm = document.getElementById('delete-password-form');
|
|
const deletePasswordInput = document.getElementById('delete-password-input');
|
|
const deletePasswordError = document.getElementById('delete-password-error');
|
|
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
|
|
const setDeletionPasswordForm = document.getElementById('set-deletion-password-form');
|
|
const deletionPasswordInput = document.getElementById('deletion-password');
|
|
const setDeletionPasswordStatus = document.getElementById('set-deletion-password-status');
|
|
const selectAllBtn = document.getElementById('select-all-btn');
|
|
|
|
let standardItems = [];
|
|
let translations = {};
|
|
let currentShoppingList = [];
|
|
let currentUser = {};
|
|
let socket;
|
|
let isDeletionPasswordSet = false; // New global variable
|
|
|
|
// --- WebSocket Functions ---
|
|
function connectWebSocket() {
|
|
const token = localStorage.getItem('access_token');
|
|
if (!token) return;
|
|
|
|
// Close existing socket if any
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
socket.close();
|
|
}
|
|
|
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
socket = new WebSocket(`${wsProtocol}://${window.location.host}/ws/${token}`);
|
|
|
|
socket.onopen = () => {
|
|
console.log("WebSocket connection established.");
|
|
};
|
|
|
|
socket.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'user_list') {
|
|
if (data.users && data.users.length > 1) {
|
|
onlineUsersList.textContent = data.users.join(', ');
|
|
onlineUsersContainer.style.display = 'block';
|
|
} else {
|
|
onlineUsersContainer.style.display = 'none';
|
|
}
|
|
} else if (data.type === 'update') {
|
|
fetchItems();
|
|
}
|
|
};
|
|
|
|
socket.onclose = () => {
|
|
console.log("WebSocket connection closed. Attempting to reconnect...");
|
|
setTimeout(connectWebSocket, 5000); // Reconnect after 5 seconds
|
|
};
|
|
|
|
socket.onerror = (error) => {
|
|
console.error("WebSocket error:", error);
|
|
socket.close();
|
|
};
|
|
}
|
|
|
|
// --- Auth Functions ---
|
|
|
|
function getAuthHeaders() {
|
|
const token = localStorage.getItem('access_token');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + token
|
|
};
|
|
}
|
|
|
|
async function handleLogin(event) {
|
|
event.preventDefault();
|
|
loginError.textContent = '';
|
|
|
|
const formData = new FormData();
|
|
formData.append('username', usernameInput.value);
|
|
formData.append('password', passwordInput.value);
|
|
|
|
try {
|
|
const response = await fetch('/token', {
|
|
method: 'POST',
|
|
body: new URLSearchParams(formData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
localStorage.setItem('access_token', data.access_token);
|
|
await showAppView();
|
|
} else {
|
|
loginError.textContent = translations.login_error_incorrect || 'Incorrect username or password.';
|
|
}
|
|
} catch (error) {
|
|
loginError.textContent = translations.login_error_generic || 'An error occurred. Please try again.';
|
|
}
|
|
}
|
|
|
|
function handleLogout() {
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
socket.close();
|
|
}
|
|
localStorage.removeItem('access_token');
|
|
location.reload();
|
|
}
|
|
|
|
async function handleCreateUser(event) {
|
|
event.preventDefault();
|
|
createUserStatus.textContent = '';
|
|
|
|
const newUser = {
|
|
username: newUsernameInput.value,
|
|
password: newPasswordInput.value,
|
|
is_admin: newUserIsAdminCheckbox.checked
|
|
};
|
|
|
|
const response = await fetch('/api/users', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(newUser)
|
|
});
|
|
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
const successMessage = (translations.user_created_success || "User '{username}' created successfully.").replace('{username}', newUser.username);
|
|
createUserStatus.textContent = successMessage;
|
|
createUserStatus.className = 'mt-2 text-success';
|
|
createUserForm.reset();
|
|
await fetchAndRenderUsers(); // Refresh the user list
|
|
} else {
|
|
createUserStatus.textContent = `Error: ${data.detail}`;
|
|
createUserStatus.className = 'mt-2 text-danger';
|
|
}
|
|
}
|
|
|
|
// --- View Management ---
|
|
|
|
function showLoginView() {
|
|
appView.style.display = 'none';
|
|
loginView.style.display = 'block';
|
|
adminSection.style.display = 'none';
|
|
if (userViewContent) userViewContent.style.display = 'none';
|
|
}
|
|
|
|
async function showAppView() {
|
|
loginView.style.display = 'none';
|
|
appView.style.display = 'block';
|
|
adminSection.style.display = 'none';
|
|
if (userViewContent) userViewContent.style.display = 'none';
|
|
connectWebSocket();
|
|
await initApp();
|
|
}
|
|
|
|
// --- App Functions ---
|
|
async function fetchCurrentUser() {
|
|
const response = await fetch(`/api/users/me?_=${new Date().getTime()}`, { headers: getAuthHeaders() });
|
|
if (response.status === 401) return handleLogout();
|
|
currentUser = await response.json();
|
|
|
|
if (currentUser.is_admin) {
|
|
adminSection.style.display = 'block';
|
|
userViewContent.style.display = 'none';
|
|
pageTitle.textContent = 'Admin Panel';
|
|
} else {
|
|
adminSection.style.display = 'none';
|
|
userViewContent.style.display = 'block';
|
|
// Title will be set by fetchTranslations for regular users
|
|
}
|
|
}
|
|
|
|
async function fetchTranslations() {
|
|
const lang = document.documentElement.lang;
|
|
const response = await fetch(`/api/translations/${lang}?_=${new Date().getTime()}`);
|
|
translations = await response.json();
|
|
updateUIWithTranslations();
|
|
}
|
|
|
|
|
|
function updateUIWithTranslations() {
|
|
document.title = translations.title;
|
|
|
|
// Login View
|
|
if(document.getElementById('login-title')) {
|
|
document.getElementById('login-title').textContent = translations.login_title;
|
|
document.getElementById('login-username-label').textContent = translations.login_username_label;
|
|
document.getElementById('login-password-label').textContent = translations.login_password_label;
|
|
document.getElementById('login-button').textContent = translations.login_button;
|
|
}
|
|
|
|
// User View
|
|
pageTitle.textContent = translations.title;
|
|
itemNameInput.placeholder = translations.placeholder;
|
|
addButton.textContent = translations.add_button;
|
|
addOneButton.textContent = translations.add_one_button;
|
|
notifyBtn.textContent = translations.notify_button;
|
|
exampleButtonExisting.textContent = translations.example_button_text || 'Example';
|
|
exampleButtonNew.textContent = translations.example_button_text || 'Example';
|
|
infoTextExisting.textContent = translations.info_text_existing || '';
|
|
infoTextNew.textContent = translations.info_text_new || '';
|
|
|
|
// Admin View
|
|
if (document.getElementById('admin-panel-title')) {
|
|
document.getElementById('admin-panel-title').textContent = translations.admin_panel_title;
|
|
document.getElementById('create-user-title').textContent = translations.create_user_title;
|
|
document.getElementById('username-label').textContent = translations.username_label;
|
|
document.getElementById('password-label').textContent = translations.password_label;
|
|
document.getElementById('is-admin-label').textContent = translations.is_admin_label;
|
|
document.getElementById('create-user-button').textContent = translations.create_user_button;
|
|
document.getElementById('change-password-title').textContent = translations.change_password_title;
|
|
document.getElementById('current-password-label').textContent = translations.current_password_label;
|
|
document.getElementById('new-password-label').textContent = translations.new_password_label;
|
|
document.getElementById('confirm-password-label').textContent = translations.confirm_password_label;
|
|
document.getElementById('change-password-button').textContent = translations.change_password_button;
|
|
document.getElementById('manage-users-title').textContent = translations.manage_users_title;
|
|
document.getElementById('set-deletion-password-title').textContent = translations.set_deletion_password_title;
|
|
document.getElementById('deletion-password').placeholder = translations.deletion_password_placeholder;
|
|
document.getElementById('set-deletion-password-button').textContent = translations.set_deletion_password_button;
|
|
}
|
|
|
|
// Deletion Modal
|
|
if (document.getElementById('delete-password-modal-label')) {
|
|
document.getElementById('delete-password-modal-label').textContent = translations.delete_password_modal_title;
|
|
document.getElementById('delete-password-label').textContent = translations.delete_password_label;
|
|
document.querySelector('#delete-password-modal .btn-secondary').textContent = translations.cancel_button;
|
|
document.getElementById('confirm-delete-btn').textContent = translations.delete_button;
|
|
}
|
|
|
|
// Common elements
|
|
if (document.getElementById('logout-btn')) {
|
|
document.getElementById('logout-btn').textContent = translations.logout_button;
|
|
}
|
|
|
|
}
|
|
|
|
async function fetchStandardItems() {
|
|
const lang = document.documentElement.lang;
|
|
const response = await fetch(`/api/standard-items/${lang}`);
|
|
standardItems = await response.json();
|
|
}
|
|
|
|
async function fetchItems() {
|
|
const response = await fetch('/api/items', { headers: getAuthHeaders() });
|
|
if (response.status === 401) return handleLogout();
|
|
const data = await response.json();
|
|
currentShoppingList = data.items;
|
|
itemList.innerHTML = '';
|
|
let markedItemsCount = 0;
|
|
data.items.forEach(item => {
|
|
const li = document.createElement('li');
|
|
li.className = 'list-group-item';
|
|
|
|
const itemName = document.createElement('span');
|
|
itemName.textContent = item.name;
|
|
li.appendChild(itemName);
|
|
|
|
const switchContainer = document.createElement('div');
|
|
switchContainer.className = 'form-check form-switch';
|
|
|
|
const switchInput = document.createElement('input');
|
|
switchInput.className = 'form-check-input';
|
|
switchInput.type = 'checkbox';
|
|
switchInput.checked = item.marked;
|
|
switchInput.addEventListener('change', () => toggleMarkItem(item.id));
|
|
|
|
switchContainer.appendChild(switchInput);
|
|
li.appendChild(switchContainer);
|
|
|
|
if (item.marked) {
|
|
markedItemsCount++;
|
|
}
|
|
|
|
itemList.appendChild(li);
|
|
});
|
|
|
|
if (markedItemsCount > 0) {
|
|
deleteMarkedBtn.style.display = 'block';
|
|
} else {
|
|
deleteMarkedBtn.style.display = 'none';
|
|
}
|
|
|
|
handleInputChange();
|
|
}
|
|
|
|
async function toggleMarkItem(id) {
|
|
const response = await fetch(`/api/items/${id}/mark`, {
|
|
method: 'PUT',
|
|
headers: getAuthHeaders()
|
|
});
|
|
if (response.status === 401) return handleLogout();
|
|
await fetchItems();
|
|
}
|
|
|
|
async function addItemsFromInput(inputString, split) {
|
|
if (!inputString.trim()) return;
|
|
|
|
let itemsToAdd = [];
|
|
if (split) {
|
|
itemsToAdd = inputString.split(/,/).map(item => item.trim()).filter(item => item);
|
|
} else {
|
|
itemsToAdd.push(inputString.trim());
|
|
}
|
|
|
|
if (itemsToAdd.length === 0) return;
|
|
|
|
const response = await fetch('/api/items', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({ names: itemsToAdd })
|
|
});
|
|
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
const result = await response.json();
|
|
if (result.status === 'exists' && result.added === 0) {
|
|
console.log('All items already exist.');
|
|
}
|
|
|
|
await fetchItems();
|
|
handleInputChange();
|
|
}
|
|
|
|
async function deleteItem(id) {
|
|
const response = await fetch(`/api/items/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
if (response.status === 401) return handleLogout();
|
|
if (response.status === 403) {
|
|
alert("Only admins can delete items."); // Simple feedback for non-admins
|
|
return;
|
|
}
|
|
await fetchItems();
|
|
}
|
|
|
|
function handleInputChange() {
|
|
const value = itemNameInput.value.trim().toLowerCase();
|
|
const currentShoppingListNames = currentShoppingList.map(item => item.name.toLowerCase());
|
|
|
|
if (value === '') {
|
|
suggestionBox.innerHTML = '';
|
|
suggestionInfo.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
if (suggestionInfo) suggestionInfo.style.display = 'block';
|
|
|
|
const searchTerms = value.split(/[ ,]+/).map(term => term.trim()).filter(term => term);
|
|
const lastTerm = searchTerms[searchTerms.length - 1];
|
|
|
|
if (!lastTerm) {
|
|
suggestionBox.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const newSuggestions = standardItems.filter(item =>
|
|
item.toLowerCase().includes(lastTerm) &&
|
|
!currentShoppingListNames.includes(item.toLowerCase())
|
|
);
|
|
|
|
const existingItems = currentShoppingList
|
|
.filter(item => item.name.toLowerCase().includes(lastTerm))
|
|
.map(item => item.name);
|
|
|
|
const existingStandardSuggestions = standardItems.filter(item =>
|
|
item.toLowerCase().includes(lastTerm) &&
|
|
currentShoppingListNames.includes(item.toLowerCase())
|
|
);
|
|
|
|
const allExistingItems = [...new Set([...existingItems, ...existingStandardSuggestions])];
|
|
|
|
displaySuggestions(allExistingItems, newSuggestions);
|
|
}
|
|
|
|
function displaySuggestions(existing, news) {
|
|
suggestionBox.innerHTML = '';
|
|
|
|
existing.slice(0, 10).forEach(suggestion => {
|
|
const btn = createSuggestionButton(suggestion, true);
|
|
suggestionBox.appendChild(btn);
|
|
});
|
|
|
|
news.slice(0, 10).forEach(suggestion => {
|
|
const btn = createSuggestionButton(suggestion, false);
|
|
suggestionBox.appendChild(btn);
|
|
});
|
|
}
|
|
|
|
function createSuggestionButton(suggestion, isExisting) {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = isExisting
|
|
? 'btn btn-primary btn-sm suggestion-btn'
|
|
: 'btn btn-outline-primary btn-sm suggestion-btn';
|
|
btn.textContent = suggestion;
|
|
btn.disabled = isExisting;
|
|
if (!isExisting) {
|
|
btn.onclick = () => onSuggestionClick(suggestion);
|
|
}
|
|
return btn;
|
|
}
|
|
|
|
async function onSuggestionClick(suggestion) {
|
|
const value = itemNameInput.value;
|
|
await addItemsFromInput(suggestion, false);
|
|
const lastSeparatorIndex = Math.max(value.lastIndexOf(','), value.lastIndexOf(' '));
|
|
let newValue = '';
|
|
if (lastSeparatorIndex !== -1) {
|
|
newValue = value.substring(0, lastSeparatorIndex + 1);
|
|
}
|
|
itemNameInput.value = newValue;
|
|
suggestionBox.innerHTML = '';
|
|
suggestionInfo.style.display = 'none';
|
|
itemNameInput.focus();
|
|
}
|
|
|
|
async function sendNotification() {
|
|
const lang = document.documentElement.lang;
|
|
notifyBtn.disabled = true;
|
|
notifyBtn.textContent = translations.sending_notification || 'Sending...';
|
|
|
|
try {
|
|
const response = await fetch(`/api/notify?lang=${lang}`, {
|
|
method: 'POST',
|
|
headers: getAuthHeaders()
|
|
});
|
|
if (response.status === 401) return handleLogout();
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.status.includes("success")) {
|
|
notifyBtn.textContent = translations.notification_sent || 'Notification sent!';
|
|
} else {
|
|
notifyBtn.textContent = translations.generic_notification_error || 'Error sending notification.';
|
|
}
|
|
} catch (error) {
|
|
notifyBtn.textContent = translations.generic_notification_error || 'Error sending notification.';
|
|
}
|
|
|
|
setTimeout(() => {
|
|
notifyBtn.disabled = false;
|
|
notifyBtn.textContent = translations.notify_button || 'Finalize Changes & Notify';
|
|
}, 3000);
|
|
}
|
|
|
|
async function fetchDbStatus() {
|
|
try {
|
|
const response = await fetch('/api/db-status', { headers: getAuthHeaders() });
|
|
if (response.status === 401) return handleLogout();
|
|
const data = await response.json();
|
|
if (data.error) {
|
|
statusBar.textContent = (translations.db_error || "Database error: {error}").replace("{error}", data.error);
|
|
} else {
|
|
statusBar.textContent = (translations.db_version || "Database version: {version}").replace("{version}", data.version);
|
|
}
|
|
} catch (error) {
|
|
statusBar.textContent = (translations.db_error || "Database error: {error}").replace("{error}", "Connection failed");
|
|
}
|
|
}
|
|
|
|
// New function to call the delete marked items API
|
|
async function callDeleteMarkedItemsApi(password) {
|
|
const response = await fetch('/api/items/delete-marked', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({ password: password })
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
const errorData = await response.json(); // Get the error detail from the response
|
|
if (errorData.detail === "Incorrect password") {
|
|
deletePasswordError.textContent = translations.incorrect_password;
|
|
} else {
|
|
handleLogout(); // This is a session issue
|
|
}
|
|
} else if (response.ok) {
|
|
deletePasswordModal.hide();
|
|
await fetchItems();
|
|
} else {
|
|
deletePasswordError.textContent = translations.generic_error;
|
|
}
|
|
}
|
|
|
|
deleteMarkedBtn.addEventListener('click', () => {
|
|
if (isDeletionPasswordSet) {
|
|
deletePasswordModal.show();
|
|
} else {
|
|
// If no password is set, delete directly without showing the modal.
|
|
callDeleteMarkedItemsApi('');
|
|
}
|
|
});
|
|
|
|
deletePasswordForm.addEventListener('submit', (event) => {
|
|
event.preventDefault();
|
|
confirmDeleteBtn.click();
|
|
});
|
|
|
|
confirmDeleteBtn.addEventListener('click', async () => {
|
|
const password = deletePasswordInput.value;
|
|
// Always send the password (can be empty) to the backend.
|
|
// The backend will determine if a password is required.
|
|
await callDeleteMarkedItemsApi(password);
|
|
});
|
|
|
|
// Clear password input on modal close
|
|
deletePasswordModal._element.addEventListener('hidden.bs.modal', () => {
|
|
deletePasswordInput.value = '';
|
|
deletePasswordError.textContent = '';
|
|
});
|
|
|
|
async function getDeletionPassword() {
|
|
const response = await fetch('/api/settings/deletion-password', { headers: getAuthHeaders() });
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
const data = await response.json();
|
|
isDeletionPasswordSet = data.is_set; // Update the global variable
|
|
if (data.is_set) {
|
|
document.getElementById('password-set-message').style.display = 'block';
|
|
document.getElementById('set-deletion-password-form').style.display = 'none';
|
|
} else {
|
|
document.getElementById('password-set-message').style.display = 'none';
|
|
document.getElementById('set-deletion-password-form').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
document.getElementById('change-password-btn').addEventListener('click', () => {
|
|
document.getElementById('password-set-message').style.display = 'none';
|
|
document.getElementById('set-deletion-password-form').style.display = 'block';
|
|
});
|
|
|
|
document.getElementById('delete-password-btn').addEventListener('click', async () => {
|
|
const response = await fetch('/api/settings/deletion-password', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({ password: '' })
|
|
});
|
|
|
|
if (response.ok) {
|
|
await getDeletionPassword();
|
|
}
|
|
});
|
|
|
|
async function setDeletionPassword(event) {
|
|
event.preventDefault();
|
|
const password = deletionPasswordInput.value;
|
|
|
|
const response = await fetch('/api/settings/deletion-password', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({ password: password })
|
|
});
|
|
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
if (response.ok) {
|
|
setDeletionPasswordStatus.textContent = translations.password_set_success;
|
|
setDeletionPasswordStatus.className = 'mt-2 text-success';
|
|
await getDeletionPassword();
|
|
} else {
|
|
const data = await response.json();
|
|
setDeletionPasswordStatus.textContent = `Error: ${data.detail}`;
|
|
setDeletionPasswordStatus.className = 'mt-2 text-danger';
|
|
}
|
|
}
|
|
|
|
async function selectAllItems() {
|
|
const allMarked = currentShoppingList.every(item => item.marked);
|
|
|
|
for (const item of currentShoppingList) {
|
|
if (allMarked || !item.marked) {
|
|
await toggleMarkItem(item.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
loginForm.addEventListener('submit', handleLogin);
|
|
logoutBtn.addEventListener('click', handleLogout);
|
|
createUserForm.addEventListener('submit', handleCreateUser);
|
|
addItemForm.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
await addItemsFromInput(itemNameInput.value, true);
|
|
itemNameInput.value = '';
|
|
suggestionBox.innerHTML = '';
|
|
suggestionInfo.style.display = 'none';
|
|
});
|
|
|
|
addOneButton.addEventListener('click', async () => {
|
|
await addItemsFromInput(itemNameInput.value, false);
|
|
itemNameInput.value = '';
|
|
suggestionBox.innerHTML = '';
|
|
suggestionInfo.style.display = 'none';
|
|
});
|
|
|
|
itemNameInput.addEventListener('input', handleInputChange);
|
|
|
|
notifyBtn.addEventListener('click', sendNotification);
|
|
|
|
selectAllBtn.addEventListener('click', selectAllItems);
|
|
|
|
setDeletionPasswordForm.addEventListener('submit', setDeletionPassword);
|
|
|
|
const toggleDeletionPassword = document.getElementById('toggle-deletion-password');
|
|
toggleDeletionPassword.addEventListener('click', () => {
|
|
const passwordInput = document.getElementById('deletion-password');
|
|
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
|
passwordInput.setAttribute('type', type);
|
|
|
|
const eyeIcon = toggleDeletionPassword.querySelector('.bi-eye');
|
|
const eyeSlashIcon = toggleDeletionPassword.querySelector('.bi-eye-slash');
|
|
|
|
if (type === 'password') {
|
|
eyeIcon.style.display = 'block';
|
|
eyeSlashIcon.style.display = 'none';
|
|
} else {
|
|
eyeIcon.style.display = 'none';
|
|
eyeSlashIcon.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
// --- Initialisierung ---
|
|
async function initApp() {
|
|
// NOTE: Translations are now fetched before this function is called.
|
|
await fetchCurrentUser();
|
|
const promises = [
|
|
fetchStandardItems(),
|
|
fetchItems(),
|
|
fetchDbStatus(),
|
|
getDeletionPassword()
|
|
];
|
|
if (currentUser.is_admin) {
|
|
promises.push(fetchAndRenderUsers());
|
|
}
|
|
const results = await Promise.allSettled(promises);
|
|
results.forEach((result, i) => {
|
|
if (result.status === 'rejected') {
|
|
console.error(`Error in initApp data fetch step ${i}:`, result.reason);
|
|
}
|
|
});
|
|
|
|
if (suggestionInfo) suggestionInfo.style.display = 'none';
|
|
}
|
|
|
|
|
|
// --- User Management Logic ---
|
|
const userList = document.getElementById('user-list');
|
|
|
|
async function unlockUser(userId, username) {
|
|
// TODO: Add translation for the confirmation message
|
|
if (!confirm(`Are you sure you want to unlock the user "${username}"?`)) {
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`/api/users/${userId}/unlock`, {
|
|
method: 'PUT',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
if (response.ok) {
|
|
await fetchAndRenderUsers();
|
|
} else {
|
|
const data = await response.json();
|
|
alert(`Error: ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function deleteUser(userId, username) {
|
|
if (!confirm(`Are you sure you want to delete the user "${username}"?`)) {
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`/api/users/${userId}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
if (response.ok) {
|
|
await fetchAndRenderUsers();
|
|
} else {
|
|
const data = await response.json();
|
|
alert(`Error: ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function fetchAndRenderUsers() {
|
|
if (!userList || !currentUser.is_admin) return;
|
|
|
|
const response = await fetch('/api/users', { headers: getAuthHeaders() });
|
|
if (response.status === 401) return handleLogout();
|
|
if (response.status === 403) return; // Non-admin users will get a 403 error
|
|
|
|
const users = await response.json();
|
|
userList.innerHTML = '';
|
|
|
|
users.forEach(user => {
|
|
const li = document.createElement('li');
|
|
li.className = 'list-group-item d-flex justify-content-between align-items-center';
|
|
|
|
const userInfo = document.createElement('span');
|
|
userInfo.textContent = `${user.username}`;
|
|
|
|
if (user.is_admin) {
|
|
const adminBadge = document.createElement('span');
|
|
adminBadge.className = 'badge bg-primary rounded-pill ms-2';
|
|
adminBadge.textContent = 'Admin';
|
|
userInfo.appendChild(adminBadge);
|
|
}
|
|
|
|
if (user.is_locked) {
|
|
const lockedBadge = document.createElement('span');
|
|
// TODO: Add translation for "Locked"
|
|
lockedBadge.className = 'badge bg-warning text-dark rounded-pill ms-2';
|
|
lockedBadge.textContent = 'Locked';
|
|
userInfo.appendChild(lockedBadge);
|
|
}
|
|
|
|
li.appendChild(userInfo);
|
|
|
|
const buttonContainer = document.createElement('div');
|
|
|
|
if (currentUser && currentUser.id !== user.id) {
|
|
if (user.is_locked) {
|
|
const unlockBtn = document.createElement('button');
|
|
unlockBtn.className = 'btn btn-success btn-sm';
|
|
// TODO: Add translation for "Unlock"
|
|
unlockBtn.textContent = 'Unlock';
|
|
unlockBtn.onclick = () => unlockUser(user.id, user.username);
|
|
buttonContainer.appendChild(unlockBtn);
|
|
} else {
|
|
const deleteBtn = document.createElement('button');
|
|
deleteBtn.className = 'btn btn-danger btn-sm ms-2';
|
|
deleteBtn.textContent = translations.delete_button || 'Delete';
|
|
deleteBtn.onclick = () => deleteUser(user.id, user.username);
|
|
buttonContainer.appendChild(deleteBtn);
|
|
}
|
|
}
|
|
|
|
li.appendChild(buttonContainer);
|
|
userList.appendChild(li);
|
|
});
|
|
}
|
|
|
|
// --- Change Password Logic ---
|
|
// --- Main Application Entry Point ---
|
|
async function main() {
|
|
// Always fetch translations first, so all UI is translated before showing.
|
|
await fetchTranslations();
|
|
|
|
// Now decide which view to show
|
|
if (localStorage.getItem('access_token')) {
|
|
showAppView();
|
|
} else {
|
|
showLoginView();
|
|
}
|
|
}
|
|
|
|
main(); // Run the app
|
|
|
|
const themeSwitcher = document.getElementById('theme-switcher');
|
|
themeSwitcher.addEventListener('click', () => {
|
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
document.documentElement.setAttribute('data-theme', newTheme);
|
|
localStorage.setItem('theme', newTheme);
|
|
themeSwitcher.textContent = newTheme === 'dark' ? '☀️' : '🌙';
|
|
});
|
|
|
|
// Set initial theme switcher icon
|
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
if (currentTheme === 'dark') {
|
|
themeSwitcher.textContent = '☀️';
|
|
} else {
|
|
themeSwitcher.textContent = '🌙';
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
</html>
|