iter 2
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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 (
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface IProjectWizardPageProps {
|
||||
className?: string
|
||||
}
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
export * from "./ProjectWorkspacePage"
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user