Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
},
"dependencies": {
"@hanghae-plus/domain": "workspace:*",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.84.1",
"axios": "^1.11.0",
Expand Down
14 changes: 12 additions & 2 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ import { Home, User } from "@/pages";
import { BASE_URL } from "@/constants";
import { withBaseLayout } from "@/components";

const Index = withBaseLayout(() => <Home />);
const StudentsPage = withBaseLayout(() => <div className="p-6">수강생 목록 페이지</div>);
const StudentDetailPage = withBaseLayout(() => <div className="p-6">수강생 상세 페이지</div>);
const AssignmentsPage = withBaseLayout(() => <div className="p-6">과제 목록 페이지</div>);
const NotFound = withBaseLayout(() => <div className="p-6">404 - 페이지를 찾을 수 없습니다</div>);

export const App = () => {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter basename={BASE_URL}>
<Routes>
<Route path="/" Component={withBaseLayout(Home)} />
<Route path="/user/:id" Component={withBaseLayout(User)} />
<Route path="/" element={<Index />} />
<Route path="/students" element={<StudentsPage />} />
<Route path="/students/:id" element={<StudentDetailPage />} />
<Route path="/assignments" element={<AssignmentsPage />} />
<Route path="/user/:id" element={<User />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</QueryClientProvider>
Expand Down
83 changes: 83 additions & 0 deletions packages/app/src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { NavLink, useLocation } from "react-router";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/Sidebar";
import { Users, BookOpen } from "lucide-react";

const navigationItems = [
{
title: "수강생 목록",
url: "/students",
icon: Users,
},
{
title: "과제 목록",
url: "/assignments",
icon: BookOpen,
},
];

export function AppSidebar() {
const { state } = useSidebar();
const location = useLocation();
const currentPath = location.pathname;

const collapsed = state === "collapsed";

const isActive = (path: string) => {
if (path === "/students") {
return currentPath === "/students" || currentPath.startsWith("/students/");
}
return currentPath === path;
};

const getNavCls = (path: string) =>
isActive(path)
? "bg-primary text-primary-foreground shadow-glow font-medium"
: "text-foreground hover:bg-secondary hover:text-secondary-foreground";

return (
<Sidebar className={collapsed ? "w-16" : "w-64"}>
<SidebarContent className="bg-card border-r border-border">
<div className="p-4">
<div className="flex items-center space-x-2 mb-6">
{!collapsed && (
<>
<div className="w-8 h-8 bg-gradient-primary rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">항</span>
</div>
<h1 className="text-lg font-bold text-primary">항해99 플러스</h1>
</>
)}
</div>
</div>

<SidebarGroup>
<SidebarGroupLabel className="text-muted-foreground px-4">{!collapsed && "학습 관리"}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu className="px-2">
{navigationItems.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild className="h-12">
<NavLink to={item.url} className={`${getNavCls(item.url)} rounded-lg transition-all duration-300`}>
<item.icon className="h-5 w-5" />
{!collapsed && <span className="ml-3">{item.title}</span>}
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
);
}
1 change: 1 addition & 0 deletions packages/app/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./ui";
export * from "./layout";
export * from "./AppSidebar";
34 changes: 12 additions & 22 deletions packages/app/src/components/layout/BaseLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
import { BookOpen } from "lucide-react";
import type { ComponentProps, PropsWithChildren } from "react";
import { AppSidebar, SidebarProvider, SidebarTrigger } from "../";

export function BaseLayout({ children }: PropsWithChildren) {
return (
<div className="bg-background-primary">
{/* 헤더 */}
<header className="border-b border-slate-800/50 backdrop-blur-sm color-background-primary sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gradient-to-r bg-red-500 rounded-lg flex items-center justify-center">
<BookOpen className="w-4 h-4 text-white" />
</div>
<div>
<h1 className="text-lg font-bold text-white">항해플러스 6기</h1>
<p className="text-xs text-slate-400">수강생 커뮤니티</p>
</div>
</div>
</div>
</div>
<SidebarProvider>
<div className="min-h-screen flex w-full">
<AppSidebar />
<div className="flex-1">
<header className="h-12 flex items-center border-b border-border bg-card px-4">
<SidebarTrigger className="mr-4 text-white" />
<h1 className="text-lg font-semibold text-primary">항해플러스 6기 프론트엔드 수강생 커뮤니티</h1>
</header>
<main className="p-6">{children}</main>
</div>
</header>

{children}
</div>
</div>
</SidebarProvider>
);
}

Expand Down
21 changes: 21 additions & 0 deletions packages/app/src/components/ui/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from "react";

import { cn } from "@/lib/utils";

function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}

export { Input };
28 changes: 28 additions & 0 deletions packages/app/src/components/ui/Separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";

import { cn } from "@/lib/utils";

function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}

export { Separator };
101 changes: 101 additions & 0 deletions packages/app/src/components/ui/Sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";

import { cn } from "@/lib/utils";

function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}

function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}

function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}

function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}

function SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}

function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}

function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5 p-4", className)} {...props} />;
}

function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="sheet-footer" className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />;
}

function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}

function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}

export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription };
Loading
Loading