diff --git a/src/apps/active-reviews-page (1).ts b/src/apps/active-reviews-page (1).ts new file mode 100644 index 000000000..8d239ce2d --- /dev/null +++ b/src/apps/active-reviews-page (1).ts @@ -0,0 +1,240 @@ +// src/apps/review/pages/active-reviews-page.tsx +import React, { useState, useEffect } from 'react'; +import { + Card, + CardContent, + CardHeader, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Skeleton +} from '@/components/ui'; +import { toast } from '@/components/ui/toast'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Challenge, + ChallengeType, + ReviewFetchParams +} from '../types/challenge'; +import { ReviewService } from '../services/reviewService'; +import { useNavigate } from 'react-router-dom'; +import { ErrorBoundary } from '@/components/ui/error-boundary'; + +export const ActiveReviewsPage: React.FC = () => { + const [challengeTypes, setChallengeTypes] = useState([]); + const [selectedType, setSelectedType] = useState('CODE'); + const [reviews, setReviews] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [pagination, setPagination] = useState({ + page: 1, + pageSize: 10, + total: 0 + }); + const navigate = useNavigate(); + + const fetchData = async () => { + setIsLoading(true); + setError(null); + try { + // Fetch challenge types + const types = await ReviewService.getChallengeTypes(); + setChallengeTypes(types); + + // Fetch reviews + const reviewParams: ReviewFetchParams = { + type: selectedType, + page: pagination.page, + pageSize: pagination.pageSize + }; + const fetchedReviews = await ReviewService.fetchReviews(reviewParams); + setReviews(fetchedReviews.data); + setPagination(prev => ({ + ...prev, + total: fetchedReviews.total + })); + } catch (err) { + const errorMessage = err instanceof Error + ? err.message + : 'An unexpected error occurred while fetching reviews'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [selectedType, pagination.page]); + + const getTimeLeftColor = (timeLeft: string) => { + if (timeLeft.startsWith('-')) return 'destructive'; + const timeValue = parseInt(timeLeft); + if (timeValue < 1) return 'warning'; + return 'success'; + }; + + const handleChallengeSelect = (challengeId: string) => { + navigate(`/reviews/${challengeId}`); + }; + + const handlePageChange = (newPage: number) => { + setPagination(prev => ({ ...prev, page: newPage })); + }; + + if (error) { + return ( + + +
+

Error Loading Reviews

+

{error}

+ +
+
+
+ ); + } + + return ( + Something went wrong}> + + +
+

Active Reviews

+ +
+
+ + {isLoading ? ( + + + + {['#', 'Project', 'Phase', 'Time Left', 'Review Progress', 'Actions'].map(header => ( + {header} + ))} + + + + {[...Array(5)].map((_, index) => ( + + {[...Array(6)].map((_, cellIndex) => ( + + + + ))} + + ))} + +
+ ) : reviews.length === 0 ? ( +
+

No active reviews found

+
+ ) : ( + <> + + + + # + Project + Phase + Time Left + Review Progress + Actions + + + + {reviews.map((review, index) => ( + + {(pagination.page - 1) * pagination.pageSize + index + 1} + handleChallengeSelect(review.id)} + > + {review.title} + + {review.currentPhase} + + + {review.timeLeft} + + + +
+
+
+ + + + + + ))} + +
+ {pagination.total > pagination.pageSize && ( +
+
+ + + Page {pagination.page} of {Math.ceil(pagination.total / pagination.pageSize)} + + +
+
+ )} + + )} +
+
+
+ ); +}; diff --git a/src/apps/challenge-review-details-page (1).ts b/src/apps/challenge-review-details-page (1).ts new file mode 100644 index 000000000..fa61137a3 --- /dev/null +++ b/src/apps/challenge-review-details-page (1).ts @@ -0,0 +1,201 @@ +// src/apps/review/pages/challenge-review-details-page.tsx +import React, { useState, useEffect } from 'react'; +import { + Card, + CardContent, + CardHeader, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Skeleton +} from '@/components/ui'; +import { toast } from '@/components/ui/toast'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Challenge, + ReviewSubmission +} from '../types/challenge'; +import { ReviewService } from '../services/reviewService'; +import { useParams, useNavigate } from 'react-router-dom'; +import { ErrorBoundary } from '@/components/ui/error-boundary'; + +export const ChallengeReviewDetailsPage: React.FC = () => { + const [challenge, setChallenge] = useState(null); + const [submissions, setSubmissions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { challengeId } = useParams<{ challengeId: string }>(); + const navigate = useNavigate(); + + const fetchChallengeDetails = async () => { + if (!challengeId) return; + + setIsLoading(true); + setError(null); + + try { + const fetchedChallenge = await ReviewService.fetchChallengeDetails(challengeId); + setChallenge(fetchedChallenge); + + const fetchedSubmissions = await ReviewService.fetchSubmissions(challengeId); + setSubmissions(fetchedSubmissions); + } catch (err) { + const errorMessage = err instanceof Error + ? err.message + : 'An unexpected error occurred while fetching challenge details'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchChallengeDetails(); + }, [challengeId]); + + const handleReviewSubmission = (submissionId: string) => { + navigate(`/reviews/${challengeId}/submissions/${submissionId}`); + }; + + if (error) { + return ( + + +
+

Error Loading Challenge Details

+

{error}

+ +
+
+
+ ); + } + + return ( + Something went wrong}> + + {isLoading ? ( + + +
+ + + +
+
+ ) : challenge ? ( + +
+

{challenge.title}

+
+ Phase: {challenge.currentPhase} + Phase End: {challenge.phaseEndDate} + + Time Left: {challenge.timeLeft} + +
+
+
+ + +
+
+ ) : null} + + + + + Registration + Submission / Screening + Review / Appeals + Winners + Action + + + {isLoading ? ( + + + + {['Submission ID', 'Handle', 'Review Date', 'Score', 'Appeals', 'Actions'].map(header => ( + {header} + ))} + + + + {[...Array(5)].map((_, index) => ( + + {[...Array(6)].map((_, cellIndex) => ( + + + + ))} + + ))} + +
+ ) : submissions.length === 0 ? ( +
+

No submissions found

+
+ ) : ( + + + + Submission ID + Handle + Review Date + Score + Appeals + Actions + + + + {submissions.map(submission => ( + + {submission.id} + + {submission.handle} + + {submission.reviewDate || 'Not Reviewed'} + + {submission.score ? + {submission.score.toFixed(2)} + : 'N/A'} + + + + {submission.appealsMade} / {submission.maxAppeals} + + + + + + + ))} + +
+ )} +
+
+
+
+
+ ); +}; diff --git a/src/apps/challenge-review-edit-page (1).ts b/src/apps/challenge-review-edit-page (1).ts new file mode 100644 index 000000000..a34f45a94 --- /dev/null +++ b/src/apps/challenge-review-edit-page (1).ts @@ -0,0 +1,382 @@ +// src/apps/review/pages/challenge-review-edit-page.tsx +import React, { useState, useEffect, useMemo } from 'react'; +import { + Card, + CardContent, + CardHeader, + Button, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, + DialogClose +} from '@/components/ui'; +import { toast } from '@/components/ui/toast'; +import { Badge } from '@/components/ui/badge'; +import { + Scorecard, + ScorecardSection, + ScorecardQuestion, + QuestionResponse +} from '../types/challenge'; +import { ReviewService } from '../services/reviewService'; +import { useParams, useNavigate } from 'react-router-dom'; +import { MarkdownEditor } from '../components/markdown-editor'; +import { ErrorBoundary } from '@/components/ui/error-boundary'; + +export const ChallengeReviewEditPage: React.FC = () => { + const [scorecard, setScorecard] = useState(null); + const [isViewMode, setIsViewMode] = useState(false); + const [totalScore, setTotalScore] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [showCancelDialog, setShowCancelDialog] = useState(false); + const { challengeId, submissionId } = useParams<{ + challengeId: string, + submissionId: string + }>(); + const navigate = useNavigate(); + + const validateReview = useMemo(() => { + if (!scorecard) return false; + + // Check if all questions have a score + return scorecard.sections.every(section => + section.questions.every(question => + question.score !== undefined && question.score !== null + ) + ); + }, [scorecard]); + + useEffect(() => { + const fetchScorecardAndStatus = async () => { + if (!submissionId) return; + + setIsLoading(true); + setError(null); + + try { + const fetchedScorecard = await ReviewService.fetchScorecard(submissionId); + setScorecard(fetchedScorecard); + + const reviewStatus = await ReviewService.getReviewStatus(submissionId); + setIsViewMode(reviewStatus === 'completed'); + + // Calculate initial total score + const initialTotalScore = fetchedScorecard.sections.reduce((total, section) => + total + section.questions.reduce((sectionTotal, q) => + sectionTotal + ((q.score || 0) * q.weight), 0), 0); + + setTotalScore(initialTotalScore); + } catch (err) { + const errorMessage = err instanceof Error + ? err.message + : 'An unexpected error occurred while fetching scorecard'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + fetchScorecardAndStatus(); + }, [submissionId]); + + const handleScoreChange = (sectionIndex: number, questionIndex: number, value: number) => { + if (!scorecard) return; + + const updatedScorecard = {...scorecard}; + const question = updatedScorecard.sections[sectionIndex].questions[questionIndex]; + question.score = value; + + // Recalculate total score + const newTotalScore = updatedScorecard.sections.reduce((total, section) => + total + section.questions.reduce((sectionTotal, q) => + sectionTotal + ((q.score || 0) * q.weight), 0), 0); + + setTotalScore(newTotalScore); + setScorecard(updatedScorecard); + }; + + const addQuestionResponse = (sectionIndex: number, questionIndex: number) => { + if (!scorecard) return; + + const updatedScorecard = {...scorecard}; + const question = updatedScorecard.sections[sectionIndex].questions[questionIndex]; + + const newResponse: QuestionResponse = { + id: `response-${(question.responses?.length || 0) + 1}`, + type: 'comment', + comment: '' + }; + + if (!question.responses) { + question.responses = []; + } + question.responses.push(newResponse); + + setScorecard(updatedScorecard); + }; + + const updateQuestionResponse = ( + sectionIndex: number, + questionIndex: number, + responseIndex: number, + comment: string + ) => { + if (!scorecard) return; + + const updatedScorecard = {...scorecard}; + const responses = updatedScorecard.sections[sectionIndex] + .questions[questionIndex].responses; + + if (responses && responses[responseIndex]) { + responses[responseIndex].comment = comment; + } + + setScorecard(updatedScorecard); + }; + + const renderQuestionInput = ( + sectionIndex: number, + questionIndex: number, + question: ScorecardQuestion + ) => { + if (isViewMode) { + return {question.score?.toFixed(2)}; + } + + switch (question.responseType) { + case 'numeric': + return ( + + ); + case 'boolean': + return ( + + ); + default: + return null; + } + }; + + const renderResponseEditor = ( + sectionIndex: number, + questionIndex: number, + question: ScorecardQuestion + ) => { + if (isViewMode) return null; + + return ( +
+ + {question.responses?.map((response, responseIndex) => ( +
+ updateQuestionResponse( + sectionIndex, + questionIndex, + responseIndex, + value + )} + readOnly={isViewMode} + /> +
+ ))} +
+ ); + }; + + const handleSubmitReview = async () => { + if (!validateReview) { + toast.error('Please complete all required scoring'); + return; + } + + try { + if (!submissionId || !scorecard) return; + + await ReviewService.submitReview(submissionId, { + scorecard, + totalScore + }); + + toast.success('Review submitted successfully'); + navigate(`/reviews/${challengeId}`); + } catch (error) { + const errorMessage = error instanceof Error + ? error.message + : 'An unexpected error occurred while submitting review'; + toast.error(errorMessage); + console.error('Error submitting review:', error); + } + }; + + const handleSaveDraft = async () => { + try { + if (!submissionId || !scorecard) return; + + await ReviewService.saveDraftReview(submissionId, { + scorecard, + totalScore + }); + + toast.success('Draft saved successfully'); + } catch (error) { + const errorMessage = error instanceof Error + ? error.message + : 'An unexpected error occurred while saving draft'; + toast.error(errorMessage); + console.error('Error saving draft:', error); + } + }; + + const handleCancel = () => { + setShowCancelDialog(true); + }; + + const confirmCancel = () => { + navigate(`/reviews/${challengeId}`); + }; + + if (isLoading) return
Loading...
; + if (error) return
Error: {error}
; + if (!scorecard) return
No scorecard found
; + + return ( + Something went wrong}> + + +
+

{scorecard.title}

+
+ + Total Score: {totalScore.toFixed(2)} + + +
+
+
+ + + {scorecard.sections.map((section: ScorecardSection, sectionIndex) => ( + + {section.title} + + {section.questions.map((question: ScorecardQuestion, questionIndex) => ( +
+
+
+ {question.title} + + (Weight: {question.weight}) + +
+ {renderQuestionInput(sectionIndex, questionIndex, question)} +
+ {question.guideline && ( +

+ Guideline: {question.guideline} +

+ )} + {renderResponseEditor(sectionIndex, questionIndex, question)} +
+ ))} +
+
+ ))} +
+ {!isViewMode && ( +
+ + + +
+ )} +
+
+ + {/* Cancel Confirmation Dialog */} + + + + Confirm Cancel + + Are you sure you want to cancel? Any unsaved changes will be lost. + + + + + + + + + + +
+ ); +}; diff --git a/src/apps/challenge-types.ts b/src/apps/challenge-types.ts new file mode 100644 index 000000000..bab4d960b --- /dev/null +++ b/src/apps/challenge-types.ts @@ -0,0 +1,60 @@ +// src/apps/review/types/challenge.ts +export interface ChallengeType { + value: string; + label: string; +} + +export interface Challenge { + id: string; + title: string; + currentPhase: string; + phaseEndDate: string; + timeLeft: string; + reviewProgress: number; + type: string; +} + +export interface ReviewSubmission { + id: string; + handle: string; + userRatingColor: string; + reviewDate: string | null; + score: number | null; + appealsMade: number; + maxAppeals: number; + reviewStatus: 'pending' | 'completed'; +} + +export interface Scorecard { + id: string; + title: string; + sections: ScorecardSection[]; +} + +export interface ScorecardSection { + id: string; + title: string; + questions: ScorecardQuestion[]; +} + +export interface ScorecardQuestion { + id: string; + title: string; + weight: number; + responseType: 'numeric' | 'boolean'; + guideline?: string; + score?: number; + responses?: QuestionResponse[]; +} + +export interface QuestionResponse { + id: string; + type: 'comment'; + comment: string; +} + +export interface ReviewFetchParams { + type: string; + page: number; + pageSize: number; +} diff --git a/src/apps/deployment-readme.md b/src/apps/deployment-readme.md new file mode 100644 index 000000000..237507316 --- /dev/null +++ b/src/apps/deployment-readme.md @@ -0,0 +1,100 @@ +# Topcoder Review App - Deployment Guide + +## Environment Configuration + +### Required Environment Variables +Create a `.env` file in the project root with the following configuration: + +```env +# Topcoder API Configuration +REACT_APP_TOPCODER_API_URL=https://api.topcoder.com/v5 +REACT_APP_TOPCODER_API_KEY=your_api_key_here + +# Deployment Environment +NODE_ENV=production + +# Additional Configuration Options +REACT_APP_CHALLENGE_TYPES=CODE,DESIGN,DATA_SCIENCE +REACT_APP_DEFAULT_PAGE_SIZE=10 +REACT_APP_REVIEW_TIMEOUT_MINUTES=30 + +# Optional: Performance and Logging +REACT_APP_ENABLE_PERFORMANCE_MONITORING=false +REACT_APP_LOGGING_LEVEL=info +``` + +### Environment Variable Checklist +- [ ] Replace `your_api_key_here` with a valid Topcoder API key +- [ ] Verify API URL is correct for your deployment environment +- [ ] Configure challenge types as needed +- [ ] Set appropriate page size and timeout values + +## Deployment Verification Checklist + +### API Integration Points +1. **Authentication** + - [ ] Verify API key works correctly + - [ ] Test authentication for different user roles + - [ ] Ensure proper error handling for invalid credentials + +2. **Challenge Type Support** + - [ ] Test CODE challenges + - [ ] Test DESIGN challenges + - [ ] Test DATA_SCIENCE challenges + - [ ] Verify challenge type filtering works as expected + +3. **Review Process Validation** + - [ ] Submit reviews for different challenge types + - [ ] Test draft saving functionality + - [ ] Verify review status transitions + - [ ] Check score calculation accuracy + +### Performance and Compatibility +- [ ] Test with Node.js 16+ +- [ ] Verify npm 8+ compatibility +- [ ] Check responsive design on multiple devices +- [ ] Validate cross-browser compatibility + +## Deployment Steps + +### 1. Prepare Environment +```bash +# Install dependencies +npm install + +# Build the application +npm run build + +# Run production build locally (optional) +npm run start:production +``` + +### 2. Deployment Strategies +- **Docker**: Use included Dockerfile for containerized deployment +- **Cloud Platforms**: Compatible with AWS, Azure, and Google Cloud +- **Static Hosting**: Build files can be deployed to static hosting services + +### 3. Post-Deployment Checks +- Verify all routes are working +- Check network requests in browser developer tools +- Test user authentication +- Validate challenge list and review functionality + +## Troubleshooting +- If API calls fail, double-check API key and URL +- Clear browser cache if UI seems inconsistent +- Check browser console for any error messages +- Ensure network connectivity to Topcoder API + +## Monitoring and Logging +Enable performance monitoring and logging in production by setting: +```env +REACT_APP_ENABLE_PERFORMANCE_MONITORING=true +REACT_APP_LOGGING_LEVEL=warn +``` + +## Security Considerations +- Never commit API keys to version control +- Use environment-specific `.env` files +- Implement proper access controls +- Regularly rotate API credentials diff --git a/src/apps/markdown-editor.ts b/src/apps/markdown-editor.ts new file mode 100644 index 000000000..d7e9d07c6 --- /dev/null +++ b/src/apps/markdown-editor.ts @@ -0,0 +1,87 @@ +// src/apps/review/components/markdown-editor.tsx +import React, { useRef, useState } from 'react'; +import { Editor, Viewer } from '@toast-ui/react-editor'; +import '@toast-ui/editor/dist/toastui-editor.css'; +import { Button } from '@/components/ui/button'; + +interface MarkdownEditorProps { + initialValue?: string; + onChange?: (value: string) => void; + onBlur?: (value: string) => void; + readOnly?: boolean; + height?: string; + placeholder?: string; +} + +export const MarkdownEditor: React.FC = ({ + initialValue = '', + onChange, + onBlur, + readOnly = false, + height = '300px', + placeholder = 'Write your comments here...' +}) => { + const editorRef = useRef(null); + const [isPreviewMode, setIsPreviewMode] = useState(false); + + const handleChange = () => { + if (editorRef.current && onChange) { + onChange(editorRef.current.getInstance().getMarkdown()); + } + }; + + const handleBlur = () => { + if (editorRef.current && onBlur) { + onBlur(editorRef.current.getInstance().getMarkdown()); + } + }; + + const togglePreviewMode = () => { + setIsPreviewMode(!isPreviewMode); + }; + + if (readOnly) { + return ( +
+ +
+ ); + } + + return ( +
+
+ +
+ {isPreviewMode ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/src/apps/review-routes.ts b/src/apps/review-routes.ts new file mode 100644 index 000000000..f2aa282c9 --- /dev/null +++ b/src/apps/review-routes.ts @@ -0,0 +1,21 @@ +// src/apps/review/routes.tsx +import React from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { ActiveReviewsPage } from './pages/active-reviews-page'; +import { ChallengeReviewDetailsPage } from './pages/challenge-review-details-page'; +import { ChallengeReviewEditPage } from './pages/challenge-review-edit-page'; + +export const ReviewRoutes: React.FC = () => { + return ( + + } /> + } /> + } + /> + {/* Redirect root of reviews to the active reviews page */} + } /> + + ); +}; diff --git a/src/apps/review-service.ts b/src/apps/review-service.ts new file mode 100644 index 000000000..f59f22858 --- /dev/null +++ b/src/apps/review-service.ts @@ -0,0 +1,147 @@ +// src/apps/review/services/reviewService.ts +import { + Challenge, + ChallengeType, + ReviewSubmission, + Scorecard, + ReviewFetchParams +} from '../types/challenge'; + +// Mock Challenge Types +const CHALLENGE_TYPES: ChallengeType[] = [ + { value: 'CODE', label: 'Code Challenge' }, + { value: 'DESIGN', label: 'Design Challenge' }, + { value: 'DATA_SCIENCE', label: 'Data Science Challenge' } +]; + +// Mock Challenges Data +const MOCK_CHALLENGES: Challenge[] = [ + { + id: 'challenge-001', + title: 'Advanced React Component Design', + currentPhase: 'Review', + phaseEndDate: 'Mar 30, 2024, 08:00 PM', + timeLeft: '4d 6h', + reviewProgress: 60, + type: 'CODE' + }, + { + id: 'challenge-002', + title: 'Machine Learning Classification Model', + currentPhase: 'Review', + phaseEndDate: 'Apr 2, 2024, 12:00 PM', + timeLeft: '2d 12h', + reviewProgress: 80, + type: 'DATA_SCIENCE' + } +]; + +// Mock Submissions Data +const MOCK_SUBMISSIONS: ReviewSubmission[] = [ + { + id: 'submission-001', + handle: 'CodeMaster2024', + userRatingColor: 'text-blue-600', + reviewDate: '2024-03-25T10:30:00Z', + score: 92.5, + appealsMade: 0, + maxAppeals: 3, + reviewStatus: 'pending' + }, + { + id: 'submission-002', + handle: 'DataNinja', + userRatingColor: 'text-green-600', + reviewDate: null, + score: null, + appealsMade: 0, + maxAppeals: 3, + reviewStatus: 'pending' + } +]; + +// Mock Scorecard Data +const MOCK_SCORECARD: Scorecard = { + id: 'scorecard-001', + title: 'React Component Review Scorecard', + sections: [ + { + id: 'section-001', + title: 'Code Quality', + questions: [ + { + id: 'question-001', + title: 'Code follows best practices', + weight: 2.0, + responseType: 'numeric', + guideline: 'Evaluate adherence to React best practices and clean code principles' + }, + { + id: 'question-002', + title: 'Component is performant', + weight: 1.5, + responseType: 'numeric', + guideline: 'Check for efficient rendering and minimal re-renders' + } + ] + }, + { + id: 'section-002', + title: 'Design Implementation', + questions: [ + { + id: 'question-003', + title: 'Matches design specifications', + weight: 2.0, + responseType: 'boolean', + guideline: 'Verify pixel-perfect implementation of provided design' + } + ] + } + ] +}; + +export class ReviewService { + static async getChallengeTypes(): Promise { + return new Promise(resolve => setTimeout(() => resolve(CHALLENGE_TYPES), 500)); + } + + static async fetchReviews(params: ReviewFetchParams): Promise<{ data: Challenge[], total: number }> { + const filteredChallenges = MOCK_CHALLENGES.filter(c => c.type === params.type); + return new Promise(resolve => + setTimeout(() => resolve({ + data: filteredChallenges.slice((params.page - 1) * params.pageSize, params.page * params.pageSize), + total: filteredChallenges.length + }), 500) + ); + } + + static async fetchChallengeDetails(challengeId: string): Promise { + return new Promise((resolve, reject) => + setTimeout(() => { + const challenge = MOCK_CHALLENGES.find(c => c.id === challengeId); + challenge ? resolve(challenge) : reject(new Error('Challenge not found')); + }, 500) + ); + } + + static async fetchSubmissions(challengeId: string): Promise { + return new Promise(resolve => setTimeout(() => resolve(MOCK_SUBMISSIONS), 500)); + } + + static async fetchScorecard(submissionId: string): Promise { + return new Promise(resolve => setTimeout(() => resolve(MOCK_SCORECARD), 500)); + } + + static async getReviewStatus(submissionId: string): Promise<'pending' | 'completed'> { + return new Promise(resolve => setTimeout(() => resolve('pending'), 500)); + } + + static async submitReview(submissionId: string, reviewData: any): Promise { + return new Promise(resolve => setTimeout(resolve, 500)); + } + + static async saveDraftReview(submissionId: string, reviewData: any): Promise { + return new Promise(resolve => setTimeout(resolve, 500)); + } +}