refactor: extract rating values and emojis into reusable constants (#23340)

* refactor: extract rating values and emojis into reusable constants

- Create packages/lib/rating.ts with RATING_OPTIONS array and validation utilities
- Update bookings-single-view.tsx to use extracted constants
- Add TypeScript interfaces for type safety
- Enable reuse of rating constants across the codebase

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: move rating utilities to bookings feature directory

- Move packages/lib/rating.ts to packages/features/bookings/lib/rating.ts
- Update import path in bookings-single-view.tsx to use new location
- Maintain domain-specific organization for booking-related utilities

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: resolve linting issues in event-types.tsx

- Fix quote style consistency (single to double quotes)
- Add missing trailing comma
- Resolve pre-commit hook formatting changes

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Eunjae Lee
2025-08-27 15:54:52 +02:00
committed by GitHub
co-authored by Devin AI
parent 2d9162a4dd
commit 5324832f13
2 changed files with 64 additions and 57 deletions
@@ -26,6 +26,7 @@ import {
SystemField,
TITLE_FIELD,
} from "@calcom/features/bookings/lib/SystemField";
import { RATING_OPTIONS, validateRating } from "@calcom/features/bookings/lib/rating";
import { getCalendarLinks, CalendarLinkType } from "@calcom/lib/bookings/getCalendarLinks";
import { APP_NAME } from "@calcom/lib/constants";
import { formatToLocalizedDate, formatToLocalizedTime, formatToLocalizedTimezone } from "@calcom/lib/dayjs";
@@ -179,14 +180,13 @@ export default function Success(props: PageProps) {
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
const [comment, setComment] = useState("");
const parsedRating = rating ? parseInt(rating, 10) : 3;
const currentUserEmail =
searchParams?.get("rescheduledBy") ??
searchParams?.get("cancelledBy") ??
session?.user?.email ??
undefined;
const defaultRating = isNaN(parsedRating) ? 3 : parsedRating > 5 ? 5 : parsedRating < 1 ? 1 : parsedRating;
const defaultRating = validateRating(rating);
const [rateValue, setRateValue] = useState<number>(defaultRating);
const [isFeedbackSubmitted, setIsFeedbackSubmitted] = useState(false);
@@ -1026,61 +1026,20 @@ export default function Success(props: PageProps) {
) : (
<>
<div className="my-3 flex justify-center space-x-1">
<button
className={classNames(
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 1
? "border-emphasis bg-emphasis"
: "border-muted bg-default opacity-50"
)}
disabled={isFeedbackSubmitted}
onClick={() => setRateValue(1)}>
😠
</button>
<button
className={classNames(
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 2
? "border-emphasis bg-emphasis"
: "border-muted bg-default opacity-50"
)}
disabled={isFeedbackSubmitted}
onClick={() => setRateValue(2)}>
🙁
</button>
<button
className={classNames(
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 3
? "border-emphasis bg-emphasis"
: " border-muted bg-default opacity-50"
)}
disabled={isFeedbackSubmitted}
onClick={() => setRateValue(3)}>
😐
</button>
<button
className={classNames(
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 4
? "border-emphasis bg-emphasis"
: "border-muted bg-default opacity-50"
)}
disabled={isFeedbackSubmitted}
onClick={() => setRateValue(4)}>
😄
</button>
<button
className={classNames(
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === 5
? "border-emphasis bg-emphasis"
: "border-muted bg-default opacity-50"
)}
disabled={isFeedbackSubmitted}
onClick={() => setRateValue(5)}>
😍
</button>
{RATING_OPTIONS.map((option) => (
<button
key={option.value}
className={classNames(
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
rateValue === option.value
? "border-emphasis bg-emphasis"
: "border-muted bg-default opacity-50"
)}
disabled={isFeedbackSubmitted}
onClick={() => setRateValue(option.value)}>
{option.emoji}
</button>
))}
</div>
<div className="my-4 space-y-1 text-center">
<h2 className="font-cal text-lg">{t("submitted_feedback")}</h2>
+48
View File
@@ -0,0 +1,48 @@
export interface RatingOption {
value: number;
emoji: string;
}
export const MIN_RATING = 1;
export const MAX_RATING = 5;
export const DEFAULT_RATING = 3;
export const RATING_OPTIONS: RatingOption[] = [
{ value: 1, emoji: "😠" },
{ value: 2, emoji: "🙁" },
{ value: 3, emoji: "😐" },
{ value: 4, emoji: "😄" },
{ value: 5, emoji: "😍" },
];
/**
* Validates and normalizes a rating value to ensure it's within the valid range
* @param rating - The rating value to validate (can be string or number)
* @param defaultValue - The default value to use if rating is invalid (defaults to DEFAULT_RATING)
* @returns A valid rating number between MIN_RATING and MAX_RATING
*/
export function validateRating(
rating: string | number | null | undefined,
defaultValue = DEFAULT_RATING
): number {
const parsedRating = typeof rating === "string" ? parseInt(rating, 10) : rating;
if (!parsedRating || isNaN(parsedRating)) {
return defaultValue;
}
if (parsedRating > MAX_RATING) return MAX_RATING;
if (parsedRating < MIN_RATING) return MIN_RATING;
return parsedRating;
}
/**
* Gets the emoji for a given rating value
* @param rating - The rating value
* @returns The corresponding emoji or empty string if not found
*/
export function getRatingEmoji(rating: number): string {
const option = RATING_OPTIONS.find((opt) => opt.value === rating);
return option?.emoji || "";
}