diff --git a/src/app/games/[id]/components/GameEditForm.tsx b/src/app/games/[id]/components/GameEditForm.tsx index 7d24e8113..709779436 100644 --- a/src/app/games/[id]/components/GameEditForm.tsx +++ b/src/app/games/[id]/components/GameEditForm.tsx @@ -419,6 +419,7 @@ export function GameEditForm(props: Props) { selectedImageUrl={getCurrentImageUrl()} onImageSelect={handleImageSelect} onError={setError} + allowIgdbProvider={isModerator} /> diff --git a/src/app/games/components/GameFilters.tsx b/src/app/games/components/GameFilters.tsx index d726bb488..8a22710fe 100644 --- a/src/app/games/components/GameFilters.tsx +++ b/src/app/games/components/GameFilters.tsx @@ -2,7 +2,7 @@ import { Joystick, Search, Filter, Eye, EyeOff, List } from 'lucide-react' import { type ChangeEvent } from 'react' -import { Input, Autocomplete, ThreeWayToggle, type ThreeWayToggleOption } from '@/components/ui' +import { Input, Autocomplete, SegmentedControl, type SegmentedControlOption } from '@/components/ui' import { api } from '@/lib/api' import { hasRolePermission } from '@/utils/permissions' import { Role } from '@orm' @@ -32,9 +32,9 @@ function GameFilters(props: Props) { const isModerator = hasRolePermission(userQuery.data?.role, Role.MODERATOR) const listingFilterOptions: [ - ThreeWayToggleOption, - ThreeWayToggleOption, - ThreeWayToggleOption, + SegmentedControlOption, + SegmentedControlOption, + SegmentedControlOption, ] = [ { value: 'all', label: 'All', icon: }, { @@ -84,7 +84,7 @@ function GameFilters(props: Props) { {isModerator && props.onListingFilterChange ? ( - { +export interface SegmentedControlOption { value: T label: string icon?: ReactNode } interface Props { - options: [ThreeWayToggleOption, ThreeWayToggleOption, ThreeWayToggleOption] + options: readonly [SegmentedControlOption, ...SegmentedControlOption[]] value: T onChange: (value: T) => void className?: string size?: 'sm' | 'md' | 'lg' } -// TODO: I feel like the english language has a better name for this -export function ThreeWayToggle(props: Props) { +export function SegmentedControl(props: Props) { const size = props.size ?? 'md' return (
- {/* Options */} {props.options.map((option) => ( + +
+ + + {allowIgdbProvider && ( + + )} -
- {selectedService === imageServiceMap.rawg +
+ {selectedService === 'rawg' ? 'Using RAWG.io for game images' - : 'Using TheGamesDB for game images'} + : selectedService === 'tgdb' + ? 'Using TheGamesDB for game images' + : 'Using IGDB for comprehensive game media'}
-
-
- {selectedService === imageServiceMap.rawg - ? 'RAWG.io provides comprehensive game data with screenshots and backgrounds' - : 'TheGamesDB offers high-quality boxart and game media from the community'} +
+ {selectedService === 'rawg' && + 'RAWG.io provides comprehensive game data with screenshots and backgrounds'} + {selectedService === 'tgdb' && + 'TheGamesDB offers high-quality boxart and game media from the community'} + {selectedService === 'igdb' && + 'IGDB provides rich media including covers, artworks, and screenshots with detailed metadata'} +
- {/* Animated Image Selector */}
- - {selectedService === imageServiceMap.rawg ? ( + + {selectedService === 'rawg' ? ( + ) : selectedService === 'igdb' ? ( + + + ) : ( + // Admin table URL parameters export const AdminTableParamsSchema = z.object({ search: z.string().default(''), page: z.number().int().positive().default(1), sortField: z.string().nullable().default(null), - sortDirection: z.enum(['asc', 'desc']).nullable().default(null), // TODO: extract + sortDirection: SortDirectionSchema.nullable().default(null), }) export const JsonValueSchema: z.ZodType = z.lazy(() => diff --git a/src/schemas/cpu.ts b/src/schemas/cpu.ts index 21490f726..683887d10 100644 --- a/src/schemas/cpu.ts +++ b/src/schemas/cpu.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { SortDirection } from '@/schemas/soc' +import { SortDirectionSchema } from '@/schemas/common' export const CpuSortField = z.enum(['brand', 'modelName', 'pcListings']) @@ -11,7 +11,7 @@ export const GetCpusSchema = z offset: z.number().default(0), page: z.number().optional(), sortField: CpuSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), }) .optional() @@ -40,7 +40,6 @@ export const UpdateCpuSchema = z.object({ export const DeleteCpuSchema = z.object({ id: z.string().uuid() }) -// Type exports for repository use export type GetCpusInput = z.input export type GetCpuOptionsInput = z.input export type CreateCpuInput = z.infer diff --git a/src/schemas/device.ts b/src/schemas/device.ts index ffca2eb24..de2b8b651 100644 --- a/src/schemas/device.ts +++ b/src/schemas/device.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { HOME_PAGE_LIMITS } from '@/data/constants' -import { SortDirection } from '@/schemas/soc' +import { SortDirectionSchema } from '@/schemas/common' export const DeviceSortField = z.enum(['brand', 'modelName', 'soc', 'listings']) @@ -13,7 +13,7 @@ export const GetDevicesSchema = z offset: z.number().default(0), page: z.number().optional(), sortField: DeviceSortField.nullable().optional(), - sortDirection: SortDirection.nullable().optional(), + sortDirection: SortDirectionSchema.nullable().optional(), }) .optional() @@ -49,7 +49,6 @@ export const GetTrendingDevicesSummarySchema = z.object({ limit: z.number().int().min(1).max(20).default(HOME_PAGE_LIMITS.TRENDING_DEVICES), }) -// Type exports for repository use export type GetDevicesInput = z.input export type GetDeviceOptionsInput = z.input export type CreateDeviceInput = z.infer diff --git a/src/schemas/deviceBrand.ts b/src/schemas/deviceBrand.ts index 4be05b887..b62aa28a2 100644 --- a/src/schemas/deviceBrand.ts +++ b/src/schemas/deviceBrand.ts @@ -1,8 +1,8 @@ import { z } from 'zod' +import { SortDirectionSchema } from '@/schemas/common' export const DeviceBrandSortField = z.enum(['name', 'devicesCount']) export const DeviceBrandCategory = z.enum(['cpu', 'gpu']) -export const SortDirection = z.enum(['asc', 'desc']) export const GetDeviceBrandsSchema = z .object({ @@ -10,7 +10,7 @@ export const GetDeviceBrandsSchema = z category: DeviceBrandCategory.optional(), limit: z.number().default(50), sortField: DeviceBrandSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), }) .optional() @@ -27,7 +27,6 @@ export const UpdateDeviceBrandSchema = z.object({ export const DeleteDeviceBrandSchema = z.object({ id: z.string().uuid() }) -// Type exports for repository use export type GetDeviceBrandsInput = z.input export type CreateDeviceBrandInput = z.infer export type UpdateDeviceBrandInput = z.infer diff --git a/src/schemas/emulator.ts b/src/schemas/emulator.ts index 241226456..ce36be38b 100644 --- a/src/schemas/emulator.ts +++ b/src/schemas/emulator.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { SortDirection } from '@/schemas/soc' +import { SortDirectionSchema } from '@/schemas/common' export const EmulatorSortField = z.enum(['name', 'systemCount', 'listingCount']) @@ -10,7 +10,7 @@ export const GetEmulatorsSchema = z offset: z.number().default(0), page: z.number().optional(), sortField: EmulatorSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), }) .optional() @@ -46,7 +46,6 @@ export const UpdateSupportedSystemsSchema = z.object({ systemIds: z.array(z.string().uuid()), }) -// Type exports for repository use export type GetEmulatorsInput = z.input export type CreateEmulatorInput = z.infer export type UpdateEmulatorInput = z.infer diff --git a/src/schemas/game.ts b/src/schemas/game.ts index 74e62059d..695525aa9 100644 --- a/src/schemas/game.ts +++ b/src/schemas/game.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { HumanVerificationTokenSchema } from '@/features/human-verification/shared/schema' +import { SortDirectionSchema } from '@/schemas/common' import { ApprovalStatus } from '@orm' export const GameSortField = z.enum([ @@ -10,8 +11,6 @@ export const GameSortField = z.enum([ 'status', ]) -export const SortDirection = z.enum(['asc', 'desc']) - export const GameListingFilter = z.enum(['all', 'withListings', 'noListings']) export const GameApprovalStatusSchema = z.enum([ @@ -41,7 +40,7 @@ export const GetGamesSchema = z offset: z.number().default(0), page: z.number().optional(), sortField: GameSortField.nullable().optional(), - sortDirection: SortDirection.nullable().optional(), + sortDirection: SortDirectionSchema.nullable().optional(), }) .optional() .transform((data) => { @@ -135,7 +134,7 @@ export const GetPendingGamesSchema = z offset: z.number().default(0), page: z.number().optional(), sortField: GameSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), }) .optional() @@ -163,7 +162,6 @@ export const GetBestThreeDsTitleIdSchema = z.object({ export const GetThreeDsGamesStatsSchema = z.object({}).optional() -// Type exports for repository use export type GetGamesInput = z.input export type CreateGameInput = z.infer export type UpdateGameInput = z.infer diff --git a/src/schemas/gpu.ts b/src/schemas/gpu.ts index fa2ad1d3f..122912d54 100644 --- a/src/schemas/gpu.ts +++ b/src/schemas/gpu.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { SortDirection } from '@/schemas/soc' +import { SortDirectionSchema } from '@/schemas/common' export const GpuSortField = z.enum(['brand', 'modelName', 'pcListings']) @@ -11,7 +11,7 @@ export const GetGpusSchema = z offset: z.number().default(0), page: z.number().optional(), sortField: GpuSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), }) .optional() @@ -40,10 +40,6 @@ export const UpdateGpuSchema = z.object({ export const DeleteGpuSchema = z.object({ id: z.string().uuid() }) -// Type exports for repository use -// Use z.input for types that include defaults (what you pass in) -// Use z.output for types after defaults are applied (what you get out) -// TODO: figure out why we use z.infer export type GetGpusInput = z.input export type GetGpuOptionsInput = z.input export type CreateGpuInput = z.infer diff --git a/src/schemas/listingReport.ts b/src/schemas/listingReport.ts index eb03bcf22..3ac18d7bd 100644 --- a/src/schemas/listingReport.ts +++ b/src/schemas/listingReport.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { SortDirection } from '@/schemas/soc' +import { SortDirectionSchema } from '@/schemas/common' import { ReportReason, ReportStatus } from '@orm' export const ReportReasonSchema = z.nativeEnum(ReportReason) @@ -25,7 +25,7 @@ export const GetListingReportsSchema = z status: ReportStatusSchema.optional(), reason: ReportReasonSchema.optional(), sortField: ListingReportSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), page: z.number().min(1).default(1), limit: z.number().min(1).max(100).default(20), }) diff --git a/src/schemas/performanceScale.ts b/src/schemas/performanceScale.ts index 1ed139621..a69268ba4 100644 --- a/src/schemas/performanceScale.ts +++ b/src/schemas/performanceScale.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { SortDirection } from '@/schemas/soc' +import { SortDirectionSchema } from '@/schemas/common' export const PerformanceScaleSortField = z.enum(['label', 'rank']) @@ -7,7 +7,7 @@ export const GetPerformanceScalesSchema = z .object({ search: z.string().optional(), sortField: PerformanceScaleSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), }) .optional() diff --git a/src/schemas/permission.ts b/src/schemas/permission.ts index a5a3d80cc..2da193bd9 100644 --- a/src/schemas/permission.ts +++ b/src/schemas/permission.ts @@ -1,11 +1,10 @@ import { z } from 'zod' +import { SortDirectionSchema } from '@/schemas/common' import { Role, PermissionActionType } from '@orm' // Sorting and filtering schemas export const PermissionSortField = z.enum(['label', 'key', 'category', 'createdAt', 'updatedAt']) -export const SortDirection = z.enum(['asc', 'desc']) - export const PermissionCategory = z.enum(['CONTENT', 'MODERATION', 'USER_MANAGEMENT', 'SYSTEM']) // Get all permissions schema @@ -14,7 +13,7 @@ export const GetAllPermissionsSchema = z search: z.string().optional(), category: PermissionCategory.optional(), sortField: PermissionSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), page: z.number().int().min(1).default(1), limit: z.number().int().min(1).max(100).default(50), includeSystemOnly: z.boolean().optional(), @@ -79,7 +78,7 @@ export const GetPermissionLogsSchema = z targetRole: z.nativeEnum(Role).optional(), permissionId: z.string().uuid().optional(), sortField: PermissionLogSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), page: z.number().int().min(1).default(1), limit: z.number().int().min(1).max(100).default(20), dateFrom: z.string().datetime().optional(), diff --git a/src/schemas/soc.ts b/src/schemas/soc.ts index 0baa2c543..7ed6bd4b5 100644 --- a/src/schemas/soc.ts +++ b/src/schemas/soc.ts @@ -1,7 +1,7 @@ import { z } from 'zod' +import { SortDirectionSchema } from '@/schemas/common' export const SoCSortField = z.enum(['name', 'manufacturer', 'devicesCount']) -export const SortDirection = z.enum(['asc', 'desc']) export const GetSoCsSchema = z .object({ @@ -10,7 +10,7 @@ export const GetSoCsSchema = z offset: z.number().default(0), page: z.number().optional(), sortField: SoCSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), }) .optional() @@ -43,7 +43,6 @@ export const DeleteSoCSchema = z.object({ export const GetSoCsByIdsSchema = z.object({ ids: z.array(z.string().uuid()).min(1).max(100) }) -// Type exports for repository use export type GetSoCsInput = z.input export type GetSoCOptionsInput = z.input export type CreateSoCInput = z.infer diff --git a/src/schemas/system.ts b/src/schemas/system.ts index 83c47bb9b..86793576a 100644 --- a/src/schemas/system.ts +++ b/src/schemas/system.ts @@ -1,13 +1,13 @@ import { z } from 'zod' +import { SortDirectionSchema } from '@/schemas/common' export const SystemSortField = z.enum(['name', 'key', 'gamesCount']) -export const SortDirection = z.enum(['asc', 'desc']) export const GetSystemsSchema = z .object({ search: z.string().nullable().optional(), sortField: SystemSortField.nullable().optional(), - sortDirection: SortDirection.nullable().optional(), + sortDirection: SortDirectionSchema.nullable().optional(), }) .optional() @@ -26,7 +26,6 @@ export const UpdateSystemSchema = z.object({ export const DeleteSystemSchema = z.object({ id: z.string().uuid() }) -// Type exports for repository use export type GetSystemsInput = z.input export type CreateSystemInput = z.infer export type UpdateSystemInput = z.infer diff --git a/src/schemas/user.ts b/src/schemas/user.ts index 76656b0bb..02ee7be7b 100644 --- a/src/schemas/user.ts +++ b/src/schemas/user.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { PAGINATION, CHAR_LIMITS } from '@/data/constants' +import { SortDirectionSchema } from '@/schemas/common' import { Role } from '@orm' export const UserSortField = z.enum([ @@ -14,13 +15,11 @@ export const UserSortField = z.enum([ 'followersCount', 'followingCount', ]) -export const SortDirection = z.enum(['asc', 'desc']) - export const GetAllUsersSchema = z .object({ search: z.string().nullable().optional(), sortField: UserSortField.nullable().optional(), - sortDirection: SortDirection.nullable().optional(), + sortDirection: SortDirectionSchema.nullable().optional(), page: z.number().int().min(1).default(1), limit: z.number().int().min(1).max(PAGINATION.MAX_LIMIT).default(PAGINATION.DEFAULT_LIMIT), }) diff --git a/src/schemas/userBan.ts b/src/schemas/userBan.ts index 71183ccfe..ed56b10e0 100644 --- a/src/schemas/userBan.ts +++ b/src/schemas/userBan.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { SortDirection } from '@/schemas/soc' +import { SortDirectionSchema } from '@/schemas/common' export const UserBanSortField = z.enum(['bannedAt', 'expiresAt', 'isActive', 'reason']) @@ -29,7 +29,7 @@ export const GetUserBansSchema = z search: z.string().optional(), isActive: z.boolean().optional(), sortField: UserBanSortField.optional(), - sortDirection: SortDirection.optional(), + sortDirection: SortDirectionSchema.optional(), page: z.number().min(1).default(1), limit: z.number().min(1).max(100).default(20), }) diff --git a/src/schemas/voteInvestigation.ts b/src/schemas/voteInvestigation.ts index c4444e404..0572edf42 100644 --- a/src/schemas/voteInvestigation.ts +++ b/src/schemas/voteInvestigation.ts @@ -1,6 +1,5 @@ import { z } from 'zod' -import { ListingType } from '@/schemas/common' -import { SortDirection } from '@/schemas/soc' +import { ListingType, SortDirectionSchema } from '@/schemas/common' export const VoteTypeFilter = z.enum(['all', 'up', 'down']) export const ListingTypeFilter = z.enum(['all', ...ListingType.options]) @@ -13,7 +12,7 @@ export const GetUserVotesSchema = z.object({ voteType: VoteTypeFilter.default('all'), listingType: ListingTypeFilter.default('all'), sortField: VoteSortField.default('createdAt'), - sortDirection: SortDirection.default('desc'), + sortDirection: SortDirectionSchema.default('desc'), includeNullified: z.boolean().default(false), }) diff --git a/src/server/services/user-profile.service.test.ts b/src/server/services/user-profile.service.test.ts index f33e45527..27f90b09d 100644 --- a/src/server/services/user-profile.service.test.ts +++ b/src/server/services/user-profile.service.test.ts @@ -1,6 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { Role, type PrismaClient } from '@orm/client' -import { checkProfileAccess, PRIVATE_PROFILE_SETTINGS } from './user-profile.service' +import { + checkProfileAccess, + PRIVATE_PROFILE_SETTINGS, + PROFILE_ACCESS_REASONS, +} from './user-profile.service' function createMockPrisma() { return { @@ -36,7 +40,7 @@ describe('user-profile.service', () => { const result = await checkProfileAccess(prisma, 'missing-id', {}) - expect(result).toEqual({ accessible: false, reason: 'not_found' }) + expect(result).toEqual({ accessible: false, reason: PROFILE_ACCESS_REASONS.NOT_FOUND }) }) it('should return banned when user has active ban and viewer is not mod', async () => { @@ -50,7 +54,7 @@ describe('user-profile.service', () => { currentUserRole: Role.USER, }) - expect(result).toEqual({ accessible: false, reason: 'banned' }) + expect(result).toEqual({ accessible: false, reason: PROFILE_ACCESS_REASONS.BANNED }) }) it('should return accessible with isBanned when user has active ban but viewer is MODERATOR', async () => { @@ -83,7 +87,7 @@ describe('user-profile.service', () => { currentUserRole: Role.USER, }) - expect(result).toEqual({ accessible: false, reason: 'private' }) + expect(result).toEqual({ accessible: false, reason: PROFILE_ACCESS_REASONS.PRIVATE }) }) it('should return accessible when profile is private but viewer is the owner', async () => { diff --git a/src/server/services/user-profile.service.ts b/src/server/services/user-profile.service.ts index d3267c8a5..1bda2581e 100644 --- a/src/server/services/user-profile.service.ts +++ b/src/server/services/user-profile.service.ts @@ -17,6 +17,15 @@ interface PrivacySettings { followingVisible: boolean } +export const PROFILE_ACCESS_REASONS = { + NOT_FOUND: 'not_found', + BANNED: 'banned', + PRIVATE: 'private', +} as const + +export type ProfileAccessReason = + (typeof PROFILE_ACCESS_REASONS)[keyof typeof PROFILE_ACCESS_REASONS] + interface AccessibleProfile { accessible: true isBanned: boolean @@ -29,7 +38,7 @@ interface AccessibleProfile { interface InaccessibleProfile { accessible: false - reason: 'not_found' | 'banned' | 'private' // TODO: use constants or enums + reason: ProfileAccessReason } export type ProfileAccessResult = AccessibleProfile | InaccessibleProfile @@ -77,7 +86,7 @@ export async function checkProfileAccess( }, }) - if (!user) return { accessible: false, reason: 'not_found' } + if (!user) return { accessible: false, reason: PROFILE_ACCESS_REASONS.NOT_FOUND } const isBanned = user.userBans.length > 0 const canViewBannedUsers = roleIncludesRole(ctx.currentUserRole, Role.MODERATOR) @@ -85,7 +94,7 @@ export async function checkProfileAccess( const isMod = canViewBannedUsers if (isBanned && !canViewBannedUsers) { - return { accessible: false, reason: 'banned' } + return { accessible: false, reason: PROFILE_ACCESS_REASONS.BANNED } } const privacySettings: PrivacySettings = { @@ -98,7 +107,7 @@ export async function checkProfileAccess( } if (!privacySettings.profilePublic && !isOwner && !isMod) { - return { accessible: false, reason: 'private' } + return { accessible: false, reason: PROFILE_ACCESS_REASONS.PRIVATE } } return { diff --git a/src/utils/badge-colors.ts b/src/utils/badge-colors.ts index a4f8883be..952b27cf3 100644 --- a/src/utils/badge-colors.ts +++ b/src/utils/badge-colors.ts @@ -72,3 +72,17 @@ export function getPermissionCategoryBadgeVariant( ): BadgeVariant { return permissionCategoryVariantMap[permissionCategory] || 'default' } + +export function getSuccessRateBarColor(rate: number): string { + if (rate >= 95) return 'bg-green-600' + if (rate >= 85) return 'bg-green-500' + if (rate >= 75) return 'bg-green-400' + if (rate >= 65) return 'bg-lime-500' + if (rate >= 55) return 'bg-yellow-400' + if (rate >= 45) return 'bg-yellow-500' + if (rate >= 35) return 'bg-orange-400' + if (rate >= 25) return 'bg-orange-500' + if (rate >= 15) return 'bg-red-400' + if (rate >= 5) return 'bg-red-500' + return 'bg-red-600' +} diff --git a/src/utils/vote.ts b/src/utils/vote.ts index d35b08317..d90463936 100644 --- a/src/utils/vote.ts +++ b/src/utils/vote.ts @@ -1,34 +1,3 @@ -/** - * Utility functions for vote-related calculations and styling - */ - -/** - * Get the color class for the success rate bar based on the rate - * TODO: probably move this to badgeColors.ts - * @param rate - Success rate percentage (0-100) - * @returns Tailwind CSS background color class - */ -export function getBarColor(rate: number): string { - if (rate >= 95) return 'bg-green-600' // Excellent - dark green - if (rate >= 85) return 'bg-green-500' // Very good - green - if (rate >= 75) return 'bg-green-400' // Good - light green - if (rate >= 65) return 'bg-lime-500' // Above average - lime - if (rate >= 55) return 'bg-yellow-400' // Average+ - light yellow - if (rate >= 45) return 'bg-yellow-500' // Average - yellow - if (rate >= 35) return 'bg-orange-400' // Below average - light orange - if (rate >= 25) return 'bg-orange-500' // Poor - orange - if (rate >= 15) return 'bg-red-400' // Bad - light red - if (rate >= 5) return 'bg-red-500' // Very bad - red - return 'bg-red-600' // Terrible - dark red -} - -/** - * Calculate the width percentage for the success rate bar - * When rate is 0 but there are votes, show full red bar (100%) - * @param rate - Success rate percentage (0-100) - * @param voteCount - Total number of votes - * @returns Width percentage for the bar - */ export function getBarWidth(rate: number, voteCount: number): number { return rate === 0 && voteCount > 0 ? 100 : rate }