From 305e72725cf5cb68b6dd88ecb319257970d35db4 Mon Sep 17 00:00:00 2001 From: Daniil Date: Sat, 28 Feb 2026 17:41:14 +0300 Subject: [PATCH] nf --- .../dashboard/ActionCard/ActionCard.d.ts | 9 ++ .../ActionCard/ActionCard.module.scss | 47 ++++++++ .../dashboard/ActionCard/ActionCard.tsx | 28 +++++ src/entities/dashboard/ActionCard/index.ts | 1 + src/entities/dashboard/index.ts | 1 + src/pages/HomePage/HomePage.d.ts | 3 + src/pages/HomePage/HomePage.module.scss | 64 ++++++----- src/pages/HomePage/HomePage.tsx | 102 ++++++++++++++++-- .../RecentProjects/RecentProjects.d.ts | 11 ++ .../RecentProjects/RecentProjects.module.scss | 38 +++++++ .../RecentProjects/RecentProjects.tsx | 56 ++++++++++ src/widgets/Dashboard/RecentProjects/index.ts | 1 + .../Dashboard/StatsGrid/StatsGrid.d.ts | 6 ++ .../Dashboard/StatsGrid/StatsGrid.module.scss | 27 +++++ src/widgets/Dashboard/StatsGrid/StatsGrid.tsx | 37 +++++++ src/widgets/Dashboard/StatsGrid/index.ts | 1 + src/widgets/Header/Header.tsx | 7 +- src/widgets/Workspace/FileTree/FileTree.tsx | 30 +++--- 18 files changed, 416 insertions(+), 53 deletions(-) create mode 100644 src/entities/dashboard/ActionCard/ActionCard.d.ts create mode 100644 src/entities/dashboard/ActionCard/ActionCard.module.scss create mode 100644 src/entities/dashboard/ActionCard/ActionCard.tsx create mode 100644 src/entities/dashboard/ActionCard/index.ts create mode 100644 src/entities/dashboard/index.ts create mode 100644 src/pages/HomePage/HomePage.d.ts create mode 100644 src/widgets/Dashboard/RecentProjects/RecentProjects.d.ts create mode 100644 src/widgets/Dashboard/RecentProjects/RecentProjects.module.scss create mode 100644 src/widgets/Dashboard/RecentProjects/RecentProjects.tsx create mode 100644 src/widgets/Dashboard/RecentProjects/index.ts create mode 100644 src/widgets/Dashboard/StatsGrid/StatsGrid.d.ts create mode 100644 src/widgets/Dashboard/StatsGrid/StatsGrid.module.scss create mode 100644 src/widgets/Dashboard/StatsGrid/StatsGrid.tsx create mode 100644 src/widgets/Dashboard/StatsGrid/index.ts diff --git a/src/entities/dashboard/ActionCard/ActionCard.d.ts b/src/entities/dashboard/ActionCard/ActionCard.d.ts new file mode 100644 index 0000000..1ed257c --- /dev/null +++ b/src/entities/dashboard/ActionCard/ActionCard.d.ts @@ -0,0 +1,9 @@ +import type { ComponentType } from "react" + +export interface IActionCardProps { + icon: ComponentType<{ size?: number; strokeWidth?: number }> + label: string + onClick: () => void + accent?: boolean + className?: string +} diff --git a/src/entities/dashboard/ActionCard/ActionCard.module.scss b/src/entities/dashboard/ActionCard/ActionCard.module.scss new file mode 100644 index 0000000..6abc049 --- /dev/null +++ b/src/entities/dashboard/ActionCard/ActionCard.module.scss @@ -0,0 +1,47 @@ +.card { + @include mixins.reset-button; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + + width: 160px; + height: 160px; + + background: variables.$bg-default; + border: 1px solid variables.$border-default; + border-radius: variables.$radius-lg; + box-shadow: var(--shadow-sm); + + color: variables.$text-secondary; + + cursor: pointer; + transition: + transform 0.15s ease, + box-shadow 0.15s ease, + border-color 0.15s ease, + color 0.15s ease; + + &:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-md); + color: variables.$text-primary; + } + + &.accent { + background: variables.$purple-50; + border-color: variables.$purple-100; + color: variables.$purple-400; + + &:hover { + background: variables.$purple-100; + border-color: variables.$purple-400; + } + } +} + +.label { + @include typography.font-body-s; +} diff --git a/src/entities/dashboard/ActionCard/ActionCard.tsx b/src/entities/dashboard/ActionCard/ActionCard.tsx new file mode 100644 index 0000000..a8ddad6 --- /dev/null +++ b/src/entities/dashboard/ActionCard/ActionCard.tsx @@ -0,0 +1,28 @@ +import type { IActionCardProps } from "./ActionCard.d" +import type { JSX } from "react" + +import { FunctionComponent } from "react" + +import cs from "classnames" + +import styles from "./ActionCard.module.scss" + +export const ActionCard: FunctionComponent = ({ + icon: Icon, + label, + onClick, + accent = false, + className, +}): JSX.Element => { + return ( + + ) +} diff --git a/src/entities/dashboard/ActionCard/index.ts b/src/entities/dashboard/ActionCard/index.ts new file mode 100644 index 0000000..1d04140 --- /dev/null +++ b/src/entities/dashboard/ActionCard/index.ts @@ -0,0 +1 @@ +export * from "./ActionCard" diff --git a/src/entities/dashboard/index.ts b/src/entities/dashboard/index.ts new file mode 100644 index 0000000..1e0d00a --- /dev/null +++ b/src/entities/dashboard/index.ts @@ -0,0 +1 @@ +export { ActionCard } from "./ActionCard" diff --git a/src/pages/HomePage/HomePage.d.ts b/src/pages/HomePage/HomePage.d.ts new file mode 100644 index 0000000..fcb071b --- /dev/null +++ b/src/pages/HomePage/HomePage.d.ts @@ -0,0 +1,3 @@ +export interface IHomePageProps { + className?: string +} diff --git a/src/pages/HomePage/HomePage.module.scss b/src/pages/HomePage/HomePage.module.scss index d2eeda2..0e718d4 100644 --- a/src/pages/HomePage/HomePage.module.scss +++ b/src/pages/HomePage/HomePage.module.scss @@ -1,36 +1,48 @@ -.homepage { +.root { + padding: 28px 24px 40px; display: flex; - align-items: center; flex-direction: column; - justify-content: center; - gap: 1rem; - - height: 100vh; - - color: #c7d0cc; - - background: #000; - - font-family: Roboto, sans-serif; - font-size: 2vw; + gap: 32px; } -.path { - font-style: italic; +.welcome { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + text-align: center; + min-height: 160px; } -.title { - font-size: 4vw; +.greeting { font-weight: 700; + font-size: 52px; + line-height: 1.1; + letter-spacing: -1px; + color: variables.$text-primary; + margin: 0; } -.hint { - padding: 0.5rem; - - pointer-events: none; - - border: rgb(199 208 204 / 5%) 1px solid; - border-radius: 15px; - - font-size: 1vw; +.subtitle { + @include typography.font-body-mr; + font-size: 18px; + color: variables.$text-secondary; + margin: 0; +} + +.actionsSection { + display: flex; + flex-direction: column; + gap: 16px; +} + +.actionsTitle { + @include typography.font-header-l; + color: variables.$text-primary; + margin: 0; +} + +.actions { + display: flex; + gap: 16px; } diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index 8551278..8e2a8ad 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -1,21 +1,103 @@ "use client" -import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext" -import { Button } from "@shared/ui" +import type { JSX } from "react" +import { FunctionComponent, useMemo, useState } from "react" + +import { FolderKanban, PlusIcon } from "lucide-react" +import { useRouter } from "next/navigation" + +import { ActionCard } from "@entities/dashboard" +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 { RecentProjects } from "@widgets/Dashboard/RecentProjects" +import { StatsGrid } from "@widgets/Dashboard/StatsGrid" + +import { IHomePageProps } from "./HomePage.d" import cls from "./HomePage.module.scss" -const HomePage = () => { +const RECENT_PROJECTS_LIMIT = 3 + +const WELCOME_SUBTITLES = [ + "Управляйте своими проектами и отслеживайте их статус", + "Что будем делать сегодня?", + "Ваши проекты ждут вас", + "Создайте новый проект или продолжите работу над существующим", + "Всё под контролем — просматривайте и управляйте проектами", +] + +export const HomePage: FunctionComponent = (): JSX.Element => { useBreadcrumbs([{ label: "Главная" }]) + const router = useRouter() + const user = useAppSelector((state) => state.user.user) + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) + + const { data: projects, isLoading, refetch } = api.useQuery("get", "/api/projects/") + + const subtitle = useMemo( + () => WELCOME_SUBTITLES[Math.floor(Math.random() * WELCOME_SUBTITLES.length)], + [], + ) + + const stats = { + total: projects?.length ?? 0, + processing: projects?.filter((p) => p.status === "PROCESSING").length ?? 0, + done: projects?.filter((p) => p.status === "DONE").length ?? 0, + failed: projects?.filter((p) => p.status === "FAILED").length ?? 0, + } + + const recentProjects = [...(projects ?? [])] + .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) + .slice(0, RECENT_PROJECTS_LIMIT) + + const userName = user?.first_name || user?.username || "пользователь" return ( -
-

Coffee Project Starter

-
-				Редактируйте src/pages/HomePage{" "}
-				чтобы начать разработку.
-			
- +
+ {isLoading && } + +
+

Добро пожаловать, {userName}

+

{subtitle}

+
+ + + + router.push(`/projects/${id}`)} + onCreateClick={() => setIsCreateModalOpen(true)} + onViewAllClick={() => router.push("/projects")} + /> + +
+

Быстрые действия

+
+ setIsCreateModalOpen(true)} + /> + router.push("/projects")} + /> +
+
+ + { + await refetch() + }} + />
) } diff --git a/src/widgets/Dashboard/RecentProjects/RecentProjects.d.ts b/src/widgets/Dashboard/RecentProjects/RecentProjects.d.ts new file mode 100644 index 0000000..9bc3a85 --- /dev/null +++ b/src/widgets/Dashboard/RecentProjects/RecentProjects.d.ts @@ -0,0 +1,11 @@ +import type { components } from "@shared/api/__generated__/openapi.types" + +type ProjectRead = components["schemas"]["ProjectRead"] + +export interface IRecentProjectsProps { + projects: ProjectRead[] + isLoading: boolean + onProjectClick: (id: string) => void + onCreateClick: () => void + onViewAllClick: () => void +} diff --git a/src/widgets/Dashboard/RecentProjects/RecentProjects.module.scss b/src/widgets/Dashboard/RecentProjects/RecentProjects.module.scss new file mode 100644 index 0000000..3279f45 --- /dev/null +++ b/src/widgets/Dashboard/RecentProjects/RecentProjects.module.scss @@ -0,0 +1,38 @@ +.section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.title { + @include typography.font-header-l; + color: variables.$text-primary; + margin: 0; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 20px; +} + +.empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + padding: 48px 0; + color: variables.$text-tertiary; +} + +.emptyText { + @include typography.font-body-mr; + color: variables.$text-secondary; + margin: 0; +} diff --git a/src/widgets/Dashboard/RecentProjects/RecentProjects.tsx b/src/widgets/Dashboard/RecentProjects/RecentProjects.tsx new file mode 100644 index 0000000..f8fab6c --- /dev/null +++ b/src/widgets/Dashboard/RecentProjects/RecentProjects.tsx @@ -0,0 +1,56 @@ +import type { IRecentProjectsProps } from "./RecentProjects.d" +import type { JSX } from "react" + +import { FunctionComponent } from "react" + +import { FolderOpenIcon, PlusIcon } from "lucide-react" + +import { ProjectCard } from "@entities/ProjectCard" +import { Button } from "@shared/ui" + +import styles from "./RecentProjects.module.scss" + +export const RecentProjects: FunctionComponent = ({ + projects, + isLoading, + onProjectClick, + onCreateClick, + onViewAllClick, +}): JSX.Element => { + const isEmpty = !isLoading && projects.length === 0 + + return ( +
+
+

Последние проекты

+ {!isEmpty && ( + + )} +
+ + {isEmpty ? ( +
+ +

У вас пока нет проектов

+ +
+ ) : ( +
+ {projects.map((project) => ( + onProjectClick(project.id)} + /> + ))} +
+ )} +
+ ) +} diff --git a/src/widgets/Dashboard/RecentProjects/index.ts b/src/widgets/Dashboard/RecentProjects/index.ts new file mode 100644 index 0000000..b58c021 --- /dev/null +++ b/src/widgets/Dashboard/RecentProjects/index.ts @@ -0,0 +1 @@ +export * from "./RecentProjects" diff --git a/src/widgets/Dashboard/StatsGrid/StatsGrid.d.ts b/src/widgets/Dashboard/StatsGrid/StatsGrid.d.ts new file mode 100644 index 0000000..0f29fa0 --- /dev/null +++ b/src/widgets/Dashboard/StatsGrid/StatsGrid.d.ts @@ -0,0 +1,6 @@ +export interface IStatsGridProps { + total: number + processing: number + done: number + failed: number +} diff --git a/src/widgets/Dashboard/StatsGrid/StatsGrid.module.scss b/src/widgets/Dashboard/StatsGrid/StatsGrid.module.scss new file mode 100644 index 0000000..c5ad536 --- /dev/null +++ b/src/widgets/Dashboard/StatsGrid/StatsGrid.module.scss @@ -0,0 +1,27 @@ +.grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; +} + +.card { + display: flex; + flex-direction: column; + gap: 4px; + padding: 20px 24px; + background: variables.$bg-default; + border: 1px solid variables.$border-default; + border-radius: variables.$radius-md; + box-shadow: var(--shadow-sm); +} + +.value { + @include typography.font-display; + color: variables.$text-primary; + line-height: 1; +} + +.label { + @include typography.font-body-s; + color: variables.$text-secondary; +} diff --git a/src/widgets/Dashboard/StatsGrid/StatsGrid.tsx b/src/widgets/Dashboard/StatsGrid/StatsGrid.tsx new file mode 100644 index 0000000..be6e00e --- /dev/null +++ b/src/widgets/Dashboard/StatsGrid/StatsGrid.tsx @@ -0,0 +1,37 @@ +import type { IStatsGridProps } from "./StatsGrid.d" +import type { JSX } from "react" + +import { FunctionComponent } from "react" + +import styles from "./StatsGrid.module.scss" + +interface IStatCardProps { + label: string + value: number + color?: string +} + +const StatCard = ({ label, value, color }: IStatCardProps) => ( +
+ + {value} + + {label} +
+) + +export const StatsGrid: FunctionComponent = ({ + total, + processing, + done, + failed, +}): JSX.Element => { + return ( +
+ + + + +
+ ) +} diff --git a/src/widgets/Dashboard/StatsGrid/index.ts b/src/widgets/Dashboard/StatsGrid/index.ts new file mode 100644 index 0000000..fc59eff --- /dev/null +++ b/src/widgets/Dashboard/StatsGrid/index.ts @@ -0,0 +1 @@ +export * from "./StatsGrid" diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index da05280..b2777fc 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -2,7 +2,7 @@ import type { JSX } from "react" -import { FolderKanban, Menu as MenuIcon } from "lucide-react" +import { FolderKanban, Home, Menu as MenuIcon } from "lucide-react" import dynamic from "next/dynamic" import Link from "next/link" import { FunctionComponent, useState } from "react" @@ -63,6 +63,11 @@ export const Header: FunctionComponent = (): JSX.Element => { open={isDrawerOpen} onClose={() => setIsDrawerOpen(false)} buttons={[ + { + label: "Главная", + icon: Home, + path: "/", + }, { label: "Проекты", icon: FolderKanban, diff --git a/src/widgets/Workspace/FileTree/FileTree.tsx b/src/widgets/Workspace/FileTree/FileTree.tsx index 58beed4..68f3c29 100644 --- a/src/widgets/Workspace/FileTree/FileTree.tsx +++ b/src/widgets/Workspace/FileTree/FileTree.tsx @@ -19,6 +19,8 @@ import { } from "lucide-react" import { FunctionComponent, useCallback, useEffect, useRef, useState } from "react" +import { Tooltip } from "@radix-ui/themes" + import { DeleteFileModal } from "@features/project" import { useDeleteArtifact, @@ -272,12 +274,11 @@ export const FileTree: FunctionComponent = ({ } > - + + {file.displayName} + {!used && ( {!used && ( {!used && (