feat(i18n): add internationalization support with English and Spanish translations
feat(kiosk): implement kiosk report submission functionality with offline support test(outbox): add tests for outbox functionality including report submission and error handling feat(outbox): create outbox management for queued reports with IndexedDB persistence feat(uuid): add UUID generation utility for client IDs feat(qr): implement QR code generation for reporting links by site and area feat(kiosk): create Kiosk page for reporting incidents without an account
This commit is contained in:
1 parent
3f6dff5f24
commit
3e964d1176
64 files changed
+10056
-267
No files matched your search
+21
-10
@@ -1,10 +1,13 @@
|
||||
import { Info, LogOut, Plus, Settings, ShieldCheck, X } from 'lucide-react'
|
||||
import { NavLink, useNavigate } from 'react-router-dom'
|
||||
import { useI18n } from '../i18n'
|
||||
import { outbox } from '../lib/outbox'
|
||||
import { initials } from '../lib/initials'
|
||||
import { roleLabel } from '../lib/labels'
|
||||
import { primaryNav } from '../lib/navigation'
|
||||
import { useLogout, type Me } from '../lib/queries'
|
||||
import { Eyebrow } from './Eyebrow'
|
||||
import { LanguageToggle } from './LanguageToggle'
|
||||
import { iconButton } from './styles'
|
||||
|
||||
const navLink = ({ isActive }: { isActive: boolean }) =>
|
||||
@@ -21,6 +24,13 @@ interface SidebarProps {
|
||||
export function Sidebar({ me, open, onClose, badges }: SidebarProps) {
|
||||
const logout = useLogout()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useI18n()
|
||||
|
||||
const signOut = () => {
|
||||
const unsent = outbox.getState().items.filter((item) => item.kind === 'report' && item.userId === me.user.id).length
|
||||
if (unsent && !confirm(t.outbox.signOutWarning(unsent))) return
|
||||
logout.mutate(undefined, { onSuccess: () => navigate('/login') })
|
||||
}
|
||||
return (
|
||||
<aside
|
||||
className={`fixed inset-y-0 z-10 flex w-[260px] flex-none flex-col overflow-y-auto bg-forest-950 px-[15px] pt-7 pb-[18px] text-forest-100 shadow-[8px_0_30px_#163c3740] transition-[left] duration-200 sm:static sm:min-h-screen sm:w-[230px] sm:shadow-none lg:w-[246px] print:hidden ${open ? 'left-0' : '-left-[270px]'}`}
|
||||
@@ -33,7 +43,7 @@ export function Sidebar({ me, open, onClose, badges }: SidebarProps) {
|
||||
<strong className="block font-display text-[19px] leading-tight font-bold tracking-[-0.5px] whitespace-nowrap sm:text-[17px] lg:text-[19px]">Open SafetyLog</strong>
|
||||
<span className="mt-0.5 block text-3xs text-forest-300">EHS · Incident intelligence</span>
|
||||
</div>
|
||||
<button type="button" className={`${iconButton} absolute top-2 right-2 sm:hidden`} onClick={onClose} aria-label="Close menu">
|
||||
<button type="button" className={`${iconButton} absolute top-2 right-2 sm:hidden`} onClick={onClose} aria-label={t.nav.closeMenu}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -41,7 +51,7 @@ export function Sidebar({ me, open, onClose, badges }: SidebarProps) {
|
||||
<div className="mb-[27px] flex items-center gap-2.5 rounded-[7px] border border-[#316158] p-[11px]">
|
||||
<div className="grid size-[29px] flex-none place-items-center rounded-full bg-[#f1c865] font-mono text-2xs font-bold text-[#143d37]">{initials(me.organization.name)}</div>
|
||||
<div className="min-w-0">
|
||||
<Eyebrow>Workspace</Eyebrow>
|
||||
<Eyebrow>{t.nav.workspace}</Eyebrow>
|
||||
<strong className="block truncate text-[13px] text-white">{me.organization.name}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,29 +60,30 @@ export function Sidebar({ me, open, onClose, badges }: SidebarProps) {
|
||||
{primaryNav.map(({ label, to, icon: Icon }) => (
|
||||
<NavLink key={to} to={to} end={to === '/'} onClick={onClose} className={navLink}>
|
||||
<Icon size={18} />
|
||||
<span>{label}</span>
|
||||
<span>{t.nav[label]}</span>
|
||||
{!!badges[to] && <em className="ml-auto rounded-[10px] bg-coral px-[7px] py-0.5 font-mono text-2xs text-white not-italic">{badges[to]}</em>}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className={navSection}>
|
||||
<Eyebrow className="px-3 pb-2 text-[#8da29b]">Manage</Eyebrow>
|
||||
<Eyebrow className="px-3 pb-2 text-[#8da29b]">{t.nav.manage}</Eyebrow>
|
||||
<NavLink to="/report" onClick={onClose} className={navLink}>
|
||||
<Plus size={18} />
|
||||
<span>Report an incident</span>
|
||||
<span>{t.nav.reportIncident}</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto">
|
||||
<NavLink to="/about" onClick={onClose} className={navLink}>
|
||||
<Info size={18} />
|
||||
<span>About</span>
|
||||
<span>{t.nav.about}</span>
|
||||
</NavLink>
|
||||
<NavLink to="/settings" onClick={onClose} className={navLink}>
|
||||
<Settings size={18} />
|
||||
<span>Settings</span>
|
||||
<span>{t.nav.settings}</span>
|
||||
</NavLink>
|
||||
<LanguageToggle className="h-[42px] w-full px-3 text-forest-200 hover:text-white" />
|
||||
<div className="mt-4 flex items-center gap-2.5 border-t border-forest-700 px-2 pt-5">
|
||||
<div className="grid size-[31px] flex-none place-items-center rounded-full bg-[#5c8274] font-mono text-2xs text-white">{initials(me.user.name)}</div>
|
||||
<div className="min-w-0">
|
||||
@@ -82,9 +93,9 @@ export function Sidebar({ me, open, onClose, badges }: SidebarProps) {
|
||||
<button
|
||||
type="button"
|
||||
className={`${iconButton} ml-auto text-[#91aaa0] hover:text-white`}
|
||||
aria-label="Sign out"
|
||||
title="Sign out"
|
||||
onClick={() => logout.mutate(undefined, { onSuccess: () => navigate('/login') })}
|
||||
aria-label={t.nav.signOut}
|
||||
title={t.nav.signOut}
|
||||
onClick={signOut}
|
||||
>
|
||||
<LogOut size={17} />
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user