diff --git a/src/features/votes/components/RelativeEvaluationBadge.svelte b/src/features/votes/components/RelativeEvaluationBadge.svelte index cefff7294..04e5683b6 100644 --- a/src/features/votes/components/RelativeEvaluationBadge.svelte +++ b/src/features/votes/components/RelativeEvaluationBadge.svelte @@ -6,6 +6,7 @@ calcGradeDiff, getRelativeEvaluationLabel, getRelativeEvaluationTooltipText, + getRelativeEvaluationBadgeColorClass, } from '$features/votes/utils/relative_evaluation'; interface Props { @@ -24,8 +25,9 @@ let { officialGrade, medianGrade, badgeId, showTooltip = true }: Props = $props(); - const label = $derived(getRelativeEvaluationLabel(calcGradeDiff(officialGrade, medianGrade))); - const isHarder = $derived(label.startsWith('+')); + const diff = $derived(calcGradeDiff(officialGrade, medianGrade)); + const label = $derived(getRelativeEvaluationLabel(diff)); + const badgeColorClass = $derived(getRelativeEvaluationBadgeColorClass(diff)); const tooltipText = $derived(getRelativeEvaluationTooltipText(label)); @@ -33,10 +35,7 @@ {#if label} {#each nonPendingGrades as grade (grade)} handleClick(grade)} class="rounded-md">
- {getTaskGradeLabel(grade)} + {getTaskGradeLabel(grade)} + {#if taskResult.grade !== TaskGrade.PENDING} + {@const diff = calcGradeDiff(taskResult.grade, grade)} + {@const relLabel = getRelativeEvaluationJapaneseLabel(diff)} + {#if relLabel} + + {relLabel} + + {/if} + {/if} {#if votedGrade === grade} - + {/if}
diff --git a/src/features/votes/utils/relative_evaluation.test.ts b/src/features/votes/utils/relative_evaluation.test.ts index 39aa29085..710bcf747 100644 --- a/src/features/votes/utils/relative_evaluation.test.ts +++ b/src/features/votes/utils/relative_evaluation.test.ts @@ -5,6 +5,9 @@ import { calcGradeDiff, getRelativeEvaluationLabel, getRelativeEvaluationTooltipText, + getRelativeEvaluationJapaneseLabel, + getRelativeEvaluationColorClass, + getRelativeEvaluationBadgeColorClass, } from './relative_evaluation'; describe('calcGradeDiff', () => { @@ -121,3 +124,75 @@ describe('getRelativeEvaluationTooltipText', () => { expect(getRelativeEvaluationTooltipText('--')).toBe('ユーザは「易しい」と評価'); }); }); + +describe('getRelativeEvaluationJapaneseLabel', () => { + test('returns "" for diff <= -3 (out of expected range)', () => { + expect(getRelativeEvaluationJapaneseLabel(-3)).toBe(''); + expect(getRelativeEvaluationJapaneseLabel(-16)).toBe(''); + }); + + test('returns "易しい" for diff === -2', () => { + expect(getRelativeEvaluationJapaneseLabel(-2)).toBe('易しい'); + }); + + test('returns "やや易しい" for diff === -1', () => { + expect(getRelativeEvaluationJapaneseLabel(-1)).toBe('やや易しい'); + }); + + test('returns "ふつう" for diff === 0', () => { + expect(getRelativeEvaluationJapaneseLabel(0)).toBe('ふつう'); + }); + + test('returns "やや難しい" for diff === 1', () => { + expect(getRelativeEvaluationJapaneseLabel(1)).toBe('やや難しい'); + }); + + test('returns "難しい" for diff === 2', () => { + expect(getRelativeEvaluationJapaneseLabel(2)).toBe('難しい'); + }); + + test('returns "" for diff >= 3 (out of expected range)', () => { + expect(getRelativeEvaluationJapaneseLabel(3)).toBe(''); + expect(getRelativeEvaluationJapaneseLabel(16)).toBe(''); + }); +}); + +describe('getRelativeEvaluationColorClass', () => { + test('returns sky text classes for negative diff (easier)', () => { + expect(getRelativeEvaluationColorClass(-1)).toBe('text-sky-500 dark:text-sky-400'); + expect(getRelativeEvaluationColorClass(-16)).toBe('text-sky-500 dark:text-sky-400'); + }); + + test('returns gray text classes for diff === 0', () => { + expect(getRelativeEvaluationColorClass(0)).toBe('text-gray-400 dark:text-gray-500'); + }); + + test('returns orange text classes for positive diff (harder)', () => { + expect(getRelativeEvaluationColorClass(1)).toBe('text-orange-400 dark:text-orange-300'); + expect(getRelativeEvaluationColorClass(16)).toBe('text-orange-400 dark:text-orange-300'); + }); +}); + +describe('getRelativeEvaluationBadgeColorClass', () => { + test('returns sky bg classes for negative diff (easier)', () => { + expect(getRelativeEvaluationBadgeColorClass(-1)).toBe( + 'bg-sky-400 text-white dark:bg-sky-500 dark:text-white', + ); + expect(getRelativeEvaluationBadgeColorClass(-16)).toBe( + 'bg-sky-400 text-white dark:bg-sky-500 dark:text-white', + ); + }); + + test('returns empty string for diff === 0 (badge not shown)', () => { + expect(getRelativeEvaluationBadgeColorClass(0)).toBe(''); + }); + + test('returns orange bg classes for positive diff (harder)', () => { + expect(getRelativeEvaluationBadgeColorClass(1)).toBe( + 'bg-orange-400 text-white dark:bg-orange-500 dark:text-white', + ); + expect(getRelativeEvaluationBadgeColorClass(16)).toBe( + 'bg-orange-400 text-white dark:bg-orange-500 dark:text-white', + ); + }); +}); diff --git a/src/features/votes/utils/relative_evaluation.ts b/src/features/votes/utils/relative_evaluation.ts index 5fd56c729..116cb58bd 100644 --- a/src/features/votes/utils/relative_evaluation.ts +++ b/src/features/votes/utils/relative_evaluation.ts @@ -13,6 +13,36 @@ export function calcGradeDiff(officialGrade: TaskGrade, medianGrade: TaskGrade): return getGradeOrder(medianGrade) - getGradeOrder(officialGrade); } +/** + * Converts a grade difference to a 5-level relative evaluation label. + * + * | diff | label | + * | ------ | ----- | + * | ≤ −2 | `--` | + * | −1 | `-` | + * | 0 | `""` | + * | +1 | `+` | + * | ≥ +2 | `++` | + * + * @param diff - The result of {@link calcGradeDiff}. + * @returns The display label string. + */ +export function getRelativeEvaluationLabel(diff: number): string { + if (diff <= -2) { + return '--'; + } + if (diff === -1) { + return '-'; + } + if (diff === 0) { + return ''; + } + if (diff === 1) { + return '+'; + } + return '++'; +} + /** * Returns a Japanese tooltip string explaining the relative evaluation label. * @@ -35,31 +65,68 @@ export function getRelativeEvaluationTooltipText(label: string): string { } /** - * Converts a grade difference to a 5-level relative evaluation label. + * Maps a grade difference to a Japanese label for display in the vote dropdown. + * Returns an empty string when the diff falls outside the expected ±2 range. * - * | diff | label | - * | ------ | ----- | - * | ≤ −2 | `--` | - * | −1 | `-` | - * | 0 | `""` | - * | +1 | `+` | - * | ≥ +2 | `++` | + * | diff | label | + * | ---- | ------------ | + * | ≤ −3 | `''` | + * | −2 | `易しい` | + * | −1 | `やや易しい` | + * | 0 | `ふつう` | + * | +1 | `やや難しい` | + * | +2 | `難しい` | + * | ≥ +3 | `''` | * - * @param diff - The result of {@link calcGradeDiff}. - * @returns The display label string. + * @param diff - A grade difference (e.g., from {@link calcGradeDiff}). + * @returns The Japanese label string, or `''` if out of expected range. */ -export function getRelativeEvaluationLabel(diff: number): string { - if (diff <= -2) { - return '--'; +export function getRelativeEvaluationJapaneseLabel(diff: number): string { + switch (diff) { + case -2: + return '易しい'; + case -1: + return 'やや易しい'; + case 0: + return 'ふつう'; + case 1: + return 'やや難しい'; + case 2: + return '難しい'; + default: + return ''; } - if (diff === -1) { - return '-'; +} + +/** + * Returns Tailwind text color classes for a diff value in the vote dropdown. + * Negative diff (easier) → sky, zero → gray, positive (harder) → orange. + * + * @param diff - The result of {@link calcGradeDiff}. + */ +export function getRelativeEvaluationColorClass(diff: number): string { + if (diff < 0) { + return 'text-sky-500 dark:text-sky-400'; } if (diff === 0) { - return ''; + return 'text-gray-400 dark:text-gray-500'; } - if (diff === 1) { - return '+'; + return 'text-orange-400 dark:text-orange-300'; +} + +/** + * Returns Tailwind background + text color classes for the relative evaluation badge. + * Negative diff (easier) → sky, positive (harder) → orange. + * Returns empty string for diff === 0 (badge is not shown). + * + * @param diff - The result of {@link calcGradeDiff}. + */ +export function getRelativeEvaluationBadgeColorClass(diff: number): string { + if (diff < 0) { + return 'bg-sky-400 text-white dark:bg-sky-500 dark:text-white'; } - return '++'; + if (diff > 0) { + return 'bg-orange-400 text-white dark:bg-orange-500 dark:text-white'; + } + return ''; }