Skip to content
Open
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
1,683 changes: 1,653 additions & 30 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"check-all": "npm run type-check && npm run lint && npm run format:check"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -23,7 +24,10 @@
"react-cookie": "^8.0.1",
"react-dom": "^18.3.1",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.9.4",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
Expand Down
10 changes: 10 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import SignupPage from '@/pages/auth/SignupPage'
import NotFoundPage from '@/pages/NotFoundPage'
import LocksPage from '@/pages/locks/LocksPage'
import ProtectedRoute from '@/components/ProtectedRoute'
import ProblemsPage from '@/pages/problems/ProblemList'
import ProblemEditor from '@/pages/problems/ProblemEditor'
import ProblemView from '@/pages/problems/ProblemView'
import ContestsPage from '@/pages/contests/ContestsPage'
import ContestView from '@/pages/contests/ContestView'


function App() {
Expand All @@ -19,6 +24,11 @@ function App() {
<Route path="/signup" element={<SignupPage />} />
<Route element={<ProtectedRoute />}>
<Route path="/locks" element={<LocksPage />} />
<Route path="/contests/:id" element={<ContestView />} />
<Route path="/problems" element={<ProblemsPage />} />
<Route path="/problems/:id" element={<ProblemView />} />
<Route path="/problems/:id/edit" element={<ProblemEditor />} />
<Route path="/contests" element={<ContestsPage />} />
{/* TODO: Protected Routes */}
{/* <Route path="/contests" element={<ContestsPage />} /> */}
{/* <Route path="/problems" element={<ProblemsPage />} /> */}
Expand Down
383 changes: 383 additions & 0 deletions src/components/contests/CreateContestModal.tsx

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions src/components/contests/LockPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useState, useEffect } from 'react'
import { lockService } from '@/services/lock.service'
import { Lock } from '@/types/lock.types'
import { FaSearch, FaLock, FaClock, FaCheckCircle, FaSpinner } from 'react-icons/fa'
import { cn, formatDateTime } from '@/lib/utils'

interface Props {
selectedId: string | null
onSelect: (lock: Lock | null) => void
}

export default function LockPicker({ selectedId, onSelect }: Props) {
const [locks, setLocks] = useState<Lock[]>([])
const [search, setSearch] = useState('')
const [loading, setLoading] = useState(false)

useEffect(() => {
const timeoutId = setTimeout(() => fetchLocks(), 300)
return () => clearTimeout(timeoutId)
}, [search])

const fetchLocks = async () => {
setLoading(true)
try {
const res = await lockService.searchLocks({
lock_name: search || undefined,
page_number: 1,
page_size: 20
})
setLocks(res)
} catch(e) {
console.error("Failed to load locks")
} finally {
setLoading(false)
}
}

return (
<div className="bg-neutral-950 border border-neutral-800 rounded-xl overflow-hidden flex flex-col h-60">
<div className="p-3 border-b border-neutral-800 flex items-center gap-2 bg-neutral-900/50">
<FaSearch className="text-neutral-500 text-xs" />
<input
className="bg-transparent border-none text-xs text-white placeholder-neutral-600 focus:outline-none flex-1"
placeholder="Search locks by name..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
{loading && <FaSpinner className="animate-spin text-neutral-500 text-xs"/>}
</div>

<div className="flex-1 overflow-y-auto p-2 space-y-1 custom-scrollbar">
{locks.map(lock => {
const isSelected = selectedId === lock.lock_id
const isTimer = lock.lock_type === 'timer'
return (
<button
key={lock.lock_id}
type="button"
// CHANGE: Pass lock object
onClick={() => onSelect(lock)}
className={cn(
"w-full text-left p-3 rounded-lg border flex justify-between items-center transition-all group",
isSelected
? "bg-amber-500/10 border-amber-500/50"
: "bg-neutral-900 border-transparent hover:border-neutral-700"
)}
>
<div>
<div className={cn("text-xs font-bold", isSelected ? "text-amber-500" : "text-neutral-300")}>
{lock.name}
</div>
<div className="text-[10px] text-neutral-500 flex items-center gap-1.5 mt-0.5">
{isTimer ? <FaClock size={8}/> : <FaLock size={8}/>}
<span className="font-mono">{isTimer && lock.timeout ? `Until: ${formatDateTime(lock.timeout)}` : 'Manual'}</span>
</div>
</div>
{isSelected && <FaCheckCircle className="text-amber-500" />}
</button>
)
})}
</div>
</div>
)
}
146 changes: 146 additions & 0 deletions src/components/problems/CreateProblemModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { FaTimes, FaCube, FaExclamationCircle, FaSpinner } from 'react-icons/fa'
import { problemService } from '@/services/problem.service'
import { getErrorMessage } from '@/config/axios'

interface Props {
isOpen: boolean
onClose: () => void
onSuccess: () => void
}

export default function CreateProblemModal({ isOpen, onClose, onSuccess }: Props) {
const navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

const [formData, setFormData] = useState({
title: '',
difficulty: 800,
evaluator: 'codeforces',
lock_id: ''
})

if (!isOpen) return null

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError(null)

try {
const res = await problemService.createProblemDraft({
title: formData.title,
difficulty: Number(formData.difficulty),
evaluator: 'codeforces',
lock_id: formData.lock_id || null
})

onSuccess()

navigate(`/problems/${res.problem.id}/edit`)
onClose()
} catch (err) {
setError(getErrorMessage(err))
} finally {
setLoading(false)
}
}

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 animate-in fade-in duration-200">
<div className="w-full max-w-md bg-neutral-950 border border-neutral-800 rounded-2xl shadow-2xl overflow-hidden transform scale-100 transition-all">

<div className="p-6 border-b border-neutral-800 flex justify-between items-center bg-neutral-900/50">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<FaCube className="text-blue-500" /> New Problem
</h2>
<button onClick={onClose} className="text-neutral-500 hover:text-white p-2 rounded-full hover:bg-neutral-800 transition-colors">
<FaTimes />
</button>
</div>

<form onSubmit={handleSubmit} className="p-6 space-y-5">
<div>
<label className="block text-xs font-bold text-neutral-500 uppercase mb-2 ml-1">Problem Title</label>
<input
required
value={formData.title}
onChange={e => setFormData({...formData, title: e.target.value})}
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-4 py-3 text-neutral-200 focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/50 outline-none transition-all placeholder-neutral-700"
placeholder="e.g. Dynamic Arrays II"
autoFocus
/>
</div>

<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-neutral-500 uppercase mb-2 ml-1">Difficulty</label>
<input
type="number"
required
min={800}
max={3500}
step={100}
value={formData.difficulty}
onChange={e => setFormData({...formData, difficulty: Number(e.target.value)})}
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-4 py-3 text-neutral-200 focus:border-blue-500/50 outline-none transition-all"
/>
</div>
<div>
<label className="block text-xs font-bold text-neutral-500 uppercase mb-2 ml-1">Evaluator</label>
<div className="relative">
<select
disabled
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-4 py-3 text-neutral-500 focus:outline-none appearance-none cursor-not-allowed"
>
<option>Codeforces Bot</option>
</select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 text-neutral-600 pointer-events-none text-xs">▼</div>
</div>
</div>
</div>

<div>
<label className="block text-xs font-bold text-neutral-500 uppercase mb-2 ml-1">
Access Lock <span className="font-normal normal-case opacity-50 ml-1">(Optional)</span>
</label>
<input
value={formData.lock_id}
onChange={e => setFormData({...formData, lock_id: e.target.value})}
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-4 py-3 text-neutral-300 font-mono text-sm focus:border-blue-500/50 outline-none transition-all placeholder-neutral-700"
placeholder="Paste UUID..."
/>
</div>

{error && (
<div className="flex items-center gap-3 bg-red-950/20 border border-red-900/30 p-3 rounded-xl text-red-400 text-sm">
<FaExclamationCircle className="shrink-0" />
{error}
</div>
)}

<div className="pt-2 flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-5 py-2.5 text-sm font-bold text-neutral-400 hover:text-white hover:bg-neutral-800 rounded-xl transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="px-8 py-2.5 bg-blue-600 hover:bg-blue-500 text-white font-bold rounded-xl disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-900/20 active:scale-95 transition-all"
>
{loading ? (
<span className="flex items-center gap-2"><FaSpinner className="animate-spin" /> Creating...</span>
) : 'Create Draft'}
</button>
</div>
</form>
</div>
</div>
)
}
30 changes: 30 additions & 0 deletions src/components/problems/MarkdownRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { cn } from '@/lib/utils'

interface Props {
content: string
className?: string
}

export default function MarkdownRenderer({ content, className }: Props) {
return (
<article className={cn("prose prose-invert prose-neutral max-w-none text-sm", className)}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
pre: ({node, ...props}) => (
<div className="bg-black/50 p-4 rounded-lg border border-neutral-800 my-4 overflow-x-auto font-mono text-xs">
<pre {...props} />
</div>
),
code: ({node, ...props}) => (
<code className="bg-neutral-800 px-1 py-0.5 rounded text-amber-200/90 font-mono text-xs" {...props} />
)
}}
>
{content}
</ReactMarkdown>
</article>
)
}
Loading