@bash-app/bash-common 30.36.0 → 30.38.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 +1 -1
- package/prisma/schema.prisma +113 -7
- package/src/definitions.ts +214 -0
- package/src/extendedSchemas.ts +12 -7
- package/src/utils/badgeUtils.ts +69 -0
- package/src/utils/reviewUtils.ts +79 -0
package/package.json
CHANGED
package/prisma/schema.prisma
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
1058
|
-
boughtTicket Boolean? @default(false)
|
|
1059
|
-
noPay Boolean? @default(false)
|
|
1060
|
-
supportedEvent Boolean? @default(false)
|
|
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,21 @@ model User {
|
|
|
1073
1075
|
unblocksReceived UnblockedUserHistory[] @relation("UserUnblocksReceived")
|
|
1074
1076
|
|
|
1075
1077
|
preferences UserPreferences?
|
|
1078
|
+
userStats UserStats?
|
|
1079
|
+
|
|
1080
|
+
// Add fields for user suspension
|
|
1081
|
+
suspendedUntil DateTime?
|
|
1082
|
+
suspendedById String?
|
|
1083
|
+
suspendedBy User? @relation("SuspendedUsers", fields: [suspendedById], references: [id], onDelete: SetNull)
|
|
1084
|
+
suspendedUsers User[] @relation("SuspendedUsers")
|
|
1085
|
+
|
|
1086
|
+
// Add relations for reports and demerits
|
|
1087
|
+
reportsMade UserReport[] @relation("ReportsMade")
|
|
1088
|
+
reportsReceived UserReport[] @relation("ReportsReceived")
|
|
1089
|
+
reportsReviewed UserReport[] @relation("ReportsReviewed")
|
|
1090
|
+
demerits Demerit[] @relation("UserDemerits")
|
|
1091
|
+
issuedDemerits Demerit[] @relation("IssuedDemerits")
|
|
1092
|
+
expungedDemerits Demerit[] @relation("ExpungedDemerits")
|
|
1076
1093
|
}
|
|
1077
1094
|
|
|
1078
1095
|
model UserPreferences {
|
|
@@ -2140,3 +2157,92 @@ enum EntityType {
|
|
|
2140
2157
|
ORGANIZATION
|
|
2141
2158
|
BASH_EVENT // For BashEvents
|
|
2142
2159
|
}
|
|
2160
|
+
|
|
2161
|
+
// Adding new models for the demerit system
|
|
2162
|
+
|
|
2163
|
+
model UserReport {
|
|
2164
|
+
id String @id @default(cuid())
|
|
2165
|
+
reporterId String // Host/reporter user ID
|
|
2166
|
+
reporter User @relation("ReportsMade", fields: [reporterId], references: [id], onDelete: Cascade)
|
|
2167
|
+
reportedId String // Reported user ID
|
|
2168
|
+
reported User @relation("ReportsReceived", fields: [reportedId], references: [id], onDelete: Cascade)
|
|
2169
|
+
bashEventId String?
|
|
2170
|
+
bashEvent BashEvent? @relation(fields: [bashEventId], references: [id], onDelete: SetNull)
|
|
2171
|
+
reason String
|
|
2172
|
+
details String
|
|
2173
|
+
createdAt DateTime @default(now())
|
|
2174
|
+
status ReportStatus @default(Pending)
|
|
2175
|
+
reviewerId String? // Super user who reviewed the report
|
|
2176
|
+
reviewer User? @relation("ReportsReviewed", fields: [reviewerId], references: [id], onDelete: SetNull)
|
|
2177
|
+
reviewedAt DateTime?
|
|
2178
|
+
reviewNotes String?
|
|
2179
|
+
demerits Demerit[]
|
|
2180
|
+
|
|
2181
|
+
@@index([reporterId])
|
|
2182
|
+
@@index([reportedId])
|
|
2183
|
+
@@index([bashEventId])
|
|
2184
|
+
@@index([status])
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
model Demerit {
|
|
2188
|
+
id String @id @default(cuid())
|
|
2189
|
+
userId String
|
|
2190
|
+
user User @relation("UserDemerits", fields: [userId], references: [id], onDelete: Cascade)
|
|
2191
|
+
reportId String?
|
|
2192
|
+
report UserReport? @relation(fields: [reportId], references: [id], onDelete: SetNull)
|
|
2193
|
+
issuerId String // Super user who issued the demerit
|
|
2194
|
+
issuer User @relation("IssuedDemerits", fields: [issuerId], references: [id], onDelete: Cascade)
|
|
2195
|
+
reason String
|
|
2196
|
+
points Int @default(1)
|
|
2197
|
+
issuedAt DateTime @default(now())
|
|
2198
|
+
expiresAt DateTime?
|
|
2199
|
+
isExpunged Boolean @default(false)
|
|
2200
|
+
expungedAt DateTime?
|
|
2201
|
+
expungedById String?
|
|
2202
|
+
expungedBy User? @relation("ExpungedDemerits", fields: [expungedById], references: [id], onDelete: SetNull)
|
|
2203
|
+
|
|
2204
|
+
@@index([userId])
|
|
2205
|
+
@@index([issuerId])
|
|
2206
|
+
@@index([reportId])
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
enum ReportStatus {
|
|
2210
|
+
Pending
|
|
2211
|
+
Approved
|
|
2212
|
+
Rejected
|
|
2213
|
+
Resolved
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// Add UserStats model for tracking statistics and rankings
|
|
2217
|
+
model UserStats {
|
|
2218
|
+
id String @id @default(cuid())
|
|
2219
|
+
userId String @unique
|
|
2220
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
2221
|
+
|
|
2222
|
+
// Event Statistics
|
|
2223
|
+
bashesHostedCount Int @default(0)
|
|
2224
|
+
bashesAttendedCount Int @default(0)
|
|
2225
|
+
totalAttendeesCount Int @default(0)
|
|
2226
|
+
totalEarnings Float @default(0)
|
|
2227
|
+
servicesBookedCount Int @default(0)
|
|
2228
|
+
followersCount Int @default(0)
|
|
2229
|
+
|
|
2230
|
+
// Rankings
|
|
2231
|
+
hostRank Int? // Rank based on number of events hosted
|
|
2232
|
+
attendeeRank Int? // Rank based on number of events attended
|
|
2233
|
+
audienceRank Int? // Rank based on total attendees
|
|
2234
|
+
earningsRank Int? // Rank based on total earnings
|
|
2235
|
+
serviceRank Int? // Rank based on services booked
|
|
2236
|
+
followerRank Int? // Rank based on number of followers
|
|
2237
|
+
|
|
2238
|
+
// Tracking for when stats were last calculated
|
|
2239
|
+
lastUpdated DateTime @default(now())
|
|
2240
|
+
lastRankingUpdate DateTime @default(now())
|
|
2241
|
+
|
|
2242
|
+
@@index([bashesHostedCount])
|
|
2243
|
+
@@index([bashesAttendedCount])
|
|
2244
|
+
@@index([totalAttendeesCount])
|
|
2245
|
+
@@index([totalEarnings])
|
|
2246
|
+
@@index([servicesBookedCount])
|
|
2247
|
+
@@index([followersCount])
|
|
2248
|
+
}
|
package/src/definitions.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/extendedSchemas.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Checkout,
|
|
10
10
|
Contact,
|
|
11
11
|
Coordinates,
|
|
12
|
+
Demerit,
|
|
12
13
|
EntertainmentService,
|
|
13
14
|
EventService,
|
|
14
15
|
EventTask,
|
|
@@ -41,26 +42,25 @@ import {
|
|
|
41
42
|
SocialMediaPlatform,
|
|
42
43
|
SocialMediaProfile,
|
|
43
44
|
Sponsor,
|
|
45
|
+
SponsoredEvent,
|
|
44
46
|
StripeAccount,
|
|
45
47
|
TargetAudience,
|
|
46
48
|
Ticket,
|
|
49
|
+
TicketMetadata,
|
|
47
50
|
TicketTier,
|
|
51
|
+
TicketTransfer,
|
|
48
52
|
User,
|
|
53
|
+
UserPreferences,
|
|
54
|
+
UserStats,
|
|
49
55
|
UserSubscription,
|
|
50
56
|
Vendor,
|
|
51
57
|
Venue,
|
|
52
58
|
VolunteerService,
|
|
53
|
-
TicketTransfer,
|
|
54
|
-
TicketMetadata,
|
|
55
|
-
SponsoredEvent,
|
|
56
|
-
UserPreferences,
|
|
57
59
|
} from "@prisma/client";
|
|
58
60
|
import { SERVICE_LINK_DATA_TO_INCLUDE } from "./definitions";
|
|
59
|
-
import { serviceKeysArray } from "./utils/service/serviceUtils";
|
|
60
61
|
import {
|
|
61
|
-
createAllTrueObject,
|
|
62
62
|
RemoveCommonProperties,
|
|
63
|
-
UnionFromArray
|
|
63
|
+
UnionFromArray
|
|
64
64
|
} from "./utils/typeUtils";
|
|
65
65
|
|
|
66
66
|
//------------------------------------------------------user subscriptions------------------------------------------------------
|
|
@@ -114,6 +114,9 @@ export const FRONT_END_USER_DATA_TO_SELECT = {
|
|
|
114
114
|
boughtTicket: true,
|
|
115
115
|
noPay: true,
|
|
116
116
|
supportedEvent: true,
|
|
117
|
+
aboutMe: true,
|
|
118
|
+
levelBadge: true,
|
|
119
|
+
temporaryBadges: true,
|
|
117
120
|
} satisfies Prisma.UserSelect;
|
|
118
121
|
|
|
119
122
|
export const PRIVATE_USER_ACCOUNT_TO_SELECT = {
|
|
@@ -782,6 +785,8 @@ export const CONTACT_DATA_TO_INCLUDE = {
|
|
|
782
785
|
export interface UserExt extends User {
|
|
783
786
|
services?: Service[] | null;
|
|
784
787
|
preferences?: UserPreferences | null;
|
|
788
|
+
stats?: UserStats | null;
|
|
789
|
+
demerits?: Demerit[] | null;
|
|
785
790
|
|
|
786
791
|
// Do not include in fetch as there could be thousands of these
|
|
787
792
|
userSubscription?: UserSubscriptionExt | null;
|
|
@@ -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
|
+
};
|