@bash-app/bash-common 30.36.0 → 30.37.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash-app/bash-common",
3
- "version": "30.36.0",
3
+ "version": "30.37.0",
4
4
  "description": "Common data and scripts to use on the frontend and backend",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -314,6 +314,7 @@ model BashEvent {
314
314
  userPromoCodeRedemption UserPromoCodeRedemption[]
315
315
  averageRating Float? @default(0)
316
316
  serviceBookings ServiceBooking[] // Add this field to create the reverse relation
317
+ userReport UserReport[]
317
318
  }
318
319
 
319
320
  model Coordinates {
@@ -984,7 +985,7 @@ model User {
984
985
  stripeCustomerId String? @unique
985
986
  stripeAccountId String? @unique
986
987
  isSuperUser Boolean @default(false)
987
- // isSuspended Boolean @default(false)
988
+ isSuspended Boolean @default(false)
988
989
  intent UserIntent?
989
990
  googleCalendarAccess String?
990
991
  givenName String?
@@ -997,6 +998,9 @@ model User {
997
998
  gender Gender?
998
999
  sex Sex?
999
1000
  roles UserRole[] @default([User])
1001
+ aboutMe String?
1002
+ levelBadge String?
1003
+ temporaryBadges String[] @default([])
1000
1004
  ownedServices Service[] @relation("OwnedService")
1001
1005
  createdServices Service[] @relation("CreatedService")
1002
1006
  createdEvents BashEvent[] @relation("CreatedEvent")
@@ -1054,13 +1058,11 @@ model User {
1054
1058
  volunteerService VolunteerService[]
1055
1059
  stripeAccounts StripeAccount[]
1056
1060
  userPromoCodeRedemption UserPromoCodeRedemption[]
1057
- accepted Boolean? @default(false) // Tracks if the user accepted the invitation
1058
- boughtTicket Boolean? @default(false) // Tracks if the user bought a ticket
1059
- noPay Boolean? @default(false) // Tracks if the user is marked as "noPay"
1060
- supportedEvent Boolean? @default(false) // Tracks if the user supported the event
1061
+ accepted Boolean? @default(false)
1062
+ boughtTicket Boolean? @default(false)
1063
+ noPay Boolean? @default(false)
1064
+ supportedEvent Boolean? @default(false)
1061
1065
 
1062
- // primaryStripeAccountDBId String? @unique
1063
- // primaryStripeAccountDB StripeAccount? @relation("PrimaryStripeAccount", fields: [primaryStripeAccountDBId], references: [id], onDelete: Restrict, onUpdate: Restrict)
1064
1066
  serviceBookingForCreator ServiceBooking[] @relation("BookingCreator")
1065
1067
  serviceBookingForUser ServiceBooking[] @relation("BookingForUser")
1066
1068
  serviceBookingCheckout ServiceBookingCheckout[]
@@ -1073,6 +1075,20 @@ model User {
1073
1075
  unblocksReceived UnblockedUserHistory[] @relation("UserUnblocksReceived")
1074
1076
 
1075
1077
  preferences UserPreferences?
1078
+
1079
+ // Add fields for user suspension
1080
+ suspendedUntil DateTime?
1081
+ suspendedById String?
1082
+ suspendedBy User? @relation("SuspendedUsers", fields: [suspendedById], references: [id], onDelete: SetNull)
1083
+ suspendedUsers User[] @relation("SuspendedUsers")
1084
+
1085
+ // Add relations for reports and demerits
1086
+ reportsMade UserReport[] @relation("ReportsMade")
1087
+ reportsReceived UserReport[] @relation("ReportsReceived")
1088
+ reportsReviewed UserReport[] @relation("ReportsReviewed")
1089
+ demerits Demerit[] @relation("UserDemerits")
1090
+ issuedDemerits Demerit[] @relation("IssuedDemerits")
1091
+ expungedDemerits Demerit[] @relation("ExpungedDemerits")
1076
1092
  }
1077
1093
 
1078
1094
  model UserPreferences {
@@ -2140,3 +2156,58 @@ enum EntityType {
2140
2156
  ORGANIZATION
2141
2157
  BASH_EVENT // For BashEvents
2142
2158
  }
2159
+
2160
+ // Adding new models for the demerit system
2161
+
2162
+ model UserReport {
2163
+ id String @id @default(cuid())
2164
+ reporterId String // Host/reporter user ID
2165
+ reporter User @relation("ReportsMade", fields: [reporterId], references: [id], onDelete: Cascade)
2166
+ reportedId String // Reported user ID
2167
+ reported User @relation("ReportsReceived", fields: [reportedId], references: [id], onDelete: Cascade)
2168
+ bashEventId String?
2169
+ bashEvent BashEvent? @relation(fields: [bashEventId], references: [id], onDelete: SetNull)
2170
+ reason String
2171
+ details String
2172
+ createdAt DateTime @default(now())
2173
+ status ReportStatus @default(Pending)
2174
+ reviewerId String? // Super user who reviewed the report
2175
+ reviewer User? @relation("ReportsReviewed", fields: [reviewerId], references: [id], onDelete: SetNull)
2176
+ reviewedAt DateTime?
2177
+ reviewNotes String?
2178
+ demerits Demerit[]
2179
+
2180
+ @@index([reporterId])
2181
+ @@index([reportedId])
2182
+ @@index([bashEventId])
2183
+ @@index([status])
2184
+ }
2185
+
2186
+ model Demerit {
2187
+ id String @id @default(cuid())
2188
+ userId String
2189
+ user User @relation("UserDemerits", fields: [userId], references: [id], onDelete: Cascade)
2190
+ reportId String?
2191
+ report UserReport? @relation(fields: [reportId], references: [id], onDelete: SetNull)
2192
+ issuerId String // Super user who issued the demerit
2193
+ issuer User @relation("IssuedDemerits", fields: [issuerId], references: [id], onDelete: Cascade)
2194
+ reason String
2195
+ points Int @default(1)
2196
+ issuedAt DateTime @default(now())
2197
+ expiresAt DateTime?
2198
+ isExpunged Boolean @default(false)
2199
+ expungedAt DateTime?
2200
+ expungedById String?
2201
+ expungedBy User? @relation("ExpungedDemerits", fields: [expungedById], references: [id], onDelete: SetNull)
2202
+
2203
+ @@index([userId])
2204
+ @@index([issuerId])
2205
+ @@index([reportId])
2206
+ }
2207
+
2208
+ enum ReportStatus {
2209
+ Pending
2210
+ Approved
2211
+ Rejected
2212
+ Resolved
2213
+ }
@@ -946,3 +946,217 @@ export interface SocialMediaProfile {
946
946
  url: string;
947
947
  userId: string;
948
948
  }
949
+
950
+ // Badge Types
951
+ export enum BadgeLevel {
952
+ Novice = "Novice",
953
+ Beginner = "Beginner",
954
+ Regular = "Regular",
955
+ Enthusiast = "Enthusiast",
956
+ Aficionado = "Aficionado",
957
+ Connoisseur = "Connoisseur",
958
+ Maverick = "Maverick",
959
+ Master = "Master",
960
+ Elite = "Elite",
961
+ Legend = "Legend"
962
+ }
963
+
964
+ export enum BadgeCategory {
965
+ Hosting = "Hosting",
966
+ Attendance = "Attendance",
967
+ Community = "Community",
968
+ Achievement = "Achievement",
969
+ Special = "Special"
970
+ }
971
+
972
+ // Type definitions for badges and reviews
973
+ export interface Badge {
974
+ id: string;
975
+ name: string;
976
+ description: string;
977
+ imageUrl: string;
978
+ category: BadgeCategory;
979
+ earnedOn?: string;
980
+ isActive: boolean;
981
+ }
982
+
983
+ export interface LevelBadge extends Badge {
984
+ level: BadgeLevel;
985
+ requiredEvents: number;
986
+ requiredRating?: number;
987
+ }
988
+
989
+ export interface TemporaryBadge extends Badge {
990
+ expiresOn?: string;
991
+ }
992
+
993
+ export interface UserLevelBadge {
994
+ badgeId: string;
995
+ earnedOn: string;
996
+ }
997
+
998
+ export interface UserTemporaryBadge {
999
+ badgeId: string;
1000
+ earnedOn: string;
1001
+ expiresOn?: string;
1002
+ }
1003
+
1004
+ export interface Review {
1005
+ id: string;
1006
+ rating: number; // 1-5 stars
1007
+ creatorId: string;
1008
+ bashEventId: string;
1009
+ createdAt: string;
1010
+ updatedAt: string;
1011
+ comments?: ReviewComment[];
1012
+ }
1013
+
1014
+ export interface ReviewComment {
1015
+ id: string;
1016
+ creatorId: string;
1017
+ content: string;
1018
+ reviewId: string;
1019
+ createdAt?: string;
1020
+ }
1021
+
1022
+ export interface ReviewWithEvent extends Review {
1023
+ bashEvent: {
1024
+ id: string;
1025
+ title: string;
1026
+ startDateTime: string;
1027
+ };
1028
+ }
1029
+
1030
+ export interface ReviewWithUser extends Review {
1031
+ creator: {
1032
+ id: string;
1033
+ givenName?: string;
1034
+ familyName?: string;
1035
+ image?: string;
1036
+ email: string;
1037
+ };
1038
+ }
1039
+
1040
+ export interface ReviewWithUserAndEvent extends ReviewWithUser, ReviewWithEvent {}
1041
+
1042
+ export interface NewReview {
1043
+ rating: number;
1044
+ bashEventId: string;
1045
+ commentContent?: string;
1046
+ }
1047
+
1048
+ export interface ReviewStats {
1049
+ averageRating: number;
1050
+ totalRatings: number;
1051
+ ratings: {
1052
+ 1: number;
1053
+ 2: number;
1054
+ 3: number;
1055
+ 4: number;
1056
+ 5: number;
1057
+ };
1058
+ }
1059
+
1060
+ // Default badge collections
1061
+ export const DEFAULT_LEVEL_BADGES: Record<string, LevelBadge> = {
1062
+ novice: {
1063
+ id: "novice",
1064
+ name: "Novice Basher",
1065
+ description: "Just getting started with Bash. Has hosted 1-2 events.",
1066
+ imageUrl: "/badges/novice.png",
1067
+ category: BadgeCategory.Hosting,
1068
+ level: BadgeLevel.Novice,
1069
+ requiredEvents: 1,
1070
+ isActive: true
1071
+ },
1072
+ regular: {
1073
+ id: "regular",
1074
+ name: "Regular Basher",
1075
+ description: "A consistent Bash user who has hosted 5+ events with good ratings.",
1076
+ imageUrl: "/badges/regular.png",
1077
+ category: BadgeCategory.Hosting,
1078
+ level: BadgeLevel.Regular,
1079
+ requiredEvents: 5,
1080
+ requiredRating: 4.0,
1081
+ isActive: true
1082
+ },
1083
+ maverick: {
1084
+ id: "maverick",
1085
+ name: "Maverick Basher",
1086
+ description: "A seasoned Bash host with 15+ successful events and excellent ratings.",
1087
+ imageUrl: "/badges/maverick.png",
1088
+ category: BadgeCategory.Hosting,
1089
+ level: BadgeLevel.Maverick,
1090
+ requiredEvents: 15,
1091
+ requiredRating: 4.5,
1092
+ isActive: true
1093
+ },
1094
+ legend: {
1095
+ id: "legend",
1096
+ name: "Bash Legend",
1097
+ description: "An elite Bash host with 30+ events and exceptional community standing.",
1098
+ imageUrl: "/badges/legend.png",
1099
+ category: BadgeCategory.Hosting,
1100
+ level: BadgeLevel.Legend,
1101
+ requiredEvents: 30,
1102
+ requiredRating: 4.8,
1103
+ isActive: true
1104
+ }
1105
+ };
1106
+
1107
+ export const DEFAULT_TEMPORARY_BADGES: Record<string, TemporaryBadge> = {
1108
+ trendingStar: {
1109
+ id: "trendingStar",
1110
+ name: "Trending Star",
1111
+ description: "Your event was trending in the last 30 days!",
1112
+ imageUrl: "/badges/trending.png",
1113
+ category: BadgeCategory.Achievement,
1114
+ isActive: true,
1115
+ expiresOn: undefined // Set dynamically when awarded
1116
+ },
1117
+ topHost: {
1118
+ id: "topHost",
1119
+ name: "Top Host",
1120
+ description: "One of the top-rated hosts this month.",
1121
+ imageUrl: "/badges/top-host.png",
1122
+ category: BadgeCategory.Hosting,
1123
+ isActive: true,
1124
+ expiresOn: undefined // Set dynamically when awarded
1125
+ },
1126
+ earlyAdopter: {
1127
+ id: "earlyAdopter",
1128
+ name: "Early Adopter",
1129
+ description: "Joined the platform early and helped shape the community.",
1130
+ imageUrl: "/badges/early-adopter.png",
1131
+ category: BadgeCategory.Special,
1132
+ isActive: true
1133
+ },
1134
+ socialButterfly: {
1135
+ id: "socialButterfly",
1136
+ name: "Social Butterfly",
1137
+ description: "Attended 10+ events in the last 30 days.",
1138
+ imageUrl: "/badges/butterfly.png",
1139
+ category: BadgeCategory.Attendance,
1140
+ isActive: true,
1141
+ expiresOn: undefined // Set dynamically when awarded
1142
+ }
1143
+ };
1144
+
1145
+ // Badge API endpoints
1146
+ export const BADGE_API_ENDPOINTS = {
1147
+ GET_USER_BADGES: (userId: string) => `/user/${userId}/badges`,
1148
+ UPDATE_USER_BADGES: (userId: string) => `/user/${userId}/badges`,
1149
+ GET_HOST_RATING: (userId: string) => `/user/${userId}/host-rating`,
1150
+ GET_USER_TRENDING_EVENTS: (userId: string) => `/user/${userId}/trending-events`,
1151
+ GET_USER_TOP_HOST_STATUS: (userId: string) => `/user/${userId}/is-top-host`,
1152
+ CALCULATE_BADGES: (userId: string) => `/user/${userId}/calculate-badges`,
1153
+ };
1154
+
1155
+ // Review API endpoints
1156
+ export const REVIEW_API_ENDPOINTS = {
1157
+ GET_HOST_REVIEWS: (userId: string) => `/user/${userId}/host-reviews`,
1158
+ GET_REVIEW_STATS: (userId: string) => `/user/${userId}/review-stats`,
1159
+ ELIGIBLE_FOR_REVIEW: (userId: string, hostId: string) =>
1160
+ `/user/${userId}/eligible-for-review/${hostId}`,
1161
+ CREATE_REVIEW: () => `/reviews`,
1162
+ };
@@ -41,26 +41,24 @@ import {
41
41
  SocialMediaPlatform,
42
42
  SocialMediaProfile,
43
43
  Sponsor,
44
+ SponsoredEvent,
44
45
  StripeAccount,
45
46
  TargetAudience,
46
47
  Ticket,
48
+ TicketMetadata,
47
49
  TicketTier,
50
+ TicketTransfer,
48
51
  User,
52
+ UserPreferences,
49
53
  UserSubscription,
50
54
  Vendor,
51
55
  Venue,
52
56
  VolunteerService,
53
- TicketTransfer,
54
- TicketMetadata,
55
- SponsoredEvent,
56
- UserPreferences,
57
57
  } from "@prisma/client";
58
58
  import { SERVICE_LINK_DATA_TO_INCLUDE } from "./definitions";
59
- import { serviceKeysArray } from "./utils/service/serviceUtils";
60
59
  import {
61
- createAllTrueObject,
62
60
  RemoveCommonProperties,
63
- UnionFromArray,
61
+ UnionFromArray
64
62
  } from "./utils/typeUtils";
65
63
 
66
64
  //------------------------------------------------------user subscriptions------------------------------------------------------
@@ -114,6 +112,9 @@ export const FRONT_END_USER_DATA_TO_SELECT = {
114
112
  boughtTicket: true,
115
113
  noPay: true,
116
114
  supportedEvent: true,
115
+ aboutMe: true,
116
+ levelBadge: true,
117
+ temporaryBadges: true,
117
118
  } satisfies Prisma.UserSelect;
118
119
 
119
120
  export const PRIVATE_USER_ACCOUNT_TO_SELECT = {
@@ -0,0 +1,69 @@
1
+ import { DEFAULT_LEVEL_BADGES, DEFAULT_TEMPORARY_BADGES, LevelBadge, TemporaryBadge, UserTemporaryBadge } from "../definitions";
2
+
3
+ /**
4
+ * Get the level badge details for a given badge ID
5
+ * @param badgeId The ID of the level badge
6
+ * @returns The level badge details or undefined if not found
7
+ */
8
+ export const getLevelBadgeById = (badgeId: string): LevelBadge | undefined => {
9
+ return DEFAULT_LEVEL_BADGES[badgeId];
10
+ };
11
+
12
+ /**
13
+ * Get the temporary badge details for a given badge ID
14
+ * @param badgeId The ID of the temporary badge
15
+ * @returns The temporary badge details or undefined if not found
16
+ */
17
+ export const getTemporaryBadgeById = (badgeId: string): TemporaryBadge | undefined => {
18
+ return DEFAULT_TEMPORARY_BADGES[badgeId];
19
+ };
20
+
21
+ /**
22
+ * Check if a temporary badge is expired
23
+ * @param badge The temporary badge to check
24
+ * @returns True if the badge is expired, false otherwise
25
+ */
26
+ export const isTemporaryBadgeExpired = (badge: UserTemporaryBadge): boolean => {
27
+ if (!badge.expiresOn) return false;
28
+
29
+ const expiryDate = new Date(badge.expiresOn);
30
+ const currentDate = new Date();
31
+
32
+ return expiryDate < currentDate;
33
+ };
34
+
35
+ /**
36
+ * Filter out expired temporary badges
37
+ * @param badges Array of temporary badges
38
+ * @returns Array of non-expired temporary badges
39
+ */
40
+ export const filterExpiredTemporaryBadges = (badges: UserTemporaryBadge[]): UserTemporaryBadge[] => {
41
+ return badges.filter(badge => !isTemporaryBadgeExpired(badge));
42
+ };
43
+
44
+ /**
45
+ * Determine the appropriate level badge based on event count and rating
46
+ * @param eventCount Number of hosted events
47
+ * @param rating Average host rating
48
+ * @returns The ID of the highest level badge the user qualifies for, or null if none
49
+ */
50
+ export const determineLevelBadge = (eventCount: number, rating: number): string | null => {
51
+ // Sort badge IDs by required events in descending order
52
+ const sortedBadgeIds = Object.keys(DEFAULT_LEVEL_BADGES).sort(
53
+ (a, b) => DEFAULT_LEVEL_BADGES[b].requiredEvents - DEFAULT_LEVEL_BADGES[a].requiredEvents
54
+ );
55
+
56
+ // Find the highest level badge the user qualifies for
57
+ for (const badgeId of sortedBadgeIds) {
58
+ const badge = DEFAULT_LEVEL_BADGES[badgeId];
59
+
60
+ if (
61
+ eventCount >= badge.requiredEvents &&
62
+ (badge.requiredRating === undefined || rating >= badge.requiredRating)
63
+ ) {
64
+ return badgeId;
65
+ }
66
+ }
67
+
68
+ return null;
69
+ };
@@ -0,0 +1,79 @@
1
+ import { Review, ReviewStats } from "../definitions";
2
+
3
+ /**
4
+ * Calculate average rating from an array of reviews
5
+ * @param reviews Array of reviews
6
+ * @returns Average rating (1-5)
7
+ */
8
+ export const calculateAverageRating = (reviews: Review[]): number => {
9
+ if (!reviews.length) return 0;
10
+
11
+ const sum = reviews.reduce((total, review) => total + review.rating, 0);
12
+ return parseFloat((sum / reviews.length).toFixed(1));
13
+ };
14
+
15
+ /**
16
+ * Generate review statistics from an array of reviews
17
+ * @param reviews Array of reviews
18
+ * @returns ReviewStats object with distribution and averages
19
+ */
20
+ export const generateReviewStats = (reviews: Review[]): ReviewStats => {
21
+ const ratings = {
22
+ 1: 0,
23
+ 2: 0,
24
+ 3: 0,
25
+ 4: 0,
26
+ 5: 0
27
+ };
28
+
29
+ // Count each rating level
30
+ reviews.forEach(review => {
31
+ const rating = review.rating;
32
+ if (rating >= 1 && rating <= 5) {
33
+ ratings[rating as keyof typeof ratings]++;
34
+ }
35
+ });
36
+
37
+ return {
38
+ averageRating: calculateAverageRating(reviews),
39
+ totalRatings: reviews.length,
40
+ ratings
41
+ };
42
+ };
43
+
44
+ /**
45
+ * Format rating as a string with star emoji
46
+ * @param rating Numeric rating
47
+ * @returns Formatted string (e.g., "4.5 ★★★★½")
48
+ */
49
+ export const formatRatingWithStars = (rating: number): string => {
50
+ const fullStars = Math.floor(rating);
51
+ const hasHalfStar = rating % 1 >= 0.5;
52
+
53
+ let stars = '★'.repeat(fullStars);
54
+ if (hasHalfStar) stars += '½';
55
+
56
+ return `${rating.toFixed(1)} ${stars}`;
57
+ };
58
+
59
+ /**
60
+ * Check if a user is eligible to review an event
61
+ * @param userId User ID
62
+ * @param eventId Event ID
63
+ * @param alreadyReviewed Array of event IDs user has already reviewed
64
+ * @param eventsAttended Array of event IDs user has attended
65
+ * @returns Boolean indicating if user can review the event
66
+ */
67
+ export const isEligibleToReview = (
68
+ userId: string,
69
+ eventId: string,
70
+ hostId: string,
71
+ alreadyReviewed: string[],
72
+ eventsAttended: string[]
73
+ ): boolean => {
74
+ return (
75
+ userId !== hostId && // User is not the host
76
+ eventsAttended.includes(eventId) && // User attended the event
77
+ !alreadyReviewed.includes(eventId) // User has not already reviewed the event
78
+ );
79
+ };