@bash-app/bash-common 30.245.0 → 30.247.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/README.md +2 -0
- package/dist/__tests__/bashSectionTitleForBashType.test.d.ts +2 -0
- package/dist/__tests__/bashSectionTitleForBashType.test.d.ts.map +1 -0
- package/dist/__tests__/bashSectionTitleForBashType.test.js +106 -0
- package/dist/__tests__/bashSectionTitleForBashType.test.js.map +1 -0
- package/dist/definitions.d.ts +18 -5
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +93 -24
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +39 -1
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js +25 -0
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/service/serviceFeaturedSortUtils.d.ts +26 -0
- package/dist/utils/service/serviceFeaturedSortUtils.d.ts.map +1 -0
- package/dist/utils/service/serviceFeaturedSortUtils.js +61 -0
- package/dist/utils/service/serviceFeaturedSortUtils.js.map +1 -0
- package/package.json +1 -1
- package/prisma/schema.prisma +25 -0
- package/src/__tests__/bashSectionTitleForBashType.test.ts +142 -0
- package/src/definitions.ts +95 -25
- package/src/extendedSchemas.ts +33 -1
- package/src/index.ts +2 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/service/serviceFeaturedSortUtils.ts +76 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section heading copy for the homepage rows and `/category/:bashType` pages.
|
|
3
|
+
*
|
|
4
|
+
* Two input shapes need to coexist:
|
|
5
|
+
* 1. `BashEventType` enum values (e.g. "ComedyShow", "Rave") — these have
|
|
6
|
+
* a singular label in `BashEventTypeToString` and get pluralized via
|
|
7
|
+
* explicit rules in `pluralizeBashEventSingularLabel`.
|
|
8
|
+
* 2. Free-text section keys persisted by scrapers as `bashEvent.eventType`
|
|
9
|
+
* strings that don't exist in the enum — for example
|
|
10
|
+
* `scrapeInsomniacToBash.mjs` defaults to `"Raves"` and
|
|
11
|
+
* `wiseguysComedyScraper.mjs` writes `"Comedy"`. These are already
|
|
12
|
+
* meaningful as headings and must be returned verbatim — never with
|
|
13
|
+
* a `" bashes"` suffix tacked on.
|
|
14
|
+
*/
|
|
15
|
+
import { BashEventType } from "@prisma/client";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
bashSectionTitleForBashType,
|
|
19
|
+
canonicalBashEventRowKey,
|
|
20
|
+
} from "../definitions.js";
|
|
21
|
+
|
|
22
|
+
describe("bashSectionTitleForBashType", () => {
|
|
23
|
+
describe("free-text scraper section keys (non-enum)", () => {
|
|
24
|
+
it("returns 'Raves' verbatim, not 'Raves bashes'", () => {
|
|
25
|
+
expect(bashSectionTitleForBashType("Raves")).toBe("Raves");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns 'Comedy' verbatim, not 'Comedy bashes'", () => {
|
|
29
|
+
expect(bashSectionTitleForBashType("Comedy")).toBe("Comedy");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns mass-noun headings as-is", () => {
|
|
33
|
+
expect(bashSectionTitleForBashType("EDM")).toBe("EDM");
|
|
34
|
+
expect(bashSectionTitleForBashType("Hip Hop")).toBe("Hip Hop");
|
|
35
|
+
expect(bashSectionTitleForBashType("Karaoke")).toBe("Karaoke");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("synthetic homepage rows", () => {
|
|
40
|
+
it("'Featured' → 'Featured'", () => {
|
|
41
|
+
expect(bashSectionTitleForBashType("Featured")).toBe("Featured");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("'Trending' → 'Trending'", () => {
|
|
45
|
+
expect(bashSectionTitleForBashType("Trending")).toBe("Trending");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("BashEventType enum values get pluralized", () => {
|
|
50
|
+
it("'Rave' enum → 'Raves'", () => {
|
|
51
|
+
expect(bashSectionTitleForBashType(BashEventType.Rave)).toBe("Raves");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("'ComedyShow' enum → 'Comedy Shows'", () => {
|
|
55
|
+
expect(bashSectionTitleForBashType(BashEventType.ComedyShow)).toBe(
|
|
56
|
+
"Comedy Shows"
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("'Birthday' enum → 'Birthday Parties' (manual override)", () => {
|
|
61
|
+
expect(bashSectionTitleForBashType(BashEventType.Birthday)).toBe(
|
|
62
|
+
"Birthday Parties"
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("'WeddingReception' enum → 'Wedding Receptions' (suffix rule)", () => {
|
|
67
|
+
expect(bashSectionTitleForBashType(BashEventType.WeddingReception)).toBe(
|
|
68
|
+
"Wedding Receptions"
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("'HouseParty' enum → 'House Parties' (Party suffix rule)", () => {
|
|
73
|
+
expect(bashSectionTitleForBashType(BashEventType.HouseParty)).toBe(
|
|
74
|
+
"House Parties"
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("homepage tier-section row keys (`LazyTierSection`, bashType=`title`)", () => {
|
|
80
|
+
it("titles ending with ' Events' are returned verbatim (no duplicate ' bashes')", () => {
|
|
81
|
+
expect(bashSectionTitleForBashType("Pro Events")).toBe("Pro Events");
|
|
82
|
+
expect(bashSectionTitleForBashType("Elite Events")).toBe("Elite Events");
|
|
83
|
+
expect(bashSectionTitleForBashType("Legend Events")).toBe("Legend Events");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("Insomniac-style scraper fallback keys matching enum literals", () => {
|
|
88
|
+
it("'Festival' → 'Festivals'", () => {
|
|
89
|
+
expect(bashSectionTitleForBashType("Festival")).toBe("Festivals");
|
|
90
|
+
});
|
|
91
|
+
it("'Concert' → 'Concerts'", () => {
|
|
92
|
+
expect(bashSectionTitleForBashType("Concert")).toBe("Concerts");
|
|
93
|
+
});
|
|
94
|
+
it("'Party' enum → 'Parties'", () => {
|
|
95
|
+
expect(bashSectionTitleForBashType("Party")).toBe("Parties");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("keys that diverge from Prisma enum literals (case or display labels)", () => {
|
|
100
|
+
it("lower-case prisma value 'party' → 'Parties'", () => {
|
|
101
|
+
expect(bashSectionTitleForBashType("party")).toBe("Parties");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("lower-case 'fundraiser' → 'Fundraisers'", () => {
|
|
105
|
+
expect(bashSectionTitleForBashType("fundraiser")).toBe("Fundraisers");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("singular display label row key 'House Party' → 'House Parties'", () => {
|
|
109
|
+
expect(bashSectionTitleForBashType("House Party")).toBe("House Parties");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("trimmed stray whitespace still pluralizes", () => {
|
|
113
|
+
expect(bashSectionTitleForBashType(" Fundraiser ")).toBe("Fundraisers");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("already-plural enum headings", () => {
|
|
118
|
+
it("normalizes casing when the row key matches a pluralized enum title", () => {
|
|
119
|
+
expect(bashSectionTitleForBashType("House Parties")).toBe(
|
|
120
|
+
"House Parties"
|
|
121
|
+
);
|
|
122
|
+
expect(bashSectionTitleForBashType("house parties")).toBe("House Parties");
|
|
123
|
+
expect(bashSectionTitleForBashType("Comedy Shows")).toBe("Comedy Shows");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("canonicalBashEventRowKey", () => {
|
|
129
|
+
it("maps singular labels and enum literals to the Prisma enum string", () => {
|
|
130
|
+
expect(canonicalBashEventRowKey("House Party")).toBe(
|
|
131
|
+
BashEventType.HouseParty
|
|
132
|
+
);
|
|
133
|
+
expect(canonicalBashEventRowKey(BashEventType.HouseParty)).toBe(
|
|
134
|
+
BashEventType.HouseParty
|
|
135
|
+
);
|
|
136
|
+
expect(canonicalBashEventRowKey("houseparty")).toBe(BashEventType.HouseParty);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("returns trimmed free-text when no enum matches", () => {
|
|
140
|
+
expect(canonicalBashEventRowKey("Graduation")).toBe("Graduation");
|
|
141
|
+
});
|
|
142
|
+
});
|
package/src/definitions.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AgePolicy,
|
|
3
2
|
BashEventType,
|
|
4
3
|
BracketType,
|
|
5
4
|
CompetitionType,
|
|
@@ -18,6 +17,7 @@ import {
|
|
|
18
17
|
User,
|
|
19
18
|
} from "@prisma/client";
|
|
20
19
|
import type {
|
|
20
|
+
AgePolicy as AgePolicyEnum,
|
|
21
21
|
BashPassTier,
|
|
22
22
|
BashEventDressTags,
|
|
23
23
|
BashEventVibeTags,
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
} from "@prisma/client";
|
|
29
29
|
import {
|
|
30
30
|
CheckoutExt,
|
|
31
|
+
PublicTeaserUser,
|
|
31
32
|
PublicUser,
|
|
32
33
|
ServiceBookingExt,
|
|
33
34
|
type UserExt,
|
|
@@ -79,6 +80,15 @@ export const GovIdRetentionPolicy = {
|
|
|
79
80
|
|
|
80
81
|
export type GovIdRetentionPolicy = GovIdRetentionPolicyEnum;
|
|
81
82
|
|
|
83
|
+
/** Mirrors Prisma `AgePolicy` — `@prisma/client` browser bundle omits runtime enum exports. */
|
|
84
|
+
export const AgePolicy = {
|
|
85
|
+
AllAges: "AllAges",
|
|
86
|
+
EighteenPlus: "EighteenPlus",
|
|
87
|
+
TwentyOnePlus: "TwentyOnePlus",
|
|
88
|
+
} as const satisfies Record<AgePolicyEnum, AgePolicyEnum>;
|
|
89
|
+
|
|
90
|
+
export type AgePolicy = AgePolicyEnum;
|
|
91
|
+
|
|
82
92
|
export const PASSWORD_MIN_LENGTH = 8 as const;
|
|
83
93
|
export const PASSWORD_REQUIREMENTS_REGEX = new RegExp(
|
|
84
94
|
String.raw`^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&^#])[A-Za-z\d@$!%*?&^#]{${PASSWORD_MIN_LENGTH},}$`
|
|
@@ -708,7 +718,7 @@ export interface LoginSession {
|
|
|
708
718
|
}
|
|
709
719
|
|
|
710
720
|
export interface AttendeeOfBashEvent {
|
|
711
|
-
user: PublicUser;
|
|
721
|
+
user: PublicUser | PublicTeaserUser;
|
|
712
722
|
tickets: Ticket[];
|
|
713
723
|
}
|
|
714
724
|
|
|
@@ -1114,7 +1124,6 @@ export const YearsOfExperienceToString: { [key in YearsOfExperienceEnum]: string
|
|
|
1114
1124
|
|
|
1115
1125
|
// Export Prisma enums for use in frontend
|
|
1116
1126
|
export {
|
|
1117
|
-
AgePolicy,
|
|
1118
1127
|
BracketType,
|
|
1119
1128
|
CompetitionType,
|
|
1120
1129
|
EntertainmentServiceType,
|
|
@@ -1439,8 +1448,8 @@ export const INCOME_RANGE_VALUES: ReadonlyArray<IncomeRange> = [
|
|
|
1439
1448
|
|
|
1440
1449
|
/** Homepage / discovery section labels for synthetic rows (not `BashEventType` enum values). */
|
|
1441
1450
|
const SYNTHETIC_BASH_SECTION_TITLE: Record<string, string> = {
|
|
1442
|
-
Featured: "Featured
|
|
1443
|
-
Trending: "Trending
|
|
1451
|
+
Featured: "Featured",
|
|
1452
|
+
Trending: "Trending",
|
|
1444
1453
|
};
|
|
1445
1454
|
|
|
1446
1455
|
/**
|
|
@@ -1481,24 +1490,25 @@ const WHOLE_WORD_BASH_LABEL_PLURAL: ReadonlyMap<string, string> = new Map([
|
|
|
1481
1490
|
]);
|
|
1482
1491
|
|
|
1483
1492
|
function pluralizeBashEventSingularLabel(label: string): string {
|
|
1484
|
-
const
|
|
1493
|
+
const t = label.trim();
|
|
1494
|
+
const manual = BASH_EVENT_SINGULAR_LABEL_TO_PLURAL_SECTION.get(t);
|
|
1485
1495
|
if (manual) return manual;
|
|
1486
1496
|
|
|
1487
|
-
const wholeWord = WHOLE_WORD_BASH_LABEL_PLURAL.get(
|
|
1497
|
+
const wholeWord = WHOLE_WORD_BASH_LABEL_PLURAL.get(t);
|
|
1488
1498
|
if (wholeWord) return wholeWord;
|
|
1489
1499
|
|
|
1490
|
-
if (
|
|
1491
|
-
if (
|
|
1492
|
-
return `${
|
|
1500
|
+
if (t === "Party") return "Parties";
|
|
1501
|
+
if (t.endsWith(" Party")) {
|
|
1502
|
+
return `${t.slice(0, -" Party".length)} Parties`;
|
|
1493
1503
|
}
|
|
1494
|
-
if (
|
|
1495
|
-
return `${
|
|
1504
|
+
if (t.endsWith("-Party")) {
|
|
1505
|
+
return `${t.slice(0, -"-Party".length)}-Parties`;
|
|
1496
1506
|
}
|
|
1497
|
-
if (
|
|
1498
|
-
return `${
|
|
1507
|
+
if (t.endsWith(" Party)")) {
|
|
1508
|
+
return `${t.replace(/ Party\)$/, " Parties)")}`;
|
|
1499
1509
|
}
|
|
1500
|
-
if (
|
|
1501
|
-
return `${
|
|
1510
|
+
if (t.endsWith(" Rave")) {
|
|
1511
|
+
return `${t.slice(0, -" Rave".length)} Raves`;
|
|
1502
1512
|
}
|
|
1503
1513
|
|
|
1504
1514
|
const replacements: ReadonlyArray<[suffix: string, plural: string]> = [
|
|
@@ -1535,37 +1545,97 @@ function pluralizeBashEventSingularLabel(label: string): string {
|
|
|
1535
1545
|
];
|
|
1536
1546
|
|
|
1537
1547
|
for (const [suffix, plural] of replacements) {
|
|
1538
|
-
if (
|
|
1539
|
-
return `${
|
|
1548
|
+
if (t.endsWith(suffix)) {
|
|
1549
|
+
return `${t.slice(0, -suffix.length)}${plural}`;
|
|
1540
1550
|
}
|
|
1541
1551
|
}
|
|
1542
1552
|
|
|
1543
|
-
|
|
1553
|
+
// No explicit rule matched. The label is almost certainly:
|
|
1554
|
+
// - already plural (e.g. "Raves" from `scrapeInsomniacToBash.mjs`)
|
|
1555
|
+
// - a mass noun used as a category heading (e.g. "Comedy" from
|
|
1556
|
+
// `wiseguysComedyScraper.mjs`, "EDM", "Hip Hop")
|
|
1557
|
+
// - a future enum value not yet covered above
|
|
1558
|
+
// For all of these, returning the label verbatim is correct. Appending
|
|
1559
|
+
// " bashes" produces "Raves bashes" / "Comedy bashes", which reads wrong.
|
|
1560
|
+
return t;
|
|
1544
1561
|
}
|
|
1545
1562
|
|
|
1546
1563
|
const BASH_EVENT_TYPE_ENUM_VALUES: ReadonlySet<string> = new Set(
|
|
1547
1564
|
Object.values(BashEventType) as string[]
|
|
1548
1565
|
);
|
|
1549
1566
|
|
|
1567
|
+
function resolveCanonicalBashEventTypeKey(raw: string): string | undefined {
|
|
1568
|
+
const t = raw.trim();
|
|
1569
|
+
if (!t) return undefined;
|
|
1570
|
+
if (BASH_EVENT_TYPE_ENUM_VALUES.has(t)) return t;
|
|
1571
|
+
const lower = t.toLowerCase();
|
|
1572
|
+
for (const v of BASH_EVENT_TYPE_ENUM_VALUES) {
|
|
1573
|
+
if (v.toLowerCase() === lower) return v;
|
|
1574
|
+
}
|
|
1575
|
+
return undefined;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* When the row key matches a `BashEventTypeToString` value (e.g. "House Party")
|
|
1580
|
+
* but not the Prisma enum literal (`HouseParty`), still pluralize like the enum.
|
|
1581
|
+
*/
|
|
1582
|
+
function resolveBashEventEnumKeyFromSingularLabel(raw: string): string | undefined {
|
|
1583
|
+
const t = raw.trim();
|
|
1584
|
+
if (!t) return undefined;
|
|
1585
|
+
const lower = t.toLowerCase();
|
|
1586
|
+
for (const enumKey of BASH_EVENT_TYPE_ENUM_VALUES) {
|
|
1587
|
+
const k = enumKey as BashEventType;
|
|
1588
|
+
const label = BashEventTypeToString[k];
|
|
1589
|
+
if (label === t || label.toLowerCase() === lower) return enumKey;
|
|
1590
|
+
}
|
|
1591
|
+
return undefined;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* Stable row key for homepage/category grouping when `eventType` may be stored
|
|
1596
|
+
* as a Prisma enum literal (e.g. `HouseParty`) or as the singular UI label
|
|
1597
|
+
* (`House Party`).
|
|
1598
|
+
*/
|
|
1599
|
+
export function canonicalBashEventRowKey(raw: string): string {
|
|
1600
|
+
const trimmed = raw.trim();
|
|
1601
|
+
if (!trimmed) return trimmed;
|
|
1602
|
+
const enumKey =
|
|
1603
|
+
resolveCanonicalBashEventTypeKey(trimmed) ??
|
|
1604
|
+
resolveBashEventEnumKeyFromSingularLabel(trimmed);
|
|
1605
|
+
if (enumKey != null) return enumKey;
|
|
1606
|
+
return trimmed;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1550
1609
|
/**
|
|
1551
1610
|
* Heading shown on the home page row and the `/category/...` page for a bash
|
|
1552
1611
|
* `eventType` / section key. Uses plural-friendly copy for Prisma
|
|
1553
1612
|
* `BashEventType` values; keeps synthetic rows and membership tier rows clear.
|
|
1554
1613
|
*/
|
|
1555
1614
|
export function bashSectionTitleForBashType(bashType: string): string {
|
|
1556
|
-
const
|
|
1615
|
+
const raw = bashType.trim();
|
|
1616
|
+
const lower = raw.toLowerCase();
|
|
1617
|
+
|
|
1618
|
+
const synthetic = SYNTHETIC_BASH_SECTION_TITLE[raw];
|
|
1557
1619
|
if (synthetic != null) return synthetic;
|
|
1620
|
+
if (/\bEvents$/i.test(raw)) return raw;
|
|
1558
1621
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1622
|
+
for (const enumKey of BASH_EVENT_TYPE_ENUM_VALUES) {
|
|
1623
|
+
const singular = BashEventTypeToString[enumKey as BashEventType];
|
|
1624
|
+
const pluralHeading = pluralizeBashEventSingularLabel(singular);
|
|
1625
|
+
if (pluralHeading.toLowerCase() === lower) {
|
|
1626
|
+
return pluralHeading;
|
|
1627
|
+
}
|
|
1561
1628
|
}
|
|
1562
1629
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1630
|
+
const enumKey =
|
|
1631
|
+
resolveCanonicalBashEventTypeKey(raw) ??
|
|
1632
|
+
resolveBashEventEnumKeyFromSingularLabel(raw);
|
|
1633
|
+
if (enumKey != null) {
|
|
1634
|
+
const singular = BashEventTypeToString[enumKey as BashEventType];
|
|
1565
1635
|
return pluralizeBashEventSingularLabel(singular);
|
|
1566
1636
|
}
|
|
1567
1637
|
|
|
1568
|
-
const spaced = insertSpacesInCamelCase(
|
|
1638
|
+
const spaced = insertSpacesInCamelCase(raw);
|
|
1569
1639
|
return pluralizeBashEventSingularLabel(spaced);
|
|
1570
1640
|
}
|
|
1571
1641
|
|
package/src/extendedSchemas.ts
CHANGED
|
@@ -199,6 +199,25 @@ export const FRONT_END_USER_DATA_TO_SELECT = {
|
|
|
199
199
|
},
|
|
200
200
|
} satisfies Prisma.UserSelect;
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Minimal `User` fields for public teaser surfaces (event detail avatars, checkout rows,
|
|
204
|
+
* non-privileged attendee rosters). Omits `email` and other account-level PII.
|
|
205
|
+
*/
|
|
206
|
+
export const PUBLIC_TEASER_USER_DATA_TO_SELECT = {
|
|
207
|
+
id: true,
|
|
208
|
+
username: true,
|
|
209
|
+
givenName: true,
|
|
210
|
+
familyName: true,
|
|
211
|
+
image: true,
|
|
212
|
+
imageUpdatedAt: true,
|
|
213
|
+
uploadedImage: true,
|
|
214
|
+
uploadedImageUpdatedAt: true,
|
|
215
|
+
} satisfies Prisma.UserSelect;
|
|
216
|
+
|
|
217
|
+
export type PublicTeaserUser = Prisma.UserGetPayload<{
|
|
218
|
+
select: typeof PUBLIC_TEASER_USER_DATA_TO_SELECT;
|
|
219
|
+
}>;
|
|
220
|
+
|
|
202
221
|
export const PRIVATE_USER_ACCOUNT_TO_SELECT = {
|
|
203
222
|
...FRONT_END_USER_DATA_TO_SELECT,
|
|
204
223
|
/** Self session only — preferred payment rail (Stripe vs Square). */
|
|
@@ -1287,7 +1306,8 @@ export type TicketFlexible = Ticket & {
|
|
|
1287
1306
|
};
|
|
1288
1307
|
|
|
1289
1308
|
export interface CheckoutExt extends Checkout {
|
|
1290
|
-
owner
|
|
1309
|
+
/** Full owner row for hosts/organizers; teaser row (no email) for other authenticated viewers. */
|
|
1310
|
+
owner: PublicUser | PublicTeaserUser;
|
|
1291
1311
|
tickets: Ticket[];
|
|
1292
1312
|
}
|
|
1293
1313
|
|
|
@@ -1302,6 +1322,18 @@ export const CHECKOUT_DATA_TO_INCLUDE = {
|
|
|
1302
1322
|
},
|
|
1303
1323
|
} satisfies Prisma.CheckoutInclude;
|
|
1304
1324
|
|
|
1325
|
+
/** Checkout list for non-privileged viewers — buyer email omitted on `owner`. */
|
|
1326
|
+
export const CHECKOUT_DATA_TO_INCLUDE_TEASER_OWNER = {
|
|
1327
|
+
owner: {
|
|
1328
|
+
select: PUBLIC_TEASER_USER_DATA_TO_SELECT,
|
|
1329
|
+
},
|
|
1330
|
+
tickets: {
|
|
1331
|
+
select: {
|
|
1332
|
+
ownerId: true,
|
|
1333
|
+
},
|
|
1334
|
+
},
|
|
1335
|
+
} satisfies Prisma.CheckoutInclude;
|
|
1336
|
+
|
|
1305
1337
|
export interface ReviewExt extends Review {
|
|
1306
1338
|
comments?: BashComment[];
|
|
1307
1339
|
}
|
package/src/index.ts
CHANGED
|
@@ -87,6 +87,7 @@ export {
|
|
|
87
87
|
EventServiceType,
|
|
88
88
|
EventTemplateCategory,
|
|
89
89
|
ExhibitorBookingStatus,
|
|
90
|
+
FeaturedReason,
|
|
90
91
|
FinancingSubType,
|
|
91
92
|
FlyerCampaignStatus,
|
|
92
93
|
FlyerConversionType,
|
|
@@ -305,6 +306,7 @@ export * from "./utils/service/attendeeOptionUtils.js";
|
|
|
305
306
|
export * from "./utils/service/billingCapUtils.js";
|
|
306
307
|
export * from "./utils/service/eventPromoterUtils.js";
|
|
307
308
|
export * from "./utils/service/regexUtils.js";
|
|
309
|
+
export * from "./utils/service/serviceFeaturedSortUtils.js";
|
|
308
310
|
export * from "./utils/service/serviceBookingExposureUtils.js";
|
|
309
311
|
export * from "./utils/service/serviceUtils.js";
|
|
310
312
|
export * from "./utils/service/venueSettlementUtils.js";
|
package/src/utils/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * from './service/frontendServiceBookingUtils.js';
|
|
|
24
24
|
export * from './service/regexUtils.js';
|
|
25
25
|
export * from './service/serviceBookingStatusUtils.js';
|
|
26
26
|
export * from './service/serviceBookingTypes.js';
|
|
27
|
+
export * from './service/serviceFeaturedSortUtils.js';
|
|
27
28
|
export * from './service/serviceRateTypes.js';
|
|
28
29
|
export * from './service/serviceRateUtils.js';
|
|
29
30
|
export * from './service/serviceUtils.js';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { FeaturedReason } from "@prisma/client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Subset of `Service` fields needed to decide featured-tier ordering.
|
|
5
|
+
* Both the API public list endpoint and the front-end browse filter sort
|
|
6
|
+
* with this comparator so server and client agree on order.
|
|
7
|
+
*/
|
|
8
|
+
export interface FeaturedServiceSortable {
|
|
9
|
+
isFeaturedService?: boolean | null;
|
|
10
|
+
featuredServiceUntil?: Date | string | null;
|
|
11
|
+
featuredAt?: Date | string | null;
|
|
12
|
+
featuredReason?: FeaturedReason | null;
|
|
13
|
+
createdAt?: Date | string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Reasons that rank above auto-features in the public sort. */
|
|
17
|
+
const PAID_TIER_REASONS: ReadonlySet<FeaturedReason> = new Set([
|
|
18
|
+
"Paid",
|
|
19
|
+
"Admin",
|
|
20
|
+
"Membership",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function toMs(value: Date | string | null | undefined): number {
|
|
24
|
+
if (!value) return 0;
|
|
25
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
26
|
+
const t = d.getTime();
|
|
27
|
+
return Number.isFinite(t) ? t : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isActiveFeatured(s: FeaturedServiceSortable, now: Date): boolean {
|
|
31
|
+
if (!s.isFeaturedService) return false;
|
|
32
|
+
if (!s.featuredServiceUntil) return true;
|
|
33
|
+
return new Date(s.featuredServiceUntil) >= now;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Tier 0 = paid / admin / membership feature, 1 = AutoNewService, 2 = unfeatured
|
|
38
|
+
* (or featured but with no reason recorded — these existed before the migration
|
|
39
|
+
* and are treated as "legacy featured" sitting just below auto-features).
|
|
40
|
+
*/
|
|
41
|
+
function featuredTier(s: FeaturedServiceSortable, now: Date): number {
|
|
42
|
+
if (!isActiveFeatured(s, now)) return 3;
|
|
43
|
+
if (s.featuredReason && PAID_TIER_REASONS.has(s.featuredReason)) return 0;
|
|
44
|
+
if (s.featuredReason === "AutoNewService") return 1;
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Public-services ORDER BY equivalent in JS:
|
|
50
|
+
* 1. paid/admin/membership features (FIFO by featuredAt asc)
|
|
51
|
+
* 2. auto-featured new services (FIFO by featuredAt asc)
|
|
52
|
+
* 3. legacy featured rows with no recorded reason (FIFO by featuredAt asc)
|
|
53
|
+
* 4. everything else (newest first by createdAt desc)
|
|
54
|
+
*
|
|
55
|
+
* Pass a single `now` so all comparisons in one sort use the same instant.
|
|
56
|
+
*/
|
|
57
|
+
export function compareFeaturedServices(
|
|
58
|
+
now: Date = new Date()
|
|
59
|
+
): (a: FeaturedServiceSortable, b: FeaturedServiceSortable) => number {
|
|
60
|
+
return (a, b) => {
|
|
61
|
+
const ta = featuredTier(a, now);
|
|
62
|
+
const tb = featuredTier(b, now);
|
|
63
|
+
if (ta !== tb) return ta - tb;
|
|
64
|
+
|
|
65
|
+
if (ta < 3) {
|
|
66
|
+
const fa = toMs(a.featuredAt);
|
|
67
|
+
const fb = toMs(b.featuredAt);
|
|
68
|
+
if (fa !== fb) return fa - fb; // ASC — earliest paid wins
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return toMs(b.createdAt) - toMs(a.createdAt); // DESC for non-featured / tie-break
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Minimum number of active featured services required for the homepage / browse "Featured Services" carousel to render. Below this floor the section feels sparse. */
|
|
76
|
+
export const MIN_FEATURED_SERVICES_FOR_SECTION = 3;
|