From 42ce5fa0fefde9cec12a64cef9f2f0da16cd3bc4 Mon Sep 17 00:00:00 2001 From: Daniil Date: Thu, 19 Feb 2026 22:25:27 +0300 Subject: [PATCH] Add dynamic breadcrumbs to Header via React Context Replace hardcoded "Coffee Project / Projects" with a BreadcrumbsContext that each page registers into via useBreadcrumbs(). The Header reads breadcrumb items dynamically, renders links for items with href, and makes "Coffee Project" clickable to open the navigation drawer. Also removes the unused currentScreenName from Redux appState. Co-Authored-By: Claude Opus 4.6 --- src/pages/HomePage/HomePage.tsx | 5 + src/pages/ProfilePage/ProfilePage.tsx | 101 ++++++++++++++++++ .../ProjectWorkspacePage.tsx | 13 +++ src/pages/ProjectsPage/ProjectsPage.tsx | 63 ++++++++++- src/shared/context/AppProviders.tsx | 9 +- src/shared/context/BreadcrumbsContext.tsx | 52 +++++++++ src/shared/store/appState/index.ts | 15 +-- src/shared/store/appState/types.ts | 4 +- src/widgets/Header/Header.module.scss | 46 ++++++-- src/widgets/Header/Header.tsx | 33 +++++- 10 files changed, 307 insertions(+), 34 deletions(-) create mode 100644 src/pages/ProfilePage/ProfilePage.tsx create mode 100644 src/shared/context/BreadcrumbsContext.tsx diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index c062181..1278a0a 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -1,8 +1,13 @@ +"use client" + +import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext" import { Button } from "@shared/ui" import cls from "./HomePage.module.scss" const HomePage = () => { + useBreadcrumbs([{ label: "Home" }]) + return (

Coffee Project Starter

diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx new file mode 100644 index 0000000..5de15df --- /dev/null +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -0,0 +1,101 @@ +"use client" + +import type { JSX } from "react" + +import moment from "moment" +import { FunctionComponent } from "react" + +import { + AvatarUpload, + ChangePasswordForm, + EditProfileForm, + LogoutButton, +} from "@features/profile" +import api from "@shared/api" +import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext" +import { StaticLoader } from "@shared/ui/Loader" +import { Card } from "@shared/ui" + +import { IProfilePageProps } from "./ProfilePage.d" +import styles from "./ProfilePage.module.scss" + +export const ProfilePage: FunctionComponent< + IProfilePageProps +> = (): JSX.Element => { + useBreadcrumbs([{ label: "Profile" }]) + + const { + data: user, + isLoading, + refetch, + } = api.useQuery("get", "/api/users/me/") + + const { mutate: updateUser } = api.useMutation( + "patch", + "/api/users/{user_id}/", + ) + + const handleAvatarChange = (url: string) => { + if (!user) return + updateUser( + { + params: { path: { user_id: user.id } }, + body: { avatar: url }, + }, + { onSuccess: () => refetch() }, + ) + } + + if (isLoading) return + + if (!user) { + return ( +
+

Не удалось загрузить данные пользователя

+
+ ) + } + + return ( +
+
+ + + + + + + + + + + +

Аккаунт

+
+
+ Логин + {user.username} +
+
+ Дата регистрации + + {moment(user.date_joined).format("DD.MM.YYYY")} + +
+
+ Статус + + {user.is_active ? "Активен" : "Неактивен"} + +
+
+
+ + +
+
+ ) +} diff --git a/src/pages/ProjectWorkspacePage/ProjectWorkspacePage.tsx b/src/pages/ProjectWorkspacePage/ProjectWorkspacePage.tsx index 8868a60..248574e 100644 --- a/src/pages/ProjectWorkspacePage/ProjectWorkspacePage.tsx +++ b/src/pages/ProjectWorkspacePage/ProjectWorkspacePage.tsx @@ -1,13 +1,26 @@ +"use client" + import type { JSX } from "react" +import { useParams } from "next/navigation" import { FunctionComponent } from "react" +import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext" + import { IProjectWorkspacePageProps } from "./ProjectWorkspacePage.d" import styles from "./ProjectWorkspacePage.module.scss" export const ProjectWorkspacePage: FunctionComponent< IProjectWorkspacePageProps > = (): JSX.Element => { + const params = useParams<{ project_id: string }>() + const projectId = params?.project_id ?? "" + + useBreadcrumbs([ + { label: "Projects", href: "/projects" }, + { label: `Project ${projectId}` }, + ]) + return (
ProjectWorkspacePage Component diff --git a/src/pages/ProjectsPage/ProjectsPage.tsx b/src/pages/ProjectsPage/ProjectsPage.tsx index 78bcf86..2fb8af8 100644 --- a/src/pages/ProjectsPage/ProjectsPage.tsx +++ b/src/pages/ProjectsPage/ProjectsPage.tsx @@ -1,12 +1,21 @@ "use client" +import type { components } from "@shared/api/__generated__/openapi.types" import type { JSX } from "react" import { PlusIcon } from "lucide-react" import { FunctionComponent, useState } from "react" +import { useRouter } from "next/navigation" + import { ProjectCard } from "@entities/ProjectCard" -import { CreateProjectModal } from "@features/CreateProjectModal" +import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext" +import { + CreateProjectModal, + DeleteProjectModal, + EditProjectModal, + RenameProjectModal, +} from "@features/project" import api from "@shared/api" import { Button } from "@shared/ui" import { StaticLoader } from "@shared/ui/Loader" @@ -14,13 +23,18 @@ import { ProjectsHeader } from "@widgets/Projects/ProjectsHeader" import { IProjectsPageProps } from "./ProjectsPage.d" import styles from "./ProjectsPage.module.scss" -import { useRouter } from "next/navigation" + +type ProjectRead = components["schemas"]["ProjectRead"] export const ProjectsPage: FunctionComponent< IProjectsPageProps > = (): JSX.Element => { - const router = useRouter(); + useBreadcrumbs([{ label: "Projects", href: "/projects" }]) + const router = useRouter() const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) + const [editProject, setEditProject] = useState(null) + const [renameProject, setRenameProject] = useState(null) + const [deleteProject, setDeleteProject] = useState(null) const { data: projects, @@ -57,6 +71,45 @@ export const ProjectsPage: FunctionComponent< }} /> + {editProject && ( + { + if (!open) setEditProject(null) + }} + project={editProject} + onUpdated={async () => { + await refetchProjects() + }} + /> + )} + + {renameProject && ( + { + if (!open) setRenameProject(null) + }} + project={renameProject} + onRenamed={async () => { + await refetchProjects() + }} + /> + )} + + {deleteProject && ( + { + if (!open) setDeleteProject(null) + }} + project={deleteProject} + onDeleted={async () => { + await refetchProjects() + }} + /> + )} +
@@ -64,12 +117,14 @@ export const ProjectsPage: FunctionComponent< router.push(`/projects/${project.id}`)} + onEdit={() => setEditProject(project)} + onRename={() => setRenameProject(project)} + onDelete={() => setDeleteProject(project)} /> ))}
diff --git a/src/shared/context/AppProviders.tsx b/src/shared/context/AppProviders.tsx index fe77477..7c69193 100644 --- a/src/shared/context/AppProviders.tsx +++ b/src/shared/context/AppProviders.tsx @@ -7,6 +7,7 @@ import { Provider as ReduxProvider } from "react-redux" import { store } from "@shared/store" +import { BreadcrumbsProvider } from "./BreadcrumbsContext" import { QueryClientProvider } from "./QueryClientProvider" import { UserSync } from "./UserSync" @@ -19,9 +20,11 @@ export const AppProviders = ({ - - {children} - + + + {children} + + ) diff --git a/src/shared/context/BreadcrumbsContext.tsx b/src/shared/context/BreadcrumbsContext.tsx new file mode 100644 index 0000000..a426d58 --- /dev/null +++ b/src/shared/context/BreadcrumbsContext.tsx @@ -0,0 +1,52 @@ +"use client" + +import { + createContext, + type JSX, + type ReactNode, + useContext, + useEffect, + useState, +} from "react" + +export interface BreadcrumbItem { + label: string + href?: string +} + +interface BreadcrumbsContextValue { + items: BreadcrumbItem[] + setItems: (items: BreadcrumbItem[]) => void +} + +const BreadcrumbsContext = createContext({ + items: [], + setItems: () => {}, +}) + +export const BreadcrumbsProvider = ({ + children, +}: { + children: ReactNode +}): JSX.Element => { + const [items, setItems] = useState([]) + + return ( + + {children} + + ) +} + +export const useBreadcrumbs = (items: BreadcrumbItem[]): void => { + const { setItems } = useContext(BreadcrumbsContext) + + useEffect(() => { + setItems(items) + return () => setItems([]) + }, [JSON.stringify(items)]) +} + +export const useBreadcrumbItems = (): BreadcrumbItem[] => { + return useContext(BreadcrumbsContext).items +} diff --git a/src/shared/store/appState/index.ts b/src/shared/store/appState/index.ts index 4e06f4e..6ed8706 100644 --- a/src/shared/store/appState/index.ts +++ b/src/shared/store/appState/index.ts @@ -1,25 +1,18 @@ -import type { PayloadAction } from "@reduxjs/toolkit" - import { createSlice } from "@reduxjs/toolkit" import { AppState } from "./types" -const initialState: AppState = { - currentScreenName: "", -} +const initialState: AppState = {} const appStateSlice = createSlice({ name: "appState", initialState, reducers: { - setCurrentScreenName(state, action: PayloadAction) { - state.currentScreenName = action.payload - }, - resetAppState(state) { - state.currentScreenName = initialState.currentScreenName + resetAppState() { + return initialState }, }, }) -export const { resetAppState, setCurrentScreenName } = appStateSlice.actions +export const { resetAppState } = appStateSlice.actions export const appStateReducer = appStateSlice.reducer diff --git a/src/shared/store/appState/types.ts b/src/shared/store/appState/types.ts index 9c30228..06e2c2e 100644 --- a/src/shared/store/appState/types.ts +++ b/src/shared/store/appState/types.ts @@ -1,3 +1 @@ -export interface AppState { - currentScreenName: string -} +export interface AppState {} diff --git a/src/widgets/Header/Header.module.scss b/src/widgets/Header/Header.module.scss index 88c1ac0..a87e01e 100644 --- a/src/widgets/Header/Header.module.scss +++ b/src/widgets/Header/Header.module.scss @@ -1,30 +1,60 @@ .root { - padding: 12px 24px; - background-color: variables.$color-white; + padding: 0 24px; + height: var(--header-height); + background-color: variables.$bg-default; display: flex; justify-content: space-between; align-items: center; + border-bottom: 1px solid variables.$border-default; + position: sticky; + top: 0; + z-index: 50; } .screenPath { display: flex; - gap: 6px; + gap: 8px; align-items: center; } .brandTitle { - @include typography.font-body-16(800); + @include typography.font-body-16(600); + color: variables.$text-primary; + cursor: pointer; + + &:hover { + color: variables.$text-secondary; + } } +.separator { + @include typography.font-body-14(400); + color: variables.$text-tertiary; +} -.separator, .screenName { - @include typography.font-body-14(500); +.screenName { + @include typography.font-body-14(400); color: variables.$text-secondary; } +.breadcrumbSegment { + display: flex; + gap: 8px; + align-items: center; +} + +.breadcrumbLink { + @include typography.font-body-14(400); + color: variables.$text-secondary; + text-decoration: none; + + &:hover { + color: variables.$text-primary; + } +} .start { display: flex; - gap: 16px; + gap: 12px; align-items: center; -} \ No newline at end of file +} diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index d71a42d..432d3ce 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -2,11 +2,17 @@ import type { JSX } from "react" -import { Menu as MenuIcon } from "lucide-react" +import { FolderKanban, Menu as MenuIcon } from "lucide-react" +import dynamic from "next/dynamic" +import Link from "next/link" import { FunctionComponent, useState } from "react" -import { NavigationDrawer } from "@entities/NavigationDrawer" +const NavigationDrawer = dynamic( + () => import("@entities/NavigationDrawer").then((m) => m.NavigationDrawer), + { ssr: false }, +) import { UserDropdown } from "@entities/UserDropdown" +import { useBreadcrumbItems } from "@shared/context/BreadcrumbsContext" import { useAppSelector } from "@shared/hooks/useAppSelector" import { Button } from "@shared/ui" @@ -15,6 +21,7 @@ import styles from "./Header.module.scss" export const Header: FunctionComponent = (): JSX.Element => { const userData = useAppSelector((state) => state.user.user) + const breadcrumbs = useBreadcrumbItems() const [isDrawerOpen, setIsDrawerOpen] = useState(false) @@ -26,9 +33,24 @@ export const Header: FunctionComponent = (): JSX.Element => {
-

Coffee Project

- / -

Projects

+

setIsDrawerOpen(true)} + > + Coffee Project +

+ {breadcrumbs.map((item, index) => ( + + / + {item.href ? ( + + {item.label} + + ) : ( + {item.label} + )} + + ))}
@@ -41,6 +63,7 @@ export const Header: FunctionComponent = (): JSX.Element => { buttons={[ { label: "Проекты", + icon: FolderKanban, path: "/projects", }, ]}