Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ echo "🚀 Running pre-push checks..."

# Run security audit
echo "🔒 Running security audit..."
npm audit --audit-level=moderate
npm audit --omit=dev --audit-level=high

# Run build check one more time
# echo "🏗️ Final build check..."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ export default function JudgingPage() {
useState<AggregatedJudgingResults | null>(null);
const [isFetchingResults, setIsFetchingResults] = useState(false);
const [winners, setWinners] = useState<JudgingResult[]>([]);
const [winnersSummary, setWinnersSummary] =
useState<AggregatedJudgingResults | null>(null);
const [isFetchingWinners, setIsFetchingWinners] = useState(false);
const [isPublishing, setIsPublishing] = useState(false);
const [isCurrentUserJudge, setIsCurrentUserJudge] = useState(false);
Expand Down Expand Up @@ -352,8 +350,7 @@ export default function JudgingPage() {
try {
const res = await getJudgingWinners(organizationId, hackathonId);
if (res.success && res.data) {
setWinners(res.data.results || []);
setWinnersSummary(res.data);
setWinners(Array.isArray(res.data) ? res.data : []);
}
} catch (error) {
console.error('Error fetching winners:', error);
Expand Down Expand Up @@ -490,6 +487,7 @@ export default function JudgingPage() {
judges={currentJudges}
isJudgesLoading={isRefreshingJudges}
currentUserId={currentUserId || undefined}
canOverrideScores={canManageJudges}
onSuccess={handleSuccess}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ModalFooterProps {
isFetchingCriteria: boolean;
hasCriteria: boolean;
existingScore: { scores: unknown[]; notes?: string } | null;
mode?: 'judge' | 'organizer-override';
onCancel: () => void;
onSubmit: () => void;
}
Expand All @@ -20,9 +21,25 @@ export const ModalFooter = ({
isFetchingCriteria,
hasCriteria,
existingScore,
mode = 'judge',
onCancel,
onSubmit,
}: ModalFooterProps) => {
const isOverride = mode === 'organizer-override';
const actionLabel = isOverride
? existingScore
? 'Update Override'
: 'Apply Override'
: existingScore
? 'Update Grade'
: 'Submit Grade';

const loadingLabel = isOverride
? 'Applying...'
: existingScore
? 'Updating...'
: 'Submitting...';

return (
<div className='flex flex-shrink-0 items-center justify-between'>
<div className='text-sm text-gray-400'>
Expand Down Expand Up @@ -59,12 +76,10 @@ export const ModalFooter = ({
{isLoading ? (
<>
<Loader2 className='mr-2 inline h-4 w-4 animate-spin' />
{existingScore ? 'Updating...' : 'Submitting...'}
{loadingLabel}
</>
) : existingScore ? (
'Update Grade'
) : (
'Submit Grade'
actionLabel
)}
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface ScoringSectionProps {
getScoreColor: (percentage: number) => string;
overallComment: string;
onOverallCommentChange: (value: string) => void;
showComments?: boolean;
}

export const ScoringSection = ({
Expand All @@ -39,6 +40,7 @@ export const ScoringSection = ({
getScoreColor,
overallComment,
onOverallCommentChange,
showComments = true,
}: ScoringSectionProps) => {
const getCriterionKey = (criterion: JudgingCriterion) => {
return criterion.id || criterion.name || criterion.title;
Expand Down Expand Up @@ -149,55 +151,58 @@ export const ScoringSection = ({
/>
</div>

{/* Comment field */}
<div className='mt-4'>
<label
htmlFor={`comment-${key}`}
className='mb-1.5 block text-[10px] font-semibold tracking-wider text-gray-500 uppercase'
>
Judge Feedback (Optional)
</label>
<textarea
id={`comment-${key}`}
value={comment}
onChange={e => onCommentChange(key, e.target.value)}
placeholder={`Share your thoughts on ${criterionTitle.toLowerCase()}...`}
className={cn(
'min-h-[80px] w-full rounded-lg border border-gray-800 bg-gray-950/50 p-3 text-sm text-gray-200 transition-all',
'focus:border-primary focus:ring-primary/20 focus:ring-1 focus:outline-none',
'resize-none placeholder:text-gray-600'
)}
/>
</div>
{showComments && (
<div className='mt-4'>
<label
htmlFor={`comment-${key}`}
className='mb-1.5 block text-[10px] font-semibold tracking-wider text-gray-500 uppercase'
>
Judge Feedback (Optional)
</label>
<textarea
id={`comment-${key}`}
value={comment}
onChange={e => onCommentChange(key, e.target.value)}
placeholder={`Share your thoughts on ${criterionTitle.toLowerCase()}...`}
className={cn(
'min-h-[80px] w-full rounded-lg border border-gray-800 bg-gray-950/50 p-3 text-sm text-gray-200 transition-all',
'focus:border-primary focus:ring-primary/20 focus:ring-1 focus:outline-none',
'resize-none placeholder:text-gray-600'
)}
/>
</div>
)}
</div>
);
})}

{/* Global Comment Section */}
<div className='mt-8 border-t border-gray-800 pt-8'>
<h4 className='mb-4 flex items-center gap-2 text-lg font-semibold text-white'>
Overall Evaluation
</h4>
<div className='rounded-xl border border-gray-800 bg-gray-900/50 p-5'>
<label
htmlFor='overall-comment'
className='mb-2 block text-[10px] font-semibold tracking-wider text-gray-500 uppercase'
>
Summary Feedback for the Entire Project
</label>
<textarea
id='overall-comment'
value={overallComment}
onChange={e => onOverallCommentChange(e.target.value)}
placeholder='Summarize your evaluation or add any final notes here...'
className={cn(
'min-h-[120px] w-full rounded-lg border border-gray-800 bg-gray-950/50 p-4 font-sans text-sm text-gray-200 transition-all',
'focus:border-primary focus:ring-primary/20 focus:ring-1 focus:outline-none',
'resize-none placeholder:text-gray-600'
)}
/>
{showComments && (
<div className='mt-8 border-t border-gray-800 pt-8'>
<h4 className='mb-4 flex items-center gap-2 text-lg font-semibold text-white'>
Overall Evaluation
</h4>
<div className='rounded-xl border border-gray-800 bg-gray-900/50 p-5'>
<label
htmlFor='overall-comment'
className='mb-2 block text-[10px] font-semibold tracking-wider text-gray-500 uppercase'
>
Summary Feedback for the Entire Project
</label>
<textarea
id='overall-comment'
value={overallComment}
onChange={e => onOverallCommentChange(e.target.value)}
placeholder='Summarize your evaluation or add any final notes here...'
className={cn(
'min-h-[120px] w-full rounded-lg border border-gray-800 bg-gray-950/50 p-4 font-sans text-sm text-gray-200 transition-all',
'focus:border-primary focus:ring-primary/20 focus:ring-1 focus:outline-none',
'resize-none placeholder:text-gray-600'
)}
/>
</div>
</div>
</div>
)}
</div>
</div>
);
Expand Down
130 changes: 129 additions & 1 deletion components/organization/cards/GradeSubmissionModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ import { useScoreCalculation } from './useScoreCalculation';
import { useJudgingCriteria } from './useJudgingCriteria';
import { useSubmissionScores } from './useSubmissionScores';
import { useScoreForm } from './useScoreForm';
import { Switch } from '@/components/ui/switch';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useState } from 'react';

interface SubmissionData {
id: string;
Expand All @@ -30,6 +41,16 @@ interface GradeSubmissionModalProps {
participantId: string;
judgingCriteria?: JudgingCriterion[];
submission: SubmissionData;
mode?: 'judge' | 'organizer-override';
overrideJudgeId?: string;
judges?: Array<{
id?: string;
userId?: string;
name?: string;
email?: string;
image?: string;
role?: string;
}>;
onSuccess?: () => void;
}

Expand All @@ -41,8 +62,42 @@ export default function GradeSubmissionModal({
participantId,
judgingCriteria,
submission,
mode = 'judge',
overrideJudgeId,
judges = [],
onSuccess,
}: GradeSubmissionModalProps) {
const isOverride = mode === 'organizer-override';
const [creditJudge, setCreditJudge] = useState(false);
const [selectedJudgeId, setSelectedJudgeId] = useState<string | undefined>(
overrideJudgeId
);

const availableJudges = judges
.map(j => ({
id: j.userId || j.id,
name: j.name || j.email || 'Unknown Judge',
email: j.email,
image: j.image,
role: j.role,
}))
.filter(j => !!j.id) as Array<{
id: string;
name: string;
email?: string;
image?: string;
role?: string;
}>;

const handleToggleCredit = (value: boolean) => {
setCreditJudge(value);
if (value && !selectedJudgeId && availableJudges.length > 0) {
setSelectedJudgeId(availableJudges[0].id);
}
if (!value) {
setSelectedJudgeId(undefined);
}
};
const { criteria, isFetchingCriteria } = useJudgingCriteria({
open,
organizationId,
Expand Down Expand Up @@ -90,6 +145,8 @@ export default function GradeSubmissionModal({
hackathonId,
participantId: submission.id,
existingScore,
mode,
overrideJudgeId: creditJudge ? selectedJudgeId : undefined,
onSuccess,
onClose: () => onOpenChange(false),
});
Expand All @@ -103,7 +160,7 @@ export default function GradeSubmissionModal({
<BoundlessSheet
open={open}
setOpen={onOpenChange}
title='Grade Submission'
title={isOverride ? 'Override Submission Score' : 'Grade Submission'}
size='xl'
>
<div className='relative flex flex-col'>
Expand All @@ -117,6 +174,75 @@ export default function GradeSubmissionModal({
) : (
<div className='mx-auto max-w-6xl'>
<ProjectHeader submission={submission} />
{isOverride && (
<div className='mb-6 space-y-4 rounded-lg border border-amber-500/20 bg-amber-500/10 px-4 py-3 text-xs text-amber-300'>
<div>
Organizer override: this action directly assigns scores and
bypasses judge assignment checks.
</div>
<div className='flex flex-wrap items-center gap-3 text-[11px] text-amber-200'>
<div className='flex items-center gap-2'>
<Switch
checked={creditJudge}
onCheckedChange={handleToggleCredit}
className='data-[state=checked]:bg-amber-500'
/>
<span>Credit judge</span>
</div>
{creditJudge && (
<div className='min-w-[220px]'>
<Select
value={selectedJudgeId}
onValueChange={value => setSelectedJudgeId(value)}
>
<SelectTrigger className='h-8 border-amber-500/30 bg-black/20 text-amber-100'>
<SelectValue placeholder='Select judge' />
</SelectTrigger>
<SelectContent className='border-amber-500/20 bg-black text-amber-100'>
{availableJudges.length === 0 && (
<SelectItem value='no-judges' disabled>
No judges available
</SelectItem>
)}
{availableJudges.map(judge => (
<SelectItem key={judge.id} value={judge.id}>
<div className='flex items-center gap-2'>
<Avatar className='h-5 w-5 border border-amber-500/20'>
<AvatarImage src={judge.image} />
<AvatarFallback className='bg-amber-500/10 text-[9px] text-amber-200'>
{judge.name.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className='min-w-0'>
<div className='truncate text-xs text-amber-100'>
{judge.name}
</div>
<div className='flex items-center gap-1 text-[10px] text-amber-300/80'>
{judge.email && (
<span className='truncate'>
{judge.email}
</span>
)}
{judge.role && (
<Badge
variant='outline'
className='border-amber-500/30 bg-amber-500/10 px-1.5 py-0 text-[9px] text-amber-200'
>
{judge.role}
</Badge>
)}
</div>
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
</div>
)}

<div className='mt-8 grid grid-cols-1 gap-8 lg:grid-cols-3'>
<div className='lg:col-span-2'>
Expand All @@ -134,6 +260,7 @@ export default function GradeSubmissionModal({
getScoreColor={getScoreColor}
overallComment={overallComment}
onOverallCommentChange={setOverallComment}
showComments={!isOverride}
/>
</div>

Expand Down Expand Up @@ -196,6 +323,7 @@ export default function GradeSubmissionModal({
isFetchingCriteria={isFetchingCriteria}
hasCriteria={criteria.length > 0}
existingScore={existingScore}
mode={mode}
onCancel={() => onOpenChange(false)}
onSubmit={handleSubmit}
/>
Expand Down
Loading
Loading