Add initial project
This commit is contained in:
+4334
File diff suppressed because it is too large
Load Diff
+2007
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - HabitForge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: { 500: '#0ea5e9', 600: '#0284c7' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white flex items-center justify-center min-h-screen">
|
||||
|
||||
<div class="w-full max-w-md p-8 bg-white dark:bg-gray-800 rounded-2xl shadow-xl mx-4">
|
||||
<div class="text-center mb-8">
|
||||
<i class="fa-solid fa-fire text-brand-500 text-3xl mb-2 animate-pulse"></i>
|
||||
<h1 class="text-2xl font-bold">HabitForge</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400 mt-2">Sign in to track your progress</p>
|
||||
</div>
|
||||
|
||||
<div class="flex border-b border-gray-200 dark:border-gray-700 mb-6">
|
||||
<button id="tab-login"
|
||||
class="flex-1 py-2 font-medium text-brand-600 border-b-2 border-brand-600 focus:outline-none">Login</button>
|
||||
<button id="tab-register"
|
||||
class="flex-1 py-2 font-medium text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none">Register</button>
|
||||
</div>
|
||||
|
||||
<form id="auth-form" class="space-y-4">
|
||||
<div id="error-msg" class="hidden bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
|
||||
role="alert">
|
||||
<span class="block sm:inline" id="error-text"></span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Username</label>
|
||||
<input type="text" name="username" required
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Password</label>
|
||||
<input type="password" name="password" required
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full bg-brand-600 hover:bg-brand-500 text-white font-medium py-2 rounded-lg transition-colors shadow-lg mt-4">
|
||||
<span id="btn-text">Sign In</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('auth-form');
|
||||
const tabLogin = document.getElementById('tab-login');
|
||||
const tabRegister = document.getElementById('tab-register');
|
||||
const btnText = document.getElementById('btn-text');
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
const errorText = document.getElementById('error-text');
|
||||
|
||||
let isLogin = true;
|
||||
|
||||
function toggleTab(login) {
|
||||
isLogin = login;
|
||||
if (login) {
|
||||
tabLogin.classList.add('text-brand-600', 'border-b-2', 'border-brand-600');
|
||||
tabLogin.classList.remove('text-gray-500');
|
||||
tabRegister.classList.remove('text-brand-600', 'border-b-2', 'border-brand-600');
|
||||
tabRegister.classList.add('text-gray-500');
|
||||
btnText.innerText = "Sign In";
|
||||
} else {
|
||||
tabRegister.classList.add('text-brand-600', 'border-b-2', 'border-brand-600');
|
||||
tabRegister.classList.remove('text-gray-500');
|
||||
tabLogin.classList.remove('text-brand-600', 'border-b-2', 'border-brand-600');
|
||||
tabLogin.classList.add('text-gray-500');
|
||||
btnText.innerText = "Create Account";
|
||||
}
|
||||
errorMsg.classList.add('hidden');
|
||||
}
|
||||
|
||||
tabLogin.addEventListener('click', () => toggleTab(true));
|
||||
tabRegister.addEventListener('click', () => toggleTab(false));
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
errorMsg.classList.add('hidden');
|
||||
|
||||
try {
|
||||
if (isLogin) {
|
||||
// Login: POST /api/auth/token (Expects form-data, not JSON)
|
||||
const body = new URLSearchParams();
|
||||
body.append('username', data.username);
|
||||
body.append('password', data.password);
|
||||
|
||||
const res = await fetch('/api/auth/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Invalid credentials');
|
||||
|
||||
const result = await res.json();
|
||||
localStorage.setItem('token', result.access_token);
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
// Register: POST /api/auth/register (Expects JSON)
|
||||
const res = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let msg = 'Registration failed';
|
||||
try {
|
||||
const err = await res.json();
|
||||
msg = err.detail || msg;
|
||||
} catch (e) {
|
||||
msg = `Server Error (${res.status}): Please check backend logs.`;
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// Auto login after register? Or just switch tab.
|
||||
alert('Account created! Please login.');
|
||||
toggleTab(true);
|
||||
form.reset();
|
||||
}
|
||||
} catch (err) {
|
||||
errorText.innerText = err.message;
|
||||
errorMsg.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
/* Glassmorphism utilities if needed */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.dark .glass {
|
||||
background: rgba(17, 24, 39, 0.7);
|
||||
}
|
||||
|
||||
/* Input number arrows hide */
|
||||
input[type=number]::-webkit-inner-spin-button,
|
||||
input[type=number]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Calendar Heatmap Cell Styles */
|
||||
.heatmap-cell {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.heatmap-cell:hover {
|
||||
transform: scale(1.1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.heatmap-grid {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(7, 1fr);
|
||||
grid-auto-flow: column;
|
||||
gap: 2px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.heatmap-square {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
Reference in New Issue
Block a user