Skip to content

Commit 2d058e1

Browse files
authored
Merge staging into main (#195)
* Fix/locations dropdown accessibility (#168) * Increase e2e setup timeout and add retry logic for CI * fix: remove non-public locations from navigation data * feat: improve locations dropdown accessibility and layout - Change to 4-column layout (one per letter bracket) - Add ARIA group labelling with role and aria-labelledby - Improve keyboard focus management with roving tabindex - Add visible focus states for keyboard navigation * Refactor/stats repository pattern (#169) * Increase e2e setup timeout and add retry logic for CI * refactor: add repository pattern for stats API to support CI without MongoDB * fix: improve console error suppression for test noise * Fix opening hours displaying incorrect days (#171) * Increase e2e setup timeout and add retry logic for CI * fix: correct day indexing for opening hours display * Fix: Run prebuild fetch scripts during Vercel builds (#172) * Increase e2e setup timeout and add retry logic for CI * fix: correct day indexing for opening hours display * fix: add buildCommand to run fetch scripts before Next.js build * ci: skip redundant tests on main branch merges from staging (#173) * Increase e2e setup timeout and add retry logic for CI * ci: skip redundant tests on main branch merges from staging * Update login button styling to stand out in navigation (#174) * Increase e2e setup timeout and add retry logic for CI * style: update login button to purple with white text * feat: add form links to contact page and reusable get in touch banner (#176) * fix: add proper label associations to organisation request form (#177) * fix: improve form accessibility with focus management, labels, and contrast (#178) * fix: update content and improve accessibility across multiple pages (#179) * feat: redesign west-midlands page (#180) * fix: update content and improve accessibility across multiple pages * feat: redesign west-midlands page with hero, location cards, find help widget and regional stats * fix: add dynamic export to west-midlands page (#182) * fix: update content and improve accessibility across multiple pages * feat: redesign west-midlands page with hero, location cards, find help widget and regional stats * fix: add dynamic export to west-midlands page for banner fetching * feat: add configurable limit prop to LocationFindHelp component (#185) * perf: optimise database queries with parallel fetching and MongoDB geospatial (#184) * feat: add configurable limit prop to LocationFindHelp component * perf: optimise database queries with parallel fetching and MongoDB geospatial * fix: resolve TypeScript error in accommodationData baseConditions type * fix: improve disabled button contrast to meet WCAG AA standards (#186) * fix: use separate API key for server-side geocoding (#189) * feat: add UK GDPR compliant cookie consent banner (#191) * Fix: Update tar to address CVE-2026-23950 (#188) * fix: improve disabled button contrast to meet WCAG AA standards * fix: update vulnerable dependencies to address CVE-2026-23950 * refactor(banners): simplify banner system to unified flexible component (#193) * fix(resources): remove pointer cursor from resource cards (#194) * Remove banner testing page - Remove /testing-banners route that was causing build failures - Test page was for development purposes only * feat(banners): add rich text description support with sanitisation (#196)
1 parent 2652c42 commit 2d058e1

20 files changed

+626
-3047
lines changed

next.config.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,22 @@ const nextConfig = {
2929
// Minimize layout shift by enforcing size requirements
3030
minimumCacheTTL: 60,
3131

32-
// Add any external domains if needed in the future
33-
remotePatterns: process.env.BLOB_STORAGE_HOSTNAME
34-
? [
35-
{
36-
protocol: 'https',
37-
hostname: process.env.BLOB_STORAGE_HOSTNAME,
38-
pathname: '/**',
39-
},
40-
]
41-
: [],
32+
remotePatterns: [
33+
...(process.env.BLOB_STORAGE_HOSTNAME
34+
? [
35+
{
36+
protocol: 'https' as const,
37+
hostname: process.env.BLOB_STORAGE_HOSTNAME,
38+
pathname: '/**',
39+
},
40+
]
41+
: []),
42+
{
43+
protocol: 'https' as const,
44+
hostname: 'placekitten.com',
45+
pathname: '/**',
46+
},
47+
],
4248
},
4349

4450
// Enable experimental features for better performance

package-lock.json

Lines changed: 98 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@sendgrid/mail": "^8.1.5",
2727
"@sentry/nextjs": "^10.12.0",
2828
"clsx": "^2.1.1",
29+
"dompurify": "^3.3.1",
2930
"google-auth-library": "^10.5.0",
3031
"lucide-react": "^0.511.0",
3132
"mongodb": "^6.17.0",
@@ -49,6 +50,7 @@
4950
"@testing-library/jest-dom": "^6.6.3",
5051
"@testing-library/react": "^16.3.0",
5152
"@testing-library/user-event": "^14.6.1",
53+
"@types/dompurify": "^3.0.5",
5254
"@types/google.maps": "^3.58.1",
5355
"@types/jest": "^29.5.14",
5456
"@types/node": "^20",

src/app/api/banners/route.ts

Lines changed: 53 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import { getClientPromise } from '@/utils/mongodb';
3-
import { AnyBannerProps, GivingCampaignProps, PartnershipCharterProps, ResourceProjectProps } from '@/types/banners';
3+
import { BannerProps } from '@/types/banners';
44

5-
// Disable caching for this API route to ensure fresh banner data
65
export const dynamic = 'force-dynamic';
76
export const revalidate = 0;
87

@@ -11,137 +10,78 @@ export async function GET(req: NextRequest) {
1110
const searchParams = req.nextUrl.searchParams;
1211
const locationSlug = searchParams.get('location');
1312

14-
// Fetch banner data from MongoDB
1513
const client = await getClientPromise();
1614
const db = client.db('streetsupport');
1715
const bannersCol = db.collection('Banners');
1816

19-
// Build query - only active banners
2017
const query: Record<string, unknown> = {
21-
IsActive: true
18+
IsActive: true,
19+
MediaType: { $exists: true }
2220
};
2321

24-
// Filter by location if provided
2522
if (locationSlug) {
2623
query.LocationSlug = locationSlug;
2724
}
2825

29-
// Fetch banners sorted by Priority (descending) and limited to 6
3026
const rawBanners = await bannersCol
3127
.find(query)
3228
.sort({ Priority: 1 })
3329
.limit(6)
3430
.toArray();
3531

36-
// Transform MongoDB documents (PascalCase) to web format (camelCase)
37-
const banners: AnyBannerProps[] = rawBanners.map(banner => {
38-
// Base banner properties
39-
const baseBanner = {
40-
id: banner._id.toString(),
41-
title: banner.Title,
42-
description: banner.Description,
43-
subtitle: banner.Subtitle,
44-
templateType: banner.TemplateType,
45-
46-
// Media
47-
logo: banner.Logo ? {
48-
url: banner.Logo.Url || '',
49-
alt: banner.Logo.Alt || '',
50-
width: banner.Logo.Width,
51-
height: banner.Logo.Height
52-
} : undefined,
53-
54-
image: banner.MainImage ? {
55-
url: banner.MainImage.Url || '',
56-
alt: banner.MainImage.Alt || '',
57-
width: banner.MainImage.Width,
58-
height: banner.MainImage.Height
59-
} : undefined,
60-
61-
// Actions
62-
ctaButtons: banner.CtaButtons?.map((cta: Record<string, unknown>) => ({
63-
label: cta.Label as string,
64-
url: cta.Url as string,
65-
variant: cta.Variant as 'primary' | 'secondary' | 'outline' | undefined,
66-
external: cta.External as boolean | undefined
67-
})) || [],
68-
69-
// Styling
70-
background: {
71-
type: banner.Background?.Type || 'solid',
72-
value: banner.Background?.Value || '#ffffff',
73-
overlay: banner.Background?.Overlay ? {
74-
colour: banner.Background.Overlay.Colour,
75-
opacity: banner.Background.Overlay.Opacity
76-
} : undefined
77-
},
78-
textColour: banner.TextColour || 'black',
79-
layoutStyle: banner.LayoutStyle || 'full-width',
80-
81-
// Scheduling
82-
startDate: banner.StartDate ? new Date(banner.StartDate).toISOString() : undefined,
83-
endDate: banner.EndDate ? new Date(banner.EndDate).toISOString() : undefined,
84-
badgeText: banner.BadgeText,
85-
86-
// CMS metadata
87-
isActive: banner.IsActive,
88-
locationSlug: banner.LocationSlug,
89-
priority: banner.Priority
90-
};
91-
92-
// Template-specific properties
93-
if (banner.TemplateType === 'giving-campaign') {
94-
const givingBanner: GivingCampaignProps = {
95-
...baseBanner,
96-
templateType: 'giving-campaign',
97-
donationGoal: banner.GivingCampaign?.DonationGoal ? {
98-
target: banner.GivingCampaign.DonationGoal.Target || 0,
99-
current: banner.GivingCampaign.DonationGoal.Current || 0,
100-
currency: banner.GivingCampaign.DonationGoal.Currency || 'GBP'
101-
} : undefined,
102-
urgencyLevel: banner.GivingCampaign?.UrgencyLevel,
103-
campaignEndDate: banner.GivingCampaign?.CampaignEndDate
104-
? new Date(banner.GivingCampaign.CampaignEndDate).toISOString()
105-
: undefined
106-
};
107-
return givingBanner;
108-
}
32+
const banners: BannerProps[] = rawBanners.map(banner => ({
33+
id: banner._id.toString(),
34+
title: banner.Title,
35+
description: banner.Description,
36+
subtitle: banner.Subtitle,
10937

110-
if (banner.TemplateType === 'partnership-charter') {
111-
const charterBanner: PartnershipCharterProps = {
112-
...baseBanner,
113-
templateType: 'partnership-charter',
114-
partnerLogos: banner.PartnershipCharter?.PartnerLogos?.map((logo: Record<string, unknown>) => ({
115-
url: logo.Url as string || '',
116-
alt: logo.Alt as string || '',
117-
width: logo.Width as number | undefined,
118-
height: logo.Height as number | undefined
119-
})),
120-
charterType: banner.PartnershipCharter?.CharterType,
121-
signatoriesCount: banner.PartnershipCharter?.SignatoriesCount
122-
};
123-
return charterBanner;
124-
}
38+
mediaType: banner.MediaType || 'image',
39+
youtubeUrl: banner.YouTubeUrl,
12540

126-
if (banner.TemplateType === 'resource-project') {
127-
const resourceBanner: ResourceProjectProps = {
128-
...baseBanner,
129-
templateType: 'resource-project',
130-
resourceType: banner.ResourceProject?.ResourceFile?.ResourceType,
131-
lastUpdated: banner.ResourceProject?.ResourceFile?.LastUpdated
132-
? new Date(banner.ResourceProject.ResourceFile.LastUpdated).toISOString()
133-
: undefined,
134-
fileSize: banner.ResourceProject?.ResourceFile?.FileSize,
135-
fileType: banner.ResourceProject?.ResourceFile?.FileType
136-
};
137-
return resourceBanner;
138-
}
41+
logo: banner.Logo ? {
42+
url: banner.Logo.Url || '',
43+
alt: banner.Logo.Alt || '',
44+
width: banner.Logo.Width,
45+
height: banner.Logo.Height
46+
} : undefined,
13947

140-
// Fallback to base banner
141-
return baseBanner as AnyBannerProps;
142-
});
48+
image: banner.MainImage ? {
49+
url: banner.MainImage.Url || '',
50+
alt: banner.MainImage.Alt || '',
51+
width: banner.MainImage.Width,
52+
height: banner.MainImage.Height
53+
} : undefined,
54+
55+
ctaButtons: banner.CtaButtons?.map((cta: Record<string, unknown>) => ({
56+
label: cta.Label as string,
57+
url: cta.Url as string,
58+
variant: cta.Variant as 'primary' | 'secondary' | 'outline' | undefined,
59+
external: cta.External as boolean | undefined
60+
})) || [],
61+
62+
background: {
63+
type: banner.Background?.Type || 'solid',
64+
value: banner.Background?.Value || '#ffffff',
65+
overlay: banner.Background?.Overlay ? {
66+
colour: banner.Background.Overlay.Colour,
67+
opacity: banner.Background.Overlay.Opacity
68+
} : undefined
69+
},
70+
textColour: banner.TextColour || 'black',
71+
layoutStyle: banner.LayoutStyle || 'full-width',
72+
73+
uploadedFile: banner.UploadedFile ? {
74+
url: banner.UploadedFile.FileUrl,
75+
fileName: banner.UploadedFile.FileName,
76+
fileSize: banner.UploadedFile.FileSize,
77+
fileType: banner.UploadedFile.FileType
78+
} : undefined,
79+
80+
isActive: banner.IsActive,
81+
locationSlug: banner.LocationSlug,
82+
priority: banner.Priority
83+
}));
14384

144-
// Return response without caching headers to ensure fresh data
14585
return NextResponse.json({
14686
status: 'success',
14787
banners
@@ -154,7 +94,7 @@ export async function GET(req: NextRequest) {
15494
});
15595
} catch (error) {
15696
console.error('[API ERROR] /api/banners:', error);
157-
97+
15898
return NextResponse.json({
15999
status: 'error',
160100
message: 'Unable to fetch banners at this time',

src/app/resources/page.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default function ResourcesPage() {
3535

3636
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
3737
{/* Alternative Giving */}
38-
<article className="card card-compact flex flex-col h-full p-8 shadow-lg">
38+
<article className="bg-white border border-brand-q rounded-md flex flex-col h-full p-8 shadow-lg">
3939
<div className="flex items-center mb-6">
4040
<Image
4141
src="/assets/img/resource-icons/alternative-giving-icon.png"
@@ -54,7 +54,7 @@ export default function ResourcesPage() {
5454
</article>
5555

5656
{/* Effective Volunteering */}
57-
<article className="card card-compact flex flex-col h-full p-8 shadow-lg">
57+
<article className="bg-white border border-brand-q rounded-md flex flex-col h-full p-8 shadow-lg">
5858
<div className="flex items-center mb-6">
5959
<Image
6060
src="/assets/img/resource-icons/volunteering-icon.png"
@@ -73,7 +73,7 @@ export default function ResourcesPage() {
7373
</article>
7474

7575
{/* Homelessness Charters */}
76-
<article className="card card-compact flex flex-col h-full p-8 shadow-lg">
76+
<article className="bg-white border border-brand-q rounded-md flex flex-col h-full p-8 shadow-lg">
7777
<div className="flex items-center mb-6">
7878
<Image
7979
src="/assets/img/resource-icons/charters-icon.png"
@@ -92,7 +92,7 @@ export default function ResourcesPage() {
9292
</article>
9393

9494
{/* Street Feeding */}
95-
<article className="card card-compact flex flex-col h-full p-8 shadow-lg">
95+
<article className="bg-white border border-brand-q rounded-md flex flex-col h-full p-8 shadow-lg">
9696
<div className="flex items-center mb-6">
9797
<Image
9898
src="/assets/img/resource-icons/streetfeeding-icon.png"
@@ -119,7 +119,7 @@ export default function ResourcesPage() {
119119

120120
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
121121
{/* Branding */}
122-
<article className="card card-compact flex flex-col h-full p-8 shadow-lg">
122+
<article className="bg-white border border-brand-q rounded-md flex flex-col h-full p-8 shadow-lg">
123123
<div className="flex items-center mb-6">
124124
<Image
125125
src="/assets/img/resource-icons/branding-icon.png"
@@ -138,7 +138,7 @@ export default function ResourcesPage() {
138138
</article>
139139

140140
{/* Partnership Communications */}
141-
<article className="card card-compact flex flex-col h-full p-8 shadow-lg">
141+
<article className="bg-white border border-brand-q rounded-md flex flex-col h-full p-8 shadow-lg">
142142
<div className="flex items-center mb-6">
143143
<Image
144144
src="/assets/img/resource-icons/partnership-comms-icon.png"
@@ -157,7 +157,7 @@ export default function ResourcesPage() {
157157
</article>
158158

159159
{/* Marketing */}
160-
<article className="card card-compact flex flex-col h-full p-8 shadow-lg">
160+
<article className="bg-white border border-brand-q rounded-md flex flex-col h-full p-8 shadow-lg">
161161
<div className="flex items-center mb-6">
162162
<Image
163163
src="/assets/img/resource-icons/marketing-icon.png"
@@ -176,7 +176,7 @@ export default function ResourcesPage() {
176176
</article>
177177

178178
{/* User Guides */}
179-
<article className="card card-compact flex flex-col h-full p-8 shadow-lg">
179+
<article className="bg-white border border-brand-q rounded-md flex flex-col h-full p-8 shadow-lg">
180180
<div className="flex items-center mb-6">
181181
<Image
182182
src="/assets/img/resource-icons/user-guides-icon.png"

0 commit comments

Comments
 (0)