feat: history sheet

This commit is contained in:
rngsurrounded
2025-03-01 23:48:12 +09:00
parent 3a879dc466
commit 01e775605e
10 changed files with 428 additions and 34 deletions
@@ -0,0 +1,130 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/shared/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
@@ -0,0 +1,134 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/shared/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
{/* Removed the default close button that was causing duplication */}
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)} // Kept original padding
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
@@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useParams, Navigate } from "react-router-dom"; import { useParams, Navigate } from "react-router-dom";
import { Task } from "@/shared/types"; import { Task } from "@/shared/types";
import { mockTasks } from "@/shared/mocks/mocks"; import { mockSolutions, mockTasks } from "@/shared/mocks/mocks";
import CompetitionHeader from "./components/CompetitionHeader"; import CompetitionHeader from "./components/CompetitionHeader";
import TaskContent from "./components/TaskContent"; import TaskContent from "./components/TaskContent";
import TaskSolution from "./modules/TaskSolution"; import TaskSolution from "./modules/TaskSolution";
@@ -40,6 +40,7 @@ const CompetitionSessionPage = () => {
<TaskContent task={currentTask} /> <TaskContent task={currentTask} />
<TaskSolution <TaskSolution
task={currentTask} task={currentTask}
solutions={mockSolutions}
answer={answer} answer={answer}
setAnswer={setAnswer} setAnswer={setAnswer}
onSubmit={handleSubmit} onSubmit={handleSubmit}
@@ -1,28 +1,51 @@
import React from 'react'; import React, { useState } from 'react';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import SolutionHistorySheet from '../SolutionHistorySheet';
import { Solution } from "@/shared/types";
import { mockSolutions } from '@/shared/mocks/mocks';
interface ActionButtonsProps { interface ActionButtonsProps {
onHistoryClick: () => void; onHistoryClick: () => void;
onSubmit: () => void; onSubmit: () => void;
solutionHistory?: Solution[];
} }
const ActionButtons: React.FC<ActionButtonsProps> = ({ onHistoryClick, onSubmit }) => { const ActionButtons: React.FC<ActionButtonsProps> = ({
onHistoryClick,
onSubmit,
solutionHistory = mockSolutions
}) => {
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const handleHistoryClick = () => {
setIsHistoryOpen(true);
onHistoryClick();
};
return ( return (
<div className="flex gap-3 justify-between"> <>
<div className="flex gap-8">
<Button <Button
variant="ghost" variant="ghost"
className="font-hse-sans bg-white hover:bg-gray-100" className="font-hse-sans bg-white hover:bg-gray-100"
onClick={onHistoryClick} onClick={handleHistoryClick}
> >
История История
</Button> </Button>
<Button <Button
onClick={onSubmit} onClick={onSubmit}
className="font-hse-sans" className="font-hse-sans flex-grow"
> >
Отправить решение Отправить решение
</Button> </Button>
</div> </div>
{/* чуть-чуть рак */}
<SolutionHistorySheet
isOpen={isHistoryOpen}
onOpenChange={setIsHistoryOpen}
solutions={solutionHistory}
/>
</>
); );
}; };
@@ -0,0 +1,51 @@
import React from 'react';
import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import SolutionStatus from '../SolutionStatus';
import { Solution } from "@/shared/types";
interface SolutionHistorySheetProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
solutions: Solution[];
}
const SolutionHistorySheet: React.FC<SolutionHistorySheetProps> = ({
isOpen,
onOpenChange,
solutions
}) => {
return (
<Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetContent className="w-[350px] sm:w-[450px] p-0">
<SheetHeader className="border-b py-3 px-4">
<div className="flex justify-between items-center">
<SheetTitle className="text-lg font-medium">История решений</SheetTitle>
<SheetClose asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<X className="h-4 w-4" />
</Button>
</SheetClose>
</div>
</SheetHeader>
<div className="flex flex-col mt-3 space-y-2.5 overflow-y-auto max-h-[calc(100vh-80px)] px-4">
{solutions.length > 0 ? (
solutions.map((solution, index) => (
<div key={index} className="w-full">
<SolutionStatus solution={solution} />
</div>
))
) : (
<div className="text-center py-8 text-gray-500">
У вас пока нет истории решений для этой задачи
</div>
)}
</div>
</SheetContent>
</Sheet>
);
};
export default SolutionHistorySheet;
@@ -1,24 +1,41 @@
import React from 'react'; import React from 'react';
import { Task } from "@/shared/types"; import { Solution, TaskStatus } from "@/shared/types";
import { getTaskBgColor, getTaskTextColor } from '@/pages/CompetitionSession/utils/utils'; import { getTaskBgColor, getTaskTextColor } from '@/pages/CompetitionSession/utils/utils';
interface SolutionStatusProps { interface SolutionStatusProps {
task: Task; solution: Solution;
} }
const SolutionStatus: React.FC<SolutionStatusProps> = ({ task }) => { const SolutionStatus: React.FC<SolutionStatusProps> = ({ solution }) => {
const getStatusText = (status: TaskStatus, score?: number, maxScore?: number) => {
switch (status) {
case 'checking':
return 'На проверке';
case 'wrong':
return 'Неверный ответ';
case 'correct':
return `Зачтено ${maxScore}/${maxScore} баллов`;
case 'partial':
return `Зачтено ${score}/${maxScore} баллов`;
case 'uncleared':
return 'Не решено';
default:
return '';
}
};
return ( return (
<div className={`${getTaskBgColor(task.status)} rounded-lg p-4 relative`}> <div className={`${getTaskBgColor(solution.status)} rounded-lg p-4 relative`}>
<div className="flex flex-col"> <div className="flex flex-col">
<span className={`${getTaskTextColor(task.status)} font-medium`}> <span className={`${getTaskTextColor(solution.status)} font-medium`}>
Решение 12345 Решение {solution.id}
</span> </span>
<span className={`${getTaskTextColor(task.status)} mt-1`}> <span className={`${getTaskTextColor(solution.status)} mt-1`}>
Зачтено 5/10 баллов {getStatusText(solution.status, solution.score, solution.maxScore)}
</span> </span>
</div> </div>
<div className={`absolute bottom-2 right-3 text-xs ${getTaskTextColor(task.status)}`}> <div className={`absolute bottom-2 right-3 text-xs ${getTaskTextColor(solution.status)}`}>
1 марта, 08:41 {solution.date}
</div> </div>
</div> </div>
); );
@@ -1,5 +1,5 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { Task } from "@/shared/types"; import { Solution, Task } from "@/shared/types";
import SolutionStatus from './components/SolutionStatus'; import SolutionStatus from './components/SolutionStatus';
import InputSolution from './components/InputSolution'; import InputSolution from './components/InputSolution';
import FileSolution from './components/FileSolution'; import FileSolution from './components/FileSolution';
@@ -8,6 +8,7 @@ import ActionButtons from './components/ActionButtons';
interface TaskSolutionProps { interface TaskSolutionProps {
task: Task; task: Task;
solutions: Solution[];
answer: string; answer: string;
setAnswer: (value: string) => void; setAnswer: (value: string) => void;
onSubmit: () => void; onSubmit: () => void;
@@ -16,6 +17,7 @@ interface TaskSolutionProps {
const TaskSolution: React.FC<TaskSolutionProps> = ({ const TaskSolution: React.FC<TaskSolutionProps> = ({
task, task,
solutions,
answer, answer,
setAnswer, setAnswer,
onSubmit, onSubmit,
@@ -26,7 +28,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
return ( return (
<div className="md:w-[500px] flex flex-col gap-4"> <div className="md:w-[500px] flex flex-col gap-4">
<SolutionStatus task={task} /> <SolutionStatus solution={solutions[0]} />
{task.solutionType === 'input' && ( {task.solutionType === 'input' && (
<InputSolution answer={answer} setAnswer={setAnswer} /> <InputSolution answer={answer} setAnswer={setAnswer} />
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState } from "react";
import { Competition, CompetitionStatus } from "@/shared/types"; import { Competition, CompetitionStatus } from "@/shared/types";
import { CompetitionGrid } from "./modules/CompetitionGrid"; import { CompetitionGrid } from "./modules/CompetitionGrid";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
+31 -2
View File
@@ -1,4 +1,4 @@
import { Competition, CompetitionStatus, Task } from "../types"; import { Competition, CompetitionStatus, Solution, Task } from "../types";
const mockCompetitions: Competition[] = [ const mockCompetitions: Competition[] = [
{ {
@@ -104,4 +104,33 @@ const mockTasks: Task[] = [
]; ];
export { mockCompetitions, mockTasks }; const mockSolutions: Solution[] = [
{
id: '1',
status: 'wrong',
date: '1 марта, 08:41',
},
{
id: '2',
status: 'partial',
score: 5,
maxScore: 10,
date: '28 февраля, 15:22',
},
{
id: '3',
status: 'correct',
score: 0,
maxScore: 10,
date: '27 февраля, 12:10',
},
{
id: '4',
status: 'checking',
date: '1 марта, 08:41',
},
];
export { mockCompetitions, mockTasks, mockSolutions };
+8 -1
View File
@@ -16,6 +16,13 @@ interface Competition {
type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong"; type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong";
type SolutionType = "input" | "file" | "code"; type SolutionType = "input" | "file" | "code";
interface Solution {
id: string,
status: TaskStatus,
date: string,
score?: number,
maxScore?: number,
}
interface Task { interface Task {
id: string; id: string;
number: string; number: string;
@@ -24,4 +31,4 @@ interface Task {
} }
export { CompetitionStatus }; export { CompetitionStatus };
export type { Competition, TaskStatus, Task }; export type { Solution, Competition, TaskStatus, Task };