new features
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./UnderMaintenancePage"
|
||||
Reference in New Issue
Block a user