Skip to content

Commit

Permalink
Merge pull request #9 from JECT-Study/feature/design-system
Browse files Browse the repository at this point in the history
[TASK-44, 45] style: 공용 Input, Button 구현
  • Loading branch information
dahyeo-n authored Dec 19, 2024
2 parents a239525 + c36307c commit 9161958
Show file tree
Hide file tree
Showing 26 changed files with 813 additions and 117 deletions.
343 changes: 338 additions & 5 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import React from 'react';
import { ReactNode } from 'react';

import '../styles/globals.css';

import MSWProvider from './providers/MSWProvider';
import ReactQueryProvider from './ReactQueryProvider';
import MSWProvider from './providers/MSWProvider';
import ReactQueryProvider from './providers/ReactQueryProvider';

import AppShell from '../components/Layout/AppShell';

Expand All @@ -25,7 +24,7 @@ export const metadata: Metadata = {
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: ReactNode;
}>) {
return (
<html lang='ko'>
Expand Down
4 changes: 1 addition & 3 deletions src/app/providers/MSWProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ const MSWProvider = ({ children }: { children: ReactNode }) => {
setMswReady(true);
};

if (!mswReady) {
init();
}
if (!mswReady) init();
}, [mswReady]);

return <>{children}</>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import { ReactNode, useState } from 'react';

const ReactQueryProvider = ({ children }: { children: ReactNode }) => {
Expand Down
31 changes: 31 additions & 0 deletions src/components/Button/CategoryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { CategoryButtonProps } from '@/types';

import { FC } from 'react';

const CategoryButton: FC<CategoryButtonProps> = ({
backgroundColor = 'bg-gray-800',
textColor = 'text-gray-300',
textSize,
children,

onClick,
ariaLabel,
}) => {
return (
<button
onClick={onClick}
aria-label={ariaLabel}
className={`
inline-flex items-center justify-center rounded-full
px-[56px] py-[17px] h-[69px]
${backgroundColor} ${textColor} ${textSize}
hover:bg-opacity-80 active:bg-opacity-60 active:scale-95
transition-all duration-300 ease-in-out
`}
>
{children}
</button>
);
};

export default CategoryButton;
60 changes: 60 additions & 0 deletions src/components/Button/FollowButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client';

import { FC, useState } from 'react';

import { FollowButtonProps } from '@/types/buttons/FollowButtonProps';

import Icon from '../Icon/Icon';

const FollowButton: FC<FollowButtonProps> = ({
backgroundColor = 'bg-gray-900',
textColor = 'text-white',
textSize = 'button-s',

onClick,
ariaLabel,
}) => {
const [isFollowing, setIsFollowing] = useState(false);

const toggleFollow = () => {
setIsFollowing((prev) => !prev);

// 외부에서 전달받은 onClick 함수 실행
if (onClick) {
onClick();
}
};

return (
<button
onClick={toggleFollow}
aria-label={ariaLabel}
className={`
flex items-center justify-center rounded-full
w-[95px] h-[37px] gap-[3px]
${backgroundColor} ${textColor} ${textSize}
${
isFollowing
? 'border border-gray-900 text-gray-900 bg-white'
: 'bg-gray-900 text-white'
}
hover:bg-opacity-80 active:bg-opacity-60 active:scale-95
transition-all duration-300 ease-in-out
`}
>
{isFollowing ? (
<>
<Icon name='Check' size='s' />
<span>팔로잉</span>
</>
) : (
<>
<Icon name='Plus' size='s' />
<span>팔로우</span>
</>
)}
</button>
);
};

export default FollowButton;
42 changes: 42 additions & 0 deletions src/components/Button/SquareButtonL.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SquareButtonLProps } from '@/types';

import { FC } from 'react';

const SquareButtonL: FC<SquareButtonLProps> = ({
backgroundColor = 'bg-gray-800',
textColor = 'text-white',
textSize,
children,

onClick,
ariaLabel,
disabled = false,
loading = false,
icon,
iconPosition,
type = 'button',
}) => {
return (
<button
type={type}
onClick={onClick}
disabled={disabled || loading}
aria-label={ariaLabel}
className={`
flex items-center justify-center rounded-md
w-[420px] h-[70px] gap-2
${backgroundColor} ${textColor} ${textSize}
${disabled ? 'text-gray-700 cursor-not-allowed' : ''}
hover:bg-opacity-80 active:bg-opacity-60 active:scale-95
transition-all duration-300 ease-in-out
`}
>
{loading && <span>로딩중...</span>}
{icon && iconPosition === 'left' && <span>{icon}</span>}
{children}
{icon && iconPosition === 'right' && <span>{icon}</span>}
</button>
);
};

export default SquareButtonL;
2 changes: 1 addition & 1 deletion src/components/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import logo from '../../../public/images/momentiaLogoSymbol.png';

const Footer = () => {
return (
<footer className='bg-black text-sm text-white px-14 py-24'>
<footer className='bg-black text-white px-14 py-24'>
<Link href='/'>
<Image src={logo} alt='모멘티아 로고' width={45} priority />
</Link>
Expand Down
17 changes: 11 additions & 6 deletions src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React from 'react';
import { ComponentType, FC } from 'react';

import { IconProps } from '@/types/IconProps';

import iconSizes from './iconSizes';
import iconSizes, { IconSize } from './iconSizes';
import { iconsNames } from './iconsNames';

const Icon: React.FC<IconProps> = ({
interface IconProps {
name: keyof typeof iconsNames;
size?: IconSize;
className?: string;
onClick?: () => void;
}

const Icon: FC<IconProps> = ({
name,
size = 'm',
className = '',
onClick,
}: IconProps) => {
const Component = iconsNames[name] as React.ComponentType<{
const Component = iconsNames[name] as ComponentType<{
className?: string;
onClick?: () => void;
}>;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Icon/icons/Close.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { FC } from 'react';

const Close: React.FC<{ className?: string; onClick?: () => void }> = ({
const Close: FC<{ className?: string; onClick?: () => void }> = ({
className,
onClick,
}) => {
Expand Down
4 changes: 3 additions & 1 deletion src/components/Icon/icons/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const Menu: React.FC<{ className?: string; onClick?: () => void }> = ({
import { FC } from 'react';

const Menu: FC<{ className?: string; onClick?: () => void }> = ({
className,
onClick,
}) => {
Expand Down
4 changes: 3 additions & 1 deletion src/components/Icon/iconsNames.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FC } from 'react';

import AlertCircle from './icons/AlertCircle';
import AlternateShare from './icons/AlternateShare';
import ArrowLeft from './icons/ArrowLeft';
Expand Down Expand Up @@ -37,7 +39,7 @@ import UploadShare from './icons/UploadShare';

export const iconsNames: Record<
string,
React.FC<{ className?: string; onClick?: () => void }>
FC<{ className?: string; onClick?: () => void }>
> = {
AlertCircle,
AlternateShare,
Expand Down
78 changes: 33 additions & 45 deletions src/components/Input/EmailInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,48 @@

import { Input } from '@nextui-org/react';

import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';

import Icon from '../Icon/Icon';

export interface EmailInputProps {
interface EmailInputProps {
mode: 'sign-up' | 'sign-in';
}

const EmailInput = ({ mode }: EmailInputProps) => {
const [value, setValue] = useState('');
const [message, setMessage] = useState('');
const [messageColor, setMessageColor] = useState('');
const [email, setEmail] = useState('');
const [validationMessage, setValidationMessage] = useState('');
const [validationMessageColor, setValidationMessageColor] = useState('');

const validateEmail = (email: string) =>
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email);
const isEmailInvalid = (email: string) =>
email !== '' && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email);

const checkEmailStatus = (email: string) => {
// TODO: 여기에 이메일 상태를 확인하는 API 호출 로직 추가
const clearEmailField = () => {
setEmail('');
};

if (mode === 'sign-up') {
if (email === '[email protected]') {
setMessage('이미 가입된 계정입니다.');
setMessageColor('text-system-error');
} else {
setMessage('사용 가능한 이메일입니다.');
setMessageColor('text-system-success');
}
} else if (mode === 'sign-in') {
const checkEmailStatus = (email: string) => {
// TODO: 이메일 상태 확인 (API 호출 로직 추가)
if (mode === 'sign-in') {
if (email === '[email protected]') {
setMessage('');
setValidationMessage('');
} else {
setMessage('가입되어 있지 않은 이메일입니다.');
setMessageColor('text-system-error');
setValidationMessage('가입되어 있지 않은 이메일입니다.');
setValidationMessageColor('text-system-error');
}
}
};

const isInvalid = React.useMemo(() => {
if (value === '') return false;

return !validateEmail(value);
}, [value]);

useEffect(() => {
if (isInvalid) {
setMessage('올바른 이메일 형식으로 입력해주세요.');
setMessageColor('text-system-error');
} else if (value) {
checkEmailStatus(value);
if (isEmailInvalid(email)) {
setValidationMessage('올바른 이메일 형식으로 입력해주세요.');
setValidationMessageColor('text-system-error');
} else if (email) {
checkEmailStatus(email);
} else {
setMessage('');
setValidationMessage('');
}
}, [value, isInvalid, mode]);

const handleEmailFieldClear = () => {
setValue('');
};
}, [email]);

return (
<>
Expand All @@ -67,20 +52,21 @@ const EmailInput = ({ mode }: EmailInputProps) => {
label='이메일'
labelPlacement='outside'
placeholder='이메일을 입력해주세요.'
value={value}
onValueChange={(newValue) => setValue(newValue)}
value={email}
onValueChange={(newEmail) => setEmail(newEmail)}
isInvalid={false}
className='w-80'
className='w-78.25'
classNames={{
label: 'custom-label',
input: 'placeholder:text-gray-700',
inputWrapper: ['bg-gray-900', 'rounded-md'],
}}
onClear={handleEmailFieldClear}
onClear={clearEmailField}
/>
{message && (

{validationMessage && (
<div className='flex items-center mt-2'>
{messageColor === 'text-system-success' ? (
{validationMessageColor === 'text-system-success' ? (
<Icon
name='CheckCircleFilled'
size='s'
Expand All @@ -93,7 +79,9 @@ const EmailInput = ({ mode }: EmailInputProps) => {
className='text-system-error mr-2'
/>
)}
<p className={`button-s ${messageColor}`}>{message}</p>
<p className={`button-s ${validationMessageColor}`}>
{validationMessage}
</p>
</div>
)}
</>
Expand Down
Loading

0 comments on commit 9161958

Please sign in to comment.