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