@natrave/shared-entities 8.34.0 → 8.36.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/dist/promotions/index.d.ts +2 -0
- package/dist/promotions/index.d.ts.map +1 -1
- package/dist/promotions/index.js +7 -1
- package/dist/promotions/promotion-creatives/promotion-creative.entity.d.ts.map +1 -1
- package/dist/promotions/promotion-creatives/promotion-creative.entity.js +2 -6
- package/dist/promotions/promotions/promotion.entity.d.ts +4 -4
- package/dist/promotions/promotions/promotion.entity.d.ts.map +1 -1
- package/dist/promotions/promotions/promotion.entity.js +11 -3
- package/dist/promotions/validation/promotion-creative.validation.d.ts +13 -0
- package/dist/promotions/validation/promotion-creative.validation.d.ts.map +1 -0
- package/dist/promotions/validation/promotion-creative.validation.js +75 -0
- package/package.json +1 -1
|
@@ -15,4 +15,6 @@ export * from './promotion-targets';
|
|
|
15
15
|
export * from './promotion-user-states';
|
|
16
16
|
export * from './promotions';
|
|
17
17
|
export type { PromotionCreativeContent, PromotionCreativeContentByTemplate, TournamentPosterContent, } from './types/promotion-content.types';
|
|
18
|
+
export type { PromotionCreativeIssue, PromotionCreativeValidationInput, } from './validation/promotion-creative.validation';
|
|
19
|
+
export { isPromotionCreativeLive, validatePromotionCreative, } from './validation/promotion-creative.validation';
|
|
18
20
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/promotions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,+CAA+C,CAAC;AAC9F,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AACzE,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,wBAAwB,EACxB,kCAAkC,EAClC,uBAAuB,GACxB,MAAM,iCAAiC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/promotions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,+CAA+C,CAAC;AAC9F,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AACzE,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,wBAAwB,EACxB,kCAAkC,EAClC,uBAAuB,GACxB,MAAM,iCAAiC,CAAC;AACzC,YAAY,EACV,sBAAsB,EACtB,gCAAgC,GACjC,MAAM,4CAA4C,CAAC;AACpD,OAAO,EACL,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,4CAA4C,CAAC"}
|
package/dist/promotions/index.js
CHANGED
|
@@ -14,6 +14,10 @@ export * from "./promotion-events/index.js";
|
|
|
14
14
|
export * from "./promotion-targets/index.js";
|
|
15
15
|
export * from "./promotion-user-states/index.js";
|
|
16
16
|
export * from "./promotions/index.js";
|
|
17
|
+
import {
|
|
18
|
+
isPromotionCreativeLive,
|
|
19
|
+
validatePromotionCreative
|
|
20
|
+
} from "./validation/promotion-creative.validation.js";
|
|
17
21
|
export {
|
|
18
22
|
PromotionAdvertiserEntityType,
|
|
19
23
|
PromotionCreativeStatus,
|
|
@@ -23,5 +27,7 @@ export {
|
|
|
23
27
|
PromotionPlacement,
|
|
24
28
|
PromotionStatus,
|
|
25
29
|
PromotionSubjectType,
|
|
26
|
-
PromotionTargetType
|
|
30
|
+
PromotionTargetType,
|
|
31
|
+
isPromotionCreativeLive,
|
|
32
|
+
validatePromotionCreative
|
|
27
33
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promotion-creative.entity.d.ts","sourceRoot":"","sources":["../../../src/promotions/promotion-creatives/promotion-creative.entity.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"promotion-creative.entity.d.ts","sourceRoot":"","sources":["../../../src/promotions/promotion-creatives/promotion-creative.entity.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAyBjF,qBAOa,iBAAiB;IAK5B,EAAE,EAAE,MAAM,CAAC;IAOX,WAAW,EAAE,MAAM,CAAC;IAOpB,KAAK,EAAE,MAAM,CAAC;IAOd,SAAS,EAAE,kBAAkB,CAAC;IAO9B,QAAQ,EAAE,yBAAyB,CAAC;IAMpC,OAAO,EAAE,wBAAwB,CAAC;IASlC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAQ3B,MAAM,EAAE,uBAAuB,CAAC;IAOhC,MAAM,EAAE,MAAM,CAAC;IASf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAQzB,SAAS,EAAE,IAAI,CAAC;IAQhB,SAAS,EAAE,IAAI,CAAC;IAShB,SAAS,EAAE,SAAS,CAAC;CACtB"}
|
|
@@ -68,7 +68,7 @@ __decorateClass([
|
|
|
68
68
|
name: "consent_text",
|
|
69
69
|
type: "text",
|
|
70
70
|
nullable: true,
|
|
71
|
-
comment: "Aviso exibido junto do bot\xE3o de interesse. Obrigat\xF3rio quando o template coleta lead."
|
|
71
|
+
comment: "Aviso exibido junto do bot\xE3o de interesse. Obrigat\xF3rio para ir ao ar quando o template coleta lead."
|
|
72
72
|
})
|
|
73
73
|
], PromotionCreative.prototype, "consentText", 2);
|
|
74
74
|
__decorateClass([
|
|
@@ -125,11 +125,7 @@ PromotionCreative = __decorateClass([
|
|
|
125
125
|
Check("CHK_promotion_creatives_placement", `"placement" IN ('HOME_MODAL')`),
|
|
126
126
|
Check("CHK_promotion_creatives_template", `"template" IN ('TOURNAMENT_POSTER')`),
|
|
127
127
|
Check("CHK_promotion_creatives_weight", `"weight" >= 1`),
|
|
128
|
-
Check("CHK_promotion_creatives_content_object", `jsonb_typeof("content") = 'object'`)
|
|
129
|
-
Check(
|
|
130
|
-
"CHK_promotion_creatives_consent",
|
|
131
|
-
`"template" <> 'TOURNAMENT_POSTER' OR ("consent_text" IS NOT NULL AND length(trim("consent_text")) > 0)`
|
|
132
|
-
)
|
|
128
|
+
Check("CHK_promotion_creatives_content_object", `jsonb_typeof("content") = 'object'`)
|
|
133
129
|
], PromotionCreative);
|
|
134
130
|
export {
|
|
135
131
|
PromotionCreative
|
|
@@ -8,18 +8,18 @@ export declare class Promotion {
|
|
|
8
8
|
id: string;
|
|
9
9
|
name: string;
|
|
10
10
|
status: PromotionStatus;
|
|
11
|
-
advertiserId: string;
|
|
11
|
+
advertiserId: string | null;
|
|
12
12
|
subjectType: PromotionSubjectType | null;
|
|
13
13
|
subjectId: string | null;
|
|
14
|
-
startsAt: Date;
|
|
15
|
-
endsAt: Date;
|
|
14
|
+
startsAt: Date | null;
|
|
15
|
+
endsAt: Date | null;
|
|
16
16
|
priority: number;
|
|
17
17
|
maxImpressionsPerUser: number;
|
|
18
18
|
contractRef: string | null;
|
|
19
19
|
createdBy: string | null;
|
|
20
20
|
createdAt: Date;
|
|
21
21
|
updatedAt: Date;
|
|
22
|
-
advertiser: PromotionAdvertiser;
|
|
22
|
+
advertiser: PromotionAdvertiser | null;
|
|
23
23
|
targets: PromotionTarget[];
|
|
24
24
|
creatives: PromotionCreative[];
|
|
25
25
|
contacts: PromotionContact[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promotion.entity.d.ts","sourceRoot":"","sources":["../../../src/promotions/promotions/promotion.entity.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,gDAAgD,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,MAAM,kDAAkD,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,8CAA8C,CAAC;
|
|
1
|
+
{"version":3,"file":"promotion.entity.d.ts","sourceRoot":"","sources":["../../../src/promotions/promotions/promotion.entity.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,gDAAgD,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,MAAM,kDAAkD,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,8CAA8C,CAAC;AAkC/E,qBAiBa,SAAS;IAKpB,EAAE,EAAE,MAAM,CAAC;IAOX,IAAI,EAAE,MAAM,CAAC;IAQb,MAAM,EAAE,eAAe,CAAC;IAQxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAS5B,WAAW,EAAE,oBAAoB,GAAG,IAAI,CAAC;IASzC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAQzB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IAQtB,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;IAOpB,QAAQ,EAAE,MAAM,CAAC;IAQjB,qBAAqB,EAAE,MAAM,CAAC;IAS9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAS3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAQzB,SAAS,EAAE,IAAI,CAAC;IAQhB,SAAS,EAAE,IAAI,CAAC;IAYhB,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAGvC,OAAO,EAAE,eAAe,EAAE,CAAC;IAG3B,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAG/B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B"}
|
|
@@ -52,7 +52,8 @@ __decorateClass([
|
|
|
52
52
|
Column({
|
|
53
53
|
name: "advertiser_id",
|
|
54
54
|
type: "uuid",
|
|
55
|
-
|
|
55
|
+
nullable: true,
|
|
56
|
+
comment: "FK promotion_advertisers.id. Quem pagou pelo destaque. Nulo s\xF3 em rascunho."
|
|
56
57
|
})
|
|
57
58
|
], Promotion.prototype, "advertiserId", 2);
|
|
58
59
|
__decorateClass([
|
|
@@ -76,14 +77,16 @@ __decorateClass([
|
|
|
76
77
|
Column({
|
|
77
78
|
name: "starts_at",
|
|
78
79
|
type: "timestamptz",
|
|
79
|
-
|
|
80
|
+
nullable: true,
|
|
81
|
+
comment: "In\xEDcio da janela em que o destaque pode aparecer. Nulo s\xF3 em rascunho."
|
|
80
82
|
})
|
|
81
83
|
], Promotion.prototype, "startsAt", 2);
|
|
82
84
|
__decorateClass([
|
|
83
85
|
Column({
|
|
84
86
|
name: "ends_at",
|
|
85
87
|
type: "timestamptz",
|
|
86
|
-
|
|
88
|
+
nullable: true,
|
|
89
|
+
comment: "Fim da janela (exclusivo). Depois disso n\xE3o aparece mais. Nulo s\xF3 em rascunho."
|
|
87
90
|
})
|
|
88
91
|
], Promotion.prototype, "endsAt", 2);
|
|
89
92
|
__decorateClass([
|
|
@@ -137,6 +140,7 @@ __decorateClass([
|
|
|
137
140
|
], Promotion.prototype, "updatedAt", 2);
|
|
138
141
|
__decorateClass([
|
|
139
142
|
ManyToOne(() => PromotionAdvertiser, (advertiser) => advertiser.promotions, {
|
|
143
|
+
nullable: true,
|
|
140
144
|
onDelete: "RESTRICT"
|
|
141
145
|
}),
|
|
142
146
|
JoinColumn({
|
|
@@ -164,6 +168,10 @@ Promotion = __decorateClass([
|
|
|
164
168
|
`("subject_type" IS NULL AND "subject_id" IS NULL) OR ("subject_type" IS NOT NULL AND "subject_type" IN ('TOURNAMENT') AND "subject_id" IS NOT NULL)`
|
|
165
169
|
),
|
|
166
170
|
Check("CHK_promotions_window", `"ends_at" > "starts_at"`),
|
|
171
|
+
Check(
|
|
172
|
+
"CHK_promotions_ready",
|
|
173
|
+
`"status" = 'DRAFT' OR ("advertiser_id" IS NOT NULL AND "starts_at" IS NOT NULL AND "ends_at" IS NOT NULL)`
|
|
174
|
+
),
|
|
167
175
|
Check("CHK_promotions_max_impressions", `"max_impressions_per_user" >= 1`)
|
|
168
176
|
], Promotion);
|
|
169
177
|
export {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface PromotionCreativeIssue {
|
|
2
|
+
field: string;
|
|
3
|
+
code: 'REQUIRED' | 'INVALID' | 'TOO_LONG' | 'UNKNOWN_TEMPLATE';
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PromotionCreativeValidationInput {
|
|
7
|
+
template: string;
|
|
8
|
+
content: unknown;
|
|
9
|
+
consentText: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function validatePromotionCreative(input: PromotionCreativeValidationInput): PromotionCreativeIssue[];
|
|
12
|
+
export declare function isPromotionCreativeLive(input: PromotionCreativeValidationInput): boolean;
|
|
13
|
+
//# sourceMappingURL=promotion-creative.validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promotion-creative.validation.d.ts","sourceRoot":"","sources":["../../../src/promotions/validation/promotion-creative.validation.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,sBAAsB;IAErC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,kBAAkB,CAAC;IAC/D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAsDD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gCAAgC,GACtC,sBAAsB,EAAE,CA+B1B;AAGD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,gCAAgC,GAAG,OAAO,CAExF"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PromotionCreativeTemplate } from "../enums/promotion-creative-template.enum.js";
|
|
2
|
+
const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
|
|
3
|
+
const HTTPS_URL = /^https:\/\/\S+$/;
|
|
4
|
+
const CONSENT_MAX_LENGTH = 280;
|
|
5
|
+
const TOURNAMENT_POSTER_RULES = {
|
|
6
|
+
posterImageUrl: {
|
|
7
|
+
required: true,
|
|
8
|
+
maxLength: 2048,
|
|
9
|
+
pattern: HTTPS_URL,
|
|
10
|
+
patternMessage: "Use uma URL https."
|
|
11
|
+
},
|
|
12
|
+
posterColor: {
|
|
13
|
+
required: false,
|
|
14
|
+
maxLength: 7,
|
|
15
|
+
pattern: HEX_COLOR,
|
|
16
|
+
patternMessage: "Use a cor no formato #RRGGBB."
|
|
17
|
+
},
|
|
18
|
+
monogram: { required: false, maxLength: 3 },
|
|
19
|
+
headline: { required: false, maxLength: 80 },
|
|
20
|
+
kicker: { required: false, maxLength: 80 },
|
|
21
|
+
badge: { required: false, maxLength: 40 },
|
|
22
|
+
ctaLabel: { required: true, maxLength: 40 },
|
|
23
|
+
dismissLabel: { required: true, maxLength: 40 },
|
|
24
|
+
contactMessage: { required: false, maxLength: 500 }
|
|
25
|
+
};
|
|
26
|
+
const isFilled = (value) => typeof value === "string" && value.trim().length > 0;
|
|
27
|
+
function validatePromotionCreative(input) {
|
|
28
|
+
if (input.template !== PromotionCreativeTemplate.TOURNAMENT_POSTER) {
|
|
29
|
+
return [{ field: "template", code: "UNKNOWN_TEMPLATE", message: "Layout desconhecido." }];
|
|
30
|
+
}
|
|
31
|
+
const issues = [];
|
|
32
|
+
if (isFilled(input.consentText) && input.consentText.trim().length > CONSENT_MAX_LENGTH) {
|
|
33
|
+
issues.push({
|
|
34
|
+
field: "consentText",
|
|
35
|
+
code: "TOO_LONG",
|
|
36
|
+
message: `Use at\xE9 ${CONSENT_MAX_LENGTH} caracteres.`
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const content = input.content;
|
|
40
|
+
if (typeof content !== "object" || content === null || Array.isArray(content)) {
|
|
41
|
+
issues.push({ field: "content", code: "INVALID", message: "Conte\xFAdo do criativo inv\xE1lido." });
|
|
42
|
+
return issues;
|
|
43
|
+
}
|
|
44
|
+
const fields = new Map(Object.entries(content));
|
|
45
|
+
for (const [name, rule] of Object.entries(TOURNAMENT_POSTER_RULES)) {
|
|
46
|
+
const issue = checkField(name, fields.get(name), rule);
|
|
47
|
+
if (issue) {
|
|
48
|
+
issues.push(issue);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return issues;
|
|
52
|
+
}
|
|
53
|
+
function isPromotionCreativeLive(input) {
|
|
54
|
+
return validatePromotionCreative(input).length === 0;
|
|
55
|
+
}
|
|
56
|
+
function checkField(name, value, rule) {
|
|
57
|
+
const field = `content.${name}`;
|
|
58
|
+
if (value === void 0 || value === null || typeof value === "string" && !value.trim()) {
|
|
59
|
+
return rule.required ? { field, code: "REQUIRED", message: "Campo obrigat\xF3rio." } : null;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== "string") {
|
|
62
|
+
return { field, code: "INVALID", message: "Use texto." };
|
|
63
|
+
}
|
|
64
|
+
if (value.trim().length > rule.maxLength) {
|
|
65
|
+
return { field, code: "TOO_LONG", message: `Use at\xE9 ${rule.maxLength} caracteres.` };
|
|
66
|
+
}
|
|
67
|
+
if (rule.pattern && !rule.pattern.test(value.trim())) {
|
|
68
|
+
return { field, code: "INVALID", message: rule.patternMessage ?? "Formato inv\xE1lido." };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
isPromotionCreativeLive,
|
|
74
|
+
validatePromotionCreative
|
|
75
|
+
};
|