feat: Display problem titles and links in submission lists and persist code drafts in local storage.
This commit is contained in:
parent
c1ac522574
commit
0a9c5aa5c9
@ -141,7 +141,11 @@ async def get_my_submissions(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
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:
|
if problem_id:
|
||||||
query = query.where(Submission.problem_id == 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())
|
query = query.order_by(Submission.created_at.desc())
|
||||||
|
|
||||||
result = await db.execute(query)
|
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])
|
@router.get("/problem/{problem_id}", response_model=list[SubmissionListResponse])
|
||||||
|
|||||||
@ -33,6 +33,8 @@ class SubmissionResponse(BaseModel):
|
|||||||
class SubmissionListResponse(BaseModel):
|
class SubmissionListResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
problem_id: int
|
problem_id: int
|
||||||
|
problem_title: str | None = None
|
||||||
|
contest_id: int | None = None
|
||||||
language_name: str | None
|
language_name: str | None
|
||||||
status: str
|
status: str
|
||||||
score: int
|
score: int
|
||||||
|
|||||||
@ -220,6 +220,39 @@ export default function ProblemPage() {
|
|||||||
const selectedLanguage = languages.find((l) => l.id.toString() === selectedLanguageId);
|
const selectedLanguage = languages.find((l) => l.id.toString() === selectedLanguageId);
|
||||||
const isJudging = lastSubmission?.status === "pending" || lastSubmission?.status === "judging";
|
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(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
api.getProblem(problemId),
|
api.getProblem(problemId),
|
||||||
@ -230,7 +263,31 @@ export default function ProblemPage() {
|
|||||||
setProblem(problemData);
|
setProblem(problemData);
|
||||||
setLanguages(languagesData);
|
setLanguages(languagesData);
|
||||||
setSubmissions(submissionsData);
|
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) =>
|
const python = languagesData.find((l) =>
|
||||||
l.name.toLowerCase().includes("python")
|
l.name.toLowerCase().includes("python")
|
||||||
);
|
);
|
||||||
@ -241,7 +298,7 @@ export default function ProblemPage() {
|
|||||||
toast.error("Ошибка загрузки задачи");
|
toast.error("Ошибка загрузки задачи");
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, [problemId]);
|
}, [problemId, draftKey]);
|
||||||
|
|
||||||
// Poll for submission status
|
// Poll for submission status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -304,7 +361,11 @@ export default function ProblemPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
|
if (!confirm("Очистить код? Черновик будет удалён.")) return;
|
||||||
setCode("");
|
setCode("");
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.removeItem(draftKey);
|
||||||
|
}
|
||||||
toast.info("Код очищен");
|
toast.info("Код очищен");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -333,6 +333,7 @@ export default function SubmissionsPage() {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-[var(--color-border)]">
|
<TableRow className="border-[var(--color-border)]">
|
||||||
<TableHead className="w-20 font-mono text-[var(--color-neon-cyan)]">ID</TableHead>
|
<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="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>
|
<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">
|
<TableCell className="font-mono text-muted-foreground">
|
||||||
<span className="text-[var(--color-neon-purple)]">#</span>{submission.id}
|
<span className="text-[var(--color-neon-purple)]">#</span>{submission.id}
|
||||||
</TableCell>
|
</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>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2 font-mono text-sm">
|
<div className="flex items-center gap-2 font-mono text-sm">
|
||||||
<Clock className="h-4 w-4 text-[var(--color-neon-cyan)]" />
|
<Clock className="h-4 w-4 text-[var(--color-neon-cyan)]" />
|
||||||
|
|||||||
@ -93,6 +93,8 @@ export interface Submission {
|
|||||||
export interface SubmissionListItem {
|
export interface SubmissionListItem {
|
||||||
id: number;
|
id: number;
|
||||||
problem_id: number;
|
problem_id: number;
|
||||||
|
problem_title: string | null;
|
||||||
|
contest_id: number | null;
|
||||||
language_name: string | null;
|
language_name: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
score: number;
|
score: number;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user