Skip to content

Commit

Permalink
feat: 개별창 띄우기 (#44)
Browse files Browse the repository at this point in the history
* icon

* fix

* 개별창 컨테이너

* card window

* query key

* mock fix

* fix
  • Loading branch information
woo-jk authored Aug 28, 2024
1 parent f3ff019 commit d55cde1
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/app/(sidebar)/(my-info)/apis/useGetCardTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { http } from '@/apis/http';
import { TagType } from '@/types';
import { useQuery } from '@tanstack/react-query';

export const GET_TAGS = 'tags';
export const GET_TAGS = 'tagList';

type GetCardTagsRseponse = TagType[];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { motion } from 'framer-motion';
import { match } from 'ts-pattern';

interface InfoCardSkeletonProps {
count: number;
Expand Down
5 changes: 4 additions & 1 deletion src/app/(sidebar)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Sidebar } from '@/container/Sidebar/Sidebar';
import { PropsWithChildren } from 'react';
import { CardWindowLayout } from '@/components/CardWindow/context';

export default function SidebarLayout({ children }: PropsWithChildren) {
return (
<div className="flex">
<Sidebar />
<div className="flex-grow">{children}</div>
<div className="flex-grow relative">
<CardWindowLayout>{children}</CardWindowLayout>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions src/app/(sidebar)/my-recruit/[id]/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ export const colorStyle = {
default: 'bg-[#F1F2F3] text-[#37383C]',
blue: 'bg-[#E8F1FF] text-[#418CC3]',
purple: 'bg-[#F1E8FF] text-[#9C6BB3]',
yellow: 'bg-[#FFF3C2] text-[#D77B0F]',
};

export const DEFAULT_TAG_MOCKS: TagType[] = [
Expand Down
5 changes: 3 additions & 2 deletions src/app/(sidebar)/write/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ export default function Page({ params: { id } }: { params: { id: string } }) {
</TagSelector.Notice>

<TagSelector.TagList title="분류">
{categoryTags.map((tag) => (
{/* FIXME */}
{/* {categoryTags.map((tag) => (
<TagSelector.Tag
key={tag.name}
className="text-neutral-75 bg-neutral-3"
Expand All @@ -113,7 +114,7 @@ export default function Page({ params: { id } }: { params: { id: string } }) {
}}>
{tag.name}
</TagSelector.Tag>
))}
))} */}
</TagSelector.TagList>
</div>
</TagSelector.Content>
Expand Down
159 changes: 159 additions & 0 deletions src/components/CardWindow/CardWindow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { TouchButton } from '@/components/TouchButton';
import { Icon, Tag } from '@/system/components';
import { color } from '@/system/token/color';
import { ReactNode, useState } from 'react';
import { useGetInfoCardDetail } from '../../hooks/apis/useGetInfoCardDetail';
import { Spacing } from '@/system/utils/Spacing';
import { formatToYYMMDD } from '@/utils/date';
import { motion } from 'framer-motion';
import { If } from '@/system/utils/If';
import { useRouter } from 'next/navigation';

interface CardWindowProps {
cardId: number;
onClose: () => void;
}

export function CardWindow({ cardId, onClose }: CardWindowProps) {
const router = useRouter();
const [isRight, setIsRight] = useState(true);

const { data: card, isLoading } = useGetInfoCardDetail(cardId);

return (
<motion.div
initial={{
y: 50,
opacity: 0,
}}
animate={{
y: 0,
opacity: 1,
}}
exit={{
scale: 0.9,
opacity: 0,
}}
style={{
right: isRight ? 16 : undefined,
left: !isRight ? 16 : undefined,
}}
transition={{
ease: 'easeIn',
duration: 0.2,
}}
layout
className="absolute bottom-16 w-368 max-h-[768px] rounded-20 bg-white shadow-[0px_3px_8px_0px_rgba(0,0,0,0.24)]">
<div className="p-20 pb-24">
<div className="flex justify-between">
<div className="flex gap-12">
<WindowButton
onClick={() => {
router.push(`/write/${cardId}`);
onClose();
}}
description="해당 글로 이동하기">
<Icon name="fullScreenCorner" color={color.neutral35} size={20} />
</WindowButton>
<WindowButton
onClick={() => setIsRight((prev) => !prev)}
description={`${isRight ? '왼쪽' : '오른쪽'}으로 옮기기`}>
<span className={!isRight ? '[&>svg]:rotate-180' : undefined}>
<Icon name="toLeft" color={color.neutral35} size={20} />
</span>
</WindowButton>
</div>
<TouchButton onClick={onClose}>
<Icon name="x" color={color.neutral40} />
</TouchButton>
</div>
</div>
<If condition={isLoading}>
<Skeleton />
</If>
<If condition={!isLoading}>
<div className="px-20">
<div className="flex items-center">
<h1 className="flex-1 text-heading1 font-bold text-neutral-95 truncate">
{card?.title || '제목을 입력해주세요'}
</h1>
<p className="text-caption1 font-medium text-neutral-20">
{formatToYYMMDD(card?.updatedDate || '', { separator: '.' })}
</p>
</div>
<Spacing size={24} />
<div className="flex gap-8">
{card?.cardTypeValueList.map((type) => (
<Tag key={type} color="yellow">
{type.replaceAll('_', ' ')}
</Tag>
))}
{card?.tagList.map(({ id, name, type }) => (
<Tag key={id} color={type === '역량' ? 'blue' : 'purple'}>
{name}
</Tag>
))}
</div>
<Spacing size={20} />
<div className="min-h-200 max-h-[600px] overflow-auto">{card?.content || '내용을 입력해주세요'}</div>
</div>
</If>
</motion.div>
);
}

interface WindowButtonProps {
onClick?: () => void;
description?: string;
children: ReactNode;
}

function WindowButton({ onClick, children, description }: WindowButtonProps) {
return (
<motion.button
whileHover="hover"
className="relative p-2 border rounded-[3.8px] transition-colors hover:bg-neutral-3"
onClick={onClick}>
{children}
<motion.div
initial={{ opacity: 0 }}
variants={{ hover: { opacity: 1 } }}
className="absolute top-[50%] translate-y-[-50%] left-28 px-10 w-max py-4 rounded-6 bg-[#70737C] text-white text-label1 font-regular pointer-events-none z-10">
{description}
</motion.div>
</motion.button>
);
}

function Skeleton() {
return (
<motion.div
variants={{
show: {
transition: {
staggerChildren: 0.1,
},
},
}}
initial="hide"
animate="show"
className="flex flex-col gap-16 mb-200">
<motion.div
transition={{ repeat: Infinity, repeatType: 'reverse', duration: 0.5 }}
variants={{
hide: { opacity: 0.1, scale: 1 },
show: { opacity: 1, scale: 0.98 },
}}
className="mx-20 h-[30px] rounded-[16px] bg-neutral-3"
/>
<motion.div
transition={{ repeat: Infinity, repeatType: 'reverse', duration: 0.5 }}
variants={{
hide: { opacity: 0.1, scale: 1 },
show: { opacity: 1, scale: 0.98 },
}}
className="mx-20 h-[30px] rounded-[16px] bg-neutral-3"
/>
</motion.div>
);
}
39 changes: 39 additions & 0 deletions src/components/CardWindow/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import { generateContext } from '@/lib';
import { useState } from 'react';
import { CardWindow } from './CardWindow';
import { AnimatePresence } from 'framer-motion';

interface CardWindowContext {
isOpen: boolean;
open: (cardId: number) => void;
close: () => void;
}

const [CardWindowProvider, useCardWindowContext] = generateContext<CardWindowContext>({
name: 'CardWindow',
});

function CardWindowLayout({ children }: { children: React.ReactNode }) {
const [cardId, setCardId] = useState<number | null>(null);

const isOpen = cardId !== null;

const open = (cardId: number) => {
setCardId(cardId);
};

const close = () => {
setCardId(null);
};

return (
<CardWindowProvider isOpen={isOpen} open={open} close={close}>
{children}
<AnimatePresence>{isOpen && <CardWindow cardId={cardId} onClose={close} />}</AnimatePresence>
</CardWindowProvider>
);
}

export { CardWindowLayout, useCardWindowContext };
12 changes: 10 additions & 2 deletions src/components/InfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import { color } from '@/system/token/color';
import { useDeleteCard } from '@/app/(sidebar)/(my-info)/apis/useDeleteCard';
import Link from 'next/link';
import { MouseEventHandler } from 'react';
import { useCardWindowContext } from './CardWindow/context';

type InfoCardProps = InfoCardType;

export function InfoCard({ id, title, updatedDate, tagList }: InfoCardProps) {
const formattedDate = formatToYYMMDD(updatedDate, { separator: '.' });

const { open } = useCardWindowContext();
const { mutate: deleteCard } = useDeleteCard();

const handleDeleteCard: MouseEventHandler<HTMLDivElement> = (event) => {
Expand All @@ -26,6 +28,12 @@ export function InfoCard({ id, title, updatedDate, tagList }: InfoCardProps) {
deleteCard(id);
};

const handleOpenCardWindow: MouseEventHandler<HTMLDivElement> = (event) => {
event.stopPropagation();

open(id);
};

return (
<Link href={`/write/${id}`}>
<div className="flex flex-col justify-between h-[140px] p-[24px] rounded-[16px] bg-white border border-neutral-5 cursor-pointer transition-colors ease-in-out hover:border-neutral-95 hover:shadow-[0px_4px_12px_0px_rgba(0,0,0,0.08)]">
Expand All @@ -46,7 +54,7 @@ export function InfoCard({ id, title, updatedDate, tagList }: InfoCardProps) {
<Icon name="delete" color="#FF5C5C" />
<div className="text-red-50 text-[15px] font-normal">삭제하기</div>
</DropdownMenuItem>
<DropdownMenuItem className="gap-[8px]">
<DropdownMenuItem className="gap-[8px]" onClick={handleOpenCardWindow}>
<Icon name="pip" color={color.neutral50} />
<div className="text-neutral-95 text-[15px] font-normal">개별창으로 띄우기</div>
</DropdownMenuItem>
Expand All @@ -64,4 +72,4 @@ export function InfoCard({ id, title, updatedDate, tagList }: InfoCardProps) {
</div>
</Link>
);
}
}
26 changes: 26 additions & 0 deletions src/hooks/apis/useGetInfoCardDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { http } from '@/apis/http';
import { InfoType, TagType } from '@/types';
import { useQuery } from '@tanstack/react-query';

export interface GetInfoCardDetailResponse {
title: string;
content: string;
updatedDate: string;
cardTypeValueList: InfoType[];
tagList: TagType[];
}

const getInfoCardDetail = (cardId: number) =>
http.get<GetInfoCardDetailResponse>({
url: `/cards/${cardId}`,
});

export const useGetInfoCardDetail = (cardId: number) =>
useQuery({
queryKey: ['get-info-card-detail', cardId],
queryFn: async () => {
const res = await getInfoCardDetail(cardId);

return res.data;
},
});
22 changes: 13 additions & 9 deletions src/system/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import { RemoveMemo } from '@/system/components/Icon/SVG/RemoveMemo';
import { SubmitArrow } from '@/system/components/Icon/SVG/SubmitArrow';
import type { IconBaseType } from '@/system/components/Icon/SVG/type';
import { Add } from './SVG/Add';
import { Bell } from './SVG/Bell';
import { Calendar } from './SVG/Calendar';
import { CalendarFill } from './SVG/CalendarFill';
import { Check } from './SVG/Check';
import { Close } from './SVG/Close';
import { Clover } from './SVG/Clover';
import { CodingSignUp } from './SVG/CodingSignUp';
import { Copy } from './SVG/Copy';
import { Delete } from './SVG/Delete';
import { DesignSignup } from './SVG/DesignSignup';
import { Division } from './SVG/Division';
import { Down } from './SVG/Down';
import { DownChevron } from './SVG/DownChevron';
import { Empty } from './SVG/Empty';
import { FilledMemo } from './SVG/FilledMemo';
import { Folder } from './SVG/Folder';
import { FolderFill } from './SVG/FolderFill';
import { Link } from './SVG/Link';
import { LogoOnly } from './SVG/LogoOnly';
import { Logout } from './SVG/Logout';
Expand All @@ -37,6 +28,17 @@ import { Shoes } from './SVG/Shoes';
import { Tag } from './SVG/Tag';
import { Trash } from './SVG/Trash';
import { Unlink } from './SVG/Unlink';
import { Calendar } from './SVG/Calendar';
import { CalendarFill } from './SVG/CalendarFill';
import { SubmitArrow } from '@/system/components/Icon/SVG/SubmitArrow';
import { FilledMemo } from './SVG/FilledMemo';
import { RemoveMemo } from '@/system/components/Icon/SVG/RemoveMemo';
import { Clover } from './SVG/Clover';
import { DownChevron } from './SVG/DownChevron';
import { FolderFill } from './SVG/FolderFill';
import { Close } from './SVG/Close';
import { FullScreenCorner } from './SVG/FullScreenCorner';
import { ToLeft } from './SVG/ToLeft';
import { Up } from './SVG/Up';
import { X } from './SVG/X';
import { WorkFill } from './SVG/WorkFill';
Expand Down Expand Up @@ -74,6 +76,8 @@ const iconMap = {
filledMemo: FilledMemo,
removeMemo: RemoveMemo,
clover: Clover,
fullScreenCorner: FullScreenCorner,
toLeft: ToLeft,
refresh: Refresh,
empty: Empty,
tag: Tag,
Expand Down
11 changes: 11 additions & 0 deletions src/system/components/Icon/SVG/FullScreenCorner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IconBaseType } from './type';

export function FullScreenCorner({ 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="M16 9V4H11" stroke={color} strokeWidth="1.25" />
<path d="M15.834 4.16615L9.5577 10.4424" stroke={color} strokeWidth="1.25" />
<path d="M8 5.5H4.00011L4.00034 16H14.5V12" stroke={color} strokeWidth="1.25" />
</svg>
);
}
Loading

0 comments on commit d55cde1

Please sign in to comment.