mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 16:47:10 +00:00
feat: added templates for user profile + competition constructor (if will be), started working on synchronizing front and back session
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface TaskDescriptionFieldProps {
|
||||
description: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const TaskDescriptionField: React.FC<TaskDescriptionFieldProps> = ({ description, onChange }) => {
|
||||
return (
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label htmlFor="description" className="text-right pt-2">
|
||||
Описание
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="col-span-3 min-h-[100px]"
|
||||
placeholder="Введите описание задачи"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDescriptionField;
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { FileIcon, X, Upload } from 'lucide-react';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface TaskFileAttachmentsProps {
|
||||
files: File[];
|
||||
onChange: (files: File[]) => void;
|
||||
}
|
||||
|
||||
const TaskFileAttachments: React.FC<TaskFileAttachmentsProps> = ({ files, onChange }) => {
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
onChange([...files, ...newFiles]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
onChange(files.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label className="text-right pt-2">
|
||||
Файлы
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
{files.map((file, index) => (
|
||||
<FileListItem
|
||||
key={index}
|
||||
file={file}
|
||||
onRemove={() => removeFile(index)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<FileUploadButton onChange={handleFileChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FileListItemProps {
|
||||
file: File;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
const FileListItem: React.FC<FileListItemProps> = ({ file, onRemove }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-2 border rounded-md">
|
||||
<div className="flex items-center">
|
||||
<FileIcon size={16} className="mr-2 text-gray-500" />
|
||||
<span className="text-sm">{file.name}</span>
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
({Math.round(file.size / 1024)} KB)
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FileUploadButtonProps {
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
const FileUploadButton: React.FC<FileUploadButtonProps> = ({ onChange }) => {
|
||||
return (
|
||||
<label className="cursor-pointer">
|
||||
<div className="flex items-center gap-2 p-2 border border-dashed rounded-md hover:bg-gray-50 transition-colors">
|
||||
<Upload size={16} className="text-gray-500" />
|
||||
<span className="text-sm text-gray-700">Добавить файлы</span>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={onChange}
|
||||
multiple
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskFileAttachments;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface TaskNumberFieldProps {
|
||||
number: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const TaskNumberField: React.FC<TaskNumberFieldProps> = ({ number, onChange }) => {
|
||||
return (
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="task-number" className="text-right">
|
||||
Номер задачи
|
||||
</Label>
|
||||
<Input
|
||||
id="task-number"
|
||||
value={number}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskNumberField;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface TaskRequirementsFieldProps {
|
||||
requirements: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const TaskRequirementsField: React.FC<TaskRequirementsFieldProps> = ({ requirements, onChange }) => {
|
||||
return (
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label htmlFor="requirements" className="text-right pt-2">
|
||||
Требования
|
||||
</Label>
|
||||
<Textarea
|
||||
id="requirements"
|
||||
value={requirements}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="col-span-3"
|
||||
placeholder="Введите требования к решению (необязательно)"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskRequirementsField;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem
|
||||
} from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface TaskSolutionTypeSelectorProps {
|
||||
solutionType: 'input' | 'file' | 'code';
|
||||
onChange: (value: 'input' | 'file' | 'code') => void;
|
||||
}
|
||||
|
||||
const TaskSolutionTypeSelector: React.FC<TaskSolutionTypeSelectorProps> = ({ solutionType, onChange }) => {
|
||||
return (
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label className="text-right pt-2">
|
||||
Тип решения
|
||||
</Label>
|
||||
<RadioGroup
|
||||
className="col-span-3"
|
||||
value={solutionType}
|
||||
onValueChange={(value) => onChange(value as 'input' | 'file' | 'code')}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="input" id="input" />
|
||||
<Label htmlFor="input">Ввод ответа</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="file" id="file" />
|
||||
<Label htmlFor="file">Загрузка файла</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="code" id="code" />
|
||||
<Label htmlFor="code">Программный код</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskSolutionTypeSelector;
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Task } from "@/shared/types";
|
||||
import TaskNumberField from './components/TaskNumberField';
|
||||
import TaskDescriptionField from './components/TaskDescriptionField';
|
||||
import TaskRequirementsField from './components/TaskRequirementsField';
|
||||
import TaskSolutionTypeSelector from './components/TaskSolutionTypeSelector';
|
||||
import TaskFileAttachments from './components/TaskFileAttachments';
|
||||
|
||||
interface TaskCreationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCreateTask: (task: Partial<Task>) => void;
|
||||
taskCount: number;
|
||||
}
|
||||
|
||||
const TaskCreationModal: React.FC<TaskCreationModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCreateTask,
|
||||
taskCount
|
||||
}) => {
|
||||
const [number, setNumber] = useState(`${taskCount + 1}`);
|
||||
const [description, setDescription] = useState('');
|
||||
const [requirements, setRequirements] = useState('');
|
||||
const [solutionType, setSolutionType] = useState<'input' | 'file' | 'code'>('input');
|
||||
const [attachedFiles, setAttachedFiles] = useState<File[]>([]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const newTask: Partial<Task> = {
|
||||
number,
|
||||
description,
|
||||
requirements: requirements || undefined,
|
||||
solutionType,
|
||||
attachments: attachedFiles.map(file => file.name)
|
||||
};
|
||||
|
||||
onCreateTask(newTask);
|
||||
|
||||
setNumber(`${taskCount + 1}`);
|
||||
setDescription('');
|
||||
setRequirements('');
|
||||
setSolutionType('input');
|
||||
setAttachedFiles([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[600px] font-hse-sans">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">Создание новой задачи</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<TaskNumberField
|
||||
number={number}
|
||||
onChange={setNumber}
|
||||
/>
|
||||
|
||||
<TaskDescriptionField
|
||||
description={description}
|
||||
onChange={setDescription}
|
||||
/>
|
||||
|
||||
<TaskRequirementsField
|
||||
requirements={requirements}
|
||||
onChange={setRequirements}
|
||||
/>
|
||||
|
||||
<TaskSolutionTypeSelector
|
||||
solutionType={solutionType}
|
||||
onChange={setSolutionType}
|
||||
/>
|
||||
|
||||
<TaskFileAttachments
|
||||
files={attachedFiles}
|
||||
onChange={setAttachedFiles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSubmit}>
|
||||
Создать задачу
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskCreationModal;
|
||||
Reference in New Issue
Block a user