new features

This commit is contained in:
Daniil
2026-02-27 23:34:17 +03:00
parent 42ce5fa0fe
commit 71b974903a
191 changed files with 11300 additions and 373 deletions
+4 -4
View File
@@ -6,16 +6,16 @@ import { Button } from "@shared/ui"
import cls from "./HomePage.module.scss"
const HomePage = () => {
useBreadcrumbs([{ label: "Home" }])
useBreadcrumbs([{ label: "Главная" }])
return (
<div className={cls.homepage}>
<p className={cls.title}>Coffee Project Starter</p>
<pre className={cls.hint}>
Edit <span className={cls.path}>src/pages/HomePage</span> to begin
building your features.
Редактируйте <span className={cls.path}>src/pages/HomePage</span>{" "}
чтобы начать разработку.
</pre>
<Button variant="primary">Get Started</Button>
<Button variant="primary">Начать</Button>
</div>
)
}
+20 -12
View File
@@ -1,29 +1,35 @@
.root {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px;
}
.form {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 520px;
max-height: 720px;
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 16px;
gap: 20px;
padding: 40px 32px;
background-color: variables.$bg-default;
border: 1px solid variables.$border-default;
border-radius: variables.$radius-lg;
box-shadow: var(--shadow-md);
}
.title {
@include typography.font-header-l;
width: 100%;
text-align: center;
color: variables.$text-primary;
}
.fields {
display: flex;
flex-direction: column;
gap: 8px;
gap: 12px;
}
.actions {
@@ -32,10 +38,12 @@
}
.link {
color: inherit;
@include typography.font-body-s;
color: variables.$text-secondary;
text-decoration: none;
transition: color 0.15s ease;
&:hover {
text-decoration: underline;
color: variables.$text-primary;
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export interface IProfilePageProps {
className?: string
}
@@ -0,0 +1,47 @@
.root {
display: flex;
justify-content: center;
padding: 32px 16px;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
width: 100%;
max-width: 640px;
}
.section {
width: 100%;
padding: 24px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.infoList {
display: flex;
flex-direction: column;
gap: 12px;
}
.infoRow {
display: flex;
justify-content: space-between;
align-items: center;
}
.infoLabel {
color: var(--gray-11);
font-size: 14px;
}
.infoValue {
font-size: 14px;
font-weight: 500;
}
+3 -3
View File
@@ -2,7 +2,6 @@
import type { JSX } from "react"
import moment from "moment"
import { FunctionComponent } from "react"
import {
@@ -12,6 +11,7 @@ import {
LogoutButton,
} from "@features/profile"
import api from "@shared/api"
import { formatDate } from "@shared/lib/dates"
import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext"
import { StaticLoader } from "@shared/ui/Loader"
import { Card } from "@shared/ui"
@@ -22,7 +22,7 @@ import styles from "./ProfilePage.module.scss"
export const ProfilePage: FunctionComponent<
IProfilePageProps
> = (): JSX.Element => {
useBreadcrumbs([{ label: "Profile" }])
useBreadcrumbs([{ label: "Профиль" }])
const {
data: user,
@@ -82,7 +82,7 @@ export const ProfilePage: FunctionComponent<
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Дата регистрации</span>
<span className={styles.infoValue}>
{moment(user.date_joined).format("DD.MM.YYYY")}
{formatDate(user.date_joined)}
</span>
</div>
<div className={styles.infoRow}>
+1
View File
@@ -0,0 +1 @@
export * from "./ProfilePage"
@@ -1,2 +1,3 @@
.root {
width: 100%;
}
@@ -5,25 +5,67 @@ import type { JSX } from "react"
import { useParams } from "next/navigation"
import { FunctionComponent } from "react"
import api from "@shared/api"
import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext"
import {
useWorkspaceFiles,
WorkspaceProvider,
} from "@shared/context/WorkspaceContext"
import { TranscriptionEditor } from "@features/project"
import {
ActionPanel,
FileTree,
VideoPlayer,
WorkspaceLayout,
} from "@widgets/Workspace"
import { IProjectWorkspacePageProps } from "./ProjectWorkspacePage.d"
import styles from "./ProjectWorkspacePage.module.scss"
/* ------------------------------------------------------------------ */
/* Inner wrapper — resolves which viewer to show based on selection */
/* ------------------------------------------------------------------ */
const WorkspaceViewer: FunctionComponent<{ projectId: string }> = ({
projectId,
}) => {
const { selectedFile } = useWorkspaceFiles()
if (selectedFile?.artifactType === "TRANSCRIPTION_JSON") {
return <TranscriptionEditor artifactId={selectedFile.id} />
}
return <VideoPlayer projectId={projectId} />
}
/* ------------------------------------------------------------------ */
/* Page */
/* ------------------------------------------------------------------ */
export const ProjectWorkspacePage: FunctionComponent<
IProjectWorkspacePageProps
> = (): JSX.Element => {
const params = useParams<{ project_id: string }>()
const projectId = params?.project_id ?? ""
const { data: project } = api.useQuery("get", "/api/projects/{project_id}/", {
params: { path: { project_id: projectId } },
})
useBreadcrumbs([
{ label: "Projects", href: "/projects" },
{ label: `Project ${projectId}` },
{ label: "Проекты", href: "/projects" },
{ label: project?.name ?? "..." },
])
return (
<div className={styles.root} data-testid="ProjectWorkspacePage">
ProjectWorkspacePage Component
</div>
<WorkspaceProvider projectId={projectId}>
<div className={styles.root} data-testid="ProjectWorkspacePage">
<WorkspaceLayout
fileTree={<FileTree projectId={projectId} />}
player={<WorkspaceViewer projectId={projectId} />}
actionPanel={<ActionPanel projectId={projectId} />}
/>
</div>
</WorkspaceProvider>
)
}
@@ -1,9 +1,9 @@
.root {
padding: 0 24px;
padding: 28px 24px 40px;
display: flex;
flex-direction: column;
gap: 32px;
gap: 24px;
}
.header {
@@ -25,14 +25,23 @@
}
.subtitle {
@include typography.font-body-16(600);
color: variables.$text-primary;
@include typography.font-body-s;
color: variables.$text-secondary;
}
.projectList {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
margin-top: 24px;
padding-bottom: 40px;
gap: 20px;
}
.empty {
display: flex;
justify-content: center;
padding: 48px 0;
}
.emptyText {
@include typography.font-body-mr;
color: variables.$text-secondary;
}
+35 -5
View File
@@ -17,9 +17,13 @@ import {
RenameProjectModal,
} from "@features/project"
import api from "@shared/api"
import { useDebounce } from "@shared/hooks/useDebounce"
import { Button } from "@shared/ui"
import { StaticLoader } from "@shared/ui/Loader"
import { ProjectsHeader } from "@widgets/Projects/ProjectsHeader"
import {
ProjectsHeader,
type ProjectStatusEnum,
} from "@widgets/Projects/ProjectsHeader"
import { IProjectsPageProps } from "./ProjectsPage.d"
import styles from "./ProjectsPage.module.scss"
@@ -29,18 +33,29 @@ type ProjectRead = components["schemas"]["ProjectRead"]
export const ProjectsPage: FunctionComponent<
IProjectsPageProps
> = (): JSX.Element => {
useBreadcrumbs([{ label: "Projects", href: "/projects" }])
useBreadcrumbs([{ label: "Проекты", href: "/projects" }])
const router = useRouter()
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
const [editProject, setEditProject] = useState<ProjectRead | null>(null)
const [renameProject, setRenameProject] = useState<ProjectRead | null>(null)
const [deleteProject, setDeleteProject] = useState<ProjectRead | null>(null)
const [search, setSearch] = useState("")
const [statusFilter, setStatusFilter] = useState<ProjectStatusEnum>("")
const debouncedSearch = useDebounce(search, 300)
const {
data: projects,
isLoading: projectsLoading,
refetch: refetchProjects,
} = api.useQuery("get", "/api/projects/")
} = api.useQuery("get", "/api/projects/", {
params: {
query: {
search: debouncedSearch || undefined,
status: statusFilter || undefined,
},
},
})
return (
<div className={styles.root} data-testid="ProjectsPage">
@@ -110,7 +125,12 @@ export const ProjectsPage: FunctionComponent<
/>
)}
<ProjectsHeader />
<ProjectsHeader
search={search}
onSearchChange={setSearch}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
/>
<div className={styles.projectList}>
{projects?.map((project) => (
@@ -119,7 +139,7 @@ export const ProjectsPage: FunctionComponent<
project={project}
progress={project.status === "PROCESSING" ? 45 : 0}
currentAction={
project.status === "PROCESSING" ? "Rendering" : undefined
project.status === "PROCESSING" ? "Рендеринг" : undefined
}
onClick={() => router.push(`/projects/${project.id}`)}
onEdit={() => setEditProject(project)}
@@ -128,6 +148,16 @@ export const ProjectsPage: FunctionComponent<
/>
))}
</div>
{!projectsLoading && projects?.length === 0 && (
<div className={styles.empty}>
<p className={styles.emptyText}>
{search || statusFilter
? "Проекты не найдены"
: "У вас пока нет проектов"}
</p>
</div>
)}
</div>
)
}
+21 -17
View File
@@ -1,29 +1,33 @@
.root {
opacity: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px;
}
.form {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 520px;
max-height: 820px;
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
max-width: 440px;
display: flex;
flex-direction: column;
gap: 20px;
padding: 40px 32px;
background-color: variables.$bg-default;
border: 1px solid variables.$border-default;
border-radius: variables.$radius-lg;
box-shadow: var(--shadow-md);
}
.title {
@include typography.font-header-l;
width: 100%;
text-align: center;
@include typography.font-header-l;
width: 100%;
text-align: center;
color: variables.$text-primary;
}
.fields {
display: flex;
flex-direction: column;
gap: 8px;
display: flex;
flex-direction: column;
gap: 12px;
}
@@ -0,0 +1,3 @@
export interface IUnderMaintenancePageProps {
className?: string
}
@@ -0,0 +1,42 @@
.root {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px;
}
.card {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 48px 40px;
background-color: variables.$bg-default;
border: 1px solid variables.$border-default;
border-radius: variables.$radius-lg;
box-shadow: var(--shadow-md);
text-align: center;
max-width: 400px;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border-radius: 50%;
background-color: variables.$purple-50;
color: variables.$purple-600;
}
.title {
@include typography.font-header-l;
color: variables.$text-primary;
}
.description {
@include typography.font-body-mr;
color: variables.$text-secondary;
}
@@ -0,0 +1,43 @@
"use client"
import type { IUnderMaintenancePageProps } from "./UnderMaintenancePage.d"
import type { JSX } from "react"
import { FunctionComponent, useEffect } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { Construction } from "lucide-react"
import api from "@shared/api"
import styles from "./UnderMaintenancePage.module.scss"
const PING_INTERVAL_MS = 5000
export const UnderMaintenancePage: FunctionComponent<IUnderMaintenancePageProps> = (): JSX.Element => {
const router = useRouter()
const searchParams = useSearchParams()
const redirectPath = searchParams.get("path") || "/"
const { isSuccess } = api.useQuery("get", "/api/ping/", {}, {
refetchInterval: PING_INTERVAL_MS,
retry: false,
})
useEffect(() => {
if (isSuccess) {
router.replace(redirectPath)
}
}, [isSuccess, redirectPath, router])
return (
<div className={styles.root} data-testid="UnderMaintenancePage">
<div className={styles.card}>
<div className={styles.icon}>
<Construction size={48} strokeWidth={1.5} />
</div>
<h1 className={styles.title}>Платформа недоступна</h1>
<p className={styles.description}>Попробуйте зайти позже.</p>
</div>
</div>
)
}
+1
View File
@@ -0,0 +1 @@
export * from "./UnderMaintenancePage"