musora-content-services 2.188.0 → 2.189.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/index.d.ts +3 -1
- package/src/index.js +3 -1
- package/src/lib/sanity/types/marketing.d.ts +4 -4
- package/src/services/marketing/promotions.ts +67 -8
- package/src/version-info.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.189.0](https://github.com/railroadmedia/musora-content-services/compare/v2.188.0...v2.189.0) (2026-10-20)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **MU2-2024:** add placeholder resolution for promotional campaign copy ([f5cebb3](https://github.com/railroadmedia/musora-content-services/commit/f5cebb3888ce1228ecba5ec17412c8bcfaff8c35))
|
|
11
|
+
|
|
5
12
|
## [2.188.0](https://github.com/railroadmedia/musora-content-services/compare/v2.187.0...v2.188.0) (2026-10-20)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -221,7 +221,8 @@ import {
|
|
|
221
221
|
dismissPromotionalCampaign,
|
|
222
222
|
fetchAvailablePromotionalCampaigns,
|
|
223
223
|
fetchPromotionalCampaigns,
|
|
224
|
-
fetchUserCampaignStates
|
|
224
|
+
fetchUserCampaignStates,
|
|
225
|
+
resolveCampaignCopy
|
|
225
226
|
} from './services/marketing/promotions.ts';
|
|
226
227
|
|
|
227
228
|
import {
|
|
@@ -901,6 +902,7 @@ declare module 'musora-content-services' {
|
|
|
901
902
|
resetAllAwards,
|
|
902
903
|
resetAllLearningPaths,
|
|
903
904
|
resetPassword,
|
|
905
|
+
resolveCampaignCopy,
|
|
904
906
|
restoreComment,
|
|
905
907
|
restoreItemFromPlaylist,
|
|
906
908
|
restoreNotification,
|
package/src/index.js
CHANGED
|
@@ -225,7 +225,8 @@ import {
|
|
|
225
225
|
dismissPromotionalCampaign,
|
|
226
226
|
fetchAvailablePromotionalCampaigns,
|
|
227
227
|
fetchPromotionalCampaigns,
|
|
228
|
-
fetchUserCampaignStates
|
|
228
|
+
fetchUserCampaignStates,
|
|
229
|
+
resolveCampaignCopy
|
|
229
230
|
} from './services/marketing/promotions.ts';
|
|
230
231
|
|
|
231
232
|
import {
|
|
@@ -900,6 +901,7 @@ export {
|
|
|
900
901
|
resetAllAwards,
|
|
901
902
|
resetAllLearningPaths,
|
|
902
903
|
resetPassword,
|
|
904
|
+
resolveCampaignCopy,
|
|
903
905
|
restoreComment,
|
|
904
906
|
restoreItemFromPlaylist,
|
|
905
907
|
restoreNotification,
|
|
@@ -117,10 +117,10 @@ export type PromotionalCampaignDocument = {
|
|
|
117
117
|
title?: string
|
|
118
118
|
body?: string
|
|
119
119
|
disclaimer?: string
|
|
120
|
-
/**
|
|
121
|
-
dark_mode_image?: string
|
|
122
|
-
/**
|
|
123
|
-
light_mode_image?: string
|
|
120
|
+
/** Dark Image */
|
|
121
|
+
dark_mode_image?: { _type: "image"; asset: { _type: "reference"; _ref: string } }
|
|
122
|
+
/** Light Image */
|
|
123
|
+
light_mode_image?: { _type: "image"; asset: { _type: "reference"; _ref: string } }
|
|
124
124
|
/** CTA Label */
|
|
125
125
|
cta_label?: string
|
|
126
126
|
/** Dismissible? */
|
|
@@ -19,6 +19,21 @@ const excludeFromGeneratedIndex = [
|
|
|
19
19
|
// TYPES
|
|
20
20
|
// ============================================
|
|
21
21
|
|
|
22
|
+
type PlacementDocument = PromotionalCampaignDocument['placements'][number]
|
|
23
|
+
|
|
24
|
+
/** A placement as `campaignProjection` returns it, with both images resolved to CDN urls. */
|
|
25
|
+
export type PromotionalCampaignPlacement = Omit<
|
|
26
|
+
PlacementDocument,
|
|
27
|
+
'dark_mode_image' | 'light_mode_image'
|
|
28
|
+
> & {
|
|
29
|
+
dark_mode_image?: string
|
|
30
|
+
light_mode_image?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type PromotionalCampaign = Omit<PromotionalCampaignDocument, 'placements'> & {
|
|
34
|
+
placements: PromotionalCampaignPlacement[]
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
export const USER_CAMPAIGN_STATE_FIELD = 'user_state' as const
|
|
23
38
|
|
|
24
39
|
/**
|
|
@@ -31,7 +46,7 @@ export interface UserCampaignState {
|
|
|
31
46
|
eligible: boolean
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
export type WithUserCampaignState<T extends
|
|
49
|
+
export type WithUserCampaignState<T extends PromotionalCampaign> = T & {
|
|
35
50
|
user_state: UserCampaignState
|
|
36
51
|
}
|
|
37
52
|
|
|
@@ -54,7 +69,7 @@ const UNTOUCHED: UserCampaignState = { claimed: false, dismissed: false, eligibl
|
|
|
54
69
|
// UTILITIES
|
|
55
70
|
// ============================================
|
|
56
71
|
|
|
57
|
-
const filterCampaignsForUser = (campaigns: WithUserCampaignState<
|
|
72
|
+
const filterCampaignsForUser = (campaigns: WithUserCampaignState<PromotionalCampaign>[]) =>
|
|
58
73
|
campaigns.filter(
|
|
59
74
|
(campaign) =>
|
|
60
75
|
campaign.user_state.eligible && !campaign.user_state.claimed && !campaign.user_state.dismissed
|
|
@@ -79,10 +94,10 @@ function slugFilter(slug: CampaignQueryOptions['slug']): string {
|
|
|
79
94
|
|
|
80
95
|
export function campaignQuery(
|
|
81
96
|
options: CampaignQueryOptions = {}
|
|
82
|
-
): GroqBuilder<
|
|
97
|
+
): GroqBuilder<PromotionalCampaign[]> {
|
|
83
98
|
const { brand, slug, on = new Date(), running = true, active = true } = options
|
|
84
99
|
|
|
85
|
-
return groq<
|
|
100
|
+
return groq<PromotionalCampaign[]>()
|
|
86
101
|
.and(
|
|
87
102
|
f.combine(
|
|
88
103
|
f.type('promotional-campaign'),
|
|
@@ -103,7 +118,7 @@ export function campaignQuery(
|
|
|
103
118
|
|
|
104
119
|
export async function fetchPromotionalCampaigns(
|
|
105
120
|
options: CampaignQueryOptions = {}
|
|
106
|
-
): Promise<
|
|
121
|
+
): Promise<PromotionalCampaign[]> {
|
|
107
122
|
return campaignQuery(options).run().recover([])
|
|
108
123
|
}
|
|
109
124
|
|
|
@@ -112,7 +127,7 @@ export async function fetchPromotionalCampaigns(
|
|
|
112
127
|
*/
|
|
113
128
|
export async function fetchAvailablePromotionalCampaigns(
|
|
114
129
|
options: CampaignQueryOptions = {}
|
|
115
|
-
): Promise<WithUserCampaignState<
|
|
130
|
+
): Promise<WithUserCampaignState<PromotionalCampaign>[]> {
|
|
116
131
|
return campaignQuery(options)
|
|
117
132
|
.run()
|
|
118
133
|
.flatMap(decorateUserCampaignState)
|
|
@@ -144,7 +159,7 @@ export function userCampaignStateDecorator(
|
|
|
144
159
|
return {
|
|
145
160
|
field: USER_CAMPAIGN_STATE_FIELD,
|
|
146
161
|
compute: async (item) => {
|
|
147
|
-
const campaign = item as unknown as
|
|
162
|
+
const campaign = item as unknown as PromotionalCampaign
|
|
148
163
|
|
|
149
164
|
return states[campaign.slug?.current] ?? UNTOUCHED
|
|
150
165
|
},
|
|
@@ -154,7 +169,7 @@ export function userCampaignStateDecorator(
|
|
|
154
169
|
/**
|
|
155
170
|
* Attaches each campaign's claim and dismiss state in a single backend request.
|
|
156
171
|
*/
|
|
157
|
-
export function decorateUserCampaignState<T extends
|
|
172
|
+
export function decorateUserCampaignState<T extends PromotionalCampaign>(
|
|
158
173
|
campaigns: T[]
|
|
159
174
|
): AsyncEither<Error, WithUserCampaignState<T>[]> {
|
|
160
175
|
return AsyncEither.attempt(async () => {
|
|
@@ -168,6 +183,50 @@ export function decorateUserCampaignState<T extends PromotionalCampaignDocument>
|
|
|
168
183
|
})
|
|
169
184
|
}
|
|
170
185
|
|
|
186
|
+
// ============================================
|
|
187
|
+
// COPY
|
|
188
|
+
// ============================================
|
|
189
|
+
|
|
190
|
+
export type CampaignCopyValues = Record<string, string>
|
|
191
|
+
|
|
192
|
+
const PLACEHOLDER = /\{(\w+)\}/g
|
|
193
|
+
|
|
194
|
+
const COPY_FIELDS = ['title', 'body', 'disclaimer', 'cta_label'] as const
|
|
195
|
+
|
|
196
|
+
const fillPlaceholders = (text: string, values: CampaignCopyValues) =>
|
|
197
|
+
text.replace(PLACEHOLDER, (placeholder, name: string) => values[name] ?? placeholder)
|
|
198
|
+
|
|
199
|
+
const hasPlaceholder = (text: string | undefined) => !!text && new RegExp(PLACEHOLDER).test(text)
|
|
200
|
+
|
|
201
|
+
function resolvePlacementCopy(
|
|
202
|
+
placement: PromotionalCampaignPlacement,
|
|
203
|
+
values: CampaignCopyValues
|
|
204
|
+
): PromotionalCampaignPlacement {
|
|
205
|
+
const resolved = { ...placement }
|
|
206
|
+
|
|
207
|
+
for (const field of COPY_FIELDS) {
|
|
208
|
+
if (resolved[field]) resolved[field] = fillPlaceholders(resolved[field], values)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return resolved
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Replaces `{name}` placeholders in each placement's copy with the given values. A placement left
|
|
216
|
+
* with a placeholder that has no value is dropped, so raw `{name}` text never reaches the user.
|
|
217
|
+
*/
|
|
218
|
+
export function resolveCampaignCopy<T extends PromotionalCampaign>(
|
|
219
|
+
campaign: T,
|
|
220
|
+
values: CampaignCopyValues
|
|
221
|
+
): T {
|
|
222
|
+
return {
|
|
223
|
+
...campaign,
|
|
224
|
+
placements: campaign.placements
|
|
225
|
+
.map((placement) => resolvePlacementCopy(placement, values))
|
|
226
|
+
.filter((placement) => !COPY_FIELDS.some((field) => hasPlaceholder(placement[field]))),
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
171
230
|
// ============================================
|
|
172
231
|
// MUTATIONS
|
|
173
232
|
// ============================================
|
package/src/version-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCS_VERSION = '2.
|
|
1
|
+
export const MCS_VERSION = '2.189.0'
|