Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lighthouse best practices & accessibility #131

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const nextConfig = {
return config;
},
cacheHandler: require.resolve('next/dist/server/lib/incremental-cache/file-system-cache.js'),
productionBrowserSourceMaps: true,
};

module.exports = nextConfig;
7 changes: 3 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import '@/styles/globals.css';

import clsx from 'clsx';
import { Metadata, Viewport } from 'next';
import { headers } from 'next/headers';
import Script from 'next/script';

import { CookieConsentPopup } from '@/components/Analytics/CookieConsentPopup';
Expand All @@ -26,9 +27,6 @@ export const metadata: Metadata = {
apple: { url: '/favicon/apple-touch-icon.png', sizes: '180x180' },
},
manifest: '/favicon/site.webmanifest',
appleWebApp: {
title: 'HungerMap',
},
};

export const viewport: Viewport = {
Expand All @@ -39,10 +37,11 @@ export const viewport: Viewport = {
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
const nonce = headers().get('x-nonce');
return (
<html suppressHydrationWarning lang="en">
<head>
<Script id="google-analytics">
<Script id="google-analytics" nonce={nonce ?? ''}>
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
Expand Down
16 changes: 15 additions & 1 deletion src/components/Accordions/AccordionBoxItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,23 @@ export default function AccordionBoxItems({
<Popover>
<PopoverTrigger>
{item.infoIcon && (
<Button isIconOnly className="w-[37px] h-[37px] p-[5.5px]" variant="light">
<Button
aria-label="More info"
isIconOnly
className="w-[37px] h-[37px] p-[5.5px]"
variant="light"
>
{item.infoIcon}
</Button>
// <div
// role="button"
// tabIndex={0}
// onClick={() => 1}
// onKeyDown={() => 1}
// className="w-[37px] h-[37px] p-[5.5px] hover:bg-default hover:opacity-40 rounded-xl"
// >
// <span className="hover:opacity-100 hover:text-white">{item.infoIcon}</span>
// </div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this please

)}
</PopoverTrigger>
<PopoverContent>
Expand Down
1 change: 1 addition & 0 deletions src/components/Analytics/CookieConsentPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function CookieConsentPopup() {
const gtagScript = document.createElement('script');
gtagScript.async = true;
gtagScript.src = `https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_ID}`;
gtagScript.nonce = document.getElementById('google-analytics')?.nonce;

const firstScript = document.getElementsByTagName('script')[0];
firstScript.parentNode?.insertBefore(gtagScript, firstScript);
Expand Down
5 changes: 5 additions & 0 deletions src/components/Charts/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Button } from '@nextui-org/button';
import { useDisclosure } from '@nextui-org/modal';
import Highcharts from 'highcharts';
import highchartsAccessibility from 'highcharts/modules/accessibility';
import HighchartsReact from 'highcharts-react-official';
import { Maximize4 } from 'iconsax-react';
import { useTheme } from 'next-themes';
Expand All @@ -18,6 +19,10 @@ import { LineChartData } from '@/domain/entities/charts/LineChartData';
import LineChartProps from '@/domain/props/LineChartProps';
import LineChartOperations from '@/operations/charts/LineChartOperations';

if (typeof Highcharts === 'object') {
highchartsAccessibility(Highcharts);
}

/**
* The LineChart component is a box that primarily renders a title, description text, and a line chart.
* This component has a width of 100%, so it adjusts to the width of its parent element in which it is used.
Expand Down
5 changes: 5 additions & 0 deletions src/components/Charts/LineChartModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button } from '@nextui-org/button';
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@nextui-org/modal';
import Highcharts from 'highcharts';
import highchartsAccessibility from 'highcharts/modules/accessibility';
import HighchartsReact from 'highcharts-react-official';
import { Minus } from 'iconsax-react';
import { useRef } from 'react';
Expand All @@ -12,6 +13,10 @@ import LineChartXAxisSlider from '@/components/Charts/helpers/LineChartXAxisSlid
import { Tooltip } from '@/components/Tooltip/Tooltip';
import LineChartModalProps from '@/domain/props/LineChartModalProps';

if (typeof Highcharts === 'object') {
highchartsAccessibility(Highcharts);
}

/**
* This component is tied to the `LineChart` component and should not be used independently.
* It renders the modal, which can be opened by the user from the `LineChart` to display the chart
Expand Down
1 change: 1 addition & 0 deletions src/components/Chatbot/Chatbot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export default function HungerMapChatbot() {
{!isOpen && (
<Tooltip text={TRIGGER_CHAT}>
<Button
aria-label="Open Chatbot"
onClick={toggleChat}
className="
relative flex items-center justify-center min-w-12 h-12 px-1 rounded-full bg-content1 hover:bg-content2 shadow-md"
Expand Down
1 change: 1 addition & 0 deletions src/components/HungerAlert/HungerAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default function HungerAlert({ countryFcsData, countryMapData }: HungerAl
columns={HungerAlertOperations.getHungerAlertModalColumns()}
data={displayedRows}
className="mb-3 min-h-[26rem]"
ariaLabel="Number of countries with high levels of hunger"
/>
<Pagination isCompact showControls page={page} total={totalPages} onChange={(newPage) => setPage(newPage)} />
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/Legend/LegendContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default function LegendContainer({ items, loading = false }: LegendContai
<Tooltip text={items.length > 1 ? 'Legends Information' : 'Legend Information'}>
<Button
onClick={() => setInfoPopup(true)}
aria-label="Map legend"
className="
relative flex items-center justify-center min-w-10 h-10 px-1 rounded-full bg-content1 shadow-md"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export function PulsingAlertMarker({ countryAlert }: { countryAlert: CountryAler
}),
[countryAlert]
);
return <Marker interactive={false} position={countryAlert.position} icon={icon} />;
return <Marker title="Country alert" interactive={false} position={countryAlert.position} icon={icon} />;
}
2 changes: 1 addition & 1 deletion src/components/Map/Alerts/HazardMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function HazardMarker({ hazard }: { hazard: Hazard }) {
[hazard]
);
return (
<Marker position={[hazard.latitude, hazard.longitude]} icon={icon}>
<Marker position={[hazard.latitude, hazard.longitude]} icon={icon} title="Hazard alert">
<Popup className="hazardMarkerPopup">
<div className="flex gap-4 items-center">
<img
Expand Down
3 changes: 0 additions & 3 deletions src/components/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,6 @@ export default function Map({ countries, disputedAreas, fcsData, alertData }: Ma
style={disputedAreaStyle}
/>
</Pane>

<ZoomControl threshold={5} callback={onZoomThresholdReached} />
<BackToGlobalButton />
</MapContainer>
);
}
2 changes: 2 additions & 0 deletions src/components/Map/ZoomControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default function ZoomControl({ threshold, callback }: ZoomControlProps) {
<div className="absolute right-4 bottom-7 z-9999 flex flex-col shadow-lg">
<Button
isDisabled={zoomLevel === MAP_MAX_ZOOM}
aria-label="Zoom in"
size="sm"
onClick={() => map.zoomIn()}
color="default"
Expand All @@ -45,6 +46,7 @@ export default function ZoomControl({ threshold, callback }: ZoomControlProps) {
</Button>
<Button
isDisabled={zoomLevel === MAP_MIN_ZOOM}
aria-label="Zoom out"
size="sm"
onClick={() => map.zoomOut()}
className="rounded-b-md px-2 min-w-0 rounded-t-none z-9999 bg-content1 hover:bg-content2"
Expand Down
1 change: 1 addition & 0 deletions src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export function Sidebar({ countryMapData, fcsData }: SidebarProps) {
classNames={{ popoverContent: 'bg-clickableSecondary' }}
variant="faded"
color="primary"
aria-label="Search a country"
selectedKey={selectedCountryId !== null ? selectedCountryId.toString() : ''}
>
{countryMapData.features
Expand Down
52 changes: 52 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'sha256-q1+DaXsZUnEJs3jpN9ZoWp6ypK1xBwXiRxG+C31xOUA=' 'strict-dynamic' 'unsafe-eval' 'unsafe-inline' ${process.env.NEXT_PUBLIC_API_URL?.slice(0, -3)};
style-src 'self' 'unsafe-inline';
connect-src 'self' https://region1.google-analytics.com ${process.env.NEXT_PUBLIC_API_URL?.slice(0, -3)} ${process.env.NEXT_PUBLIC_V3_API_URL?.slice(0, -3)} ${process.env.NEXT_PUBLIC_CHATBOT_API_URL ?? ''};
img-src 'self' blob: data: https://static.hungermapdata.org https://dev.api.earthobservation.vam.wfp.org;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;
// Replace newline characters and spaces
const contentSecurityPolicyHeaderValue = cspHeader.replace(/\s{2,}/g, ' ').trim();

const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);

requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);

const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);
return response;
}

export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
};
Loading