feature: add projects page (2 parts works)

This commit is contained in:
Daniil
2026-01-29 00:57:22 +03:00
parent 3dfb9453ec
commit 2e4820ac91
65 changed files with 2223 additions and 45 deletions
@@ -0,0 +1,25 @@
import type { components } from "@shared/api/__generated__/openapi.types"
import api from "@shared/api"
export type ProjectCreateBody = components["schemas"]["ProjectCreate"]
export type ProjectRead = components["schemas"]["ProjectRead"]
interface IUseCreateProjectParams {
onSuccess?: (project: ProjectRead) => void
onError?: (error: unknown) => void
}
export const useCreateProject = ({
onSuccess,
onError,
}: IUseCreateProjectParams = {}) => {
return api.useMutation("post", "/api/projects/", {
onSuccess: (project) => {
onSuccess?.(project)
},
onError: (error) => {
onError?.(error)
},
})
}
+3
View File
@@ -0,0 +1,3 @@
export { CreateProjectModal } from "./ui/CreateProjectModal"
export type { ICreateProjectModalProps } from "./model/CreateProjectModal.d"
@@ -0,0 +1,9 @@
import type { Dialog } from "@radix-ui/themes"
import type { ComponentProps } from "react"
export interface ICreateProjectModalProps extends Pick<
ComponentProps<typeof Dialog.Root>,
"open" | "onOpenChange"
> {
onCreated?: () => void | Promise<void>
}
@@ -0,0 +1,25 @@
.root {
min-width: 520px;
}
.fields {
display: grid;
gap: 12px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 16px;
}
.selectField {
display: grid;
gap: 6px;
}
.selectLabel {
font-size: 14px;
font-weight: 500;
}
@@ -0,0 +1,179 @@
"use client"
import type { ProjectCreateBody } from "../api/useCreateProject"
import type { ICreateProjectModalProps } from "../model/CreateProjectModal.d"
import type { JSX } from "react"
import { FunctionComponent, useEffect } from "react"
import { Controller, useForm } from "react-hook-form"
import { Button, Form, Modal, Select, SelectItem, TextField } from "@shared/ui"
import { useCreateProject } from "../api/useCreateProject"
import styles from "./CreateProjectModal.module.scss"
type ProjectStatus = ProjectCreateBody["status"]
interface ICreateProjectFormData {
name: string
description?: string
language: string
folder?: string
status: ProjectStatus
}
const STATUS_OPTIONS: Array<{ value: ProjectStatus; label: string }> = [
{ value: "DRAFT", label: "Draft" },
{ value: "PROCESSING", label: "Processing" },
{ value: "DONE", label: "Done" },
{ value: "FAILED", label: "Failed" },
]
const LANGUAGE_OPTIONS: Array<{ value: string; label: string }> = [
{ value: "auto", label: "Auto" },
{ value: "ru", label: "Russian" },
{ value: "en", label: "English" },
]
export const CreateProjectModal: FunctionComponent<
ICreateProjectModalProps
> = ({ open, onOpenChange, onCreated }): JSX.Element => {
const { control, register, handleSubmit, reset, formState } =
useForm<ICreateProjectFormData>({
defaultValues: {
name: "",
description: "",
folder: "",
language: "auto",
status: "DRAFT",
},
})
const { mutate, isPending } = useCreateProject({
onSuccess: async () => {
await onCreated?.()
onOpenChange?.(false)
},
onError: (error) => {
console.error("Create project failed:", error)
},
})
useEffect(() => {
if (!open) reset()
}, [open, reset])
const onSubmit = (data: ICreateProjectFormData): void => {
const name = data.name.trim()
const description = data.description?.trim()
const folder = data.folder?.trim()
mutate({
body: {
name,
description: description?.length ? description : undefined,
folder: folder?.length ? folder : undefined,
language: data.language,
status: data.status,
},
})
}
return (
<Modal
open={open}
onOpenChange={onOpenChange}
title="Создать проект"
description="Заполните основные поля проекта"
>
<div className={styles.root} data-testid="CreateProjectModal">
<Form onSubmit={handleSubmit(onSubmit)}>
<div className={styles.fields}>
<TextField
id="project_name"
label="Название"
placeholder="Например: Мой первый проект"
error={Boolean(formState.errors.name)}
undertitle={formState.errors.name?.message}
{...register("name", {
required: "Введите название проекта",
validate: (v) =>
v.trim().length > 0 || "Введите название проекта",
})}
/>
<TextField
id="project_description"
label="Описание"
placeholder="Коротко опишите проект (необязательно)"
{...register("description")}
/>
<TextField
id="project_folder"
label="Папка"
placeholder="Например: /projects/my-project (необязательно)"
{...register("folder")}
/>
<div className={styles.selectField}>
<div className={styles.selectLabel}>Язык</div>
<Controller
name="language"
control={control}
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
placeholder="Выберите язык"
>
{LANGUAGE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</Select>
)}
/>
</div>
<div className={styles.selectField}>
<div className={styles.selectLabel}>Статус</div>
<Controller
name="status"
control={control}
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
placeholder="Выберите статус"
>
{STATUS_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</Select>
)}
/>
</div>
</div>
<div className={styles.actions}>
<Button
type="button"
variant="ghost"
disabled={isPending}
onClick={() => onOpenChange?.(false)}
>
Отмена
</Button>
<Button type="submit" variant="primary" disabled={isPending}>
Создать
</Button>
</div>
</Form>
</div>
</Modal>
)
}