This commit is contained in:
Daniil
2026-04-04 14:51:40 +03:00
parent 10a1d28f77
commit 0523ef3d72
191 changed files with 12065 additions and 2658 deletions
+5 -5
View File
@@ -1,8 +1,8 @@
.root {
padding: 28px 24px 40px;
padding: 32px 24px 48px;
display: flex;
flex-direction: column;
gap: 32px;
gap: 36px;
}
.welcome {
@@ -15,10 +15,10 @@
}
.greeting {
font-weight: 700;
font-weight: 800;
font-size: 52px;
line-height: 1.1;
letter-spacing: -1px;
line-height: 1.05;
letter-spacing: -0.03em;
color: variables.$text-primary;
margin: 0;
}
-19
View File
@@ -1,19 +0,0 @@
import "@testing-library/jest-dom"
import { render, screen } from "@testing-library/react"
import HomePage from "./HomePage"
describe("Page", () => {
test("renders a yunglocokid", () => {
render(<HomePage />)
const yunglocokid: HTMLElement = screen.getByText("yunglocokid")
expect(yunglocokid).toBeInTheDocument()
})
test("renders a hint", () => {
render(<HomePage />)
screen.debug()
const hint: HTMLElement = screen.getByTestId("hint-code")
expect(hint).toBeInTheDocument()
})
})
+42 -17
View File
@@ -4,6 +4,7 @@ import type { JSX } from "react"
import { FunctionComponent, useMemo, useState } from "react"
import { motion } from "framer-motion"
import { FolderKanban, PlusIcon } from "lucide-react"
import { useRouter } from "next/navigation"
@@ -12,7 +13,12 @@ import { CreateProjectModal } from "@features/project"
import api from "@shared/api"
import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext"
import { useAppSelector } from "@shared/hooks/useAppSelector"
import { StaticLoader } from "@shared/ui/Loader"
import {
STAGGER_CONTAINER,
SLIDE_UP,
EASE_OUT_TRANSITION,
} from "@shared/lib/motion"
import { StatsGridSkeleton, RecentProjectsSkeleton } from "@shared/ui/Skeleton"
import { RecentProjects } from "@widgets/Dashboard/RecentProjects"
import { StatsGrid } from "@widgets/Dashboard/StatsGrid"
@@ -56,25 +62,44 @@ export const HomePage: FunctionComponent<IHomePageProps> = (): JSX.Element => {
const userName = user?.first_name || user?.username || "пользователь"
return (
<div className={cls.root}>
{isLoading && <StaticLoader fullscreen />}
<div className={cls.welcome}>
<motion.div
className={cls.root}
variants={STAGGER_CONTAINER}
initial="initial"
animate="animate"
>
<motion.div
className={cls.welcome}
variants={SLIDE_UP}
transition={EASE_OUT_TRANSITION}
>
<h1 className={cls.greeting}>Добро пожаловать, {userName}</h1>
<p className={cls.subtitle}>{subtitle}</p>
</div>
</motion.div>
<StatsGrid {...stats} />
<motion.div variants={SLIDE_UP} transition={EASE_OUT_TRANSITION}>
{isLoading ? <StatsGridSkeleton /> : <StatsGrid {...stats} />}
</motion.div>
<RecentProjects
projects={recentProjects}
isLoading={isLoading}
onProjectClick={(id) => router.push(`/projects/${id}`)}
onCreateClick={() => setIsCreateModalOpen(true)}
onViewAllClick={() => router.push("/projects")}
/>
<motion.div variants={SLIDE_UP} transition={EASE_OUT_TRANSITION}>
{isLoading ? (
<RecentProjectsSkeleton />
) : (
<RecentProjects
projects={recentProjects}
isLoading={isLoading}
onProjectClick={(id) => router.push(`/projects/${id}`)}
onCreateClick={() => setIsCreateModalOpen(true)}
onViewAllClick={() => router.push("/projects")}
/>
)}
</motion.div>
<div className={cls.actionsSection}>
<motion.div
className={cls.actionsSection}
variants={SLIDE_UP}
transition={EASE_OUT_TRANSITION}
>
<h2 className={cls.actionsTitle}>Быстрые действия</h2>
<div className={cls.actions}>
<ActionCard
@@ -89,7 +114,7 @@ export const HomePage: FunctionComponent<IHomePageProps> = (): JSX.Element => {
onClick={() => router.push("/projects")}
/>
</div>
</div>
</motion.div>
<CreateProjectModal
open={isCreateModalOpen}
@@ -98,7 +123,7 @@ export const HomePage: FunctionComponent<IHomePageProps> = (): JSX.Element => {
await refetch()
}}
/>
</div>
</motion.div>
)
}
+24 -6
View File
@@ -4,6 +4,10 @@
justify-content: center;
min-height: 100vh;
padding: 24px;
background:
radial-gradient(ellipse 80% 60% at 50% -10%, hsla(262, 68%, 52%, 0.06) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 100% 100%, hsla(150, 40%, 42%, 0.04) 0%, transparent 50%),
var(--bg-canvas);
}
.form {
@@ -11,16 +15,19 @@
max-width: 400px;
display: flex;
flex-direction: column;
gap: 20px;
gap: 24px;
padding: 40px 32px;
background-color: variables.$bg-default;
border: 1px solid variables.$border-default;
border-radius: variables.$radius-lg;
box-shadow: var(--shadow-md);
box-shadow: var(--shadow-lg);
animation: formEntrance 0.5s var(--ease-out) both;
}
.title {
@include typography.font-header-l;
font-size: 22px;
font-weight: 700;
letter-spacing: -0.02em;
width: 100%;
text-align: center;
color: variables.$text-primary;
@@ -29,7 +36,7 @@
.fields {
display: flex;
flex-direction: column;
gap: 12px;
gap: 14px;
}
.actions {
@@ -41,9 +48,20 @@
@include typography.font-body-s;
color: variables.$text-secondary;
text-decoration: none;
transition: color 0.15s ease;
transition: color var(--duration-normal) var(--ease-out);
&:hover {
color: variables.$text-primary;
color: variables.$color-secondary;
}
}
@keyframes formEntrance {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@@ -20,7 +20,8 @@
.sectionTitle {
font-size: 18px;
font-weight: 600;
font-weight: 700;
letter-spacing: -0.017em;
margin-bottom: 16px;
}
+13 -2
View File
@@ -13,12 +13,23 @@ import {
import api from "@shared/api"
import { formatDate } from "@shared/lib/dates"
import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext"
import { StaticLoader } from "@shared/ui/Loader"
import { Skeleton } from "@shared/ui/Skeleton"
import { Card } from "@shared/ui"
import { IProfilePageProps } from "./ProfilePage.d"
import styles from "./ProfilePage.module.scss"
const ProfileSkeleton = () => (
<div className={styles.root}>
<div className={styles.container}>
<Skeleton width="120px" height="120px" borderRadius="50%" />
<Skeleton width="100%" height="200px" borderRadius="10px" />
<Skeleton width="100%" height="160px" borderRadius="10px" />
<Skeleton width="100%" height="140px" borderRadius="10px" />
</div>
</div>
)
export const ProfilePage: FunctionComponent<
IProfilePageProps
> = (): JSX.Element => {
@@ -46,7 +57,7 @@ export const ProfilePage: FunctionComponent<
)
}
if (isLoading) return <StaticLoader fullscreen />
if (isLoading) return <ProfileSkeleton />
if (!user) {
return (
+3
View File
@@ -0,0 +1,3 @@
export interface IProjectWizardPageProps {
className?: string
}
@@ -0,0 +1,48 @@
"use client"
import type { IProjectWizardPageProps } from "./ProjectWizardPage.d"
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 { WizardProvider } from "@shared/context/WizardContext"
import { WorkspaceProvider } from "@shared/context/WorkspaceContext"
import { ProjectWizard } from "@widgets/ProjectWizard"
import styles from "./ProjectWizardPage.module.scss"
export const ProjectWizardPage: FunctionComponent<
IProjectWizardPageProps
> = (): 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: "Проекты", href: "/projects" },
{ label: project?.name ?? "..." },
])
return (
<WorkspaceProvider projectId={projectId}>
<WizardProvider projectId={projectId}>
<div
className={styles.root}
data-testid="ProjectWizardPage"
>
<ProjectWizard />
</div>
</WizardProvider>
</WorkspaceProvider>
)
}
+1
View File
@@ -0,0 +1 @@
export * from "./ProjectWizardPage"
@@ -1,3 +0,0 @@
export interface IProjectWorkspacePageProps {
message?: string
}
@@ -1,71 +0,0 @@
"use client"
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: "Проекты", href: "/projects" },
{ label: project?.name ?? "..." },
])
return (
<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
View File
@@ -1 +0,0 @@
export * from "./ProjectWorkspacePage"
+19 -16
View File
@@ -19,7 +19,7 @@ import {
import api from "@shared/api"
import { useDebounce } from "@shared/hooks/useDebounce"
import { Button } from "@shared/ui"
import { StaticLoader } from "@shared/ui/Loader"
import { ProjectCardSkeleton } from "@shared/ui/Skeleton"
import {
ProjectsHeader,
type ProjectStatusEnum,
@@ -59,7 +59,6 @@ export const ProjectsPage: FunctionComponent<
return (
<div className={styles.root} data-testid="ProjectsPage">
{projectsLoading && <StaticLoader fullscreen />}
<div className={styles.header}>
<div className={styles.titles}>
<h1 className={styles.title}>Мои проекты</h1>
@@ -133,20 +132,24 @@ export const ProjectsPage: FunctionComponent<
/>
<div className={styles.projectList}>
{projects?.map((project) => (
<ProjectCard
key={project.id}
project={project}
progress={project.status === "PROCESSING" ? 45 : 0}
currentAction={
project.status === "PROCESSING" ? "Рендеринг" : undefined
}
onClick={() => router.push(`/projects/${project.id}`)}
onEdit={() => setEditProject(project)}
onRename={() => setRenameProject(project)}
onDelete={() => setDeleteProject(project)}
/>
))}
{projectsLoading
? Array.from({ length: 6 }).map((_, i) => (
<ProjectCardSkeleton key={i} />
))
: projects?.map((project) => (
<ProjectCard
key={project.id}
project={project}
progress={project.status === "PROCESSING" ? 45 : 0}
currentAction={
project.status === "PROCESSING" ? "Рендеринг" : undefined
}
onClick={() => router.push(`/projects/${project.id}`)}
onEdit={() => setEditProject(project)}
onRename={() => setRenameProject(project)}
onDelete={() => setDeleteProject(project)}
/>
))}
</div>
{!projectsLoading && projects?.length === 0 && (
@@ -16,7 +16,7 @@ const PING_INTERVAL_MS = 5000
export const UnderMaintenancePage: FunctionComponent<IUnderMaintenancePageProps> = (): JSX.Element => {
const router = useRouter()
const searchParams = useSearchParams()
const redirectPath = searchParams.get("path") || "/"
const redirectPath = searchParams?.get("path") || "/"
const { isSuccess } = api.useQuery("get", "/api/ping/", {}, {
refetchInterval: PING_INTERVAL_MS,