Skip to content

Commit 9d660e3

Browse files
Merge pull request #45 from StreetSupport/feature/3017-create-location-specific-content-management-system
3017 - implement banners
2 parents 09cb7a2 + c697564 commit 9d660e3

File tree

11 files changed

+122
-202
lines changed

11 files changed

+122
-202
lines changed

src/controllers/bannerController.ts

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Request, Response } from 'express';
22
import { asyncHandler } from '../utils/asyncHandler.js';
3-
import { BannerTemplateType } from '../types/index.js';
3+
import { BackgroundType, BannerTemplateType } from '../types/index.js';
44
import { validateBanner } from '../schemas/bannerSchema.js';
55
import Banner from '../models/bannerModel.js';
66
import { deleteFile } from '../middleware/uploadMiddleware.js';
@@ -139,7 +139,10 @@ export const createBanner = asyncHandler(async (req: Request, res: Response) =>
139139
}
140140

141141
// Handle resource project specific logic
142-
const finalBannerData = _handleResourceProjectBannerLogic({ ...validation.data });
142+
let finalBannerData = _handleResourceProjectBannerLogic({ ...validation.data });
143+
144+
// Handle background image specific logic
145+
finalBannerData = handleBackgroundImageLogic(finalBannerData);
143146

144147
// Add creator information and system fields
145148
const bannerData = {
@@ -181,8 +184,17 @@ export const updateBanner = asyncHandler(async (req: Request, res: Response) =>
181184
// Store old banner data for file cleanup
182185
const oldBannerData = banner.toObject();
183186

187+
// Handle template type change from RESOURCE_PROJECT to another type
188+
if (oldBannerData.TemplateType === BannerTemplateType.RESOURCE_PROJECT &&
189+
validation?.data?.TemplateType !== BannerTemplateType.RESOURCE_PROJECT) {
190+
await handleResourceProjectTemplateChange(oldBannerData, validation.data);
191+
}
192+
184193
// Handle resource project specific logic
185-
const finalBannerData = _handleResourceProjectBannerLogic({ ...validation.data });
194+
let finalBannerData = _handleResourceProjectBannerLogic({ ...validation.data });
195+
196+
// Handle background image specific logic
197+
finalBannerData = handleBackgroundImageLogic(finalBannerData);
186198

187199
// Preserve existing activation date fields and IsActive (not editable in edit form)
188200
finalBannerData.StartDate = banner.StartDate;
@@ -331,20 +343,63 @@ function _handleResourceProjectBannerLogic(bannerData: any): any {
331343
if (resourceFile.Url && !resourceFile.FileUrl) {
332344
bannerData.ResourceProject.ResourceFile.FileUrl = resourceFile.Url;
333345
}
334-
// Add foreach to populate Url depending on AutomaticallyPopulatedUrl
346+
335347
// Update the 'Download' CTA button URL to use the file URL
336348
const fileUrl = bannerData.ResourceProject.ResourceFile.FileUrl;
337349
if (bannerData.CtaButtons && bannerData.CtaButtons.length > 0 && fileUrl) {
338350
const downloadButtonIndex = 0;
339351
const button = bannerData.CtaButtons[downloadButtonIndex];
340-
if (button && button.AutomaticallyPopulatedUrl) {
352+
if (button) {
341353
button.Url = fileUrl;
342354
}
343355
}
344356
}
345357
return bannerData;
346358
}
347359

360+
// Private helper to handle template type change from RESOURCE_PROJECT
361+
// Cleans up resource file and CTA button with blob URL when template type changes
362+
async function handleResourceProjectTemplateChange(oldBannerData: any, newBannerData: any): Promise<void> {
363+
// Check if old banner had a resource file with a blob URL
364+
if (oldBannerData.ResourceProject?.ResourceFile?.FileUrl) {
365+
const fileUrl = oldBannerData.ResourceProject.ResourceFile.FileUrl;
366+
367+
// Delete the resource file from blob storage if it's a blob URL
368+
if (fileUrl.includes('blob.core.windows.net')) {
369+
try {
370+
await deleteFile(fileUrl);
371+
console.log(`Cleaned up resource file during template type change: ${fileUrl}`);
372+
} catch (error) {
373+
console.error(`Failed to delete resource file ${fileUrl} during template type change:`, error);
374+
// Don't throw error - file cleanup failure shouldn't break the update
375+
}
376+
}
377+
}
378+
}
379+
380+
// Private helper to handle background image logic
381+
// Automatically populates Background.Value with BackgroundImage URL when Background.Type is 'image'
382+
function handleBackgroundImageLogic(bannerData: any): any {
383+
// Ensure Background object exists
384+
if (!bannerData.Background) {
385+
return bannerData;
386+
}
387+
388+
// If Background.Type is 'image', handle the BackgroundImage URL
389+
if (bannerData.Background.Type === BackgroundType.IMAGE) {
390+
// Case 1: BackgroundImage exists - populate Background.Value with its URL
391+
if (bannerData.BackgroundImage && bannerData.BackgroundImage.Url) {
392+
bannerData.Background.Value = bannerData.BackgroundImage.Url;
393+
}
394+
// Case 2: BackgroundImage was removed - clear Background.Value
395+
else {
396+
bannerData.Background.Value = '';
397+
}
398+
}
399+
400+
return bannerData;
401+
}
402+
348403
// Helper function to extract all file URLs from a banner
349404
function extractFileUrls(banner: any): string[] {
350405
const urls: string[] = [];
@@ -353,8 +408,6 @@ function extractFileUrls(banner: any): string[] {
353408
if (banner.Logo?.Url) urls.push(banner.Logo.Url);
354409
if (banner.BackgroundImage?.Url) urls.push(banner.BackgroundImage.Url);
355410
if (banner.MainImage?.Url) urls.push(banner.MainImage.Url);
356-
// TODO: Uncomment if AccentGraphic is needed. In the other case, remove.
357-
// if (banner.AccentGraphic?.Url) urls.push(banner.AccentGraphic.Url);
358411

359412
// Partner logos for partnership charter banners (nested structure)
360413
if (banner.PartnershipCharter?.PartnerLogos && Array.isArray(banner.PartnershipCharter.PartnerLogos)) {
@@ -377,8 +430,6 @@ function processMediaFields(req: Request): any {
377430
// Note: I still not sure if it makes sense to use this merging instead of req.body
378431
const processedData = { ...req.body, ...req.preValidatedData };
379432

380-
// TODO: Uncomment if AccentGraphic is needed. In the other case, remove.
381-
// Process single media assets: Logo, BackgroundImage, MainImage, AccentGraphic
382433
['Logo', 'BackgroundImage', 'MainImage' /*, 'AccentGraphic'*/].forEach(field => {
383434
const newFileData = processedData[`newfile_${field}`];
384435
const newMetadata = processedData[`newmetadata_${field}`]

src/middleware/uploadMiddleware.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,7 @@ async function processUploads(req: Request, res: Response, next: NextFunction) {
158158
}
159159

160160
// Store in appropriate field
161-
// TODO: Uncomment if AccentGraphic is needed. In the other case, remove.
162-
if (fieldName === 'newfile_Logo' || fieldName === 'newfile_BackgroundImage' || fieldName === 'newfile_MainImage' /* || fieldName === 'newfile_AccentGraphic' */) {
161+
if (fieldName === 'newfile_Logo' || fieldName === 'newfile_BackgroundImage' || fieldName === 'newfile_MainImage') {
163162
uploadedAssets[fieldName] = asset;
164163
} else if (fieldName === 'newfile_PartnerLogos') {
165164
if (!uploadedAssets[fieldName]) {
@@ -208,11 +207,8 @@ const handleMultipartData = upload.fields([
208207
{ name: 'newfile_Logo', maxCount: 1 },
209208
{ name: 'newfile_BackgroundImage', maxCount: 1 },
210209
{ name: 'newfile_MainImage', maxCount: 1 },
211-
// TODO: Uncomment if AccentGraphic is needed. In the other case, remove.
212-
// { name: 'newfile_AccentGraphic', maxCount: 1 },
213210
{ name: 'newfile_PartnerLogos', maxCount: 5 },
214211
{ name: 'newfile_ResourceFile', maxCount: 1 }
215-
// SWEP banner image uses uploadSwepImage middleware
216212
]);
217213

218214
// Middleware for Banners

src/models/bannerModel.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
// TODO: Uncomment if AccentGraphic is needed. In the other case, remove.
21
import {
32
BannerTemplateType,
43
CharterType,
54
IBanner,
65
LayoutStyle,
76
TextColour,
87
UrgencyLevel,
9-
// AccentGraphicSchema,
108
BannerBackgroundSchema,
119
CTAButtonSchema,
1210
DonationGoalSchema,
@@ -17,7 +15,7 @@ import mongoose, { Schema } from 'mongoose';
1715

1816
// Template-specific nested schemas
1917
const GivingCampaignSchema = new Schema({
20-
UrgencyLevel: { type: String, enum: Object.values(UrgencyLevel) },
18+
UrgencyLevel: { type: String, enum: Object.values(UrgencyLevel), required: true },
2119
CampaignEndDate: { type: Date },
2220
DonationGoal: DonationGoalSchema
2321
}, { _id: false });
@@ -34,11 +32,6 @@ const ResourceProjectSchema = new Schema({
3432

3533
// Main Banner Schema
3634
export const BannerSchema = new Schema({
37-
// We should check if we need it
38-
// _id: {
39-
// type: mongoose.Schema.Types.ObjectId,
40-
// required: true,
41-
// },
4235
DocumentCreationDate: {
4336
type: Date,
4437
default: Date.now,
@@ -53,9 +46,9 @@ export const BannerSchema = new Schema({
5346
},
5447

5548
// Core content
56-
Title: { type: String, required: true, maxlength: 200 },
57-
Description: { type: String, maxlength: 1000 },
58-
Subtitle: { type: String, maxlength: 300 },
49+
Title: { type: String, required: true, maxlength: 50 },
50+
Description: { type: String, maxlength: 200 },
51+
Subtitle: { type: String, maxlength: 50 },
5952

6053
// Template type
6154
TemplateType: {
@@ -68,8 +61,6 @@ export const BannerSchema = new Schema({
6861
Logo: MediaAssetSchema,
6962
BackgroundImage: MediaAssetSchema,
7063
MainImage: MediaAssetSchema, // Separate image for split layout (not background)
71-
// TODO: Uncomment if AccentGraphic is needed. In the other case, remove.
72-
// AccentGraphic: AccentGraphicSchema,
7364

7465
// Actions
7566
CtaButtons: {
@@ -102,6 +93,7 @@ export const BannerSchema = new Schema({
10293
// CMS metadata
10394
IsActive: { type: Boolean, default: true },
10495
LocationSlug: { type: String, required: true },
96+
LocationName: { type: String, required: true },
10597
Priority: { type: Number, min: 1, max: 10, default: 5 },
10698

10799
// Analytics

src/schemas/bannerSchema.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { z } from 'zod';
22
import {
33
MediaAssetSchemaCore,
4-
// TODO: Uncomment if AccentGraphic is needed. In the other case, remove.
5-
// AccentGraphicSchemaCore,
64
BannerBackgroundSchemaCore,
75
CTAButtonSchemaCore,
86
DonationGoalSchemaCore,
@@ -27,8 +25,6 @@ import { BannerTemplateType, UrgencyLevel, CharterType, LayoutStyle, TextColour,
2725

2826
// API-specific schemas with preprocessing for FormData
2927
export const MediaAssetSchema = MediaAssetSchemaCore;
30-
// TODO: Uncomment if AccentGraphic is needed. In the other case, remove.
31-
// export const AccentGraphicSchema = AccentGraphicSchemaCore;
3228
export const BannerBackgroundSchema = BannerBackgroundSchemaCore;
3329
export const CTAButtonSchema = CTAButtonSchemaCore;
3430

@@ -43,7 +39,7 @@ export const ResourceFileSchema = z.preprocess(preprocessJSON, ResourceFileSchem
4339

4440
// API-specific schema for ResourceFile to handle nested date preprocessing
4541
export const ResourceFileApiSchema = ResourceFileSchemaCore.extend({
46-
LastUpdated: z.preprocess(preprocessDate, z.date()).optional(),
42+
LastUpdated: z.preprocess(preprocessDate, z.date()),
4743
}).optional();
4844

4945
// API-specific schema for ResourceProject to use ResourceFileApiSchema
@@ -117,7 +113,7 @@ export const BannerPreUploadApiSchema = z.object({
117113
// Styling (excluding file-based background)
118114
Background: z.preprocess(preprocessJSON, z.object({
119115
Type: z.nativeEnum(BackgroundType),
120-
Value: z.string().optional(),
116+
Value: z.string().min(1, 'Background value is required'),
121117
Overlay: z.object({
122118
Colour: z.string().optional(),
123119
Opacity: z.preprocess(preprocessNumber, z.number().min(0).max(1)).optional()

0 commit comments

Comments
 (0)