@bash-app/bash-common 30.224.0 → 30.226.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.
Files changed (109) hide show
  1. package/dist/__tests__/agePolicy.test.d.ts +2 -0
  2. package/dist/__tests__/agePolicy.test.d.ts.map +1 -0
  3. package/dist/__tests__/agePolicy.test.js +203 -0
  4. package/dist/__tests__/agePolicy.test.js.map +1 -0
  5. package/dist/__tests__/getDisplayGender.test.d.ts +2 -0
  6. package/dist/__tests__/getDisplayGender.test.d.ts.map +1 -0
  7. package/dist/__tests__/getDisplayGender.test.js +34 -0
  8. package/dist/__tests__/getDisplayGender.test.js.map +1 -0
  9. package/dist/__tests__/statusEnums.test.d.ts +2 -0
  10. package/dist/__tests__/statusEnums.test.d.ts.map +1 -0
  11. package/dist/__tests__/statusEnums.test.js +24 -0
  12. package/dist/__tests__/statusEnums.test.js.map +1 -0
  13. package/dist/aiApproval.d.ts +64 -0
  14. package/dist/aiApproval.d.ts.map +1 -0
  15. package/dist/aiApproval.js +54 -0
  16. package/dist/aiApproval.js.map +1 -0
  17. package/dist/definitions.d.ts +44 -2
  18. package/dist/definitions.d.ts.map +1 -1
  19. package/dist/definitions.js +106 -2
  20. package/dist/definitions.js.map +1 -1
  21. package/dist/extendedSchemas.d.ts +237 -104
  22. package/dist/extendedSchemas.d.ts.map +1 -1
  23. package/dist/extendedSchemas.js +12 -2
  24. package/dist/extendedSchemas.js.map +1 -1
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/legalTemplates.d.ts +1 -1
  30. package/dist/legalTemplates.js +1 -1
  31. package/dist/utils/__tests__/badgeUtils.test.d.ts +2 -0
  32. package/dist/utils/__tests__/badgeUtils.test.d.ts.map +1 -0
  33. package/dist/utils/__tests__/badgeUtils.test.js +157 -0
  34. package/dist/utils/__tests__/badgeUtils.test.js.map +1 -0
  35. package/dist/utils/__tests__/bashPointsPaymentUtils.test.d.ts +2 -0
  36. package/dist/utils/__tests__/bashPointsPaymentUtils.test.d.ts.map +1 -0
  37. package/dist/utils/__tests__/bashPointsPaymentUtils.test.js +114 -0
  38. package/dist/utils/__tests__/bashPointsPaymentUtils.test.js.map +1 -0
  39. package/dist/utils/__tests__/contentFilterUtils.test.d.ts +2 -0
  40. package/dist/utils/__tests__/contentFilterUtils.test.d.ts.map +1 -0
  41. package/dist/utils/__tests__/contentFilterUtils.test.js +443 -0
  42. package/dist/utils/__tests__/contentFilterUtils.test.js.map +1 -0
  43. package/dist/utils/__tests__/displayDateUtils.test.d.ts +2 -0
  44. package/dist/utils/__tests__/displayDateUtils.test.d.ts.map +1 -0
  45. package/dist/utils/__tests__/displayDateUtils.test.js +105 -0
  46. package/dist/utils/__tests__/displayDateUtils.test.js.map +1 -0
  47. package/dist/utils/__tests__/getBackendHost.test.d.ts +2 -0
  48. package/dist/utils/__tests__/getBackendHost.test.d.ts.map +1 -0
  49. package/dist/utils/__tests__/getBackendHost.test.js +104 -0
  50. package/dist/utils/__tests__/getBackendHost.test.js.map +1 -0
  51. package/dist/utils/__tests__/getFrontendHost.test.d.ts +2 -0
  52. package/dist/utils/__tests__/getFrontendHost.test.d.ts.map +1 -0
  53. package/dist/utils/__tests__/getFrontendHost.test.js +207 -0
  54. package/dist/utils/__tests__/getFrontendHost.test.js.map +1 -0
  55. package/dist/utils/__tests__/promoCodesUtils.test.d.ts +2 -0
  56. package/dist/utils/__tests__/promoCodesUtils.test.d.ts.map +1 -0
  57. package/dist/utils/__tests__/promoCodesUtils.test.js +183 -0
  58. package/dist/utils/__tests__/promoCodesUtils.test.js.map +1 -0
  59. package/dist/utils/__tests__/qrCodeUtils.test.d.ts +2 -0
  60. package/dist/utils/__tests__/qrCodeUtils.test.d.ts.map +1 -0
  61. package/dist/utils/__tests__/qrCodeUtils.test.js +99 -0
  62. package/dist/utils/__tests__/qrCodeUtils.test.js.map +1 -0
  63. package/dist/utils/__tests__/stripeAccountUtils.test.d.ts +2 -0
  64. package/dist/utils/__tests__/stripeAccountUtils.test.d.ts.map +1 -0
  65. package/dist/utils/__tests__/stripeAccountUtils.test.js +75 -0
  66. package/dist/utils/__tests__/stripeAccountUtils.test.js.map +1 -0
  67. package/dist/utils/blog/__tests__/getRelatedBlogPosts.test.d.ts +2 -0
  68. package/dist/utils/blog/__tests__/getRelatedBlogPosts.test.d.ts.map +1 -0
  69. package/dist/utils/blog/__tests__/getRelatedBlogPosts.test.js +233 -0
  70. package/dist/utils/blog/__tests__/getRelatedBlogPosts.test.js.map +1 -0
  71. package/dist/utils/blog/blogCommentDbUtils.d.ts +161 -0
  72. package/dist/utils/blog/blogCommentDbUtils.d.ts.map +1 -0
  73. package/dist/utils/blog/blogCommentDbUtils.js +111 -0
  74. package/dist/utils/blog/blogCommentDbUtils.js.map +1 -0
  75. package/dist/utils/blog/blogDbUtils.d.ts +10 -0
  76. package/dist/utils/blog/blogDbUtils.d.ts.map +1 -1
  77. package/dist/utils/blog/blogDbUtils.js +71 -0
  78. package/dist/utils/blog/blogDbUtils.js.map +1 -1
  79. package/dist/utils/urlUtils.d.ts +10 -0
  80. package/dist/utils/urlUtils.d.ts.map +1 -1
  81. package/dist/utils/urlUtils.js +24 -5
  82. package/dist/utils/urlUtils.js.map +1 -1
  83. package/dist/utils/userUtils.d.ts +32 -0
  84. package/dist/utils/userUtils.d.ts.map +1 -1
  85. package/dist/utils/userUtils.js +27 -0
  86. package/dist/utils/userUtils.js.map +1 -1
  87. package/package.json +1 -1
  88. package/prisma/schema.prisma +951 -845
  89. package/src/__tests__/agePolicy.test.ts +257 -0
  90. package/src/__tests__/getDisplayGender.test.ts +50 -0
  91. package/src/__tests__/statusEnums.test.ts +36 -0
  92. package/src/definitions.ts +138 -1
  93. package/src/extendedSchemas.ts +41 -2
  94. package/src/index.ts +1 -0
  95. package/src/legalTemplates.ts +1 -1
  96. package/src/utils/__tests__/badgeUtils.test.ts +191 -0
  97. package/src/utils/__tests__/bashPointsPaymentUtils.test.ts +153 -0
  98. package/src/utils/__tests__/contentFilterUtils.test.ts +547 -0
  99. package/src/utils/__tests__/displayDateUtils.test.ts +113 -0
  100. package/src/utils/__tests__/getBackendHost.test.ts +128 -0
  101. package/src/utils/__tests__/getFrontendHost.test.ts +232 -0
  102. package/src/utils/__tests__/promoCodesUtils.test.ts +237 -0
  103. package/src/utils/__tests__/qrCodeUtils.test.ts +143 -0
  104. package/src/utils/__tests__/stripeAccountUtils.test.ts +93 -0
  105. package/src/utils/blog/__tests__/getRelatedBlogPosts.test.ts +271 -0
  106. package/src/utils/blog/blogCommentDbUtils.ts +161 -0
  107. package/src/utils/blog/blogDbUtils.ts +95 -1
  108. package/src/utils/urlUtils.ts +27 -5
  109. package/src/utils/userUtils.ts +40 -0
@@ -345,4 +345,98 @@ export async function getRecentBlogPosts(
345
345
  });
346
346
 
347
347
  return posts as BlogPostExt[];
348
- }
348
+ }
349
+
350
+ export interface RelatedBlogPost {
351
+ id: string;
352
+ slug: string | null;
353
+ title: string;
354
+ excerpt: string | null;
355
+ coverImage: string | null;
356
+ readTime: number | null;
357
+ publishedAt: Date | null;
358
+ }
359
+
360
+ // Score candidate posts by (1) tag overlap with the source post, (2) shared
361
+ // category, then (3) recency. Drives the "Related posts" module that internal-
362
+ // links blog detail pages -- a major SEO lever per the backlink/SEO plan.
363
+ // Falls back to recent same-category posts when the source has no tags.
364
+ export async function getRelatedBlogPosts(
365
+ prisma: PrismaClient,
366
+ postId: string,
367
+ limit: number = 4
368
+ ): Promise<RelatedBlogPost[]> {
369
+ const source = await prisma.blogPost.findUnique({
370
+ where: { id: postId },
371
+ select: {
372
+ id: true,
373
+ categoryId: true,
374
+ tags: { select: { id: true } },
375
+ },
376
+ });
377
+ if (!source) {
378
+ return [];
379
+ }
380
+ const tagIds = source.tags.map((t) => t.id);
381
+
382
+ const orClauses: Prisma.BlogPostWhereInput[] = [];
383
+ if (tagIds.length > 0) {
384
+ orClauses.push({ tags: { some: { id: { in: tagIds } } } });
385
+ }
386
+ if (source.categoryId) {
387
+ orClauses.push({ categoryId: source.categoryId });
388
+ }
389
+
390
+ const where: Prisma.BlogPostWhereInput = {
391
+ id: { not: postId },
392
+ status: BlogStatus.Published,
393
+ publishedAt: { not: null },
394
+ ...(orClauses.length > 0 ? { OR: orClauses } : {}),
395
+ };
396
+
397
+ const candidates = await prisma.blogPost.findMany({
398
+ where,
399
+ select: {
400
+ id: true,
401
+ slug: true,
402
+ title: true,
403
+ excerpt: true,
404
+ coverImage: true,
405
+ readTime: true,
406
+ publishedAt: true,
407
+ categoryId: true,
408
+ tags: { select: { id: true } },
409
+ },
410
+ orderBy: { publishedAt: 'desc' },
411
+ take: limit * 4,
412
+ });
413
+
414
+ const scored = candidates.map((c) => {
415
+ const sharedTagCount = c.tags.filter((t) => tagIds.includes(t.id)).length;
416
+ const sharedCategory =
417
+ source.categoryId && c.categoryId === source.categoryId ? 1 : 0;
418
+ const recency = c.publishedAt ? c.publishedAt.getTime() : 0;
419
+ return { c, sharedTagCount, sharedCategory, recency };
420
+ });
421
+
422
+ scored.sort((a, b) => {
423
+ if (b.sharedTagCount !== a.sharedTagCount) {
424
+ return b.sharedTagCount - a.sharedTagCount;
425
+ }
426
+ if (b.sharedCategory !== a.sharedCategory) {
427
+ return b.sharedCategory - a.sharedCategory;
428
+ }
429
+ return b.recency - a.recency;
430
+ });
431
+
432
+ return scored.slice(0, limit).map(({ c }) => ({
433
+ id: c.id,
434
+ slug: c.slug,
435
+ title: c.title,
436
+ excerpt: c.excerpt,
437
+ coverImage: c.coverImage,
438
+ readTime: c.readTime,
439
+ publishedAt: c.publishedAt,
440
+ }));
441
+ }
442
+
@@ -1,4 +1,3 @@
1
- import { isProduction } from "./apiUtils.js";
2
1
  import { BASH_DETAIL_URL, STRIPE_CHECKOUT_TIMEOUT_MS, STRIPE_REDIRECT_TIMEOUT_MS } from "../definitions.js";
3
2
  import { generateBashDetailUrl } from "./slugUtils.js";
4
3
 
@@ -17,11 +16,34 @@ function isCapacitorNativePlatform(): boolean {
17
16
  return cap?.isNativePlatform?.() === true;
18
17
  }
19
18
 
19
+ /** True when the current page is being served from a developer's local machine (not ngrok, not deployed). */
20
+ function isLocalDevHostname(hostname: string): boolean {
21
+ return (
22
+ hostname === "localhost" ||
23
+ hostname === "127.0.0.1" ||
24
+ hostname.startsWith("192.168.")
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Canonical frontend origin for share links, embeds, QR codes, etc.
30
+ *
31
+ * Browser-hostname-driven (NOT env-var driven) so the answer is correct in any
32
+ * deployed environment — including ones where `process.env.ENVIRONMENT` /
33
+ * `BASH_ENV` aren't inlined into the bundle (Vite only inlines REACT_APP_* /
34
+ * VITE_* / NODE_ENV / PUBLIC_URL). Previously this depended on `isProduction()`
35
+ * and produced URLs like `https://bash.community:3000/bash/...` in production,
36
+ * which broke Facebook/iMessage previews because `:3000` isn't publicly reachable.
37
+ */
20
38
  export function getFrontendHost(): string {
21
- const host = isProduction()
22
- ? `${window.location.protocol}//${window.location.host}`
23
- : `${window.location.protocol}//${window.location.hostname}:3000`;
24
- return host;
39
+ if (typeof window === "undefined") {
40
+ return "";
41
+ }
42
+ const { protocol, hostname, host, port } = window.location;
43
+ if (isLocalDevHostname(hostname)) {
44
+ return `${protocol}//${hostname}:${port || "3000"}`;
45
+ }
46
+ return `${protocol}//${host}`;
25
47
  }
26
48
 
27
49
  export function getBackendHost(): string {
@@ -1 +1,41 @@
1
+ import { Gender } from "@prisma/client";
2
+
1
3
  export const MAX_USER_NAME_CHANGES = 3 as const;
4
+
5
+ /**
6
+ * Minimum shape needed by `getDisplayGender` — accepts any object that has the
7
+ * two relevant columns (e.g. `User`, `UserExt`, `PublicUser`, or a partial
8
+ * select). The free-text `genderIdentity` is the source of truth for *display*
9
+ * because it is user-self-described and may not map to a `Gender` enum value
10
+ * (e.g. "Non-binary", "Genderfluid").
11
+ */
12
+ export interface GenderDisplayUser {
13
+ gender?: Gender | null;
14
+ genderIdentity?: string | null;
15
+ }
16
+
17
+ /**
18
+ * Resolve the gender label that should be **displayed in the UI** (profile
19
+ * header, badges, organizer cards, etc.) for a given user.
20
+ *
21
+ * Precedence:
22
+ * 1. `genderIdentity` (free-text self-description) when present and
23
+ * non-blank — wins over the enum so the user sees their own words.
24
+ * 2. `gender` (enum) when present.
25
+ * 3. `null` when neither is set.
26
+ *
27
+ * IMPORTANT: This helper is for **display only**. Analytics, demographics
28
+ * cohorts, target-audience matching, and anything that needs a stable
29
+ * categorical value should keep reading `user.gender` directly. Adding a
30
+ * comment at those sites (e.g. "intentional: analytics uses raw enum") makes
31
+ * the divergence explicit.
32
+ *
33
+ * @returns the user-facing string (free-text or enum value), or `null` if no
34
+ * gender info has been provided.
35
+ */
36
+ export function getDisplayGender(user: GenderDisplayUser): string | null {
37
+ const trimmedIdentity = user.genderIdentity?.trim();
38
+ if (trimmedIdentity) return trimmedIdentity;
39
+ if (user.gender) return user.gender;
40
+ return null;
41
+ }