Files
baby-monitor/src/baby_monitor/static/settings.html
T
Brian Bjarke Jensen ec0e1f6140
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m3s
Python Code Quality / python-code-quality (pull_request) Successful in 10s
Python Test / python-test (pull_request) Successful in 17s
code quality changes
2026-01-27 21:21:55 +01:00

1057 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Settings - Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: var(--bg-primary);
min-height: 100vh;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.container {
background: var(--bg-secondary);
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
}
h1 {
color: var(--text-primary);
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
margin-bottom: 30px;
font-size: 14px;
}
.section {
margin-bottom: 40px;
padding: 20px;
background: var(--bg-tertiary);
border-radius: 8px;
}
.section h2 {
color: var(--text-primary);
font-size: 20px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
font-weight: 600;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color, #ddd);
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
background-color: var(--input-bg);
color: var(--input-text);
}
.form-group input:focus {
outline: none;
border-color: var(--input-focus-border);
}
.form-group input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.button {
background: var(--gradient-purple);
background-color: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition:
transform 0.2s,
box-shadow 0.2s;
text-decoration: none;
display: inline-block;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.button.secondary {
background: #6c757d;
}
.button.secondary:hover {
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.4);
}
.button.success {
background: var(--gradient-success);
}
.button.success:hover {
box-shadow: 0 4px 12px rgba(86, 171, 47, 0.4);
}
.children-list {
display: grid;
gap: 15px;
margin-top: 20px;
}
.child-card {
background: var(--bg-secondary);
padding: 20px;
border-radius: 8px;
box-shadow: var(--shadow-md);
}
.child-card h3 {
color: var(--text-primary);
margin-bottom: 10px;
font-size: 18px;
}
.child-info {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 15px;
}
.child-info p {
margin-bottom: 5px;
}
.child-actions {
display: flex;
gap: 10px;
}
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
}
.alert {
padding: 12px 20px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.hidden {
display: none;
}
.quick-action {
display: block;
width: 100%;
padding: 15px;
margin-bottom: 15px;
background: var(--bg-secondary);
border: 2px solid #667eea;
color: #667eea;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-align: left;
}
.quick-action:hover {
background: #667eea;
color: white;
transform: translateX(5px);
}
</style>
</head>
<body>
<div class="container">
<h1>⚙️ Settings</h1>
<p class="subtitle">Manage your account and children</p>
<div id="alert" class="hidden"></div>
<div class="section">
<h2>👤 Account Information</h2>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" disabled />
</div>
</div>
<div class="section">
<h2>🌓 Theme Preference</h2>
<p
style="
color: var(--text-secondary);
margin-bottom: 20px;
font-size: 14px;
"
>
Choose how Baby Monitor looks to you.
</p>
<div class="form-group">
<div style="display: flex; flex-direction: column; gap: 15px">
<label
style="
display: flex;
align-items: center;
cursor: pointer;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
transition: all 0.2s;
"
class="theme-option"
data-theme="light"
>
<input
type="radio"
name="theme"
value="light"
style="
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
"
/>
<div>
<div
style="
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
"
>
☀️ Light Mode
</div>
<div style="font-size: 12px; color: var(--text-secondary)">
Always use light theme
</div>
</div>
</label>
<label
style="
display: flex;
align-items: center;
cursor: pointer;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
transition: all 0.2s;
"
class="theme-option"
data-theme="dark"
>
<input
type="radio"
name="theme"
value="dark"
style="
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
"
/>
<div>
<div
style="
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
"
>
🌙 Dark Mode
</div>
<div style="font-size: 12px; color: var(--text-secondary)">
Always use dark theme
</div>
</div>
</label>
<label
style="
display: flex;
align-items: center;
cursor: pointer;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
transition: all 0.2s;
"
class="theme-option"
data-theme="auto"
>
<input
type="radio"
name="theme"
value="auto"
style="
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
"
/>
<div>
<div
style="
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
"
>
🔄 Auto
</div>
<div style="font-size: 12px; color: var(--text-secondary)">
Match your browser settings
</div>
</div>
</label>
</div>
</div>
</div>
<div class="section">
<h2>🔑 Change Password</h2>
<form id="userForm">
<div class="form-group">
<label for="currentPassword">Current Password</label>
<input type="password" id="currentPassword" />
</div>
<div class="form-group">
<label for="newPassword">New Password</label>
<input type="password" id="newPassword" />
</div>
<div class="form-group">
<label for="confirmPassword">Confirm New Password</label>
<input type="password" id="confirmPassword" />
</div>
<button type="submit" class="button" id="saveUserBtn">
💾 Save Changes
</button>
</form>
<div
style="
margin-top: 40px;
padding-top: 30px;
border-top: 2px solid #dc3545;
"
>
<h3 style="color: #dc3545; margin-bottom: 15px">⚠️ Danger Zone</h3>
<p
style="
color: var(--text-secondary);
margin-bottom: 15px;
font-size: 14px;
"
>
Deleting your account is permanent and cannot be undone. This will:
</p>
<ul
style="
color: var(--text-secondary);
margin-bottom: 20px;
margin-left: 20px;
font-size: 14px;
"
>
<li>Delete your account permanently</li>
<li>Delete all children that have no other parents</li>
<li>
Delete all logs (feedings, diaper changes, sleep) for orphaned
children
</li>
<li>
Remove you as a parent from shared children (they will remain for
other parents)
</li>
</ul>
<button
class="button"
style="background: var(--gradient-error)"
onclick="confirmDeleteAccount()"
>
🗑️ Delete Account
</button>
</div>
</div>
<div class="section">
<h2>👶 Your Children</h2>
<button
class="quick-action"
onclick="window.location.href = '/add-child.html'"
>
Add New Child
</button>
<button
class="quick-action"
onclick="showRedeemCodeModal()"
style="border-color: #56ab2f; color: #56ab2f"
>
🔗 Redeem Share Code
</button>
<div id="childrenContent" class="loading">
<p>Loading children...</p>
</div>
</div>
</div>
<!-- Redeem Code Modal -->
<div
class="modal"
id="redeemCodeModal"
style="
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
"
>
<div
style="
background: var(--bg-secondary);
padding: 30px;
border-radius: 12px;
max-width: 400px;
width: 90%;
"
>
<h2 style="margin-top: 0; color: var(--text-primary)">
🔗 Redeem Share Code
</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px">
Enter the share code to get access to a child.
</p>
<form id="redeemCodeForm">
<div class="form-group">
<label for="shareCode">Share Code</label>
<input
type="text"
id="shareCode"
placeholder="Enter code"
required
style="
width: 100%;
padding: 12px;
border: 2px solid var(--border-color, #ddd);
border-radius: 6px;
font-size: 14px;
background-color: var(--input-bg);
color: var(--input-text);
"
/>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px">
<button
type="button"
class="button secondary"
onclick="closeRedeemCodeModal()"
style="flex: 1"
>
Cancel
</button>
<button type="submit" class="button success" style="flex: 1">
Redeem
</button>
</div>
</form>
</div>
</div>
<!-- Share Code Modal -->
<div
class="modal"
id="shareCodeModal"
style="
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
"
>
<div
style="
background: var(--bg-secondary);
padding: 30px;
border-radius: 12px;
max-width: 400px;
width: 90%;
"
>
<h2 style="margin-top: 0; color: var(--text-primary)">🔗 Share Code</h2>
<p
style="color: var(--text-secondary); margin-bottom: 20px"
id="shareCodeChildName"
></p>
<div
style="
background: var(--bg-tertiary);
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
"
>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
"
>
<span
style="
font-size: 24px;
font-weight: bold;
color: var(--text-primary);
letter-spacing: 2px;
"
id="shareCodeValue"
></span>
<button
onclick="copyShareCode()"
style="
background: #667eea;
color: white;
border: none;
padding: 10px 15px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
"
>
📋 Copy
</button>
</div>
</div>
<p
style="
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 20px;
"
>
Share this code with another user. They can use it in Settings >
Redeem Share Code to get access to this child.
<br /><br />
This code expires in 7 days.
</p>
<button
type="button"
class="button"
onclick="closeShareCodeModal()"
style="width: 100%"
>
Close
</button>
</div>
</div>
<script src="/menu.js"></script>
<script>
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
// Initialize burger menu
initBurgerMenu({ includeHome: true });
// Initialize theme selector
function initThemeSelector() {
const currentTheme = window.getThemePreference();
const themeRadio = document.querySelector(
`input[name="theme"][value="${currentTheme}"]`,
);
if (themeRadio) {
themeRadio.checked = true;
}
// Add visual feedback for selected theme option
updateThemeOptionStyles();
// Listen for theme changes
document.querySelectorAll('input[name="theme"]').forEach((radio) => {
radio.addEventListener("change", (e) => {
window.setTheme(e.target.value);
updateThemeOptionStyles();
});
});
}
function updateThemeOptionStyles() {
const selectedValue = document.querySelector(
'input[name="theme"]:checked',
)?.value;
document.querySelectorAll(".theme-option").forEach((option) => {
const isSelected = option.dataset.theme === selectedValue;
if (isSelected) {
option.style.borderColor = "#667eea";
option.style.background = "rgba(102, 126, 234, 0.05)";
} else {
option.style.borderColor = "#e0e0e0";
option.style.background = "transparent";
}
});
}
// Initialize theme selector on load
initThemeSelector();
let currentUser = null;
let children = [];
// Load user data and children
async function loadData() {
try {
// Fetch user info
const userResponse = await fetch("/api/me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (userResponse.ok) {
currentUser = await userResponse.json();
document.getElementById("username").value = currentUser.username;
} else {
window.location.href = "/login.html";
return;
}
// Fetch children
const childrenResponse = await fetch("/api/children", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (childrenResponse.ok) {
children = await childrenResponse.json();
displayChildren();
} else {
showAlert("Failed to load children", "error");
}
} catch (error) {
console.error("Error loading data:", error);
showAlert("Network error. Please try again.", "error");
}
}
function displayChildren() {
const container = document.getElementById("childrenContent");
if (children.length === 0) {
container.innerHTML = `
<p style="color: #666; text-align: center; padding: 20px;">
No children added yet. Click "Add New Child" to get started.
</p>
`;
return;
}
container.innerHTML = `
<div class="children-list">
${children
.map((child) => {
// Get list of other users (excluding current user)
const otherUsers =
child.parent_usernames && child.parent_usernames.length > 1
? child.parent_usernames.filter(
(username) => username !== currentUser.username,
)
: [];
return `
<div class="child-card">
<h3 style="margin: 0;">👶 ${child.name}</h3>
<div class="child-info">
<p><strong>Birth Date:</strong> ${formatDate(child.birth_time)}</p>
<p><strong>Birth Weight:</strong> ${child.birth_weight}g</p>
<p><strong>Added:</strong> ${formatDate(child.created_at)}</p>
${otherUsers.length > 0 ? `<p><strong>Shared with:</strong> ${otherUsers.join(", ")}</p>` : ""}
</div>
<div class="child-actions">
<button class="button secondary" onclick="editChild(${child.id})">
✏️ Edit
</button>
<button class="button success" onclick="shareChild(${child.id})">
🔗 Share
</button>
</div>
</div>
`;
})
.join("")}
</div>
`;
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
function editChild(childId) {
const child = children.find((c) => c.id === childId);
if (!child) return;
// Redirect to add-child page with child data in query params
const params = new URLSearchParams({
id: child.id,
name: child.name,
birth_time: child.birth_time,
birth_weight: child.birth_weight,
});
window.location.href = `/add-child.html?${params.toString()}`;
}
async function shareChild(childId) {
const child = children.find((c) => c.id === childId);
if (!child) return;
try {
const response = await fetch("/api/children/invitations", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
child_id: childId,
}),
});
if (!response.ok) {
throw new Error("Failed to create invitation");
}
const invitation = await response.json();
// Show the share code modal
document.getElementById("shareCodeChildName").textContent =
`Share code for ${child.name}`;
document.getElementById("shareCodeValue").textContent =
invitation.code;
window.currentShareCode = invitation.code; // Store for copy function
document.getElementById("shareCodeModal").style.display = "flex";
} catch (error) {
console.error("Error creating share code:", error);
showAlert("Failed to create share code. Please try again.", "error");
}
}
function closeShareCodeModal() {
document.getElementById("shareCodeModal").style.display = "none";
window.currentShareCode = null;
}
function copyShareCode() {
const code = window.currentShareCode;
if (!code) return;
if (navigator.clipboard) {
navigator.clipboard
.writeText(code)
.then(() => {
showAlert("Share code copied to clipboard!", "success");
})
.catch((err) => {
console.error("Failed to copy to clipboard:", err);
showAlert(
"Failed to copy. Please copy the code manually.",
"error",
);
});
} else {
// Fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = code;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
showAlert("Share code copied to clipboard!", "success");
} catch (err) {
showAlert(
"Failed to copy. Please copy the code manually.",
"error",
);
}
document.body.removeChild(textArea);
}
}
function showRedeemCodeModal() {
document.getElementById("redeemCodeModal").style.display = "flex";
}
function closeRedeemCodeModal() {
document.getElementById("redeemCodeModal").style.display = "none";
document.getElementById("redeemCodeForm").reset();
}
document
.getElementById("redeemCodeForm")
.addEventListener("submit", async (e) => {
e.preventDefault();
const code = document.getElementById("shareCode").value.trim();
try {
const response = await fetch("/api/children/invitations/redeem", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ code }),
});
if (!response.ok) {
const error = await response.json();
// Show the specific error message from the backend in a popup
alert(error.detail || "Failed to redeem code");
return;
}
const result = await response.json();
alert(result.message);
closeRedeemCodeModal();
// Reload children list
await loadData();
} catch (error) {
console.error("Error redeeming code:", error);
// Display the actual error message in a popup
alert(error.message);
}
});
async function confirmDeleteAccount() {
const confirmed = confirm(
"⚠️ WARNING: This action cannot be undone!\n\n" +
"Are you absolutely sure you want to delete your account?\n\n" +
"This will:\n" +
"• Delete your account permanently\n" +
"• Delete all children that have no other parents\n" +
"• Delete ALL logs for orphaned children\n" +
"• Remove you as a parent from shared children\n\n" +
'Type "DELETE" in the next prompt to confirm.',
);
if (!confirmed) {
return;
}
const confirmText = prompt("Type DELETE to confirm account deletion:");
if (confirmText !== "DELETE") {
showAlert(
"Account deletion cancelled. You must type DELETE exactly.",
"info",
);
return;
}
try {
const response = await fetch("/api/account", {
method: "DELETE",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (!response.ok) {
throw new Error("Failed to delete account");
}
const result = await response.json();
// Show deletion summary
let message = "✅ Account deleted successfully!\n\n";
if (result.deleted_children && result.deleted_children.length > 0) {
message += `Deleted children: ${result.deleted_children.join(", ")}\n`;
}
if (result.deleted_logs) {
message += `\nDeleted logs:\n`;
message += `• Feedings: ${result.deleted_logs.feedings}\n`;
message += `• Diaper Changes: ${result.deleted_logs.diaper_changes}\n`;
message += `• Sleep Sessions: ${result.deleted_logs.sleep_sessions}`;
}
alert(message);
// Logout and redirect
localStorage.removeItem("token");
window.location.href = "/login.html";
} catch (error) {
console.error("Error deleting account:", error);
showAlert("Failed to delete account. Please try again.", "error");
}
}
function showAlert(message, type) {
const alert = document.getElementById("alert");
alert.className = `alert alert-${type}`;
alert.textContent = message;
alert.classList.remove("hidden");
setTimeout(() => {
alert.classList.add("hidden");
}, 5000);
}
// Handle user form submission
document
.getElementById("userForm")
.addEventListener("submit", async (e) => {
e.preventDefault();
const currentPassword =
document.getElementById("currentPassword").value;
const newPassword = document.getElementById("newPassword").value;
const confirmPassword =
document.getElementById("confirmPassword").value;
// Validate passwords
if (newPassword && newPassword !== confirmPassword) {
alert("New passwords do not match");
return;
}
if (newPassword && !currentPassword) {
alert("Current password is required to change password");
return;
}
// If no password change requested, just show success
if (!newPassword) {
alert("No changes to save");
return;
}
const saveBtn = document.getElementById("saveUserBtn");
saveBtn.disabled = true;
saveBtn.textContent = "Saving...";
try {
const response = await fetch("/api/change-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Failed to change password");
}
const result = await response.json();
alert(result.message);
// Clear form
document.getElementById("userForm").reset();
} catch (error) {
console.error("Error updating password:", error);
alert(error.message || "Failed to update password");
} finally {
saveBtn.disabled = false;
saveBtn.textContent = "💾 Save Changes";
}
});
// Load data on page load
loadData();
</script>
</body>
</html>