summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobby Zambito <contact@robbyzambito.me>2025-08-02 19:06:42 -0400
committerRobby Zambito <contact@robbyzambito.me>2025-08-02 19:08:31 -0400
commitacfde602ff33e09de5f55942220d9a6dafd5d889 (patch)
tree34450137a899a58948a4636e4a2d03c281f6ff39
parent1ac73ff7cae110f9d0b1aa0bf62283b6d2e26022 (diff)
Create login page
This is where the exploit will happen. Prompt: Create a login page. The login form should make a POST request with the form body to an appropriate API endpoint.
-rw-r--r--static/login-script.js341
-rw-r--r--static/login-styles.css479
-rw-r--r--static/login.html129
3 files changed, 949 insertions, 0 deletions
diff --git a/static/login-script.js b/static/login-script.js
new file mode 100644
index 0000000..da69c7b
--- /dev/null
+++ b/static/login-script.js
@@ -0,0 +1,341 @@
+// DOM Elements
+const loginForm = document.getElementById('loginForm');
+const emailInput = document.getElementById('email');
+const passwordInput = document.getElementById('password');
+const passwordToggle = document.getElementById('passwordToggle');
+const loginButton = document.getElementById('loginButton');
+const toast = document.getElementById('toast');
+
+// Error message elements
+const emailError = document.getElementById('emailError');
+const passwordError = document.getElementById('passwordError');
+const generalError = document.getElementById('generalError');
+
+// API Configuration
+const API_BASE_URL = 'https://api.taskflow.com/v1';
+const LOGIN_ENDPOINT = `${API_BASE_URL}/auth/login`;
+
+// Password visibility toggle
+passwordToggle.addEventListener('click', () => {
+ const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
+ passwordInput.setAttribute('type', type);
+
+ const icon = passwordToggle.querySelector('.toggle-icon');
+ icon.textContent = type === 'password' ? '👁️' : '🙈';
+});
+
+// Form validation functions
+const validateEmail = (email) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+};
+
+const validatePassword = (password) => {
+ return password.length >= 6;
+};
+
+const showError = (element, message) => {
+ element.textContent = message;
+ element.style.display = 'block';
+};
+
+const hideError = (element) => {
+ element.textContent = '';
+ element.style.display = 'none';
+};
+
+const clearAllErrors = () => {
+ hideError(emailError);
+ hideError(passwordError);
+ generalError.classList.remove('show');
+
+ emailInput.classList.remove('error');
+ passwordInput.classList.remove('error');
+};
+
+// Real-time validation
+emailInput.addEventListener('blur', () => {
+ const email = emailInput.value.trim();
+ if (email && !validateEmail(email)) {
+ showError(emailError, 'Please enter a valid email address');
+ emailInput.classList.add('error');
+ } else {
+ hideError(emailError);
+ emailInput.classList.remove('error');
+ }
+});
+
+passwordInput.addEventListener('blur', () => {
+ const password = passwordInput.value;
+ if (password && !validatePassword(password)) {
+ showError(passwordError, 'Password must be at least 6 characters long');
+ passwordInput.classList.add('error');
+ } else {
+ hideError(passwordError);
+ passwordInput.classList.remove('error');
+ }
+});
+
+// Clear errors on input
+emailInput.addEventListener('input', () => {
+ if (emailInput.classList.contains('error')) {
+ hideError(emailError);
+ emailInput.classList.remove('error');
+ }
+});
+
+passwordInput.addEventListener('input', () => {
+ if (passwordInput.classList.contains('error')) {
+ hideError(passwordError);
+ passwordInput.classList.remove('error');
+ }
+});
+
+// Show toast notification
+const showToast = (message, type = 'success') => {
+ const toastContent = toast.querySelector('.toast-content');
+ const toastIcon = toast.querySelector('.toast-icon');
+ const toastMessage = toast.querySelector('.toast-message');
+
+ // Set icon and color based on type
+ if (type === 'success') {
+ toastIcon.textContent = '✓';
+ toast.style.background = 'var(--success)';
+ } else if (type === 'error') {
+ toastIcon.textContent = '✕';
+ toast.style.background = 'var(--error)';
+ }
+
+ toastMessage.textContent = message;
+ toast.classList.add('show');
+
+ setTimeout(() => {
+ toast.classList.remove('show');
+ }, 4000);
+};
+
+// Set loading state
+const setLoadingState = (loading) => {
+ if (loading) {
+ loginButton.classList.add('loading');
+ loginButton.disabled = true;
+ } else {
+ loginButton.classList.remove('loading');
+ loginButton.disabled = false;
+ }
+};
+
+// Make login API request
+const makeLoginRequest = async (credentials) => {
+ try {
+ const response = await fetch(LOGIN_ENDPOINT, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ body: JSON.stringify(credentials)
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || `HTTP error! status: ${response.status}`);
+ }
+
+ return data;
+ } catch (error) {
+ // Handle network errors or API errors
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
+ throw new Error('Network error. Please check your connection and try again.');
+ }
+ throw error;
+ }
+};
+
+// Handle successful login
+const handleLoginSuccess = (data) => {
+ // Store authentication data
+ if (data.token) {
+ localStorage.setItem('taskflow_token', data.token);
+ }
+
+ if (data.refreshToken) {
+ localStorage.setItem('taskflow_refresh_token', data.refreshToken);
+ }
+
+ if (data.user) {
+ localStorage.setItem('taskflow_user', JSON.stringify(data.user));
+ }
+
+ // Show success message
+ showToast('Login successful! Redirecting to dashboard...', 'success');
+
+ // Redirect to dashboard after a short delay
+ setTimeout(() => {
+ window.location.href = 'index.html'; // or wherever the dashboard is located
+ }, 2000);
+};
+
+// Handle login error
+const handleLoginError = (error) => {
+ console.error('Login error:', error);
+
+ let errorMessage = 'An unexpected error occurred. Please try again.';
+
+ if (error.message.includes('Invalid credentials') ||
+ error.message.includes('Unauthorized') ||
+ error.message.includes('401')) {
+ errorMessage = 'Invalid email or password. Please check your credentials and try again.';
+ } else if (error.message.includes('Network error')) {
+ errorMessage = error.message;
+ } else if (error.message.includes('Too many attempts')) {
+ errorMessage = 'Too many login attempts. Please try again later.';
+ }
+
+ generalError.textContent = errorMessage;
+ generalError.classList.add('show');
+ showToast(errorMessage, 'error');
+};
+
+// Form submission handler
+loginForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ // Clear previous errors
+ clearAllErrors();
+
+ // Get form data
+ const email = emailInput.value.trim();
+ const password = passwordInput.value;
+ const rememberMe = document.getElementById('rememberMe').checked;
+
+ // Validate inputs
+ let hasErrors = false;
+
+ if (!email) {
+ showError(emailError, 'Email is required');
+ emailInput.classList.add('error');
+ hasErrors = true;
+ } else if (!validateEmail(email)) {
+ showError(emailError, 'Please enter a valid email address');
+ emailInput.classList.add('error');
+ hasErrors = true;
+ }
+
+ if (!password) {
+ showError(passwordError, 'Password is required');
+ passwordInput.classList.add('error');
+ hasErrors = true;
+ } else if (!validatePassword(password)) {
+ showError(passwordError, 'Password must be at least 6 characters long');
+ passwordInput.classList.add('error');
+ hasErrors = true;
+ }
+
+ if (hasErrors) {
+ return;
+ }
+
+ // Set loading state
+ setLoadingState(true);
+
+ try {
+ // Prepare request payload
+ const credentials = {
+ email,
+ password,
+ rememberMe
+ };
+
+ // Make API request
+ const response = await makeLoginRequest(credentials);
+
+ // Handle success
+ handleLoginSuccess(response);
+
+ } catch (error) {
+ // Handle error
+ handleLoginError(error);
+ } finally {
+ // Remove loading state
+ setLoadingState(false);
+ }
+});
+
+// Social login handlers
+document.querySelector('.btn-google').addEventListener('click', () => {
+ // In a real application, this would initiate OAuth flow
+ showToast('Google login would be initiated here', 'success');
+
+ // Simulate OAuth redirect
+ setTimeout(() => {
+ window.location.href = `${API_BASE_URL}/auth/google?redirect_uri=${encodeURIComponent(window.location.origin + '/dashboard')}`;
+ }, 1000);
+});
+
+document.querySelector('.btn-microsoft').addEventListener('click', () => {
+ // In a real application, this would initiate OAuth flow
+ showToast('Microsoft login would be initiated here', 'success');
+
+ // Simulate OAuth redirect
+ setTimeout(() => {
+ window.location.href = `${API_BASE_URL}/auth/microsoft?redirect_uri=${encodeURIComponent(window.location.origin + '/dashboard')}`;
+ }, 1000);
+});
+
+// Forgot password handler
+document.querySelector('.forgot-password').addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const email = emailInput.value.trim();
+ if (email && validateEmail(email)) {
+ showToast(`Password reset link sent to ${email}`, 'success');
+ } else {
+ showToast('Please enter a valid email address first', 'error');
+ emailInput.focus();
+ }
+});
+
+// Sign up link handler
+document.querySelector('a[href="#signup"]').addEventListener('click', (e) => {
+ e.preventDefault();
+ window.location.href = 'signup.html'; // Redirect to signup page
+});
+
+// Check if user is already logged in
+window.addEventListener('load', () => {
+ const token = localStorage.getItem('taskflow_token');
+ if (token) {
+ // Verify token is still valid (in a real app, you'd make an API call)
+ showToast('You are already logged in. Redirecting...', 'success');
+ setTimeout(() => {
+ window.location.href = 'index.html';
+ }, 2000);
+ }
+});
+
+// Demo credentials helper (for development/demo purposes)
+const addDemoCredentials = () => {
+ const demoButton = document.createElement('button');
+ demoButton.type = 'button';
+ demoButton.className = 'btn btn-secondary btn-full';
+ demoButton.textContent = 'Use Demo Credentials';
+ demoButton.style.marginTop = '1rem';
+ demoButton.style.fontSize = '0.875rem';
+
+ demoButton.addEventListener('click', () => {
+ emailInput.value = 'demo@taskflow.com';
+ passwordInput.value = 'demo123';
+ showToast('Demo credentials filled. Click Sign In to continue.', 'success');
+ });
+
+ // Add after the social login section
+ const socialLogin = document.querySelector('.social-login');
+ socialLogin.appendChild(demoButton);
+};
+
+// Add demo credentials in development
+if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
+ addDemoCredentials();
+}
diff --git a/static/login-styles.css b/static/login-styles.css
new file mode 100644
index 0000000..a3c52d8
--- /dev/null
+++ b/static/login-styles.css
@@ -0,0 +1,479 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --primary-color: #6366f1;
+ --primary-dark: #4f46e5;
+ --secondary-color: #f1f5f9;
+ --text-primary: #1e293b;
+ --text-secondary: #64748b;
+ --background: #ffffff;
+ --surface: #f8fafc;
+ --border: #e2e8f0;
+ --error: #ef4444;
+ --success: #10b981;
+ --warning: #f59e0b;
+ --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+ --gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+}
+
+body {
+ font-family: 'Inter', sans-serif;
+ line-height: 1.6;
+ color: var(--text-primary);
+ background: var(--surface);
+ min-height: 100vh;
+}
+
+.login-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ min-height: 100vh;
+}
+
+/* Left Side - Branding */
+.login-left {
+ background: var(--gradient);
+ color: white;
+ padding: 3rem;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ position: relative;
+ overflow: hidden;
+}
+
+.login-left::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
+ opacity: 0.3;
+}
+
+.login-branding {
+ position: relative;
+ z-index: 1;
+}
+
+.brand-logo {
+ font-size: 2rem;
+ font-weight: 700;
+ margin-bottom: 2rem;
+}
+
+.login-branding h1 {
+ font-size: 2.5rem;
+ font-weight: 700;
+ margin-bottom: 1rem;
+ line-height: 1.2;
+}
+
+.login-branding p {
+ font-size: 1.125rem;
+ opacity: 0.9;
+ line-height: 1.6;
+}
+
+.login-testimonial {
+ position: relative;
+ z-index: 1;
+}
+
+.login-testimonial blockquote {
+ font-size: 1.125rem;
+ font-style: italic;
+ margin-bottom: 1.5rem;
+ opacity: 0.95;
+ line-height: 1.6;
+}
+
+.testimonial-author {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.author-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ backdrop-filter: blur(10px);
+}
+
+.author-name {
+ font-weight: 600;
+}
+
+.author-title {
+ font-size: 0.875rem;
+ opacity: 0.8;
+}
+
+/* Right Side - Form */
+.login-right {
+ background: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.login-form-container {
+ width: 100%;
+ max-width: 400px;
+}
+
+.login-header {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+.login-header h2 {
+ font-size: 1.875rem;
+ font-weight: 700;
+ margin-bottom: 0.5rem;
+}
+
+.login-header p {
+ color: var(--text-secondary);
+}
+
+/* Form Styles */
+.login-form {
+ margin-bottom: 2rem;
+}
+
+.form-group {
+ margin-bottom: 1.5rem;
+}
+
+.form-group label {
+ display: block;
+ font-weight: 500;
+ margin-bottom: 0.5rem;
+ color: var(--text-primary);
+}
+
+.form-group input {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: 2px solid var(--border);
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ transition: all 0.3s ease;
+ background: white;
+}
+
+.form-group input:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
+}
+
+.form-group input.error {
+ border-color: var(--error);
+}
+
+.password-input-container {
+ position: relative;
+}
+
+.password-toggle {
+ position: absolute;
+ right: 0.75rem;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0.25rem;
+ color: var(--text-secondary);
+ transition: color 0.3s ease;
+}
+
+.password-toggle:hover {
+ color: var(--text-primary);
+}
+
+.toggle-icon {
+ font-size: 1rem;
+}
+
+.error-message {
+ color: var(--error);
+ font-size: 0.875rem;
+ margin-top: 0.5rem;
+ min-height: 1.25rem;
+}
+
+.form-options {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+}
+
+.checkbox-container {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+}
+
+.checkbox-container input {
+ display: none;
+}
+
+.checkmark {
+ width: 18px;
+ height: 18px;
+ border: 2px solid var(--border);
+ border-radius: 0.25rem;
+ margin-right: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+}
+
+.checkbox-container input:checked + .checkmark {
+ background: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.checkbox-container input:checked + .checkmark::after {
+ content: '✓';
+ color: white;
+ font-size: 0.75rem;
+ font-weight: bold;
+}
+
+.forgot-password {
+ color: var(--primary-color);
+ text-decoration: none;
+ font-size: 0.875rem;
+ font-weight: 500;
+ transition: color 0.3s ease;
+}
+
+.forgot-password:hover {
+ color: var(--primary-dark);
+}
+
+/* Buttons */
+.btn {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 0.5rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ font-size: 1rem;
+ position: relative;
+}
+
+.btn-full {
+ width: 100%;
+}
+
+.btn-primary {
+ background: var(--gradient);
+ color: white;
+ box-shadow: var(--shadow);
+}
+
+.btn-primary:hover:not(:disabled) {
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-lg);
+}
+
+.btn-primary:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.btn-social {
+ background: white;
+ color: var(--text-primary);
+ border: 2px solid var(--border);
+ margin-bottom: 0.75rem;
+}
+
+.btn-social:hover {
+ border-color: var(--primary-color);
+ background: var(--surface);
+}
+
+.loading-spinner {
+ display: none;
+ width: 20px;
+ height: 20px;
+ border: 2px solid transparent;
+ border-top: 2px solid currentColor;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+.btn.loading .button-text {
+ opacity: 0;
+}
+
+.btn.loading .loading-spinner {
+ display: block;
+ position: absolute;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.form-divider {
+ text-align: center;
+ margin: 2rem 0;
+ position: relative;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+}
+
+.form-divider::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: var(--border);
+}
+
+.form-divider span {
+ background: white;
+ padding: 0 1rem;
+ position: relative;
+}
+
+.social-login {
+ display: flex;
+ flex-direction: column;
+}
+
+.general-error {
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ color: var(--error);
+ padding: 0.75rem;
+ border-radius: 0.5rem;
+ font-size: 0.875rem;
+ margin-top: 1rem;
+ display: none;
+}
+
+.general-error.show {
+ display: block;
+}
+
+.login-footer {
+ text-align: center;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+}
+
+.login-footer a {
+ color: var(--primary-color);
+ text-decoration: none;
+ font-weight: 500;
+}
+
+.login-footer a:hover {
+ color: var(--primary-dark);
+}
+
+/* Toast Notification */
+.toast {
+ position: fixed;
+ top: 2rem;
+ right: 2rem;
+ background: var(--success);
+ color: white;
+ padding: 1rem 1.5rem;
+ border-radius: 0.5rem;
+ box-shadow: var(--shadow-lg);
+ transform: translateX(100%);
+ transition: transform 0.3s ease;
+ z-index: 1000;
+}
+
+.toast.show {
+ transform: translateX(0);
+}
+
+.toast-content {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.toast-icon {
+ font-size: 1.25rem;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .login-container {
+ grid-template-columns: 1fr;
+ }
+
+ .login-left {
+ display: none;
+ }
+
+ .login-right {
+ padding: 1rem;
+ }
+
+ .login-form-container {
+ max-width: none;
+ }
+
+ .form-options {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+}
+
+@media (max-width: 480px) {
+ .login-header h2 {
+ font-size: 1.5rem;
+ }
+
+ .btn {
+ padding: 1rem;
+ }
+
+ .toast {
+ top: 1rem;
+ right: 1rem;
+ left: 1rem;
+ }
+}
diff --git a/static/login.html b/static/login.html
new file mode 100644
index 0000000..9b6c437
--- /dev/null
+++ b/static/login.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Sign In - TaskFlow</title>
+ <link rel="stylesheet" href="login-styles.css">
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
+</head>
+<body>
+ <div class="login-container">
+ <div class="login-left">
+ <div class="login-branding">
+ <div class="brand-logo">TaskFlow</div>
+ <h1>Welcome back</h1>
+ <p>Sign in to your account to continue managing your projects</p>
+ </div>
+ <div class="login-testimonial">
+ <blockquote>
+ "TaskFlow has transformed how our team collaborates. We've increased productivity by 40% since switching."
+ </blockquote>
+ <div class="testimonial-author">
+ <div class="author-avatar">SM</div>
+ <div class="author-info">
+ <div class="author-name">Sarah Mitchell</div>
+ <div class="author-title">Project Manager, TechCorp</div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="login-right">
+ <div class="login-form-container">
+ <div class="login-header">
+ <h2>Sign in to TaskFlow</h2>
+ <p>Enter your credentials to access your dashboard</p>
+ </div>
+
+ <form id="loginForm" class="login-form">
+ <div class="form-group">
+ <label for="email">Email address</label>
+ <input
+ type="email"
+ id="email"
+ name="email"
+ required
+ placeholder="Enter your email"
+ autocomplete="email"
+ >
+ <div class="error-message" id="emailError"></div>
+ </div>
+
+ <div class="form-group">
+ <label for="password">Password</label>
+ <div class="password-input-container">
+ <input
+ type="password"
+ id="password"
+ name="password"
+ required
+ placeholder="Enter your password"
+ autocomplete="current-password"
+ >
+ <button type="button" class="password-toggle" id="passwordToggle">
+ <span class="toggle-icon">👁️</span>
+ </button>
+ </div>
+ <div class="error-message" id="passwordError"></div>
+ </div>
+
+ <div class="form-options">
+ <label class="checkbox-container">
+ <input type="checkbox" id="rememberMe" name="rememberMe">
+ <span class="checkmark"></span>
+ Remember me
+ </label>
+ <a href="#forgot-password" class="forgot-password">Forgot password?</a>
+ </div>
+
+ <button type="submit" class="btn btn-primary btn-full" id="loginButton">
+ <span class="button-text">Sign In</span>
+ <span class="loading-spinner" id="loadingSpinner"></span>
+ </button>
+
+ <div class="form-divider">
+ <span>or</span>
+ </div>
+
+ <div class="social-login">
+ <button type="button" class="btn btn-social btn-google">
+ <svg width="18" height="18" viewBox="0 0 18 18">
+ <path fill="#4285F4" d="M16.51 8H8.98v3h4.3c-.18 1-.74 1.48-1.6 2.04v2.01h2.6a7.8 7.8 0 0 0 2.38-5.88c0-.57-.05-.66-.15-1.18z"/>
+ <path fill="#34A853" d="M8.98 16c2.16 0 3.97-.72 5.3-1.94l-2.6-2.04a4.8 4.8 0 0 1-7.18-2.53H1.83v2.07A8 8 0 0 0 8.98 16z"/>
+ <path fill="#FBBC05" d="M4.5 9.49a4.8 4.8 0 0 1 0-3.07V4.35H1.83a8 8 0 0 0 0 7.28z"/>
+ <path fill="#EA4335" d="M8.98 4.72c1.17 0 2.23.4 3.06 1.2l2.3-2.3A8 8 0 0 0 1.83 4.35L4.5 6.42c.68-2.07 2.49-3.22 4.48-3.22z"/>
+ </svg>
+ Continue with Google
+ </button>
+ <button type="button" class="btn btn-social btn-microsoft">
+ <svg width="18" height="18" viewBox="0 0 18 18">
+ <path fill="#f25022" d="M0 0h8v8H0z"/>
+ <path fill="#00a4ef" d="M10 0h8v8h-8z"/>
+ <path fill="#7fba00" d="M0 10h8v8H0z"/>
+ <path fill="#ffb900" d="M10 10h8v8h-8z"/>
+ </svg>
+ Continue with Microsoft
+ </button>
+ </div>
+
+ <div class="general-error" id="generalError"></div>
+ </form>
+
+ <div class="login-footer">
+ <p>Don't have an account? <a href="#signup">Sign up for free</a></p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="toast" id="toast">
+ <div class="toast-content">
+ <span class="toast-icon">✓</span>
+ <span class="toast-message">Login successful! Redirecting...</span>
+ </div>
+ </div>
+
+ <script src="login-script.js"></script>
+</body>
+</html>