-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 메인 레이아웃 셸 구현 (네비게이션/타임 사이드바, 헤더, 라우트 그룹) #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
742012f
72cde20
4e2be51
ddc7b0e
327be60
e7b84be
7391d65
000f128
59e1e05
ccf24c6
f275ad4
46d6868
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| "use client"; | ||
|
|
||
| import { cn } from "@repo/timo-design-system/utils"; | ||
| import { useState } from "react"; | ||
|
|
||
| import { | ||
| TIME_SIDEBAR_COLLAPSED_MARGIN_CLASS_NAME, | ||
| TIME_SIDEBAR_MARGIN_CLASS_NAME, | ||
| type TimeSidebarSize, | ||
| } from "@/components/layout/sidebar/time/time-sidebar-size"; | ||
| import { TimeSidebar } from "@/components/layout/sidebar/time/TimeSidebar"; | ||
| import { ROUTES } from "@/constants/routes"; | ||
| import { usePathname } from "@/i18n/navigation"; | ||
|
|
||
| interface WithTimeSidebarContainerProps { | ||
| children: React.ReactNode; | ||
| } | ||
|
|
||
| export const WithTimeSidebarContainer = ({ | ||
| children, | ||
| }: WithTimeSidebarContainerProps) => { | ||
| const [isOpen, setIsOpen] = useState<boolean>(true); | ||
|
|
||
| const pathname = usePathname(); | ||
|
|
||
| const size: TimeSidebarSize = pathname.startsWith(ROUTES.TODAY) ? "lg" : "sm"; | ||
|
|
||
| return ( | ||
| <> | ||
| <div | ||
| className={cn( | ||
| "transition-[margin-right] duration-200 ease-in-out", | ||
| isOpen | ||
| ? TIME_SIDEBAR_MARGIN_CLASS_NAME[size] | ||
| : TIME_SIDEBAR_COLLAPSED_MARGIN_CLASS_NAME, | ||
| )} | ||
| > | ||
| {children} | ||
| </div> | ||
|
kimminna marked this conversation as resolved.
|
||
|
|
||
| <TimeSidebar | ||
| size={size} | ||
| isOpen={isOpen} | ||
| onToggleCollapse={() => setIsOpen((prev) => !prev)} | ||
| /> | ||
| </> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
|
|
||
| import { Header } from "@/components/layout/header/Header"; | ||
| import { useNavigationSidebar } from "@/components/layout/sidebar/navigation/NavigationSidebarContext"; | ||
|
|
||
| const VIEW_OPTIONS = ["기본", "7일"] as const; | ||
|
|
||
| type ViewOption = (typeof VIEW_OPTIONS)[number]; | ||
|
|
||
| const isViewOption = (value: string): value is ViewOption => | ||
| (VIEW_OPTIONS as readonly string[]).includes(value); | ||
|
|
||
| export const HomeHeaderContainer = () => { | ||
| const [view, setView] = useState<ViewOption>(VIEW_OPTIONS[0]); | ||
| const { isOpen, toggle } = useNavigationSidebar(); | ||
|
|
||
| const handleChangeView = (value: string) => { | ||
| if (isViewOption(value)) { | ||
| setView(value); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <Header | ||
| left={ | ||
| <> | ||
| <Header.SidebarButton isOpen={isOpen} onClick={toggle} /> | ||
| <Header.TodayButton /> | ||
| </> | ||
| } | ||
| right={ | ||
| <Header.ViewDropdown | ||
| items={[...VIEW_OPTIONS]} | ||
| value={view} | ||
| onChange={handleChangeView} | ||
| /> | ||
| } | ||
| /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { HomeHeaderContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeHeaderContainer"; | ||
| import { HomeTodoContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer"; | ||
|
|
||
| export default function HomePage() { | ||
| return ( | ||
| <> | ||
| <HomeHeaderContainer /> | ||
| <section> | ||
| <HomeTodoContainer /> | ||
| </section> | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { WithTimeSidebarContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/_containers/WithTimeSidebarContainer"; | ||
|
|
||
| interface WithTimeSidebarLayoutProps { | ||
| children: React.ReactNode; | ||
| } | ||
|
|
||
| export default function WithTimeSidebarLayout({ | ||
| children, | ||
| }: Readonly<WithTimeSidebarLayoutProps>) { | ||
| return <WithTimeSidebarContainer>{children}</WithTimeSidebarContainer>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| "use client"; | ||
|
|
||
| import { Header } from "@/components/layout/header/Header"; | ||
| import { useNavigationSidebar } from "@/components/layout/sidebar/navigation/NavigationSidebarContext"; | ||
|
|
||
| export const TodayHeaderContainer = () => { | ||
| const { isOpen, toggle } = useNavigationSidebar(); | ||
|
|
||
| return ( | ||
| <Header left={<Header.SidebarButton isOpen={isOpen} onClick={toggle} />} /> | ||
| ); | ||
| }; | ||
|
Comment on lines
+1
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift 패턴 자체는 깔끔해요, 다만 중복이 좀 신경 쓰이네요.
React 공식 문서의 Custom Hooks로 로직 재사용하기 참고하시면 도움이 될 것 같습니다. ♻️ 제안 방향 예시// components/layout/header/NavigationSidebarHeader.tsx
"use client";
import { Header } from "`@/components/layout/header/Header`";
import { useNavigationSidebar } from "`@/components/layout/sidebar/navigation/NavigationSidebarContext`";
export const NavigationSidebarHeader = () => {
const { isOpen, toggle } = useNavigationSidebar();
return (
<Header left={<Header.SidebarButton isOpen={isOpen} onClick={toggle} />} />
);
};각 페이지 layout에서 🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 책임 분리 가능성에 따라 지금을 유지하도록 하겠습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR description images show the Timo app dashboard in various states (Today view with tasks, empty Today view, Focus Mode, Statistics with month navigation, and a collapsed/blank state) — these illustrate the overall
✏️ Learnings added
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { TodayHeaderContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayHeaderContainer"; | ||
|
|
||
| export default function TodayPage() { | ||
| return <TodayHeaderContainer />; | ||
| } | ||
|
kimminna marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| "use client"; | ||
|
|
||
| import { cn } from "@repo/timo-design-system/utils"; | ||
|
|
||
| import { NavigationSidebar } from "@/components/layout/sidebar/navigation/NavigationSidebar"; | ||
| import { | ||
| NavigationSidebarProvider, | ||
| useNavigationSidebar, | ||
| } from "@/components/layout/sidebar/navigation/NavigationSidebarContext"; | ||
|
|
||
| interface MainShellContainerProps { | ||
| children: React.ReactNode; | ||
| } | ||
|
|
||
| const MAIN_MARGIN_CLASS_NAME = { | ||
| open: "ml-55", | ||
| collapsed: "ml-5", | ||
| } as const; | ||
|
|
||
| const MainContent = ({ children }: { children: React.ReactNode }) => { | ||
| const { isOpen } = useNavigationSidebar(); | ||
|
|
||
| return ( | ||
| <main | ||
| className={cn( | ||
| "rounded-8 h-full overflow-y-auto bg-white transition-[margin-left] duration-200 ease-in-out", | ||
| isOpen ? MAIN_MARGIN_CLASS_NAME.open : MAIN_MARGIN_CLASS_NAME.collapsed, | ||
| )} | ||
| > | ||
|
kimminna marked this conversation as resolved.
|
||
| {children} | ||
| </main> | ||
| ); | ||
| }; | ||
|
|
||
| export const MainShellContainer = ({ children }: MainShellContainerProps) => { | ||
| return ( | ||
| <NavigationSidebarProvider> | ||
| <NavigationSidebar /> | ||
| <MainContent>{children}</MainContent> | ||
| </NavigationSidebarProvider> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| "use client"; | ||
|
|
||
| import { Header } from "@/components/layout/header/Header"; | ||
| import { useNavigationSidebar } from "@/components/layout/sidebar/navigation/NavigationSidebarContext"; | ||
|
|
||
| export const FocusHeaderContainer = () => { | ||
| const { isOpen, toggle } = useNavigationSidebar(); | ||
|
|
||
| return ( | ||
| <Header left={<Header.SidebarButton isOpen={isOpen} onClick={toggle} />} /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import { FocusHeaderContainer } from "@/app/[locale]/(main)/focus/_containers/FocusHeaderContainer"; | ||
|
|
||
| export default function FocusPage() { | ||
| return <></>; | ||
| return <FocusHeaderContainer />; | ||
| } |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,13 @@ | ||
| import { NavigationSidebar } from "@/components/layout/sidebar/NavigationSidebar"; | ||
| import { MainShellContainer } from "@/app/[locale]/(main)/_containers/MainShellContainer"; | ||
|
|
||
| interface MainLayoutProps { | ||
| children: React.ReactNode; | ||
| } | ||
|
|
||
| export default function MainLayout({ children }: Readonly<MainLayoutProps>) { | ||
| return ( | ||
| <div className="flex min-h-screen"> | ||
| <NavigationSidebar /> | ||
| <main className="flex-1">{children}</main> | ||
| <div className="bg-timo-gray-300 h-screen overflow-hidden py-5"> | ||
| <MainShellContainer>{children}</MainShellContainer> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| "use client"; | ||
|
|
||
| import { Header } from "@/components/layout/header/Header"; | ||
| import { useNavigationSidebar } from "@/components/layout/sidebar/navigation/NavigationSidebarContext"; | ||
|
|
||
| export const SettingsHeaderContainer = () => { | ||
| const { isOpen, toggle } = useNavigationSidebar(); | ||
|
|
||
| return ( | ||
| <Header left={<Header.SidebarButton isOpen={isOpen} onClick={toggle} />} /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,14 @@ | ||
| export default function SettingsLayout({ | ||
| import { SettingsHeaderContainer } from "@/app/[locale]/(main)/settings/_containers/SettingsHeaderContainer"; | ||
|
|
||
| export default function SettingsLayout({ | ||
| children, | ||
| }: { | ||
| children: React.ReactNode; | ||
| }) { | ||
| return <>{children}</>; | ||
| return ( | ||
| <> | ||
| <SettingsHeaderContainer /> | ||
| {children} | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
|
|
||
| import { Header } from "@/components/layout/header/Header"; | ||
| import { useNavigationSidebar } from "@/components/layout/sidebar/navigation/NavigationSidebarContext"; | ||
|
|
||
| const MONTH_LABEL_FORMATTER = new Intl.DateTimeFormat("ko-KR", { | ||
| year: "numeric", | ||
| month: "long", | ||
| }); | ||
|
|
||
| const addMonths = (date: Date, amount: number) => | ||
| new Date(date.getFullYear(), date.getMonth() + amount, 1); | ||
|
kimminna marked this conversation as resolved.
|
||
|
|
||
| export const StatisticsHeaderContainer = () => { | ||
| const [currentMonth, setCurrentMonth] = useState(() => new Date()); | ||
| const { isOpen, toggle } = useNavigationSidebar(); | ||
|
|
||
| const handlePrev = () => setCurrentMonth((prev) => addMonths(prev, -1)); | ||
| const handleNext = () => setCurrentMonth((prev) => addMonths(prev, 1)); | ||
|
|
||
| return ( | ||
| <Header | ||
| left={ | ||
| <> | ||
| <Header.SidebarButton isOpen={isOpen} onClick={toggle} /> | ||
| <Header.WeeklyNav | ||
| onPrev={handlePrev} | ||
| onNext={handleNext} | ||
| label={MONTH_LABEL_FORMATTER.format(currentMonth)} | ||
| /> | ||
| </> | ||
| } | ||
| /> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import { StatisticsHeaderContainer } from "@/app/[locale]/(main)/statistics/_containers/StatisticsHeaderContainer"; | ||
|
|
||
| export default function StatisticsPage() { | ||
| return <></>; | ||
| return <StatisticsHeaderContainer />; | ||
| } |
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.