Skip to content
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: 개별창 띄우기 #44

Merged
merged 9 commits into from
Aug 28, 2024
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
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
Loading