Code updated added styles to public pages as well as adminpanel pages.
This commit is contained in:
parent
a95d7710c3
commit
5246560f1f
|
|
@ -3,6 +3,12 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<title>Adherence Pack Study</title>
|
<title>Adherence Pack Study</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,16 @@ export default function SiteFooter() {
|
||||||
return (
|
return (
|
||||||
<footer className="site-footer">
|
<footer className="site-footer">
|
||||||
<div className="site-footer-inner">
|
<div className="site-footer-inner">
|
||||||
<p>Chest Research and Training Pvt. Ltd.</p>
|
<p className="site-footer-credit">
|
||||||
|
<span>Chest Research and Training Pvt. Ltd.</span>
|
||||||
|
<span className="site-footer-sep" aria-hidden="true" />
|
||||||
|
<span>
|
||||||
|
Powered by Virtual Engage by{' '}
|
||||||
|
<a href="https://stratawiz.com/" target="_blank" rel="noopener noreferrer">
|
||||||
|
Stratawiz
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
{supportEmail ? (
|
{supportEmail ? (
|
||||||
<p>
|
<p>
|
||||||
<a href={`mailto:${supportEmail}`}>{supportEmail}</a>
|
<a href={`mailto:${supportEmail}`}>{supportEmail}</a>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,377 @@
|
||||||
@import 'tailwindcss/theme.css' layer(theme);
|
@import 'tailwindcss/theme.css' layer(theme);
|
||||||
@import 'tailwindcss/utilities.css' layer(utilities);
|
@import 'tailwindcss/utilities.css' layer(utilities);
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: Poppins, system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell,
|
||||||
|
.admin-login {
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: #1c1917;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f7fbfb;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse 55% 45% at 8% 12%, rgb(232 245 233 / 0.95), transparent 62%),
|
||||||
|
radial-gradient(ellipse 50% 50% at 92% 8%, rgb(219 234 254 / 0.8), transparent 58%),
|
||||||
|
radial-gradient(ellipse 60% 50% at 88% 92%, rgb(204 251 241 / 0.8), transparent 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -12% -8%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 22%, #c8e6c9 0%, transparent 42%),
|
||||||
|
radial-gradient(circle at 88% 16%, #bbdefb 0%, transparent 38%),
|
||||||
|
radial-gradient(circle at 78% 86%, #b2dfdb 0%, transparent 42%);
|
||||||
|
filter: blur(80px);
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login .site-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40.0625rem) {
|
||||||
|
.admin-login .site-brand {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login-body {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell {
|
||||||
|
display: flex;
|
||||||
|
background: #f7fbfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-title {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: #134e4a;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login-card .admin-title {text-align: center;}
|
||||||
|
.admin-title-inline {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-card {
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: #fff;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 118 110 / 0.06), 0 18px 40px rgb(19 78 74 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-label {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
color: #78716c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-value {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #134e4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login-card .admin-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login-logo {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 250px;
|
||||||
|
height: auto;
|
||||||
|
margin: auto;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding: 0.0rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
background: #f7faf9;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-kicker {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #0f766e;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #d6d3d1;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.7rem 0.85rem;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1c1917;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-input:focus {
|
||||||
|
outline: 2px solid #0f766e;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-file {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 28rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.35rem;
|
||||||
|
border: 1px dashed #99f6e4;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
background: #f0fdfa;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #57534e;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-file::file-selector-button {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
background: linear-gradient(135deg, #0f766e 0%, #1578c8 100%);
|
||||||
|
color: #f0fdfa;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-file::file-selector-button:hover {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-file:focus-visible {
|
||||||
|
outline: 2px solid #0f766e;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-input-inline {
|
||||||
|
width: auto;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 12rem;
|
||||||
|
padding: 0.4rem 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0.65rem 1.1rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.55rem;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #0f766e 0%, #1578c8 100%);
|
||||||
|
color: #f0fdfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-primary:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #0d6a63 0%, #1d4ed8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-secondary {
|
||||||
|
background: #eef2f1;
|
||||||
|
color: #134e4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-secondary:hover:not(:disabled) {
|
||||||
|
background: #e4eceb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-danger {
|
||||||
|
background: #991b1b;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-danger:hover:not(:disabled) {
|
||||||
|
background: #7f1d1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: #ccfbf1;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-ghost:hover:not(:disabled) {
|
||||||
|
background: rgb(15 118 110 / 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 118 110 / 0.06), 0 18px 40px rgb(19 78 74 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #f7faf9;
|
||||||
|
color: #57534e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table th,
|
||||||
|
.admin-table td {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid #f5f5f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table tbody tr:hover {
|
||||||
|
background: rgb(240 253 250 / 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table a {
|
||||||
|
color: #0f766e;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
flex: none;
|
||||||
|
width: 14rem;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #134e4a;
|
||||||
|
transition:
|
||||||
|
width 0.3s ease,
|
||||||
|
visibility 0s linear 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar.is-collapsed {
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
width 0.3s ease,
|
||||||
|
visibility 0s linear 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav {
|
||||||
|
width: 14rem;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar.is-collapsed .admin-nav {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.admin-sidebar,
|
||||||
|
.admin-sidebar.is-collapsed,
|
||||||
|
.admin-nav {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #e4e4e0;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
background: #fff;
|
||||||
|
color: #134e4a;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 118 110 / 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav-toggle:hover {
|
||||||
|
background: #f0fdfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav-toggle:focus-visible {
|
||||||
|
outline: 2px solid #0f766e;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-logout {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-nav-logo {
|
.admin-nav-logo {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,16 @@ const LINKS = [
|
||||||
['/admin/audit', 'Audit log'],
|
['/admin/audit', 'Audit log'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const NAV_KEY = 'crest-admin-nav';
|
||||||
|
|
||||||
export default function AdminLayout() {
|
export default function AdminLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [admin, setAdmin] = useState(null);
|
const [admin, setAdmin] = useState(null);
|
||||||
|
const [navOpen, setNavOpen] = useState(() => localStorage.getItem(NAV_KEY) !== 'closed');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(NAV_KEY, navOpen ? 'open' : 'closed');
|
||||||
|
}, [navOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
@ -46,18 +53,18 @@ export default function AdminLayout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-stone-100 text-stone-900">
|
<div className="admin-shell font-sans">
|
||||||
<div className="flex min-h-screen">
|
<aside className={`admin-sidebar ${navOpen ? '' : 'is-collapsed'}`}>
|
||||||
<nav className="admin-nav flex w-56 flex-col gap-2 bg-stone-900 p-4">
|
<nav id="admin-nav" className="admin-nav flex w-56 flex-col gap-2 bg-[#134e4a] p-4">
|
||||||
<img src={logo} alt="CREST" className="admin-nav-logo" />
|
<img src={logo} alt="CREST" className="admin-nav-logo" />
|
||||||
<p className="admin-nav-heading mb-2 text-sm font-semibold">CREST admin</p>
|
<p className="mb-2 truncate text-xs text-teal-100/70">{admin.email}</p>
|
||||||
{LINKS.map(([to, label]) => (
|
{LINKS.map(([to, label]) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={to}
|
key={to}
|
||||||
to={to}
|
to={to}
|
||||||
end={to === '/admin'}
|
end={to === '/admin'}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`admin-nav-link rounded px-3 py-2 text-sm ${isActive ? 'bg-stone-700' : 'hover:bg-stone-800'}`
|
`admin-nav-link rounded-lg px-3 py-2 text-sm ${isActive ? 'bg-[#0f766e]' : 'hover:bg-[#0d5c56]'}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|
@ -65,16 +72,52 @@ export default function AdminLayout() {
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mt-auto rounded px-3 py-2 text-left text-sm"
|
className="admin-btn admin-btn-ghost admin-logout mt-auto text-sm"
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
>
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||||
|
<path d="M16 17l5-5-5-5" />
|
||||||
|
<path d="M21 12H9" />
|
||||||
|
</svg>
|
||||||
Log out
|
Log out
|
||||||
</button>
|
</button>
|
||||||
<p className="truncate text-xs text-stone-400">{admin.email}</p>
|
|
||||||
</nav>
|
</nav>
|
||||||
<div className="min-w-0 flex-1 p-6">
|
</aside>
|
||||||
<Outlet />
|
<div className="admin-main">
|
||||||
</div>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-nav-toggle"
|
||||||
|
aria-controls="admin-nav"
|
||||||
|
aria-expanded={navOpen}
|
||||||
|
aria-label={navOpen ? 'Hide menu' : 'Show menu'}
|
||||||
|
title={navOpen ? 'Hide menu' : 'Show menu'}
|
||||||
|
onClick={() => setNavOpen((open) => !open)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
>
|
||||||
|
<path d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -99,21 +99,21 @@ export default function ApplicationDetailPage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4 flex flex-wrap items-center gap-3">
|
<div className="mb-4 flex flex-wrap items-center gap-3">
|
||||||
<h1 className="text-2xl font-semibold">{application.full_name || 'Application'}</h1>
|
<h1 className="admin-title admin-title-inline">{application.full_name || 'Application'}</h1>
|
||||||
<StatusBadge status={application.status} />
|
<StatusBadge status={application.status} />
|
||||||
</div>
|
</div>
|
||||||
{error ? <p className="mb-3 text-red-700">{error}</p> : null}
|
{error ? <p className="mb-3 text-red-700">{error}</p> : null}
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<div className="rounded-lg bg-white p-4 shadow-sm">
|
<div className="admin-card">
|
||||||
<dl className="grid gap-2 text-sm">
|
<dl className="grid gap-2 text-sm">
|
||||||
{FIELDS.map(([key, label]) => (
|
{FIELDS.map(([key, label]) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<dt className="text-stone-500">{label}</dt>
|
<dt className="text-stone-500">{label}</dt>
|
||||||
<dd>{application[key] || '—'}</dd>
|
<dd className="m-0">{application[key] || '—'}</dd>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
<h2 className="mt-6 font-semibold">Signature</h2>
|
<h2 className="mt-6 font-semibold text-[#134e4a]">Signature</h2>
|
||||||
{application.signature ? (
|
{application.signature ? (
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{application.signature.type} at {application.signature.signedAt}
|
{application.signature.type} at {application.signature.signedAt}
|
||||||
|
|
@ -129,21 +129,21 @@ export default function ApplicationDetailPage() {
|
||||||
<div className="mt-4 flex gap-2">
|
<div className="mt-4 flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded bg-green-800 px-3 py-2 text-white"
|
className="admin-btn admin-btn-primary"
|
||||||
onClick={() => setConfirming(true)}
|
onClick={() => setConfirming(true)}
|
||||||
>
|
>
|
||||||
Approve
|
Approve
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded bg-red-800 px-3 py-2 text-white"
|
className="admin-btn admin-btn-danger"
|
||||||
onClick={() => setRejecting(true)}
|
onClick={() => setRejecting(true)}
|
||||||
>
|
>
|
||||||
Reject
|
Reject
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<h2 className="mt-6 font-semibold">Events</h2>
|
<h2 className="mt-6 font-semibold text-[#134e4a]">Events</h2>
|
||||||
<ol className="mt-2 list-disc pl-5 text-sm">
|
<ol className="mt-2 list-disc pl-5 text-sm">
|
||||||
{application.audit.map((event) => (
|
{application.audit.map((event) => (
|
||||||
<li key={event.id}>
|
<li key={event.id}>
|
||||||
|
|
@ -151,7 +151,7 @@ export default function ApplicationDetailPage() {
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
<h2 className="mt-6 font-semibold">Emails</h2>
|
<h2 className="mt-6 font-semibold text-[#134e4a]">Emails</h2>
|
||||||
{application.emails.some(
|
{application.emails.some(
|
||||||
(email) => email.status === 'bounced' || email.status === 'dropped',
|
(email) => email.status === 'bounced' || email.status === 'dropped',
|
||||||
) ? (
|
) ? (
|
||||||
|
|
@ -169,7 +169,7 @@ export default function ApplicationDetailPage() {
|
||||||
{application.status === 'APPROVED' ? (
|
{application.status === 'APPROVED' ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mt-3 rounded bg-stone-900 px-3 py-2 text-sm text-white"
|
className="admin-btn admin-btn-primary mt-3 text-sm"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={resendSurveyLink}
|
onClick={resendSurveyLink}
|
||||||
>
|
>
|
||||||
|
|
@ -177,7 +177,7 @@ export default function ApplicationDetailPage() {
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-[70vh] rounded-lg bg-white p-2 shadow-sm">
|
<div className="admin-card min-h-[70vh] p-2">
|
||||||
{application.pdfUrl ? (
|
{application.pdfUrl ? (
|
||||||
<iframe title="Agreement PDF" className="h-[70vh] w-full" src={application.pdfUrl} />
|
<iframe title="Agreement PDF" className="h-[70vh] w-full" src={application.pdfUrl} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -190,7 +190,7 @@ export default function ApplicationDetailPage() {
|
||||||
<p className="mb-3 text-sm">This adds the CREST signature and emails the survey link.</p>
|
<p className="mb-3 text-sm">This adds the CREST signature and emails the survey link.</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded bg-green-800 px-3 py-2 text-white"
|
className="admin-btn admin-btn-primary"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={approve}
|
onClick={approve}
|
||||||
>
|
>
|
||||||
|
|
@ -207,14 +207,14 @@ export default function ApplicationDetailPage() {
|
||||||
id="reject-reason"
|
id="reject-reason"
|
||||||
required
|
required
|
||||||
minLength={10}
|
minLength={10}
|
||||||
className="min-h-24 rounded border border-stone-300 px-3 py-2"
|
className="admin-input min-h-24"
|
||||||
value={reason}
|
value={reason}
|
||||||
onChange={(event) => setReason(event.target.value)}
|
onChange={(event) => setReason(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="rounded bg-red-800 px-3 py-2 text-white"
|
className="admin-btn admin-btn-danger"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
{busy ? 'Rejecting…' : 'Reject'}
|
{busy ? 'Rejecting…' : 'Reject'}
|
||||||
|
|
|
||||||
|
|
@ -43,20 +43,20 @@ export default function ApplicationsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-4 text-2xl font-semibold">Applications</h1>
|
<h1 className="admin-title">Applications</h1>
|
||||||
<form className="mb-4 grid gap-3 md:grid-cols-6">
|
<form className="admin-card mb-4 grid gap-3 md:grid-cols-6">
|
||||||
<input
|
<input
|
||||||
name="q"
|
name="q"
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
placeholder="Name, email, registration, PAN"
|
placeholder="Name, email, registration, PAN"
|
||||||
className="rounded border border-stone-300 px-3 py-2 md:col-span-2"
|
className="admin-input md:col-span-2"
|
||||||
value={filters.q}
|
value={filters.q}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
name="status"
|
name="status"
|
||||||
aria-label="Status"
|
aria-label="Status"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={filters.status}
|
value={filters.status}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
>
|
>
|
||||||
|
|
@ -70,7 +70,7 @@ export default function ApplicationsPage() {
|
||||||
name="from"
|
name="from"
|
||||||
aria-label="From"
|
aria-label="From"
|
||||||
type="date"
|
type="date"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={filters.from}
|
value={filters.from}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
/>
|
/>
|
||||||
|
|
@ -78,14 +78,14 @@ export default function ApplicationsPage() {
|
||||||
name="to"
|
name="to"
|
||||||
aria-label="To"
|
aria-label="To"
|
||||||
type="date"
|
type="date"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={filters.to}
|
value={filters.to}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
name="sort"
|
name="sort"
|
||||||
aria-label="Sort"
|
aria-label="Sort"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={filters.sort}
|
value={filters.sort}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
>
|
>
|
||||||
|
|
@ -96,31 +96,29 @@ export default function ApplicationsPage() {
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
{error ? <p className="text-red-700">{error}</p> : null}
|
{error ? <p className="text-red-700">{error}</p> : null}
|
||||||
<div className="overflow-x-auto rounded-lg bg-white shadow-sm">
|
<div className="admin-table-wrap">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="admin-table">
|
||||||
<thead className="border-b border-stone-200 text-stone-500">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2">Name</th>
|
<th>Name</th>
|
||||||
<th className="px-3 py-2">Email</th>
|
<th>Email</th>
|
||||||
<th className="px-3 py-2">Registration</th>
|
<th>Registration</th>
|
||||||
<th className="px-3 py-2">Status</th>
|
<th>Status</th>
|
||||||
<th className="px-3 py-2">Submitted</th>
|
<th>Submitted</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{result?.items.map((item) => (
|
{result?.items.map((item) => (
|
||||||
<tr key={item.id} className="border-b border-stone-100">
|
<tr key={item.id}>
|
||||||
<td className="px-3 py-2">
|
<td>
|
||||||
<Link className="font-medium underline" to={`/admin/applications/${item.id}`}>
|
<Link to={`/admin/applications/${item.id}`}>{item.full_name || 'Untitled'}</Link>
|
||||||
{item.full_name || 'Untitled'}
|
|
||||||
</Link>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">{item.email}</td>
|
<td>{item.email}</td>
|
||||||
<td className="px-3 py-2">{item.registration_no}</td>
|
<td>{item.registration_no}</td>
|
||||||
<td className="px-3 py-2">
|
<td>
|
||||||
<StatusBadge status={item.status} />
|
<StatusBadge status={item.status} />
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">{item.submitted_at || '—'}</td>
|
<td>{item.submitted_at || '—'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -130,18 +128,18 @@ export default function ApplicationsPage() {
|
||||||
<div className="mt-4 flex items-center gap-3">
|
<div className="mt-4 flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded border border-stone-300 px-3 py-1"
|
className="admin-btn admin-btn-secondary"
|
||||||
disabled={result.page <= 1}
|
disabled={result.page <= 1}
|
||||||
onClick={() => setFilters((current) => ({ ...current, page: current.page - 1 }))}
|
onClick={() => setFilters((current) => ({ ...current, page: current.page - 1 }))}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<span className="text-sm">
|
<span className="text-sm text-stone-600">
|
||||||
Page {result.page} of {Math.max(1, Math.ceil(result.total / result.pageSize))}
|
Page {result.page} of {Math.max(1, Math.ceil(result.total / result.pageSize))}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded border border-stone-300 px-3 py-1"
|
className="admin-btn admin-btn-secondary"
|
||||||
disabled={result.page * result.pageSize >= result.total}
|
disabled={result.page * result.pageSize >= result.total}
|
||||||
onClick={() => setFilters((current) => ({ ...current, page: current.page + 1 }))}
|
onClick={() => setFilters((current) => ({ ...current, page: current.page + 1 }))}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -29,20 +29,20 @@ export default function AuditPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-4 text-2xl font-semibold">Audit log</h1>
|
<h1 className="admin-title">Audit log</h1>
|
||||||
<form className="mb-4 grid gap-3 md:grid-cols-5">
|
<form className="admin-card mb-4 grid gap-3 md:grid-cols-5">
|
||||||
<input
|
<input
|
||||||
name="entity"
|
name="entity"
|
||||||
aria-label="Entity"
|
aria-label="Entity"
|
||||||
placeholder="Entity"
|
placeholder="Entity"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={filters.entity}
|
value={filters.entity}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
name="actor"
|
name="actor"
|
||||||
aria-label="Actor"
|
aria-label="Actor"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={filters.actor}
|
value={filters.actor}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
>
|
>
|
||||||
|
|
@ -55,7 +55,7 @@ export default function AuditPage() {
|
||||||
name="action"
|
name="action"
|
||||||
aria-label="Action"
|
aria-label="Action"
|
||||||
placeholder="Action"
|
placeholder="Action"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={filters.action}
|
value={filters.action}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
/>
|
/>
|
||||||
|
|
@ -63,7 +63,7 @@ export default function AuditPage() {
|
||||||
name="from"
|
name="from"
|
||||||
aria-label="From"
|
aria-label="From"
|
||||||
type="date"
|
type="date"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={filters.from}
|
value={filters.from}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
/>
|
/>
|
||||||
|
|
@ -71,31 +71,31 @@ export default function AuditPage() {
|
||||||
name="to"
|
name="to"
|
||||||
aria-label="To"
|
aria-label="To"
|
||||||
type="date"
|
type="date"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={filters.to}
|
value={filters.to}
|
||||||
onChange={update}
|
onChange={update}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
{error ? <p className="text-red-700">{error}</p> : null}
|
{error ? <p className="text-red-700">{error}</p> : null}
|
||||||
<div className="overflow-x-auto rounded-lg bg-white shadow-sm">
|
<div className="admin-table-wrap">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="admin-table">
|
||||||
<thead className="border-b border-stone-200 text-stone-500">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2">When</th>
|
<th>When</th>
|
||||||
<th className="px-3 py-2">Actor</th>
|
<th>Actor</th>
|
||||||
<th className="px-3 py-2">Action</th>
|
<th>Action</th>
|
||||||
<th className="px-3 py-2">Entity</th>
|
<th>Entity</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{result?.items.map((item) => (
|
{result?.items.map((item) => (
|
||||||
<tr key={item.id} className="border-b border-stone-100">
|
<tr key={item.id}>
|
||||||
<td className="px-3 py-2">{item.createdAt}</td>
|
<td>{item.createdAt}</td>
|
||||||
<td className="px-3 py-2">
|
<td>
|
||||||
{item.actorType} {item.actorId ?? ''}
|
{item.actorType} {item.actorId ?? ''}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">{item.action}</td>
|
<td>{item.action}</td>
|
||||||
<td className="px-3 py-2">
|
<td>
|
||||||
{item.entityType} {item.entityId ?? ''}
|
{item.entityType} {item.entityId ?? ''}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -107,16 +107,16 @@ export default function AuditPage() {
|
||||||
<div className="mt-4 flex items-center gap-3">
|
<div className="mt-4 flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded border border-stone-300 px-3 py-1"
|
className="admin-btn admin-btn-secondary"
|
||||||
disabled={result.page <= 1}
|
disabled={result.page <= 1}
|
||||||
onClick={() => setFilters((current) => ({ ...current, page: current.page - 1 }))}
|
onClick={() => setFilters((current) => ({ ...current, page: current.page - 1 }))}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<span className="text-sm">Page {result.page}</span>
|
<span className="text-sm text-stone-600">Page {result.page}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded border border-stone-300 px-3 py-1"
|
className="admin-btn admin-btn-secondary"
|
||||||
disabled={result.page * result.pageSize >= result.total}
|
disabled={result.page * result.pageSize >= result.total}
|
||||||
onClick={() => setFilters((current) => ({ ...current, page: current.page + 1 }))}
|
onClick={() => setFilters((current) => ({ ...current, page: current.page + 1 }))}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -31,22 +31,22 @@ export default function DashboardPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-4 text-2xl font-semibold">Dashboard</h1>
|
<h1 className="admin-title">Dashboard</h1>
|
||||||
<div className="mb-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
<div className="mb-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||||
{cards.map(([label, value]) => (
|
{cards.map(([label, value]) => (
|
||||||
<div key={label} className="rounded-lg bg-white p-4 shadow-sm">
|
<div key={label} className="admin-card admin-stat-card">
|
||||||
<p className="text-sm text-stone-500">{label}</p>
|
<p className="admin-stat-label">{label}</p>
|
||||||
<p className="text-2xl font-semibold">{value}</p>
|
<p className="admin-stat-value">{value}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-72 rounded-lg bg-white p-4 shadow-sm">
|
<div className="admin-card h-72">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={chart}>
|
<BarChart data={chart}>
|
||||||
<XAxis dataKey="status" />
|
<XAxis dataKey="status" />
|
||||||
<YAxis allowDecimals={false} />
|
<YAxis allowDecimals={false} />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Bar dataKey="count" fill="#1c1917" />
|
<Bar dataKey="count" fill="#0f766e" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import SiteHeader from '../SiteHeader.jsx';
|
||||||
import { adminApi } from './adminApi.js';
|
import { adminApi } from './adminApi.js';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
|
@ -28,46 +29,51 @@ export default function LoginPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mt-24 max-w-sm rounded-lg bg-white p-6 shadow">
|
<div className="admin-login">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">Admin login</h1>
|
<SiteHeader />
|
||||||
<form className="grid gap-3" onSubmit={submit}>
|
<div className="admin-login-body">
|
||||||
<label className="grid gap-1 text-sm" htmlFor="admin-email">
|
<div className="admin-card admin-login-card">
|
||||||
Email
|
{/* <p className="admin-kicker">CREST admin</p> */}
|
||||||
<input
|
<h1 className="admin-title">Admin login</h1>
|
||||||
id="admin-email"
|
<form className="grid gap-3" onSubmit={submit}>
|
||||||
type="email"
|
<label className="grid gap-1 text-sm font-medium text-stone-600" htmlFor="admin-email">
|
||||||
autoComplete="username"
|
Email
|
||||||
required
|
<input
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
id="admin-email"
|
||||||
value={email}
|
type="email"
|
||||||
onChange={(event) => setEmail(event.target.value)}
|
autoComplete="username"
|
||||||
/>
|
required
|
||||||
</label>
|
className="admin-input"
|
||||||
<label className="grid gap-1 text-sm" htmlFor="admin-password">
|
value={email}
|
||||||
Password
|
onChange={(event) => setEmail(event.target.value)}
|
||||||
<input
|
/>
|
||||||
id="admin-password"
|
</label>
|
||||||
type="password"
|
<label
|
||||||
autoComplete="current-password"
|
className="grid gap-1 text-sm font-medium text-stone-600"
|
||||||
required
|
htmlFor="admin-password"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
>
|
||||||
value={password}
|
Password
|
||||||
onChange={(event) => setPassword(event.target.value)}
|
<input
|
||||||
/>
|
id="admin-password"
|
||||||
</label>
|
type="password"
|
||||||
{error ? (
|
autoComplete="current-password"
|
||||||
<p className="text-sm text-red-700" role="alert">
|
required
|
||||||
{error}
|
className="admin-input"
|
||||||
</p>
|
value={password}
|
||||||
) : null}
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
<button
|
/>
|
||||||
type="submit"
|
</label>
|
||||||
className="rounded bg-stone-900 px-3 py-2 text-white disabled:opacity-60"
|
{error ? (
|
||||||
disabled={submitting}
|
<p className="text-sm text-red-700" role="alert">
|
||||||
>
|
{error}
|
||||||
{submitting ? 'Signing in…' : 'Sign in'}
|
</p>
|
||||||
</button>
|
) : null}
|
||||||
</form>
|
<button type="submit" className="admin-btn admin-btn-primary" disabled={submitting}>
|
||||||
|
{submitting ? 'Signing in…' : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
export default function Modal({ title, children, onClose }) {
|
export default function Modal({ title, children, onClose }) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-20 flex items-center justify-center bg-stone-900/40 p-4">
|
<div className="fixed inset-0 z-20 flex items-center justify-center bg-[#134e4a]/35 p-4">
|
||||||
<div
|
<div
|
||||||
className="w-full max-w-md rounded-lg bg-white p-5 shadow-lg"
|
className="admin-card w-full max-w-md"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
<h2 className="mb-3 text-lg font-semibold">{title}</h2>
|
<h2 className="mb-3 text-lg font-semibold text-[#134e4a]">{title}</h2>
|
||||||
{children}
|
{children}
|
||||||
<button type="button" className="mt-3 text-sm text-stone-600" onClick={onClose}>
|
<button type="button" className="admin-btn admin-btn-secondary mt-3" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -51,36 +51,32 @@ export default function ReportsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">Reports</h1>
|
<h1 className="admin-title">Reports</h1>
|
||||||
<p className="mb-4 rounded bg-amber-100 px-3 py-2 text-sm" role="status">
|
<p className="mb-4 rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-900" role="status">
|
||||||
This file contains personal data.
|
This file contains personal data.
|
||||||
</p>
|
</p>
|
||||||
<form className="grid gap-3 rounded-lg bg-white p-4 shadow-sm" onSubmit={download}>
|
<form className="admin-card grid gap-3" onSubmit={download}>
|
||||||
<label className="grid gap-1 text-sm" htmlFor="export-from">
|
<label className="grid gap-1 text-sm font-medium text-stone-600" htmlFor="export-from">
|
||||||
From
|
From
|
||||||
<input
|
<input
|
||||||
id="export-from"
|
id="export-from"
|
||||||
type="date"
|
type="date"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={from}
|
value={from}
|
||||||
onChange={(event) => setFrom(event.target.value)}
|
onChange={(event) => setFrom(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="grid gap-1 text-sm" htmlFor="export-to">
|
<label className="grid gap-1 text-sm font-medium text-stone-600" htmlFor="export-to">
|
||||||
To
|
To
|
||||||
<input
|
<input
|
||||||
id="export-to"
|
id="export-to"
|
||||||
type="date"
|
type="date"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={to}
|
value={to}
|
||||||
onChange={(event) => setTo(event.target.value)}
|
onChange={(event) => setTo(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button type="submit" className="admin-btn admin-btn-primary" disabled={busy}>
|
||||||
type="submit"
|
|
||||||
className="w-fit rounded bg-stone-900 px-3 py-2 text-white"
|
|
||||||
disabled={busy}
|
|
||||||
>
|
|
||||||
{busy ? 'Exporting…' : 'Export to Excel'}
|
{busy ? 'Exporting…' : 'Export to Excel'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -89,44 +89,57 @@ export default function SettingsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">Settings</h1>
|
<h1 className="admin-title">Settings</h1>
|
||||||
<form className="grid gap-3 rounded-lg bg-white p-4 shadow-sm" onSubmit={save}>
|
<form className="admin-card grid gap-3" onSubmit={save}>
|
||||||
<label className="grid gap-1 text-sm" htmlFor="signatory-name">
|
<label className="grid gap-1 text-sm font-medium text-stone-600" htmlFor="signatory-name">
|
||||||
Signatory name
|
Signatory name
|
||||||
<input
|
<input
|
||||||
id="signatory-name"
|
id="signatory-name"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={settings.name}
|
value={settings.name}
|
||||||
onChange={(event) => setSettings({ ...settings, name: event.target.value })}
|
onChange={(event) => setSettings({ ...settings, name: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="grid gap-1 text-sm" htmlFor="signatory-designation">
|
<label
|
||||||
|
className="grid gap-1 text-sm font-medium text-stone-600"
|
||||||
|
htmlFor="signatory-designation"
|
||||||
|
>
|
||||||
Designation
|
Designation
|
||||||
<input
|
<input
|
||||||
id="signatory-designation"
|
id="signatory-designation"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={settings.designation}
|
value={settings.designation}
|
||||||
onChange={(event) => setSettings({ ...settings, designation: event.target.value })}
|
onChange={(event) => setSettings({ ...settings, designation: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" className="w-fit rounded bg-stone-900 px-3 py-2 text-white">
|
<button type="submit" className="admin-btn admin-btn-primary">
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div className="mt-4 rounded-lg bg-white p-4 shadow-sm">
|
<div className="admin-card mt-4">
|
||||||
<h2 className="mb-2 font-semibold">Signature image</h2>
|
<h2 className="mb-2 font-semibold text-[#134e4a]">Signature image</h2>
|
||||||
<p className="mb-3 text-sm text-stone-600">
|
<p className="mb-3 text-sm text-stone-600">
|
||||||
PNG only, up to 500KB. A transparent background is recommended.
|
PNG only, up to 500KB. A transparent background is recommended.
|
||||||
</p>
|
</p>
|
||||||
{settings.signatureUrl ? (
|
{settings.signatureUrl ? (
|
||||||
<img src={settings.signatureUrl} alt="CREST signature" className="mb-3 h-16" />
|
<img
|
||||||
|
src={settings.signatureUrl}
|
||||||
|
alt="CREST signature"
|
||||||
|
className="mb-3 block h-16 rounded-lg border border-stone-200 bg-stone-50 p-2"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="mb-3 text-sm">No signature uploaded</p>
|
<p className="mb-3 text-sm">No signature uploaded</p>
|
||||||
)}
|
)}
|
||||||
<input aria-label="Signature image" type="file" accept="image/png" onChange={upload} />
|
<input
|
||||||
|
aria-label="Signature image"
|
||||||
|
type="file"
|
||||||
|
accept="image/png"
|
||||||
|
className="admin-file"
|
||||||
|
onChange={upload}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<form className="mt-4 grid gap-3 rounded-lg bg-white p-4 shadow-sm" onSubmit={saveReminders}>
|
<form className="admin-card mt-4 grid gap-3" onSubmit={saveReminders}>
|
||||||
<h2 className="font-semibold">Reminders</h2>
|
<h2 className="font-semibold text-[#134e4a]">Reminders</h2>
|
||||||
<label className="flex items-center gap-2 text-sm" htmlFor="reminders-enabled">
|
<label className="flex items-center gap-2 text-sm" htmlFor="reminders-enabled">
|
||||||
<input
|
<input
|
||||||
id="reminders-enabled"
|
id="reminders-enabled"
|
||||||
|
|
@ -144,7 +157,7 @@ export default function SettingsPage() {
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={30}
|
max={30}
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={reminders.draftReminderDays}
|
value={reminders.draftReminderDays}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setReminders({ ...reminders, draftReminderDays: event.target.value })
|
setReminders({ ...reminders, draftReminderDays: event.target.value })
|
||||||
|
|
@ -158,14 +171,14 @@ export default function SettingsPage() {
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={30}
|
max={30}
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={reminders.surveyReminderDays}
|
value={reminders.surveyReminderDays}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setReminders({ ...reminders, surveyReminderDays: event.target.value })
|
setReminders({ ...reminders, surveyReminderDays: event.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" className="w-fit rounded bg-stone-900 px-3 py-2 text-white">
|
<button type="submit" className="admin-btn admin-btn-primary">
|
||||||
Save reminders
|
Save reminders
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -63,16 +63,13 @@ export default function UsersPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-4 text-2xl font-semibold">Admin users</h1>
|
<h1 className="admin-title">Admin users</h1>
|
||||||
{error ? <p className="mb-3 text-red-700">{error}</p> : null}
|
{error ? <p className="mb-3 text-red-700">{error}</p> : null}
|
||||||
<form
|
<form className="admin-card mb-6 grid gap-3 md:grid-cols-4" onSubmit={create}>
|
||||||
className="mb-6 grid gap-3 rounded-lg bg-white p-4 shadow-sm md:grid-cols-4"
|
|
||||||
onSubmit={create}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
aria-label="Name"
|
aria-label="Name"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(event) => setForm({ ...form, name: event.target.value })}
|
onChange={(event) => setForm({ ...form, name: event.target.value })}
|
||||||
/>
|
/>
|
||||||
|
|
@ -80,7 +77,7 @@ export default function UsersPage() {
|
||||||
aria-label="Email"
|
aria-label="Email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={form.email}
|
value={form.email}
|
||||||
onChange={(event) => setForm({ ...form, email: event.target.value })}
|
onChange={(event) => setForm({ ...form, email: event.target.value })}
|
||||||
/>
|
/>
|
||||||
|
|
@ -88,46 +85,46 @@ export default function UsersPage() {
|
||||||
aria-label="Password"
|
aria-label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
className="rounded border border-stone-300 px-3 py-2"
|
className="admin-input"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(event) => setForm({ ...form, password: event.target.value })}
|
onChange={(event) => setForm({ ...form, password: event.target.value })}
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="rounded bg-stone-900 px-3 py-2 text-white">
|
<button type="submit" className="admin-btn admin-btn-primary">
|
||||||
Create admin
|
Create admin
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div className="overflow-x-auto rounded-lg bg-white shadow-sm">
|
<div className="admin-table-wrap">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="admin-table">
|
||||||
<thead className="border-b border-stone-200 text-stone-500">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2">Name</th>
|
<th>Name</th>
|
||||||
<th className="px-3 py-2">Email</th>
|
<th>Email</th>
|
||||||
<th className="px-3 py-2">Active</th>
|
<th>Active</th>
|
||||||
<th className="px-3 py-2">Password</th>
|
<th>Password</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{users?.map((user) => (
|
{users?.map((user) => (
|
||||||
<tr key={user.id} className="border-b border-stone-100">
|
<tr key={user.id}>
|
||||||
<td className="px-3 py-2">{user.name}</td>
|
<td>{user.name}</td>
|
||||||
<td className="px-3 py-2">{user.email}</td>
|
<td>{user.email}</td>
|
||||||
<td className="px-3 py-2">
|
<td>
|
||||||
{user.isActive ? 'Yes' : 'No'}
|
{user.isActive ? 'Yes' : 'No'}
|
||||||
{user.isActive && user.id !== me?.id ? (
|
{user.isActive && user.id !== me?.id ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-2 underline"
|
className="admin-btn admin-btn-danger ml-2"
|
||||||
onClick={() => deactivate(user.id)}
|
onClick={() => deactivate(user.id)}
|
||||||
>
|
>
|
||||||
Deactivate
|
Deactivate
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">
|
<td>
|
||||||
<input
|
<input
|
||||||
aria-label={`New password for ${user.email}`}
|
aria-label={`New password for ${user.email}`}
|
||||||
type="password"
|
type="password"
|
||||||
className="mr-2 rounded border border-stone-300 px-2 py-1"
|
className="admin-input admin-input-inline mr-2"
|
||||||
value={passwords[user.id] || ''}
|
value={passwords[user.id] || ''}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setPasswords({ ...passwords, [user.id]: event.target.value })
|
setPasswords({ ...passwords, [user.id]: event.target.value })
|
||||||
|
|
@ -135,7 +132,7 @@ export default function UsersPage() {
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="underline"
|
className="admin-btn admin-btn-secondary"
|
||||||
onClick={() => resetPassword(user.id)}
|
onClick={() => resetPassword(user.id)}
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,167 @@
|
||||||
|
:root {
|
||||||
|
--brand: #0f766e;
|
||||||
|
--brand-dark: #134e4a;
|
||||||
|
--brand-blue: #1d4ed8;
|
||||||
|
--page: #f7fbfb;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-muted: #f7faf9;
|
||||||
|
--text: #1c1917;
|
||||||
|
--text-muted: #57534e;
|
||||||
|
--border: #e4e4e0;
|
||||||
|
--green-wash: #e8f5e9;
|
||||||
|
--teal-wash: #ccfbf1;
|
||||||
|
--blue-wash: #dbeafe;
|
||||||
|
--radius: 1rem;
|
||||||
|
--radius-sm: 0.6rem;
|
||||||
|
--shadow: 0 1px 2px rgb(15 118 110 / 0.06), 0 18px 40px rgb(19 78 74 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: Poppins, system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: Georgia, 'Times New Roman', serif;
|
font-family: inherit;
|
||||||
color: #1c1917;
|
color: var(--text);
|
||||||
background: #fafaf9;
|
background: var(--page);
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
.site main {
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
margin: 4rem auto;
|
margin: 2.5rem auto;
|
||||||
padding: 0 1.5rem;
|
padding: 1.75rem 2rem;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.site h1 {
|
||||||
font-size: 2rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.5rem;
|
letter-spacing: -0.02em;
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
color: var(--brand-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.site h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site p {
|
||||||
margin: 0 0 1rem;
|
margin: 0 0 1rem;
|
||||||
color: #44403c;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
.page-lead {
|
||||||
display: grid;
|
margin-bottom: 1.25rem;
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
legend {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
.site form {
|
||||||
textarea,
|
display: grid;
|
||||||
button {
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site fieldset {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.55rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1.15rem 1.2rem 1.25rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site legend {
|
||||||
|
padding: 0 0.35rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
color: var(--brand-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #44403c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site input,
|
||||||
|
.site textarea,
|
||||||
|
.site button,
|
||||||
|
.site select {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
.site input,
|
||||||
textarea {
|
.site textarea,
|
||||||
|
.site select {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site input,
|
||||||
|
.site textarea,
|
||||||
|
.site select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0.55rem 0.7rem;
|
padding: 0.7rem 0.85rem;
|
||||||
border: 1px solid #d6d3d1;
|
border: 1px solid #d6d3d1;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.5rem;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:focus-visible {
|
a:focus-visible {
|
||||||
outline: 2px solid #1d4ed8;
|
outline: 2px solid var(--brand);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site {
|
.site {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
overflow: hidden;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #f4f7f8;
|
color: var(--text);
|
||||||
color: #1c1917;
|
background-color: var(--page);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse 55% 45% at 8% 12%, rgb(232 245 233 / 0.95), transparent 62%),
|
||||||
|
radial-gradient(ellipse 50% 50% at 92% 8%, rgb(219 234 254 / 0.8), transparent 58%),
|
||||||
|
radial-gradient(ellipse 60% 50% at 88% 92%, rgb(204 251 241 / 0.8), transparent 55%),
|
||||||
|
radial-gradient(ellipse 40% 35% at 16% 88%, rgb(241 248 233 / 0.85), transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -12% -8%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 22%, #c8e6c9 0%, transparent 42%),
|
||||||
|
radial-gradient(circle at 88% 16%, #bbdefb 0%, transparent 38%),
|
||||||
|
radial-gradient(circle at 78% 86%, #b2dfdb 0%, transparent 42%);
|
||||||
|
filter: blur(80px);
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header,
|
||||||
|
.site-footer,
|
||||||
|
.site main {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-header {
|
.site-header {
|
||||||
background: #fff;
|
background: rgb(255 255 255 / 0.92);
|
||||||
border-bottom: 3px solid #0f766e;
|
backdrop-filter: blur(16px);
|
||||||
|
border-bottom: 3px solid var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-brand {
|
.site-brand {
|
||||||
|
|
@ -84,7 +171,7 @@ a:focus-visible {
|
||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
color: #134e4a;
|
color: var(--brand-dark);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
@ -92,63 +179,153 @@ a:focus-visible {
|
||||||
.site-brand img {
|
.site-brand img {
|
||||||
height: 56px;
|
height: 56px;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
border-right: 1px dotted var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40.0625rem) {
|
||||||
|
.site-brand span {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.site main {
|
.site main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: calc(100% - 2rem);
|
||||||
|
isolation: isolate;
|
||||||
|
background: rgb(255 255 255 / 0.94);
|
||||||
|
border: 1px solid rgb(255 255 255 / 0.8);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site main::before,
|
||||||
|
.site main::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site main::before {
|
||||||
|
top: -40%;
|
||||||
|
right: -18%;
|
||||||
|
width: 17rem;
|
||||||
|
height: 17rem;
|
||||||
|
background: radial-gradient(circle, rgb(187 222 251 / 0.5), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site main::after {
|
||||||
|
bottom: -36%;
|
||||||
|
left: -16%;
|
||||||
|
width: 15rem;
|
||||||
|
height: 15rem;
|
||||||
|
background: radial-gradient(circle, rgb(200 230 201 / 0.42), transparent 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-footer {
|
.site-footer {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
background: #134e4a;
|
background: var(--brand-dark);
|
||||||
color: #f0fdfa;
|
color: #f0fdfa;
|
||||||
font-size: 0.95rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-footer-inner {
|
.site-footer-inner {
|
||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1.25rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-footer p {
|
.site-footer p {
|
||||||
margin: 0 0 0.35rem;
|
margin: 0;
|
||||||
color: #f0fdfa;
|
color: #f0fdfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-footer a {
|
.site-footer-credit {
|
||||||
color: #ccfbf1;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer-sep {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1px;
|
||||||
|
height: 0.85rem;
|
||||||
|
background: #99f6e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site .site-footer a {
|
||||||
|
color: #99f6e4;
|
||||||
|
font-weight: 600;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site .site-footer a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site .site-footer a:focus-visible {
|
||||||
|
outline-color: #99f6e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site a {
|
||||||
|
color: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site button {
|
.site button {
|
||||||
background: #0f766e;
|
padding: 0.7rem 1.15rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.55rem;
|
||||||
|
background: linear-gradient(135deg, var(--brand) 0%, #1578c8 100%);
|
||||||
color: #f0fdfa;
|
color: #f0fdfa;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site button:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #0d6a63 0%, #1d4ed8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site button:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site button.secondary,
|
||||||
.site .tabs button[aria-selected='false'],
|
.site .tabs button[aria-selected='false'],
|
||||||
.site .survey-actions button.secondary {
|
.site .survey-actions button.secondary {
|
||||||
background: #e7e5e4;
|
background: #eef2f1;
|
||||||
color: #1c1917;
|
color: var(--brand-dark);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site button.secondary:hover:not(:disabled),
|
||||||
|
.site .tabs button[aria-selected='false']:hover,
|
||||||
|
.site .survey-actions button.secondary:hover:not(:disabled) {
|
||||||
|
background: #e4eceb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site input:focus,
|
.site input:focus,
|
||||||
.site textarea:focus,
|
.site textarea:focus,
|
||||||
|
.site select:focus,
|
||||||
.site button:focus,
|
.site button:focus,
|
||||||
.site a:focus-visible {
|
.site a:focus-visible {
|
||||||
outline: 2px solid #0f766e;
|
outline: 2px solid var(--brand);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site .progress span {
|
.site .progress span {
|
||||||
background: #0f766e;
|
background: linear-gradient(90deg, var(--brand), #1578c8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-steps {
|
.flow-steps {
|
||||||
display: flex;
|
display: flex;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0 0 1.5rem;
|
margin: 0 0 1.5rem;
|
||||||
padding: 0;
|
padding: 0.35rem 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-steps li {
|
.flow-steps li {
|
||||||
|
|
@ -159,7 +336,8 @@ a:focus-visible {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.8rem;
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,12 +357,12 @@ a:focus-visible {
|
||||||
|
|
||||||
.flow-steps li.done,
|
.flow-steps li.done,
|
||||||
.flow-steps li.current {
|
.flow-steps li.current {
|
||||||
color: #134e4a;
|
color: var(--brand-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-steps li.done::before,
|
.flow-steps li.done::before,
|
||||||
.flow-steps li.current::before {
|
.flow-steps li.current::before {
|
||||||
background: #0f766e;
|
background: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-mark {
|
.flow-mark {
|
||||||
|
|
@ -202,104 +380,113 @@ a:focus-visible {
|
||||||
|
|
||||||
.flow-steps li.current .flow-mark,
|
.flow-steps li.current .flow-mark,
|
||||||
.flow-steps li.done .flow-mark {
|
.flow-steps li.done .flow-mark {
|
||||||
border-color: #0f766e;
|
border-color: var(--brand);
|
||||||
background: #0f766e;
|
background: linear-gradient(135deg, var(--brand), #1578c8);
|
||||||
color: #f0fdfa;
|
color: #f0fdfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-steps li.current .flow-label {
|
.flow-steps li.current .flow-label {
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus,
|
.site textarea {
|
||||||
textarea:focus,
|
|
||||||
button:focus {
|
|
||||||
outline: 2px solid #1d4ed8;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
min-height: 6rem;
|
min-height: 6rem;
|
||||||
}
|
resize: vertical;
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
background: #1c1917;
|
|
||||||
color: #fafaf9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accept {
|
.accept {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.accept input {
|
.accept input {
|
||||||
width: auto;
|
width: auto;
|
||||||
margin-top: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
|
accent-color: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agreement-text {
|
.agreement-text {
|
||||||
max-height: 16rem;
|
max-height: max(16rem, calc(100vh - 31rem));
|
||||||
|
max-height: max(16rem, calc(100svh - 31rem));
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
padding: 1rem;
|
padding: 1.1rem 1.2rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
border: 1px solid #e7e5e4;
|
border: 1px solid var(--border);
|
||||||
background: #fff;
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--surface-muted);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
main.wide {
|
.site main.wide {
|
||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.85rem 1rem;
|
||||||
background: #fef3c7;
|
background: #fff8e8;
|
||||||
border: 1px solid #fcd34d;
|
border: 1px solid #f5e0a8;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner p {
|
.banner p {
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
|
color: #92400e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-frame {
|
.preview-frame {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
border: 1px solid #e7e5e4;
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
width: fit-content;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef2f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs button {
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs button[aria-selected='false'] {
|
.tabs button[aria-selected='false'] {
|
||||||
background: #e7e5e4;
|
background: transparent;
|
||||||
color: #1c1917;
|
color: var(--brand-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-pad {
|
.signature-pad {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border: 1px solid #d6d3d1;
|
margin: 0.35rem 0 0.65rem;
|
||||||
border-radius: 0.4rem;
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
@ -309,14 +496,90 @@ main.wide {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline .done {
|
.timeline {
|
||||||
|
list-style: none;
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline li {
|
||||||
|
position: relative;
|
||||||
|
padding: 0.45rem 0 0.45rem 1.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline li::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0.35rem;
|
||||||
|
top: 0.75rem;
|
||||||
|
width: 0.55rem;
|
||||||
|
height: 0.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline .current {
|
||||||
|
color: var(--brand-dark);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset:disabled {
|
.timeline .current::before {
|
||||||
|
background: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline .done {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline .done::before {
|
||||||
|
background: linear-gradient(135deg, var(--brand), #1578c8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site fieldset:disabled {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-fields {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-fields > label {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-fields > fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-fields > fieldset > legend {
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #44403c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-review p {
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-review strong {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #44403c;
|
||||||
|
}
|
||||||
|
|
||||||
.survey-help {
|
.survey-help {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
@ -330,6 +593,7 @@ fieldset:disabled {
|
||||||
.survey-choice input {
|
.survey-choice input {
|
||||||
width: auto;
|
width: auto;
|
||||||
margin-top: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
|
accent-color: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.survey-grid {
|
.survey-grid {
|
||||||
|
|
@ -340,10 +604,46 @@ fieldset:disabled {
|
||||||
|
|
||||||
.survey-grid th,
|
.survey-grid th,
|
||||||
.survey-grid td {
|
.survey-grid td {
|
||||||
border: 1px solid #e7e5e4;
|
border: 1px solid var(--border);
|
||||||
padding: 0.45rem;
|
padding: 0.55rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid th {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid-input input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid-suffix {
|
||||||
|
flex: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid td.survey-grid-choice {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid-choice .survey-choice {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid-choice .survey-choice input {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.survey-total.warn {
|
.survey-total.warn {
|
||||||
|
|
@ -364,6 +664,7 @@ fieldset:disabled {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
background: #e7e5e4;
|
background: #e7e5e4;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress span {
|
.progress span {
|
||||||
|
|
@ -392,20 +693,38 @@ fieldset:disabled {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: rgba(28, 25, 23, 0.4);
|
background: rgba(19 78 74 / 0.35);
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.survey-dialog div {
|
.survey-dialog > div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 28rem;
|
max-width: 28rem;
|
||||||
padding: 1.25rem;
|
padding: 1.35rem;
|
||||||
background: #fff;
|
background: rgb(255 255 255 / 0.96);
|
||||||
border-radius: 0.5rem;
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 40rem) {
|
@media (max-width: 40rem) {
|
||||||
main {
|
.site main {
|
||||||
margin: 2rem auto;
|
margin: 1.25rem auto;
|
||||||
|
padding: 1.25rem;
|
||||||
|
width: calc(100% - 1.5rem);
|
||||||
|
border-radius: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site main::before,
|
||||||
|
.site main::after {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-steps li {
|
||||||
|
font-size: 0.68rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-label {
|
.cell-label {
|
||||||
|
|
@ -435,12 +754,40 @@ fieldset:disabled {
|
||||||
|
|
||||||
.survey-grid tr {
|
.survey-grid tr {
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
border: 1px solid #e7e5e4;
|
border: 1px solid var(--border);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.survey-grid th,
|
.survey-grid th,
|
||||||
.survey-grid td {
|
.survey-grid td {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-grid td.survey-grid-choice {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid-choice .survey-choice {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid-choice .cell-label {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-grid-choice .survey-choice .visually-hidden {
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
clip: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ export default function AgreementPage() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<h1>Agreement</h1>
|
<h1>Agreement</h1>
|
||||||
|
<p className="page-lead">Read the terms, then enter your details to continue.</p>
|
||||||
<FlowSteps current={readOnly ? 'done' : accepted ? 'details' : 'read'} />
|
<FlowSteps current={readOnly ? 'done' : accepted ? 'details' : 'read'} />
|
||||||
{application?.rejection_reason ? (
|
{application?.rejection_reason ? (
|
||||||
<div className="banner" role="status">
|
<div className="banner" role="status">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ export default function PrivacyPage() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<h1>Privacy</h1>
|
<h1>Privacy</h1>
|
||||||
|
<p className="page-lead">
|
||||||
|
How Chest Research and Training Pvt. Ltd. uses information for this study.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Chest Research and Training Pvt. Ltd. uses the personal information you provide for the Adherence Pack
|
Chest Research and Training Pvt. Ltd. uses the personal information you provide for the Adherence Pack
|
||||||
Study, for payments, for statutory compliance, and for audit.
|
Study, for payments, for statutory compliance, and for audit.
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ export default function ResumePage() {
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p role="status">Checking your private link…</p>
|
<p className="page-lead" role="status">
|
||||||
|
Checking your private link…
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ export default function ReviewPage() {
|
||||||
return (
|
return (
|
||||||
<main className="wide">
|
<main className="wide">
|
||||||
<h1>Review agreement</h1>
|
<h1>Review agreement</h1>
|
||||||
|
<p className="page-lead">Check the preview before you sign.</p>
|
||||||
<FlowSteps current={locked ? 'done' : 'review'} />
|
<FlowSteps current={locked ? 'done' : 'review'} />
|
||||||
{application?.rejection_reason ? (
|
{application?.rejection_reason ? (
|
||||||
<div className="banner" role="status">
|
<div className="banner" role="status">
|
||||||
|
|
@ -86,7 +87,7 @@ export default function ReviewPage() {
|
||||||
) : null}
|
) : null}
|
||||||
{locked ? null : (
|
{locked ? null : (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<button type="button" onClick={editDetails}>
|
<button type="button" className="secondary" onClick={editDetails}>
|
||||||
Edit details
|
Edit details
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => navigate('/sign')}>
|
<button type="button" onClick={() => navigate('/sign')}>
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ export default function SignPage() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<h1>Sign the agreement</h1>
|
<h1>Sign the agreement</h1>
|
||||||
|
<p className="page-lead">Draw your signature, or type it instead.</p>
|
||||||
<FlowSteps current="sign" />
|
<FlowSteps current="sign" />
|
||||||
<form onSubmit={submitSignature}>
|
<form onSubmit={submitSignature}>
|
||||||
<div className="tabs" role="tablist">
|
<div className="tabs" role="tablist">
|
||||||
|
|
@ -115,7 +116,7 @@ export default function SignPage() {
|
||||||
'aria-label': 'Draw your signature',
|
'aria-label': 'Draw your signature',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button type="button" onClick={() => padRef.current?.clear()}>
|
<button type="button" className="secondary" onClick={() => padRef.current?.clear()}>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,13 @@ export default function SubmittedPage() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<h1>Agreement submitted</h1>
|
<h1>Agreement submitted</h1>
|
||||||
|
<p className="page-lead">Your signed agreement has been sent for review.</p>
|
||||||
<FlowSteps current="done" />
|
<FlowSteps current="done" />
|
||||||
{error ? (
|
{error ? (
|
||||||
<p className="error" role="alert">
|
<p className="error" role="alert">
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<p>Your signed agreement has been sent for review.</p>
|
|
||||||
<ol className="timeline">
|
<ol className="timeline">
|
||||||
<li className="done">Submitted</li>
|
<li className="done">Submitted</li>
|
||||||
<li className={decided ? 'done' : 'current'}>Under review</li>
|
<li className={decided ? 'done' : 'current'}>Under review</li>
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,11 @@ function formatAnswer(question, value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ question: Record<string, unknown>, value: unknown, invalid?: boolean, onChange: (value: unknown) => void }} props
|
* @param {{ question: Record<string, unknown>, value: unknown, invalid?: boolean, hideLabel?: boolean, onChange: (value: unknown) => void }} props
|
||||||
*/
|
*/
|
||||||
function QuestionField({ question, value, invalid = false, onChange }) {
|
function QuestionField({ question, value, invalid = false, hideLabel = false, onChange }) {
|
||||||
|
const legendClass = hideLabel ? 'visually-hidden' : undefined;
|
||||||
|
|
||||||
if (question.type === 'help') {
|
if (question.type === 'help') {
|
||||||
return <p className="survey-help">{question.text}</p>;
|
return <p className="survey-help">{question.text}</p>;
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +120,7 @@ function QuestionField({ question, value, invalid = false, onChange }) {
|
||||||
const current = value && typeof value === 'object' ? value : {};
|
const current = value && typeof value === 'object' ? value : {};
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{question.label}</legend>
|
<legend className={legendClass}>{question.label}</legend>
|
||||||
{question.options.map((option) => (
|
{question.options.map((option) => (
|
||||||
<label key={option.id} className="survey-choice">
|
<label key={option.id} className="survey-choice">
|
||||||
<input
|
<input
|
||||||
|
|
@ -165,7 +167,7 @@ function QuestionField({ question, value, invalid = false, onChange }) {
|
||||||
const grid = value && typeof value === 'object' ? value : {};
|
const grid = value && typeof value === 'object' ? value : {};
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{question.label}</legend>
|
<legend className={legendClass}>{question.label}</legend>
|
||||||
<table className="survey-grid">
|
<table className="survey-grid">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -184,26 +186,30 @@ function QuestionField({ question, value, invalid = false, onChange }) {
|
||||||
{question.columns.map((column) => (
|
{question.columns.map((column) => (
|
||||||
<td key={column.id}>
|
<td key={column.id}>
|
||||||
<span className="cell-label">{column.label}</span>
|
<span className="cell-label">{column.label}</span>
|
||||||
<input
|
<div className="survey-grid-input">
|
||||||
type="number"
|
<input
|
||||||
inputMode={question.integer ? 'numeric' : 'decimal'}
|
type="number"
|
||||||
min={question.min}
|
inputMode={question.integer ? 'numeric' : 'decimal'}
|
||||||
max={question.max}
|
min={question.min}
|
||||||
aria-invalid={invalid || undefined}
|
max={question.max}
|
||||||
aria-label={`${row.label}, ${column.label}`}
|
aria-invalid={invalid || undefined}
|
||||||
value={grid[row.id]?.[column.id] ?? ''}
|
aria-label={`${row.label}, ${column.label}`}
|
||||||
onChange={(event) => {
|
value={grid[row.id]?.[column.id] ?? ''}
|
||||||
const raw = event.target.value;
|
onChange={(event) => {
|
||||||
onChange({
|
const raw = event.target.value;
|
||||||
...grid,
|
onChange({
|
||||||
[row.id]: {
|
...grid,
|
||||||
...(grid[row.id] ?? {}),
|
[row.id]: {
|
||||||
[column.id]: raw === '' ? '' : Number(raw),
|
...(grid[row.id] ?? {}),
|
||||||
},
|
[column.id]: raw === '' ? '' : Number(raw),
|
||||||
});
|
},
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
{question.suffix ? <span> {question.suffix}</span> : null}
|
/>
|
||||||
|
{question.suffix ? (
|
||||||
|
<span className="survey-grid-suffix">{question.suffix}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -243,7 +249,7 @@ function QuestionField({ question, value, invalid = false, onChange }) {
|
||||||
const selected = value && typeof value === 'object' ? value : {};
|
const selected = value && typeof value === 'object' ? value : {};
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{question.label}</legend>
|
<legend className={legendClass}>{question.label}</legend>
|
||||||
<table className="survey-grid">
|
<table className="survey-grid">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -260,7 +266,7 @@ function QuestionField({ question, value, invalid = false, onChange }) {
|
||||||
<tr key={row.id}>
|
<tr key={row.id}>
|
||||||
<th scope="row">{row.label}</th>
|
<th scope="row">{row.label}</th>
|
||||||
{question.scale.map((point) => (
|
{question.scale.map((point) => (
|
||||||
<td key={point.id}>
|
<td key={point.id} className="survey-grid-choice">
|
||||||
<span className="cell-label">{point.label}</span>
|
<span className="cell-label">{point.label}</span>
|
||||||
<label className="survey-choice">
|
<label className="survey-choice">
|
||||||
<input
|
<input
|
||||||
|
|
@ -356,6 +362,18 @@ export default function SurveyPage() {
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [screen, token]);
|
}, [screen, token]);
|
||||||
|
|
||||||
|
const shownSection = useRef(sectionIndex);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shownSection.current === sectionIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shownSection.current = sectionIndex;
|
||||||
|
const page = document.scrollingElement || document.documentElement;
|
||||||
|
page.scrollTo?.({ top: 0, behavior: 'smooth' });
|
||||||
|
}, [sectionIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stepErrors.length > 0) {
|
if (stepErrors.length > 0) {
|
||||||
stepAlertRef.current?.scrollIntoView?.({ block: 'nearest' });
|
stepAlertRef.current?.scrollIntoView?.({ block: 'nearest' });
|
||||||
|
|
@ -497,7 +515,7 @@ export default function SurveyPage() {
|
||||||
<span style={{ width: `${progress}%` }} />
|
<span style={{ width: `${progress}%` }} />
|
||||||
</div>
|
</div>
|
||||||
{review ? (
|
{review ? (
|
||||||
<section>
|
<section className="survey-review">
|
||||||
<h2>Review</h2>
|
<h2>Review</h2>
|
||||||
{sections.map((item) => (
|
{sections.map((item) => (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
|
|
@ -528,15 +546,21 @@ export default function SurveyPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{section.questions.map((question) => (
|
<div className="survey-fields">
|
||||||
<QuestionField
|
{section.questions.map((question, index) => (
|
||||||
key={question.id}
|
<QuestionField
|
||||||
question={question}
|
key={question.id}
|
||||||
invalid={stepErrors.some((item) => item.id === question.id)}
|
question={question}
|
||||||
value={answers[question.id]}
|
hideLabel={
|
||||||
onChange={(value) => updateAnswer(question.id, value)}
|
section.questions[index - 1]?.type === 'section_header' &&
|
||||||
/>
|
section.questions[index - 1].label === question.label
|
||||||
))}
|
}
|
||||||
|
invalid={stepErrors.some((item) => item.id === question.id)}
|
||||||
|
value={answers[question.id]}
|
||||||
|
onChange={(value) => updateAnswer(question.id, value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{error ? (
|
{error ? (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue