More vibin

This commit is contained in:
Tim Zöller 2025-11-28 21:04:38 +01:00
parent 1901daf5ce
commit c1729a629d
47 changed files with 5754 additions and 41 deletions

View file

@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}">
<head>
<title>Login</title>
</head>
<body>
<div layout:fragment="content">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card shadow-sm">
<div class="card-body p-5">
<h2 class="text-center mb-4">
<i class="bi bi-box-arrow-in-right text-primary"></i>
Sign In
</h2>
<p class="text-muted text-center mb-4">
Welcome back to FitPub
</p>
<!-- Login Form -->
<form id="loginForm">
<!-- Error Alert -->
<div id="errorAlert" class="alert alert-danger d-none" role="alert">
<i class="bi bi-exclamation-triangle-fill"></i>
<span id="errorMessage"></span>
</div>
<!-- Username or Email -->
<div class="mb-3">
<label for="usernameOrEmail" class="form-label">
Username or Email
</label>
<input type="text"
class="form-control form-control-lg"
id="usernameOrEmail"
name="usernameOrEmail"
placeholder="Enter username or email"
required
autocomplete="username">
<div class="invalid-feedback">
Please enter your username or email.
</div>
</div>
<!-- Password -->
<div class="mb-4">
<label for="password" class="form-label">
Password
</label>
<input type="password"
class="form-control form-control-lg"
id="password"
name="password"
placeholder="Enter password"
required
autocomplete="current-password">
<div class="invalid-feedback">
Please enter your password.
</div>
</div>
<!-- Remember Me (Optional for future) -->
<div class="mb-3 form-check">
<input type="checkbox"
class="form-check-input"
id="rememberMe"
name="rememberMe">
<label class="form-check-label" for="rememberMe">
Remember me
</label>
</div>
<!-- Submit Button -->
<div class="d-grid mb-3">
<button type="submit" class="btn btn-primary btn-lg" id="loginBtn">
<span id="loginBtnText">
<i class="bi bi-box-arrow-in-right"></i> Sign In
</span>
<span id="loginBtnSpinner" class="d-none">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Signing in...
</span>
</button>
</div>
</form>
<!-- Divider -->
<div class="text-center my-3">
<hr>
</div>
<!-- Register Link -->
<div class="text-center">
<p class="text-muted mb-0">
Don't have an account?
<a th:href="@{/register}" class="text-decoration-none fw-bold">Create one</a>
</p>
</div>
</div>
</div>
<!-- Help Box -->
<div class="card border-0 bg-light mt-4">
<div class="card-body">
<h6><i class="bi bi-question-circle text-primary"></i> Need Help?</h6>
<p class="text-muted small mb-0">
Forgot your password? Contact your instance administrator or create a new account.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Custom Scripts -->
<th:block layout:fragment="scripts">
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('loginForm');
const loginBtn = document.getElementById('loginBtn');
const loginBtnText = document.getElementById('loginBtnText');
const loginBtnSpinner = document.getElementById('loginBtnSpinner');
const errorAlert = document.getElementById('errorAlert');
const errorMessage = document.getElementById('errorMessage');
// Form submission
form.addEventListener('submit', async function(e) {
e.preventDefault();
// Validate form
if (!form.checkValidity()) {
form.classList.add('was-validated');
return;
}
// Hide error alert
errorAlert.classList.add('d-none');
// Show loading state
loginBtn.disabled = true;
loginBtnText.classList.add('d-none');
loginBtnSpinner.classList.remove('d-none');
// Prepare request data
const formData = {
usernameOrEmail: document.getElementById('usernameOrEmail').value,
password: document.getElementById('password').value
};
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (response.ok) {
// Store JWT token
localStorage.setItem('jwtToken', data.token);
localStorage.setItem('username', data.username);
// Redirect to activities page
window.location.href = '/activities';
} else {
// Show error message
errorMessage.textContent = data.message || 'Invalid username/email or password.';
errorAlert.classList.remove('d-none');
// Reset button state
loginBtn.disabled = false;
loginBtnText.classList.remove('d-none');
loginBtnSpinner.classList.add('d-none');
}
} catch (error) {
console.error('Login error:', error);
errorMessage.textContent = 'An unexpected error occurred. Please try again.';
errorAlert.classList.remove('d-none');
// Reset button state
loginBtn.disabled = false;
loginBtnText.classList.remove('d-none');
loginBtnSpinner.classList.add('d-none');
}
});
});
</script>
</th:block>
</body>
</html>

View file

@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}">
<head>
<title>Register</title>
</head>
<body>
<div layout:fragment="content">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow-sm">
<div class="card-body p-5">
<h2 class="text-center mb-4">
<i class="bi bi-person-plus-fill text-primary"></i>
Create Account
</h2>
<p class="text-muted text-center mb-4">
Join the federated fitness community
</p>
<!-- Registration Form -->
<form id="registerForm">
<!-- Error Alert -->
<div id="errorAlert" class="alert alert-danger d-none" role="alert">
<i class="bi bi-exclamation-triangle-fill"></i>
<span id="errorMessage"></span>
</div>
<!-- Success Alert -->
<div id="successAlert" class="alert alert-success d-none" role="alert">
<i class="bi bi-check-circle-fill"></i>
Registration successful! Redirecting to login...
</div>
<!-- Username -->
<div class="mb-3">
<label for="username" class="form-label">
Username <span class="text-danger">*</span>
</label>
<input type="text"
class="form-control"
id="username"
name="username"
placeholder="Choose a username"
required
minlength="3"
maxlength="30"
pattern="[a-zA-Z0-9_]+"
autocomplete="username">
<div class="form-text">
3-30 characters. Letters, numbers, and underscores only.
</div>
<div class="invalid-feedback">
Please provide a valid username.
</div>
</div>
<!-- Email -->
<div class="mb-3">
<label for="email" class="form-label">
Email <span class="text-danger">*</span>
</label>
<input type="email"
class="form-control"
id="email"
name="email"
placeholder="your@email.com"
required
autocomplete="email">
<div class="invalid-feedback">
Please provide a valid email address.
</div>
</div>
<!-- Display Name -->
<div class="mb-3">
<label for="displayName" class="form-label">
Display Name
</label>
<input type="text"
class="form-control"
id="displayName"
name="displayName"
placeholder="Your name (optional)"
maxlength="100"
autocomplete="name">
<div class="form-text">
This is how your name will appear to others.
</div>
</div>
<!-- Password -->
<div class="mb-3">
<label for="password" class="form-label">
Password <span class="text-danger">*</span>
</label>
<input type="password"
class="form-control"
id="password"
name="password"
placeholder="Create a strong password"
required
minlength="8"
autocomplete="new-password">
<div class="form-text">
At least 8 characters.
</div>
<div class="invalid-feedback">
Password must be at least 8 characters.
</div>
</div>
<!-- Confirm Password -->
<div class="mb-4">
<label for="confirmPassword" class="form-label">
Confirm Password <span class="text-danger">*</span>
</label>
<input type="password"
class="form-control"
id="confirmPassword"
name="confirmPassword"
placeholder="Confirm your password"
required
minlength="8"
autocomplete="new-password">
<div class="invalid-feedback">
Passwords do not match.
</div>
</div>
<!-- Submit Button -->
<div class="d-grid mb-3">
<button type="submit" class="btn btn-primary btn-lg" id="registerBtn">
<span id="registerBtnText">
<i class="bi bi-person-plus"></i> Create Account
</span>
<span id="registerBtnSpinner" class="d-none">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Creating account...
</span>
</button>
</div>
</form>
<!-- Login Link -->
<div class="text-center">
<p class="text-muted mb-0">
Already have an account?
<a th:href="@{/login}" class="text-decoration-none">Sign in</a>
</p>
</div>
</div>
</div>
<!-- Info Box -->
<div class="card border-0 bg-light mt-4">
<div class="card-body">
<h6><i class="bi bi-info-circle text-primary"></i> About FitPub</h6>
<p class="text-muted small mb-0">
FitPub is a federated fitness tracking platform. Your account can interact with
users on Mastodon, Pleroma, and other ActivityPub-compatible platforms.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Custom Scripts -->
<th:block layout:fragment="scripts">
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('registerForm');
const registerBtn = document.getElementById('registerBtn');
const registerBtnText = document.getElementById('registerBtnText');
const registerBtnSpinner = document.getElementById('registerBtnSpinner');
const errorAlert = document.getElementById('errorAlert');
const successAlert = document.getElementById('successAlert');
const errorMessage = document.getElementById('errorMessage');
// Password confirmation validation
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirmPassword');
confirmPassword.addEventListener('input', function() {
if (password.value !== confirmPassword.value) {
confirmPassword.setCustomValidity('Passwords do not match');
} else {
confirmPassword.setCustomValidity('');
}
});
// Form submission
form.addEventListener('submit', async function(e) {
e.preventDefault();
// Validate form
if (!form.checkValidity()) {
form.classList.add('was-validated');
return;
}
// Hide alerts
errorAlert.classList.add('d-none');
successAlert.classList.add('d-none');
// Show loading state
registerBtn.disabled = true;
registerBtnText.classList.add('d-none');
registerBtnSpinner.classList.remove('d-none');
// Prepare request data
const formData = {
username: document.getElementById('username').value,
email: document.getElementById('email').value,
password: document.getElementById('password').value,
displayName: document.getElementById('displayName').value || null
};
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (response.ok) {
// Store JWT token
localStorage.setItem('jwtToken', data.token);
localStorage.setItem('username', data.username);
// Show success message
successAlert.classList.remove('d-none');
// Redirect to activities page after 1.5 seconds
setTimeout(() => {
window.location.href = '/activities';
}, 1500);
} else {
// Show error message
errorMessage.textContent = data.message || 'Registration failed. Please try again.';
errorAlert.classList.remove('d-none');
// Reset button state
registerBtn.disabled = false;
registerBtnText.classList.remove('d-none');
registerBtnSpinner.classList.add('d-none');
}
} catch (error) {
console.error('Registration error:', error);
errorMessage.textContent = 'An unexpected error occurred. Please try again.';
errorAlert.classList.remove('d-none');
// Reset button state
registerBtn.disabled = false;
registerBtnText.classList.remove('d-none');
registerBtnSpinner.classList.add('d-none');
}
});
});
</script>
</th:block>
</body>
</html>