218 lines
9.8 KiB
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">
|
|
© {{ 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
|