Skip to content

Commit

Permalink
[Feat] QFEED-66 Q-Space 상세 페이지 컴포넌트 구현 (#31)
Browse files Browse the repository at this point in the history
* feat: qfeed-66 토론방 상세 페이지 방장 프로필 컨테이너 추가

* feat: qfeed-66 토론방 상세 멤버 관리 버튼 추가

* feat: qfeed-66 KebabMenu 컴포넌트 추가

* feat: qfeed-66 MembeContainer 추가
  • Loading branch information
se0kcess authored Nov 29, 2024
1 parent f3b72b2 commit 3201eca
Show file tree
Hide file tree
Showing 11 changed files with 587 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/components/ui/Logo/Logo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Logo } from './Logo';

const meta: Meta<typeof Logo> = {
title: 'Components/Logo',
title: 'Components/ui/Logo',
component: Logo,
tags: ['autodocs'],
argTypes: {
Expand Down
22 changes: 9 additions & 13 deletions src/components/ui/ProfileImageCon/ProfileImageCon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @jsxImportSource @emotion/react */
import theme from '@/styles/theme';
import { css } from '@emotion/react';
import React from 'react';
import { GoPerson } from 'react-icons/go';
import { IoPerson } from 'react-icons/io5';

// Props 타입 정의
interface ProfileImageProps {
Expand All @@ -11,19 +11,15 @@ interface ProfileImageProps {
alt?: string; // 대체 텍스트
}

const ProfileImage: React.FC<ProfileImageProps> = ({
const ProfileImage = ({
src,
size = 50, // 기본 크기 50px
bgColor = '#d3cdcd', // 기본 배경색
alt = 'Profile Image', // 기본 대체 텍스트
}) => {
}: ProfileImageProps) => {
return (
<div css={containerStyle(size, bgColor)}>
{src ? (
<img css={imageStyle(size)} src={src} alt={alt} />
) : (
<GoPerson css={iconStyle(size)} />
)}
{src ? <img css={imageStyle(size)} src={src} alt={alt} /> : <IoPerson css={iconStyle()} />}
</div>
);
};
Expand All @@ -48,10 +44,10 @@ const imageStyle = (size: number) => css`
`;

// 아이콘 스타일
const iconStyle = (size: number) => css`
width: ${size * 0.6}px; /* 아이콘은 크기의 60%로 설정 */
height: ${size * 0.6}px;
color: #5f5959; /* 아이콘 색상 */
const iconStyle = () => css`
width: 80%;
height: 80%;
color: ${theme.colors.gray[100]};
`;

export default ProfileImage;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ReplyContainer } from './ReplyContainer';
import { action } from '@storybook/addon-actions';

const meta = {
title: 'Components/ReplyContainer',
title: 'Components/ui/ReplyContainer',
component: ReplyContainer,
parameters: {
layout: 'centered',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Meta, StoryObj } from '@storybook/react';
import DetailsHeader from './DetailsHeader';

const meta = {
title: 'Components/QSpaceDetail/DetailsHeader',
component: DetailsHeader,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
title: {
control: 'text',
description: '토론방의 제목',
},
creator: {
control: 'text',
description: '방 개설자의 닉네임',
},
profileImage: {
control: 'text',
description: '방 개설자의 프로필 이미지 URL',
},
},
} satisfies Meta<typeof DetailsHeader>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
title: '제주도 맛집 토론방',
creator: '닉네임',
},
};

export const WithProfileImage: Story = {
args: {
title: '제주도 맛집 토론방',
creator: '닉네임',
profileImage: 'https://via.placeholder.com/40',
},
};

export const LongTitle: Story = {
args: {
title: '아주 긴 제목의 토론방입니다. 이렇게 길게 써도 잘 보여야 합니다.',
creator: '닉네임',
},
};

export const LongCreatorName: Story = {
args: {
title: '제주도 맛집 토론방',
creator: '아주긴닉네임입니다만어떠신가요',
},
};
51 changes: 51 additions & 0 deletions src/pages/QSpaceDetail/components/DetailsHeader/DetailsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import styled from '@emotion/styled';
import theme from '@/styles/theme';
import ProfileImage from '@/components/ui/ProfileImageCon/ProfileImageCon';

interface DetailsHeaderProps {
title: string;
creator: string;
profileImage?: string;
}

const DetailsHeader = ({ title, creator, profileImage }: DetailsHeaderProps) => {
return (
<Container>
<ProfileImage src={profileImage} size={40} bgColor={theme.colors.gray[200]} alt='프로필 이미지' />
<TextContent>
<Title>{title}</Title>
<CreatorInfo>{creator}</CreatorInfo>
</TextContent>
</Container>
);
};

const Container = styled.div`
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
`;

const TextContent = styled.div`
display: flex;
flex-direction: column;
gap: 0.25rem;
`;

const Title = styled.h2`
font-family: ${theme.typography.fontFamily.korean};
font-size: ${theme.typography.body1.size};
font-weight: ${theme.typography.weights.semiBold};
color: ${theme.colors.black};
margin: 0;
`;

const CreatorInfo = styled.p`
font-family: ${theme.typography.fontFamily.korean};
font-size: ${theme.typography.body3.size};
color: ${theme.colors.gray[400]};
margin: 0;
`;

export default DetailsHeader;
45 changes: 45 additions & 0 deletions src/pages/QSpaceDetail/components/KebabMenu/KebabMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// KebabMenu.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import KebabMenu from './KebabMenu';

const meta = {
title: 'Components/QSpaceDetail/KebabMenu',
component: KebabMenu,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
initialRecruitmentStatus: {
control: 'boolean',
description: '초기 모집 상태 (true: 모집 중, false: 모집 완료)',
},
onEditClick: {
action: 'edited',
description: '수정 버튼 클릭 핸들러',
},
onDeleteClick: {
action: 'deleted',
description: '삭제 버튼 클릭 핸들러',
},
onRecruitmentStatusChange: {
action: 'recruitment status changed',
description: '모집 상태 변경 핸들러',
},
},
} satisfies Meta<typeof KebabMenu>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
initialRecruitmentStatus: true,
},
};

export const RecruitingComplete: Story = {
args: {
initialRecruitmentStatus: false,
},
};
120 changes: 120 additions & 0 deletions src/pages/QSpaceDetail/components/KebabMenu/KebabMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useState, useRef, useEffect } from 'react';
import styled from '@emotion/styled';
import { VscKebabVertical } from 'react-icons/vsc';
import theme from '@/styles/theme';

interface KebabMenuProps {
onEditClick?: () => void;
onDeleteClick?: () => void;
onRecruitmentStatusChange?: (isRecruiting: boolean) => void;
initialRecruitmentStatus?: boolean;
}

const KebabMenu = ({
onEditClick,
onDeleteClick,
onRecruitmentStatusChange,
initialRecruitmentStatus = true, // true: 모집 중, false: 모집 완료
}: KebabMenuProps) => {
const [isOpen, setIsOpen] = useState(false);
const [isRecruiting, setIsRecruiting] = useState(initialRecruitmentStatus);
const menuRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

const handleToggle = () => {
setIsOpen(!isOpen);
};

const handleRecruitmentStatusClick = () => {
const newStatus = !isRecruiting;
setIsRecruiting(newStatus);
onRecruitmentStatusChange?.(newStatus);
setIsOpen(false);
};

return (
<Container ref={menuRef}>
<IconButton onClick={handleToggle}>
<VscKebabVertical size={24} />
</IconButton>

{isOpen && (
<MenuPopup>
<MenuItem onClick={handleRecruitmentStatusClick}>
{isRecruiting ? '모집 완료로 변경' : '모집 중으로 변경'}
</MenuItem>
<MenuItem onClick={onEditClick}>글 수정하기</MenuItem>
<MenuItem onClick={onDeleteClick}>글 삭제하기</MenuItem>
</MenuPopup>
)}
</Container>
);
};

const Container = styled.div`
position: relative;
display: inline-block;
`;

const IconButton = styled.button`
background: none;
border: none;
padding: 0.5rem;
cursor: pointer;
color: ${theme.colors.gray[400]};
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease-in-out;
&:hover {
color: ${theme.colors.gray[600]};
}
`;

const MenuPopup = styled.div`
position: absolute;
top: 100%;
right: 0;
min-width: 160px;
background-color: ${theme.colors.white};
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
z-index: 1000;
`;

const MenuItem = styled.button`
width: 100%;
padding: 0.75rem 1rem;
border: none;
background: none;
text-align: left;
font-family: ${theme.typography.fontFamily.korean};
font-size: ${theme.typography.body2.size};
color: ${theme.colors.gray[600]};
cursor: pointer;
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: ${theme.colors.sub};
}
&:not(:last-child) {
border-bottom: 1px solid ${theme.colors.gray[100]};
}
`;

export default KebabMenu;
Loading

0 comments on commit 3201eca

Please sign in to comment.