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",
},
]}