feat: Display problem titles and links in submission lists and persist code drafts in local storage.

This commit is contained in:
n.tolstov 2025-12-01 00:54:24 +03:00
parent c1ac522574
commit 0a9c5aa5c9
5 changed files with 104 additions and 4 deletions

View File

@ -141,7 +141,11 @@ async def get_my_submissions(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = select(Submission).where(Submission.user_id == current_user.id)
query = (
select(Submission)
.options(selectinload(Submission.problem))
.where(Submission.user_id == current_user.id)
)
if problem_id:
query = query.where(Submission.problem_id == problem_id)
@ -151,7 +155,25 @@ async def get_my_submissions(
query = query.order_by(Submission.created_at.desc())
result = await db.execute(query)
return result.scalars().all()
submissions = result.scalars().all()
# Add problem_title to each submission
return [
SubmissionListResponse(
id=s.id,
problem_id=s.problem_id,
problem_title=s.problem.title if s.problem else None,
contest_id=s.contest_id,
language_name=s.language_name,
status=s.status,
score=s.score,
total_points=s.total_points,
tests_passed=s.tests_passed,
tests_total=s.tests_total,
created_at=s.created_at,
)
for s in submissions
]
@router.get("/problem/{problem_id}", response_model=list[SubmissionListResponse])

View File

@ -33,6 +33,8 @@ class SubmissionResponse(BaseModel):
class SubmissionListResponse(BaseModel):
id: int
problem_id: int
problem_title: str | None = None
contest_id: int | None = None
language_name: str | None
status: str
score: int

View File

@ -220,6 +220,39 @@ export default function ProblemPage() {
const selectedLanguage = languages.find((l) => l.id.toString() === selectedLanguageId);
const isJudging = lastSubmission?.status === "pending" || lastSubmission?.status === "judging";
// localStorage key for this problem's draft
const draftKey = `code_draft_problem_${problemId}`;
// Load saved draft from localStorage
useEffect(() => {
if (typeof window !== "undefined") {
try {
const saved = localStorage.getItem(draftKey);
if (saved) {
const draft = JSON.parse(saved);
if (draft.code) setCode(draft.code);
if (draft.languageId) setSelectedLanguageId(draft.languageId);
}
} catch (e) {
// Ignore parse errors
}
}
}, [draftKey]);
// Save draft to localStorage when code or language changes
useEffect(() => {
if (typeof window !== "undefined" && (code || selectedLanguageId)) {
try {
localStorage.setItem(
draftKey,
JSON.stringify({ code, languageId: selectedLanguageId })
);
} catch (e) {
// Ignore storage errors
}
}
}, [code, selectedLanguageId, draftKey]);
useEffect(() => {
Promise.all([
api.getProblem(problemId),
@ -230,7 +263,31 @@ export default function ProblemPage() {
setProblem(problemData);
setLanguages(languagesData);
setSubmissions(submissionsData);
// Default to Python
// Check if we have a saved language preference
const savedDraft = typeof window !== "undefined"
? localStorage.getItem(draftKey)
: null;
if (savedDraft) {
try {
const draft = JSON.parse(savedDraft);
if (draft.languageId) {
// Verify the saved language still exists
const savedLang = languagesData.find(
(l) => l.id.toString() === draft.languageId
);
if (savedLang) {
setSelectedLanguageId(draft.languageId);
return;
}
}
} catch (e) {
// Fall through to default
}
}
// Default to Python if no saved preference
const python = languagesData.find((l) =>
l.name.toLowerCase().includes("python")
);
@ -241,7 +298,7 @@ export default function ProblemPage() {
toast.error("Ошибка загрузки задачи");
})
.finally(() => setIsLoading(false));
}, [problemId]);
}, [problemId, draftKey]);
// Poll for submission status
useEffect(() => {
@ -304,7 +361,11 @@ export default function ProblemPage() {
};
const handleReset = () => {
if (!confirm("Очистить код? Черновик будет удалён.")) return;
setCode("");
if (typeof window !== "undefined") {
localStorage.removeItem(draftKey);
}
toast.info("Код очищен");
};

View File

@ -333,6 +333,7 @@ export default function SubmissionsPage() {
<TableHeader>
<TableRow className="border-[var(--color-border)]">
<TableHead className="w-20 font-mono text-[var(--color-neon-cyan)]">ID</TableHead>
<TableHead className="font-mono text-[var(--color-neon-cyan)]">Задача</TableHead>
<TableHead className="font-mono text-[var(--color-neon-cyan)]">Время</TableHead>
<TableHead className="font-mono text-[var(--color-neon-cyan)]">Язык</TableHead>
<TableHead className="text-center font-mono text-[var(--color-neon-cyan)]">Статус</TableHead>
@ -362,6 +363,18 @@ export default function SubmissionsPage() {
<TableCell className="font-mono text-muted-foreground">
<span className="text-[var(--color-neon-purple)]">#</span>{submission.id}
</TableCell>
<TableCell>
{submission.problem_title ? (
<Link
href={`/contests/${submission.contest_id}/problems/${submission.problem_id}`}
className="text-[var(--color-neon-green)] hover:underline font-medium"
>
{submission.problem_title}
</Link>
) : (
<span className="text-muted-foreground"></span>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2 font-mono text-sm">
<Clock className="h-4 w-4 text-[var(--color-neon-cyan)]" />

View File

@ -93,6 +93,8 @@ export interface Submission {
export interface SubmissionListItem {
id: number;
problem_id: number;
problem_title: string | null;
contest_id: number | null;
language_name: string | null;
status: string;
score: number;