Files
Verde-Web/resources/views/auth/login.blade.php

218 lines
9.8 KiB
PHP

@extends('layouts.web', ['title' => 'Sign in — Verde'])
@section('content')
<main class="grid min-h-screen grid-cols-1 lg:grid-cols-5">
{{-- Brand panel --}}
<aside class="hidden lg:col-span-2 lg:flex lg:flex-col lg:justify-between bg-verde-900 p-12 text-white">
<div class="flex items-center gap-2.5">
<div class="flex h-9 w-9 items-center justify-center rounded-md bg-verde-500">
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-white" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2c-3 5-7 8-7 13a7 7 0 0 0 14 0c0-5-4-8-7-13z"/>
</svg>
</div>
<span class="text-lg font-semibold tracking-tight">Verde</span>
</div>
<div class="space-y-6">
<h1 class="text-3xl font-semibold leading-tight tracking-tight">
Cleaner cities,<br>
one barangay at a time.
</h1>
<p class="max-w-sm text-sm text-verde-100/80">
Geo-tagged collection, QR verification, and live truck tracking
for waste operations across the Philippines.
</p>
</div>
<div class="text-xs text-verde-200/60">
&copy; {{ date('Y') }} Verde. Built for LGUs.
</div>
</aside>
{{-- Form panel --}}
<section class="col-span-1 lg:col-span-3 flex items-center justify-center px-6 py-16 sm:px-12">
<div class="w-full max-w-sm">
{{-- Mobile-only logo --}}
<div class="mb-10 flex items-center gap-2.5 lg:hidden">
<div class="flex h-9 w-9 items-center justify-center rounded-md bg-verde-600">
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-white" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2c-3 5-7 8-7 13a7 7 0 0 0 14 0c0-5-4-8-7-13z"/>
</svg>
</div>
<span class="text-lg font-semibold tracking-tight text-neutral-900">Verde</span>
</div>
<header class="mb-8">
<h2 class="text-2xl font-semibold tracking-tight text-neutral-900">Sign in</h2>
<p class="mt-1.5 text-sm text-neutral-500">
Use your admin email to access the dashboard.
</p>
</header>
<form id="login-form" class="space-y-5" autocomplete="off">
<div>
<label for="email" class="form-label">Email</label>
<input
id="email"
name="email"
type="email"
required
autocomplete="email"
spellcheck="false"
autofocus
class="form-input"
placeholder="you@verde.local">
</div>
<div>
<div class="mb-1.5 flex items-center justify-between">
<label for="password" class="text-sm font-medium text-neutral-700">Password</label>
<a href="#" class="text-xs font-medium text-verde-700 hover:text-verde-800">Forgot password?</a>
</div>
<input
id="password"
name="password"
type="password"
required
autocomplete="current-password"
class="form-input"
placeholder="••••••••">
</div>
<div id="error-banner" aria-live="polite" class="hidden rounded-md border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700"></div>
<button type="submit" id="submit-btn" class="btn-primary w-full">
<span class="btn-label">Sign in</span>
<svg class="btn-spinner ml-2 hidden h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z"/>
</svg>
</button>
</form>
{{-- Demo creds --}}
<details class="mt-10 rounded-md border border-neutral-200 bg-white">
<summary class="cursor-pointer list-none px-4 py-3 text-xs font-medium text-neutral-600 hover:text-neutral-900">
<span class="inline-flex items-center gap-1.5">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
Demo accounts
</span>
</summary>
<div class="border-t border-neutral-200 px-4 py-3 text-xs">
<p class="mb-2 text-neutral-500">Click an account to autofill.</p>
<div class="space-y-1.5 font-mono">
@foreach ([
['email' => 'admin@verde.local', 'note' => 'Primary admin'],
['email' => 'ops@verde.local', 'note' => 'Operations'],
['email' => 'store0@verde.local', 'note' => 'Store Partner'],
['email' => 'demo@verde.local', 'note' => 'Demo'],
] as $cred)
<button type="button"
class="demo-cred flex w-full items-center justify-between rounded px-2 py-1.5 text-left transition hover:bg-neutral-50"
data-email="{{ $cred['email'] }}">
<span class="text-neutral-900">{{ $cred['email'] }}</span>
<span class="text-[10px] uppercase tracking-wider text-neutral-400">{{ $cred['note'] }}</span>
</button>
@endforeach
</div>
<p class="mt-3 text-neutral-500">Password for all demo accounts: <code class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-neutral-700">password</code></p>
</div>
</details>
</div>
</section>
</main>
<script type="module">
const form = document.getElementById('login-form');
const errorBanner = document.getElementById('error-banner');
const submitBtn = document.getElementById('submit-btn');
const btnLabel = submitBtn.querySelector('.btn-label');
const btnSpinner = submitBtn.querySelector('.btn-spinner');
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
document.querySelectorAll('.demo-cred').forEach(btn => {
btn.addEventListener('click', () => {
emailInput.value = btn.dataset.email;
passwordInput.value = 'password';
passwordInput.focus();
});
});
function showError(message) {
errorBanner.textContent = message;
errorBanner.classList.remove('hidden');
}
function clearError() {
errorBanner.classList.add('hidden');
errorBanner.textContent = '';
}
function setLoading(on) {
submitBtn.disabled = on;
btnLabel.textContent = on ? 'Signing in…' : 'Sign in';
btnSpinner.classList.toggle('hidden', !on);
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
clearError();
setLoading(true);
const payload = {
email: emailInput.value.trim(),
password: passwordInput.value,
device_name: 'admin-web',
};
try {
const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
const res = await fetch('/api/v1/auth/login', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': csrf,
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify(payload),
});
const body = await res.json();
console.log('[Login] Response:', body);
if (!res.ok || !body.success) {
const fallback = body.message || 'Sign in failed. Try again.';
showError(fallback);
return;
}
const role = body.data?.user?.role;
const allowedRoles = ['admin', 'super_admin', 'store_partner'];
console.log('[Login] Role:', role);
if (!allowedRoles.includes(role)) {
showError('Access denied. This portal is for Admins and Store Partners only.');
return;
}
// Use shared helper if available
if (window.Verde?.setSession) {
window.Verde.setSession(body.data.token, body.data.user);
} else {
localStorage.setItem('verde:token', body.data.token);
localStorage.setItem('verde:user', JSON.stringify(body.data.user));
}
const target = role === 'store_partner' ? '/store/dashboard' : '/dashboard';
console.log('[Login] Redirecting to:', target);
window.location.href = target;
} catch (err) {
showError('Network error. Check your connection and try again.');
} finally {
setLoading(false);
}
});
</script>
@endsection