Merge branch 'master' of gitlab.prodcontest.ru:team-15/project

This commit is contained in:
ITQ
2025-03-01 20:15:44 +03:00
8 changed files with 129 additions and 63 deletions
+7 -3
View File
@@ -4,6 +4,7 @@ from typing import Any
import jwt import jwt
from django.conf import settings from django.conf import settings
from django.http import HttpRequest from django.http import HttpRequest
from ninja.errors import AuthenticationError
from ninja.security import HttpBearer from ninja.security import HttpBearer
from apps.user.models import User from apps.user.models import User
@@ -11,9 +12,12 @@ from apps.user.models import User
class BearerAuth(HttpBearer): class BearerAuth(HttpBearer):
def authenticate(self, request: HttpRequest, token: str) -> Any | None: def authenticate(self, request: HttpRequest, token: str) -> Any | None:
data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) try:
if data["exp"] < datetime.datetime.now().timestamp(): data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
return None if data["exp"] < datetime.datetime.now().timestamp():
return None
except Exception:
raise AuthenticationError
user = User.objects.get(id=data["id"]) user = User.objects.get(id=data["id"])
return user return user
+2 -1
View File
@@ -1,5 +1,6 @@
from http import HTTPStatus as status from http import HTTPStatus as status
from django.contrib.auth.hashers import check_password
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from ninja import Router from ninja import Router
from ninja.errors import AuthenticationError from ninja.errors import AuthenticationError
@@ -46,7 +47,7 @@ def sign_in(request, data: LoginSchema):
user = User.objects.filter(email=data.email).first() user = User.objects.filter(email=data.email).first()
if not user: if not user:
raise AuthenticationError raise AuthenticationError
if not user.check_password(data.password): if not check_password(data.password, user.password):
raise AuthenticationError raise AuthenticationError
token = BearerAuth.generate_jwt(user) token = BearerAuth.generate_jwt(user)
@@ -1,21 +1,99 @@
import React, { useState } from 'react';
import { DataRush } from "@/components/ui/icons/datarush"; import { DataRush } from "@/components/ui/icons/datarush";
import { ChevronDown } from "lucide-react"; import { ChevronDown, User, Settings, BarChart2, LogOut } from "lucide-react";
import { Link } from "react-router"; import { Link } from "react-router";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetClose
} from "@/components/ui/sheet";
const Header = () => { const Header = () => {
const [isProfileOpen, setIsProfileOpen] = useState(false);
return ( return (
<header className="bg-card sticky top-0 z-30 flex h-[72px] w-full items-center justify-center"> <header className="bg-card sticky top-0 z-30 flex h-[72px] w-full items-center justify-center">
<div className="flex w-full max-w-5xl items-center justify-between"> <div className="flex w-full max-w-5xl items-center justify-between">
<Link to="/"> <Link to="/">
<DataRush /> <DataRush />
</Link> </Link>
<div className="flex items-center gap-1"> <div
<span className="text-lg font-semibold">itqdev</span> className="flex items-center gap-1 cursor-pointer hover:opacity-80 transition-opacity px-2 py-1 rounded-md"
onClick={() => setIsProfileOpen(true)}
>
<span className="text-lg font-semibold font-hse-sans">itqdev</span>
<ChevronDown size={20} /> <ChevronDown size={20} />
</div> </div>
</div> </div>
<Sheet open={isProfileOpen} onOpenChange={setIsProfileOpen}>
<SheetContent className="w-[300px] sm:w-[350px] p-0">
<SheetHeader className="border-b py-4 px-5">
<SheetTitle className="font-hse-sans text-lg font-medium">Профиль</SheetTitle>
</SheetHeader>
<div className="py-4 px-2">
<ProfileOption
icon={<User size={18} />}
label="Ваш профиль"
onClick={() => {
setIsProfileOpen(false);
}}
/>
<ProfileOption
icon={<Settings size={18} />}
label="Настройки"
onClick={() => {
setIsProfileOpen(false);
}}
/>
<ProfileOption
icon={<BarChart2 size={18} />}
label="Статистика"
onClick={() => {
setIsProfileOpen(false);
}}
/>
<div className="border-t mt-2 pt-2">
<ProfileOption
icon={<LogOut size={18} />}
label="Выйти"
onClick={() => {
setIsProfileOpen(false);
}}
/>
</div>
</div>
</SheetContent>
</Sheet>
</header> </header>
); );
}; };
interface ProfileOptionProps {
icon: React.ReactNode;
label: string;
onClick: () => void;
className?: string;
}
const ProfileOption: React.FC<ProfileOptionProps> = ({ icon, label, onClick, className }) => {
return (
<SheetClose asChild>
<button
className={`flex items-center gap-3 w-full px-3 py-2.5 rounded-md text-left transition-colors hover:bg-gray-100 ${className || ''}`}
onClick={onClick}
>
<span className="text-gray-600">{icon}</span>
<span className="font-hse-sans">{label}</span>
</button>
</SheetClose>
);
};
export { Header }; export { Header };
@@ -9,15 +9,15 @@ interface SolutionStatusProps {
const SolutionStatus: React.FC<SolutionStatusProps> = ({ solution }) => { const SolutionStatus: React.FC<SolutionStatusProps> = ({ solution }) => {
const getStatusText = (status: TaskStatus, score?: number, maxScore?: number) => { const getStatusText = (status: TaskStatus, score?: number, maxScore?: number) => {
switch (status) { switch (status) {
case 'checking': case TaskStatus.Checking:
return 'На проверке'; return 'На проверке';
case 'wrong': case TaskStatus.Wrong:
return 'Неверный ответ'; return 'Неверный ответ';
case 'correct': case TaskStatus.Correct:
return `Зачтено ${maxScore}/${maxScore} баллов`; return `Зачтено ${maxScore}/${maxScore} баллов`;
case 'partial': case TaskStatus.Partial:
return `Зачтено ${score}/${maxScore} баллов`; return `Зачтено ${score}/${maxScore} баллов`;
case 'uncleared': case TaskStatus.Uncleared:
return 'Не решено'; return 'Не решено';
default: default:
return ''; return '';
@@ -1,21 +1,21 @@
import { TaskStatus } from "@/shared/types"; import { TaskStatus } from "@/shared/types";
const getTaskBgColor = (status: TaskStatus): string => { const getTaskBgColor = (status: TaskStatus): string => {
switch (status) { switch (status) {
case "uncleared": return "bg-[var(--color-task-uncleared)]"; case TaskStatus.Uncleared: return "bg-[var(--color-task-uncleared)]";
case "checking": return "bg-[var(--color-task-checking)]"; case TaskStatus.Checking: return "bg-[var(--color-task-checking)]";
case "correct": return "bg-[var(--color-task-correct)]"; case TaskStatus.Correct: return "bg-[var(--color-task-correct)]";
case "partial": return "bg-[var(--color-task-partial)]"; case TaskStatus.Partial: return "bg-[var(--color-task-partial)]";
case "wrong": return "bg-[var(--color-task-wrong)]"; case TaskStatus.Wrong: return "bg-[var(--color-task-wrong)]";
} }
}; };
const getTaskTextColor = (status: TaskStatus): string => { const getTaskTextColor = (status: TaskStatus): string => {
switch (status) { switch (status) {
case "uncleared": return "text-[var(--color-task-text-uncleared)]"; case TaskStatus.Uncleared: return "text-[var(--color-task-text-uncleared)]";
case "checking": return "text-[var(--color-task-text-checking)]"; case TaskStatus.Checking: return "text-[var(--color-task-text-checking)]";
case "correct": return "text-[var(--color-task-text-correct)]"; case TaskStatus.Correct: return "text-[var(--color-task-text-correct)]";
case "partial": return "text-[var(--color-task-text-partial)]"; case TaskStatus.Partial: return "text-[var(--color-task-text-partial)]";
case "wrong": return "text-[var(--color-task-text-wrong)]"; case TaskStatus.Wrong: return "text-[var(--color-task-text-wrong)]";
} }
}; };
+13 -13
View File
@@ -1,4 +1,4 @@
import { Competition, CompetitionStatus, Solution, Task } from "../types"; import { Competition, CompetitionStatus, Solution, Task, TaskStatus } from "../types";
const mockCompetitions: Competition[] = [ const mockCompetitions: Competition[] = [
{ {
@@ -56,49 +56,49 @@ const mockTasks: Task[] = [
{ {
id: "1", id: "1",
number: "1.1", number: "1.1",
status: "uncleared", status: TaskStatus.Uncleared,
solutionType: "input" solutionType: "input"
}, },
{ {
id: "2", id: "2",
number: "1.2", number: "1.2",
status: "checking", status: TaskStatus.Checking,
solutionType: "file" solutionType: "file"
}, },
{ {
id: "3", id: "3",
number: "1.3", number: "1.3",
status: "correct", status: TaskStatus.Correct,
solutionType: "code" solutionType: "code"
}, },
{ {
id: "4", id: "4",
number: "2.1", number: "2.1",
status: "partial", status: TaskStatus.Partial,
solutionType: "input" solutionType: "input"
}, },
{ {
id: "5", id: "5",
number: "2.2", number: "2.2",
status: "wrong", status: TaskStatus.Wrong,
solutionType: "file" solutionType: "file"
}, },
{ {
id: "6", id: "6",
number: "2.3", number: "2.3",
status: "uncleared", status: TaskStatus.Uncleared,
solutionType: "code" solutionType: "code"
}, },
{ {
id: "7", id: "7",
number: "3.1", number: "3.1",
status: "checking", status: TaskStatus.Checking,
solutionType: "file" solutionType: "file"
}, },
{ {
id: "8", id: "8",
number: "3.2", number: "3.2",
status: "correct", status: TaskStatus.Correct,
solutionType: "input" solutionType: "input"
}, },
]; ];
@@ -107,26 +107,26 @@ const mockTasks: Task[] = [
const mockSolutions: Solution[] = [ const mockSolutions: Solution[] = [
{ {
id: '1', id: '1',
status: 'wrong', status: TaskStatus.Wrong,
date: '1 марта, 08:41', date: '1 марта, 08:41',
}, },
{ {
id: '2', id: '2',
status: 'partial', status: TaskStatus.Partial,
score: 5, score: 5,
maxScore: 10, maxScore: 10,
date: '28 февраля, 15:22', date: '28 февраля, 15:22',
}, },
{ {
id: '3', id: '3',
status: 'correct', status: TaskStatus.Correct,
score: 0, score: 0,
maxScore: 10, maxScore: 10,
date: '27 февраля, 12:10', date: '27 февраля, 12:10',
}, },
{ {
id: '4', id: '4',
status: 'checking', status: TaskStatus.Checking,
date: '1 марта, 08:41', date: '1 марта, 08:41',
}, },
+10 -3
View File
@@ -4,6 +4,14 @@ enum CompetitionStatus {
Completed = "Завершено", Completed = "Завершено",
} }
enum TaskStatus {
Uncleared = "uncleared",
Checking = "checking",
Correct = "correct",
Partial = "partial",
Wrong = "wrong"
}
interface Competition { interface Competition {
id: string; id: string;
name: string; name: string;
@@ -13,7 +21,6 @@ interface Competition {
description?: string; description?: string;
} }
type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong";
type SolutionType = "input" | "file" | "code"; type SolutionType = "input" | "file" | "code";
interface Solution { interface Solution {
@@ -30,5 +37,5 @@ interface Task {
solutionType: SolutionType; solutionType: SolutionType;
} }
export { CompetitionStatus }; export { CompetitionStatus, TaskStatus };
export type { Solution, Competition, TaskStatus, Task }; export type { Solution, Competition, Task };
@@ -1,24 +0,0 @@
import { ChevronDown } from "lucide-react";
const Navbar = () => {
return (
<nav className="bg-white border-b border-gray-200 py-3 px-4 fixed top-0 left-0 right-0 z-10">
<div className="container mx-auto flex justify-between items-center">
<div className="flex items-center">
<div className="bg-black px-3 py-2 rounded font-hse-sans">
<span className="font-bold text-yellow-400">DATA</span>
<span className="font-bold text-white">RUSH</span>
</div>
</div>
<div className="flex items-center cursor-pointer">
<span className="mr-2 font-semibold font-hse-sans">itqdev</span>
<ChevronDown size={16} />
</div>
</div>
</nav>
);
};
export default Navbar