mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 23:46:35 +00:00
ba70ad28b8
Arabic runs the app right to left, but margins, paddings, insets, text alignment, borders and corner radii were written for the left side, so icons sat against the wrong edge of their labels, indents and badges pointed the wrong way and amount columns aligned left. They now use the logical forms (ms-, ps-, start-, text-start, marginInlineStart, insetInlineEnd and their CSS equivalents), which are the very same sides in a left to right language. Geometry stays physical on purpose: maps, positions measured from the pointer or a bounding box, time axes, sliders, transforms and print output. Amounts drawn as several spans keep their order in a right to left row, the time picker keeps hours before minutes, and two switches whose thumb slid out of its track in Arabic now move the right way. lint:rtl counts what is still physical per file against a baseline that may only shrink (1512 uses in 370 files before, 319 in 113 now), and CI runs it, so new code reaches for the logical forms.
130 lines
5.9 KiB
TypeScript
130 lines
5.9 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { useNavigate } from 'react-router'
|
|
import { useAuthStore } from '../../store/authStore'
|
|
import { useInAppNotificationStore } from '../../store/inAppNotificationStore'
|
|
import { useTranslation } from '../../i18n'
|
|
import { Bell, Settings, Shield, LogOut } from 'lucide-react'
|
|
|
|
// Mobile-only: a slim strip at the very top of the dashboard with the
|
|
// notification + profile icons (right-aligned). Scrolls with the page.
|
|
export default function MobileTopBar() {
|
|
const { t } = useTranslation()
|
|
const navigate = useNavigate()
|
|
const { user, isAuthenticated } = useAuthStore()
|
|
const unread = useInAppNotificationStore(s => s.unreadCount)
|
|
const fetchUnreadCount = useInAppNotificationStore(s => s.fetchUnreadCount)
|
|
const [showProfile, setShowProfile] = useState(false)
|
|
|
|
useEffect(() => { if (isAuthenticated) fetchUnreadCount() }, [isAuthenticated])
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className="md:hidden flex items-center justify-end gap-2 px-4"
|
|
style={{ paddingTop: 'calc(10px + env(safe-area-inset-top, 0px))', paddingBottom: 10 }}
|
|
>
|
|
<button type="button"
|
|
onClick={() => navigate('/notifications')}
|
|
aria-label={t('notifications.title')}
|
|
className="relative grid place-items-center rounded-full active:scale-95 transition-transform"
|
|
style={{ width: 36, height: 36, color: 'var(--ink-2, #52525b)' }}
|
|
>
|
|
<Bell size={20} strokeWidth={1.9} />
|
|
{unread > 0 && (
|
|
<span style={{ position: 'absolute', top: 7, insetInlineEnd: 7, width: 8, height: 8, borderRadius: '50%', background: 'oklch(0.7 0.17 38)', boxShadow: '0 0 0 2px var(--bg, #fff)' }} />
|
|
)}
|
|
</button>
|
|
<button type="button"
|
|
onClick={() => setShowProfile(true)}
|
|
aria-label={t('nav.profile')}
|
|
className="grid place-items-center rounded-full text-white font-semibold text-[12px] active:scale-95 transition-transform"
|
|
style={{ width: 34, height: 34, background: 'linear-gradient(135deg, oklch(0.7 0.14 38), oklch(0.55 0.13 25))' }}
|
|
>
|
|
{(user?.username || '?')[0].toUpperCase()}
|
|
</button>
|
|
</div>
|
|
|
|
{showProfile && createPortal(<ProfileSheet onClose={() => setShowProfile(false)} />, document.body)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
function ProfileSheet({ onClose }: { onClose: () => void }) {
|
|
const { t } = useTranslation()
|
|
const { user, logout } = useAuthStore()
|
|
const navigate = useNavigate()
|
|
|
|
const handleNav = (path: string) => { onClose(); navigate(path) }
|
|
const handleLogout = () => { onClose(); logout(); navigate('/login') }
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[300] md:hidden" role="presentation" onClick={onClose}>
|
|
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
|
<div
|
|
className="absolute bottom-0 inset-x-0 bg-white dark:bg-zinc-900 rounded-t-2xl overflow-hidden"
|
|
style={{ animation: 'slideUp 0.25s ease-out', paddingBottom: 'env(safe-area-inset-bottom, 0px)' }}
|
|
role="presentation"
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
<div className="flex justify-center pt-3 pb-2">
|
|
<div className="w-10 h-1 rounded-full bg-zinc-300 dark:bg-zinc-700" />
|
|
</div>
|
|
|
|
<div className="px-6 pb-4 pt-1">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-11 h-11 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 flex items-center justify-center text-[16px] font-bold">
|
|
{(user?.username || '?')[0].toUpperCase()}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-[15px] font-semibold text-zinc-900 dark:text-white">{user?.username}</p>
|
|
<p className="text-[12px] text-zinc-500 truncate">{user?.email}</p>
|
|
</div>
|
|
{user?.role === 'admin' && (
|
|
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 text-[10px] font-semibold text-zinc-600 dark:text-zinc-400 uppercase tracking-wide">
|
|
<Shield size={10} /> Admin
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="h-px bg-zinc-100 dark:bg-zinc-800 mx-4" />
|
|
|
|
<div className="py-2 px-2">
|
|
<button type="button"
|
|
onClick={() => handleNav('/settings')}
|
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-start hover:bg-zinc-50 dark:hover:bg-zinc-800 active:bg-zinc-100 dark:active:bg-zinc-800 transition-colors"
|
|
>
|
|
<Settings size={18} className="text-zinc-500" />
|
|
<span className="text-[14px] font-medium text-zinc-900 dark:text-white">{t('nav.bottomSettings')}</span>
|
|
</button>
|
|
|
|
{user?.role === 'admin' && (
|
|
<button type="button"
|
|
onClick={() => handleNav('/admin')}
|
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-start hover:bg-zinc-50 dark:hover:bg-zinc-800 active:bg-zinc-100 dark:active:bg-zinc-800 transition-colors"
|
|
>
|
|
<Shield size={18} className="text-zinc-500" />
|
|
<span className="text-[14px] font-medium text-zinc-900 dark:text-white">{t('nav.bottomAdmin')}</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="h-px bg-zinc-100 dark:bg-zinc-800 mx-4" />
|
|
|
|
<div className="py-2 px-2">
|
|
<button type="button"
|
|
onClick={handleLogout}
|
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-start hover:bg-red-50 dark:hover:bg-red-900/20 active:bg-red-100 transition-colors"
|
|
>
|
|
<LogOut size={18} className="text-red-500" />
|
|
<span className="text-[14px] font-medium text-red-600 dark:text-red-400">{t('nav.bottomLogout')}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="h-4" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|