-
Notifications
You must be signed in to change notification settings - Fork 0
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
feat: 내 정보 뽀각 페이지 UI 구현 #15
Changes from all commits
c43f09d
9c76079
d581a29
c8979c3
9147c3c
cacfb55
c7ec629
b292977
d99b86c
3c2e350
21f8294
6732439
616c404
6e26999
2ec446a
73d4230
ac69449
2eb7714
632d906
8c5fd69
d35099d
c05b498
bafab50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { Icon } from '@/system/components'; | ||
import { Tag } from '@/system/components/Tag/Tag'; | ||
import { formatToYYMMDD } from '@/utils/date'; | ||
import { | ||
DropdownMenu, | ||
DropdownMenuContent, | ||
DropdownMenuItem, | ||
DropdownMenuTrigger, | ||
} from '@/system/components/DropdownMenu/DropdownMenu'; | ||
import { InfoCard } from '@/types/info'; | ||
|
||
interface Props extends InfoCard {} | ||
|
||
const TAG_TYPE_COLOR = { | ||
역량: 'blue', | ||
인성: 'purple', | ||
} as const; | ||
|
||
export function InfoCardItem({ title, updatedDate, cardTagList }: Props) { | ||
const formattedDate = formatToYYMMDD(updatedDate, { separator: '.' }); | ||
|
||
return ( | ||
<div className="flex flex-col justify-between min-w-[343px] h-[140px] p-[24px] rounded-[16px] bg-white border border-neutral-5 cursor-pointer hover:border-neutral-30"> | ||
<div className="flex"> | ||
<div className="flex-1 overflow-hidden"> | ||
<div className="mb-[9px] text-[12px] text-neutral-20">{formattedDate}</div> | ||
<div className="w-auto truncate text-[16px] font-semibold">{title}</div> | ||
</div> | ||
<div> | ||
<DropdownMenu> | ||
<DropdownMenuTrigger asChild> | ||
<button className="rounded-[4px] hover:bg-neutral-1" aria-label="more button"> | ||
<Icon name="more" color="#1B1C1E" /> | ||
</button> | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent align="end"> | ||
<DropdownMenuItem className="gap-[8px]"> | ||
<Icon name="delete" color="#FF5C5C" /> | ||
<div className="text-red-50 text-[15px] font-normal">삭제하기</div> | ||
</DropdownMenuItem> | ||
<DropdownMenuItem className="gap-[8px]"> | ||
<Icon name="pip" color="#70737C" /> | ||
<div className="text-neutral-95 text-[15px] font-normal">개별창으로 띄우기</div> | ||
</DropdownMenuItem> | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
</div> | ||
</div> | ||
<div className="flex gap-[8px]"> | ||
{cardTagList.map(({ id, type, name }) => ( | ||
<Tag key={id} color={TAG_TYPE_COLOR[type]}> | ||
{name} | ||
</Tag> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
'use client'; | ||
|
||
import { useState } from 'react'; | ||
import { mockInfoCount, mockInfoList } from '../mock'; | ||
import { cn } from '@/utils/tailwind-util'; | ||
import { Icon } from '@/system/components'; | ||
import { InfoCardItem } from './InfoCardItem'; | ||
|
||
const INFO_OPTIONS = ['경험 정리', '자기소개서', '면접 질문'] as const; | ||
|
||
export function InfoCardList() { | ||
const [currentOption, setCurrentOption] = useState<(typeof INFO_OPTIONS)[number]>('경험 정리'); | ||
|
||
// TODO: API 연동 시 response data로 변경 | ||
const infoCount = mockInfoCount; | ||
const infoList = mockInfoList; | ||
Comment on lines
+14
to
+16
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. Cool ! |
||
|
||
return ( | ||
<section> | ||
<div className="mb-[28px] flex justify-between"> | ||
<div className="flex gap-[24px]"> | ||
{INFO_OPTIONS.map((option) => ( | ||
<button | ||
key={option} | ||
className="flex gap-[6px] items-center cursor-pointer" | ||
onClick={() => setCurrentOption(option)}> | ||
<div | ||
className={cn( | ||
'text-[18px] text-neutral-10 font-semibold', | ||
currentOption === option && 'text-neutral-80', | ||
)}> | ||
{option} | ||
</div> | ||
<div | ||
className={cn( | ||
'px-[8px] py-[2px] bg-neutral-10 rounded-[6px] text-neutral-1 text-[14px] font-semibold', | ||
currentOption === option && 'bg-neutral-80', | ||
)}> | ||
{infoCount[option]} | ||
</div> | ||
</button> | ||
))} | ||
</div> | ||
<button className="flex items-center gap-[8px] bg-neutral-95 py-[6px] pl-[16px] pr-[10px] rounded-[6px]"> | ||
<div className="text-white text-[14px] font-semibold">카드 추가</div> | ||
<Icon name="add" color="#08F29B" /> | ||
</button> | ||
</div> | ||
<div className="grid grid-cols-[repeat(auto-fill,minmax(343px,1fr))] gap-[16px]"> | ||
{infoList.map((info) => ( | ||
<InfoCardItem key={info.id} {...info} /> | ||
))} | ||
</div> | ||
</section> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { InfoCard } from '@/types/info'; | ||
|
||
export const mockInfoCount = { | ||
'경험 정리': 1, | ||
자기소개서: 3, | ||
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. 목데이터라 큰 의미 업겟지만... 혹시.. 여기만 string으로 안묶은 이유가 따로 잇는건가요?? 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.
|
||
'면접 질문': 2, | ||
}; | ||
|
||
export const mockInfoList: InfoCard[] = [ | ||
{ | ||
id: 1, | ||
title: '제목', | ||
updatedDate: '2024-07-20T20:00:00', | ||
cardTagList: [ | ||
{ | ||
id: 1, | ||
name: '인성태그1', | ||
type: '인성', | ||
}, | ||
{ | ||
id: 2, | ||
name: '역량태그1', | ||
type: '역량', | ||
}, | ||
], | ||
}, | ||
{ | ||
id: 2, | ||
title: 'test title2', | ||
updatedDate: '2024-07-10T20:00:00', | ||
cardTagList: [ | ||
{ | ||
id: 1, | ||
name: '인성태그1', | ||
type: '인성', | ||
}, | ||
{ | ||
id: 2, | ||
name: '역량태그1', | ||
type: '역량', | ||
}, | ||
], | ||
}, | ||
{ | ||
id: 3, | ||
title: '제목제목제목제목제목제목제목제목제목제목제목제목제목제목제목제목제목', | ||
updatedDate: '2024-07-15T20:00:00', | ||
cardTagList: [ | ||
{ | ||
id: 1, | ||
name: '인성태그1', | ||
type: '인성', | ||
}, | ||
{ | ||
id: 2, | ||
name: '역량태그1', | ||
type: '역량', | ||
}, | ||
], | ||
}, | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Icon } from '@/system/components'; | ||
import { InfoCardList } from './components/InfoCardList'; | ||
|
||
export default function MyInfo() { | ||
return ( | ||
<div className="max-w-[1700px] py-[64px] px-[80px] mx-auto"> | ||
<div className="mb-[72px] flex justify-between"> | ||
<h1 className="text-[28px] font-bold">내 정보 뽀각</h1> | ||
qkrdmstlr3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<button className="flex gap-[24px] p-[16px] border rounded-[8px] border-neutral-5 bg-white"> | ||
<div className="text-[16px] font-semibold">우정균님의 기본정보</div> | ||
<Icon name="down" color="#878A93" /> | ||
</button> | ||
</div> | ||
<InfoCardList /> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Sidebar } from '@/container/Sidebar/Sidebar'; | ||
import { PropsWithChildren } from 'react'; | ||
|
||
export default function SidebarLayout({ children }: PropsWithChildren) { | ||
return ( | ||
<div className="flex"> | ||
<Sidebar /> | ||
<div className="flex-grow">{children}</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,8 @@ | ||
import type { Metadata } from 'next'; | ||
import { Sidebar } from '@/container/Sidebar/Sidebar'; | ||
import { QueryProvider } from '@/lib'; | ||
import clsx from 'clsx'; | ||
import { Inter } from 'next/font/google'; | ||
import './globals.css'; | ||
import { cn } from '@/utils/tailwind-util'; | ||
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. 이거 clsx 대신 쓰는 것 맞죠?? 🎉 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. 넹 clsx와 twMerge의 기능을 같이 해주는 유틸함수입니다! |
||
|
||
const inter = Inter({ subsets: ['latin'] }); | ||
|
||
|
@@ -19,12 +18,8 @@ export default function RootLayout({ | |
}>) { | ||
return ( | ||
<html lang="en"> | ||
<body className={clsx(inter.className, 'flex')}> | ||
<Sidebar /> | ||
{/* FIXME: */} | ||
<div className="flex-grow"> | ||
<QueryProvider>{children}</QueryProvider> | ||
</div> | ||
<body className={cn(inter.className, 'bg-neutral-1')}> | ||
<QueryProvider>{children}</QueryProvider> | ||
</body> | ||
</html> | ||
); | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
'use client'; | ||
|
||
import * as React from 'react'; | ||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; | ||
|
||
import { cn } from '@/utils/tailwind-util'; | ||
|
||
const DropdownMenu = DropdownMenuPrimitive.Root; | ||
|
||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; | ||
|
||
const DropdownMenuGroup = DropdownMenuPrimitive.Group; | ||
|
||
const DropdownMenuContent = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> | ||
>(({ className, sideOffset = 4, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.Portal> | ||
<DropdownMenuPrimitive.Content | ||
ref={ref} | ||
sideOffset={sideOffset} | ||
className={cn( | ||
'z-50 min-w-[8rem] overflow-hidden rounded-[12px] bg-white p-1 text-slate-950 shadow-[0px_2px_8px_0px_rgba(99,99,99,0.20)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50', | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
</DropdownMenuPrimitive.Portal> | ||
)); | ||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; | ||
|
||
const DropdownMenuItem = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.Item>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { | ||
inset?: boolean; | ||
} | ||
>(({ className, inset, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.Item | ||
ref={ref} | ||
className={cn( | ||
'relative flex cursor-default select-none items-center rounded-[12px] py-[12px] pl-[12px] pr-[16px] text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50', | ||
inset && 'pl-8', | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
)); | ||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; | ||
|
||
const DropdownMenuSeparator = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> | ||
>(({ className, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.Separator | ||
ref={ref} | ||
className={cn('-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-800', className)} | ||
{...props} | ||
/> | ||
)); | ||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; | ||
|
||
export { | ||
DropdownMenu, | ||
DropdownMenuTrigger, | ||
DropdownMenuContent, | ||
DropdownMenuItem, | ||
DropdownMenuSeparator, | ||
DropdownMenuGroup, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { IconBaseType } from './type'; | ||
|
||
export function Add({ size, color }: IconBaseType) { | ||
return ( | ||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
<mask | ||
id="mask0_1054_1585" | ||
style={{ maskType: 'alpha' }} | ||
maskUnits="userSpaceOnUse" | ||
x="0" | ||
y="0" | ||
width="24" | ||
height="24"> | ||
<rect width="24" height="24" fill="#D9D9D9" /> | ||
</mask> | ||
<g mask="url(#mask0_1054_1585)"> | ||
<path d="M11.25 12.75H5.5V11.25H11.25V5.5H12.75V11.25H18.5V12.75H12.75V18.5H11.25V12.75Z" fill={color} /> | ||
</g> | ||
</svg> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { IconBaseType } from './type'; | ||
|
||
export function Delete({ size, color }: IconBaseType) { | ||
return ( | ||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
<path | ||
d="M4.62598 7.40259H19.3736L18.0473 21.25H5.95222L4.62598 7.40259Z" | ||
stroke={color} | ||
strokeWidth="1.5" | ||
strokeLinecap="square" | ||
/> | ||
<path d="M7.81177 2.75H16.1884" stroke={color} strokeWidth="1.5" strokeLinecap="square" /> | ||
<path d="M12 11.9995L12 16.6525" stroke={color} strokeWidth="1.5" strokeLinecap="square" /> | ||
</svg> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 div 태그가 없어도 괜찮을 것이라고 생각하는데 어떻게 생각하시나요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 div 태그가 없다면 상단 div의 flex 속성 때문에 위 사진처럼 more 버튼의 height가 늘어나게 됩니다(원래는 정사각형).
버튼의 모양은 유지하면서 버튼의 배치만 지정하기 위해 div 태그로 한 번 감싸주게 되었습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 !