Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/components/AppLayout/BetaBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { useIntl } from '@/i18n'
import { DismissibleBanner } from './DismissibleBanner'

export const BetaBanner = () => (
<DismissibleBanner storageKey="mr-beta-banner-dismissed">
This is a beta version of MapRoulette and is actively being worked on. Some features may be
incomplete or unavailable.
</DismissibleBanner>
)
export const BetaBanner = () => {
const { t } = useIntl()
return (
<DismissibleBanner storageKey="mr-beta-banner-dismissed">
{t(
'appLayout.betaBanner.message',
undefined,
'This is a beta version of MapRoulette and is actively being worked on. Some features may be incomplete or unavailable.'
)}
</DismissibleBanner>
)
}
2 changes: 1 addition & 1 deletion src/components/AppLayout/DismissibleBanner.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cleanup, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, describe, expect, it } from 'vitest'
import { cleanup, render, screen } from '@/test/testUtils'
import { DismissibleBanner } from './DismissibleBanner.tsx'

afterEach(() => {
Expand Down
4 changes: 3 additions & 1 deletion src/components/AppLayout/DismissibleBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { X } from 'lucide-react'
import { type ReactNode, useState } from 'react'
import { useIntl } from '@/i18n'

interface Props {
/** localStorage key used to persist the dismissed state across reloads. */
Expand All @@ -8,6 +9,7 @@ interface Props {
}

export const DismissibleBanner = ({ storageKey, children }: Props) => {
const { t } = useIntl()
const [dismissed, setDismissed] = useState(() => localStorage.getItem(storageKey) === 'true')

if (dismissed) return null
Expand All @@ -24,7 +26,7 @@ export const DismissibleBanner = ({ storageKey, children }: Props) => {
type="button"
onClick={handleDismiss}
className="-translate-y-1/2 absolute top-1/2 right-3 rounded p-1 hover:bg-slate-800 dark:hover:bg-slate-200"
aria-label="Dismiss banner"
aria-label={t('appLayout.dismissibleBanner.dismiss', undefined, 'Dismiss banner')}
>
<X className="h-4 w-4" />
</button>
Expand Down
13 changes: 11 additions & 2 deletions src/components/AppLayout/Header/DesktopNav.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Link } from '@tanstack/react-router'
import { ExternalLink } from 'lucide-react'
import { useNavigationContext } from '@/contexts/NavigationContext'
import { useIntl } from '@/i18n'

export const DesktopNav = () => {
const { allNavigationItems } = useNavigationContext()
const { t } = useIntl()

return (
<nav aria-label="Primary" className="hidden text-sm lg:flex lg:gap-6">
<nav
aria-label={t('common.primary', undefined, 'Primary')}
className="hidden text-sm lg:flex lg:gap-6"
>
{allNavigationItems.map((item) => (
<Link
key={item.to}
Expand All @@ -19,7 +24,11 @@ export const DesktopNav = () => {
{item.openInNewTab && (
<ExternalLink
className="size-3.5 text-current"
aria-label={`Open ${item.label} in a new tab`}
aria-label={t(
'appLayout.header.nav.openInNewTab',
{ label: item.label },
'Open {label} in a new tab'
)}
/>
)}
</Link>
Expand Down
46 changes: 35 additions & 11 deletions src/components/AppLayout/Header/DropdownMenuNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { Card } from '@/components/ui/Card'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs'
import { useNotificationsContext } from '@/contexts/NotificationsContext'
import { useIntl } from '@/i18n'
import type { User } from '@/types/User'

export const DropdownMenuNotifications = ({ user }: { user: User }) => {
const { notifications, isLoading, markAllAsRead, closeThread } = useNotificationsContext()
const navigate = useNavigate()
const [open, setOpen] = useState(false)
const { t } = useIntl()

const unreadNotifications = notifications.filter((n) => !n.isRead)
const unreadCount = unreadNotifications.length
Expand All @@ -33,7 +35,11 @@ export const DropdownMenuNotifications = ({ user }: { user: User }) => {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
aria-label={`Notifications for ${user.osmProfile.displayName}`}
aria-label={t(
'appLayout.header.notifications.triggerLabel',
{ name: user.osmProfile.displayName },
'Notifications for {name}'
)}
className="relative flex"
>
<Bell className="size-5" aria-hidden="true" />
Expand All @@ -42,7 +48,13 @@ export const DropdownMenuNotifications = ({ user }: { user: User }) => {
aria-live="polite"
className="-right-0.75 -top-0.75 absolute size-1.25 rounded-full bg-red-400 motion-safe:animate-pulse"
>
<span className="sr-only">You have unread notifications</span>
<span className="sr-only">
{t(
'appLayout.header.notifications.unreadSrOnly',
undefined,
'You have unread notifications'
)}
</span>
</span>
)}
</PopoverTrigger>
Expand All @@ -54,23 +66,27 @@ export const DropdownMenuNotifications = ({ user }: { user: User }) => {
<Tabs defaultValue="unread" className="flex min-h-0 flex-1 flex-col">
<div className="mb-4 flex items-center justify-between gap-2">
<TabsList>
<TabsTrigger value="unread">Unread ({unreadCount})</TabsTrigger>
<TabsTrigger value="all">All ({notifications.length})</TabsTrigger>
<TabsTrigger value="unread">
{t('common.unread', { count: unreadCount }, 'Unread ({count})')}
</TabsTrigger>
<TabsTrigger value="all">
{t('common.all', { count: notifications.length }, 'All ({count})')}
</TabsTrigger>
</TabsList>
<Button
variant="outline"
size="sm"
onClick={handleMarkAllAsRead}
disabled={unreadCount === 0}
>
Mark all as read
{t('common.markAllAsRead', undefined, 'Mark all as read')}
</Button>
</div>

{isLoading ? (
<Card className="p-6">
<div className="text-center text-sm text-zinc-500 dark:text-slate-500">
Loading notifications...
{t('common.loadingNotifications', undefined, 'Loading notifications...')}
</div>
</Card>
) : (
Expand All @@ -81,8 +97,12 @@ export const DropdownMenuNotifications = ({ user }: { user: User }) => {
>
<NotificationList
notifications={unreadNotifications}
emptyTitle="You're all up to date"
emptyDescription="You have no unread notifications at the moment."
emptyTitle={t('common.youreAllUpToDate', undefined, "You're all up to date")}
emptyDescription={t(
'common.noUnreadNotificationsYet',
undefined,
'You have no unread notifications at the moment.'
)}
onLinkClick={() => setOpen(false)}
/>
</TabsContent>
Expand All @@ -92,8 +112,12 @@ export const DropdownMenuNotifications = ({ user }: { user: User }) => {
>
<NotificationList
notifications={notifications}
emptyTitle="You're all up to date"
emptyDescription="You have no notifications."
emptyTitle={t('common.youreAllUpToDate', undefined, "You're all up to date")}
emptyDescription={t(
'common.noNotificationsYet',
undefined,
'You have no notifications.'
)}
onLinkClick={() => setOpen(false)}
/>
</TabsContent>
Expand All @@ -107,7 +131,7 @@ export const DropdownMenuNotifications = ({ user }: { user: User }) => {
onClick={() => setOpen(false)}
className="link block text-center font-medium text-sm"
>
View all notifications
{t('common.viewAllNotifications', undefined, 'View all notifications')}
</Link>
</div>
</PopoverContent>
Expand Down
31 changes: 22 additions & 9 deletions src/components/AppLayout/Header/DropdownMenuUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/DropdownMenu'
import { useAuthContext } from '@/contexts/AuthContext'
import { useIntl } from '@/i18n'
import { isSuperUser } from '@/lib/SuperAdminGuard'
import { initials } from '@/lib/utils'
import { frontendVersion } from '@/lib/version'
import type { User } from '@/types/User'

export const DropdownMenuUser = ({ user }: { user: User }) => {
const { logout } = useAuthContext()
const { t } = useIntl()
const { data: serviceInfo, isError: serviceInfoError } = api.service.info()
const backendVersion = serviceInfoError ? 'unknown' : (serviceInfo?.compiletime.version ?? null)
return (
Expand All @@ -29,7 +31,9 @@ export const DropdownMenuUser = ({ user }: { user: User }) => {
<AvatarImage src={user.osmProfile.avatarURL} alt={user.osmProfile.displayName} />
<AvatarFallback>{initials(user.osmProfile.displayName)}</AvatarFallback>
</Avatar>
<span className="sr-only">User menu</span>
<span className="sr-only">
{t('appLayout.header.userMenu.triggerSrOnly', undefined, 'User menu')}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent className={'w-56'} sideOffset={10} alignOffset={-10} align="end">
<DropdownMenuGroup>
Expand All @@ -43,7 +47,11 @@ export const DropdownMenuUser = ({ user }: { user: User }) => {
<br />
<Link to="/profile">
<span className="text-xs text-zinc-500 hover:text-zinc-400 dark:text-slate-500 dark:hover:text-slate-400">
View public profile
{t(
'appLayout.header.userMenu.viewPublicProfile',
undefined,
'View public profile'
)}
</span>
</Link>
</div>
Expand All @@ -52,23 +60,27 @@ export const DropdownMenuUser = ({ user }: { user: User }) => {
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/settings">
<CircleUser className="size-4" aria-hidden="true" /> Account Settings
<CircleUser className="size-4" aria-hidden="true" />{' '}
{t('appLayout.header.userMenu.accountSettings', undefined, 'Account Settings')}
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/manage">
<LayoutDashboard className="size-4" aria-hidden="true" /> Manage
<LayoutDashboard className="size-4" aria-hidden="true" />{' '}
{t('common.manage', undefined, 'Manage')}
</Link>
</DropdownMenuItem>
{isSuperUser(user) && (
<DropdownMenuItem asChild>
<Link to="/super-admin">
<Shield className="size-4" aria-hidden="true" /> Super Admin
<Shield className="size-4" aria-hidden="true" />{' '}
{t('appLayout.header.userMenu.superAdmin', undefined, 'Super Admin')}
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={logout}>
<LogOut className="size-4" aria-hidden="true" /> Sign out
<LogOut className="size-4" aria-hidden="true" />{' '}
{t('appLayout.header.userMenu.signOut', undefined, 'Sign out')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
Expand All @@ -79,14 +91,15 @@ export const DropdownMenuUser = ({ user }: { user: User }) => {
>
<div className="flex items-center justify-between gap-2">
<span className="flex gap-2">
<SwatchBook className="size-4" aria-hidden="true" /> Theme
<SwatchBook className="size-4" aria-hidden="true" />{' '}
{t('appLayout.header.userMenu.theme', undefined, 'Theme')}
</span>
<ThemeSwitcher />
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<dl className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-0.5 px-2 py-1.5 text-[10px] text-zinc-400 leading-tight dark:text-slate-500">
<dt>Frontend</dt>
<dt>{t('appLayout.header.userMenu.frontend', undefined, 'Frontend')}</dt>
<dd>
<a
href={`https://github.com/maproulette/maproulette3/releases/tag/v${frontendVersion}`}
Expand All @@ -97,7 +110,7 @@ export const DropdownMenuUser = ({ user }: { user: User }) => {
v{frontendVersion}
</a>
</dd>
<dt>Backend</dt>
<dt>{t('appLayout.header.userMenu.backend', undefined, 'Backend')}</dt>
<dd>
{backendVersion && backendVersion !== 'unknown' ? (
<a
Expand Down
Loading
Loading