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

Ryan FE seminar 1 #2

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions 2024-summer-FE-seminar/packages/web/src/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
Comment on lines +1 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 왜 생긴 파일인가요?

23 changes: 23 additions & 0 deletions 2024-summer-FE-seminar/packages/web/src/app/ryan/my/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use client"

import FlexWrapper from "@sparcs-clubs/web/common/components/FlexWrapper";
import PageHead from "@sparcs-clubs/web/common/components/PageHead";
import MyClubFrame from "@sparcs-clubs/web/features/ryan/frames/MyClubFrame";
import MyInfoFrame from "@sparcs-clubs/web/features/ryan/frames/MyInfoFrame";
import MyServiceFrame from "@sparcs-clubs/web/features/ryan/frames/MyServiceFrame";

const MyRyan: React.FC = () => (
<FlexWrapper direction="column" gap={60}>
<PageHead
items={[
{ name: "ryan 과제", path: "/ryan "},
{ name: "마이페이지", path: "/ryan/my" }]}
title="마이페이지"
/>
<MyInfoFrame/>
<MyClubFrame/>
<MyServiceFrame/>
</FlexWrapper>
);

export default MyRyan;
102 changes: 102 additions & 0 deletions 2024-summer-FE-seminar/packages/web/src/app/ryan/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-disable react/no-children-prop */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실제로 태스크할 때는 eslint disable 최대한 사용하지 말아주세요!


"use client"

import { useState } from "react";

import styled from "styled-components";

import Button from "@sparcs-clubs/web/common/components/Button";
import Card from "@sparcs-clubs/web/common/components/Card";
import FlexWrapper from "@sparcs-clubs/web/common/components/FlexWrapper";
import PageHead from "@sparcs-clubs/web/common/components/PageHead";
import ItemNumberInput from "@sparcs-clubs/web/common/components/ryan/ItemNumberInput";
import TextInput from "@sparcs-clubs/web/common/components/ryan/TextInput";
import Typography from "@sparcs-clubs/web/common/components/Typography";


const MailInput = styled.div`
display: flex;
flex-direction: row;
gap: 10px;
width: 100%;
`
Comment on lines +18 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlexWrapper도 아마 이걸 대체할 수 있을거에요!


const RyanAssignmentPage: React.FC = () => {
const [ mailId, setMailId ] = useState("");
const [ mailDomain, setMailDomain ] = useState("");
const [ idErrorMessage, setIdErrorMessage ] = useState("");
const [ domainErrorMessage, setDomainErrorMessage ] = useState("");
const [ fixDomain, setFixDomain ] = useState(false);

const validateEmailId = (id: string) => {
const idRegex = /^[a-zA-Z0-9._%+-]+$/;
if (!idRegex.test(id)) {
return '유효한 메일 ID를 입력하세요.';
}
return '';
};

const validateEmailDomain = (domain: string) => {
const domainRegex = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!domainRegex.test(domain)) {
return '유효한 메일 도메인을 입력하세요.';
}
return '';
};
Comment on lines +40 to +46
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이런것도 했네요 좋아요


return (
<FlexWrapper direction="column" gap={20}>
<PageHead
items={[{ name: "ryan 과제", path: "/ryan" }]}
title="ryan의 FE 세미나 #1 과제"
/>
<Card outline>
<Typography>메일 주소를 입력하세요.</Typography>
<MailInput>
<TextInput
placeholder="메일 ID"
disabled={false}
value={mailId}
errorMessage={idErrorMessage}
handleChange={(value: string) => {
setMailId(value);
setIdErrorMessage(validateEmailId(value));
}
}/>
<div style={{ position: 'relative', top: '8px' }}>@</div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

웬만하면 div 그대로는 사용하지 말아주세요! 이 경우에는 Typography를 사용할 수 있겠네요

<TextInput
placeholder="메일 도메인 (ex. kaist.ac.kr)"
disabled={fixDomain}
value={mailDomain}
errorMessage={domainErrorMessage}
handleChange={(value: string) => {
setMailDomain(value);
setDomainErrorMessage(validateEmailDomain(value))
}
}/>
<Button
style={{height: '36px'}}
children={fixDomain ? "직접 입력" : "kaist.ac.kr"}
onClick={(_) => {
if (!fixDomain) setDomainErrorMessage("");
setMailDomain(fixDomain ? "" : "kaist.ac.kr");
setFixDomain(!fixDomain);
}}
/>
Comment on lines +78 to +86
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 기능 직접 만들어본거 좋아요 👍

</MailInput>
</Card>
<Card outline>
<Typography>구매 수량을 입력하세요</Typography>
<ItemNumberInput
placeholder="숫자만 입력하세요"
maxValue={50}
unitString="개"
handleChange={(_) => {}}
/>
</Card>
</FlexWrapper>
);
}

export default RyanAssignmentPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* eslint-disable react/prop-types */

import { ChangeEvent, InputHTMLAttributes, useState } from "react";

import styled, { css } from "styled-components";

import FormError from "../FormError";
import Typography from "../Typography";

interface TextInputProps
extends InputHTMLAttributes<HTMLInputElement> {
placeholder: string
maxValue: number
unitString: string
handleChange: (value: number) => void
}


const errorBorderStyle = css`
border-color: ${({ theme }) => theme.colors.RED[600]};
`;

const StyledNumberInput = styled.div<TextInputProps & { hasError: boolean }>`
display: flex;
flex-direction: row;
width: 100%;
padding: 8px 12px 8px 12px;
border: 1px solid ${({ theme }) => theme.colors.GRAY[200]};
border-radius: 4px;
gap: 8px;
color: ${({ theme }) => theme.colors.BLACK};
background-color: ${({ theme }) => theme.colors.WHITE};
&:focus {
border-color: ${({ theme, hasError, disabled }) =>
!hasError && !disabled && theme.colors.PRIMARY};
}
&:hover:not(:focus) {
border-color: ${({ theme, hasError, disabled }) =>
!hasError && !disabled && theme.colors.GRAY[300]};
}
${({ hasError }) => hasError && errorBorderStyle}
`;

const StyledInput = styled.input`
outline: none;
border: none;
flex: 1;
font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD};
font-size: 16px;
line-height: 20px;
font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR};
&::placeholder {
color: ${({ theme }) => theme.colors.GRAY[200]};
}
`

const StyledInputWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`

const ItemNumberInput: React.FC<TextInputProps> = ({
placeholder = "",
maxValue = 0,
unitString = "개",
handleChange = () => {},
...props
}) => {
const [ value, setValue ] = useState(0);
const [ errorMessage, setErrorMessage ] = useState("");

function validate(inputValue: number): string {
if (inputValue > maxValue)
return "신청 가능 개수를 초과했습니다"
return ""
}

const onValueChange = (e: ChangeEvent<HTMLInputElement>) => {
const inputString = e.target.value;
const inputStringWithoutUnit =
inputString.endsWith(unitString)
? inputString.slice(0, -unitString.length)
: inputString;
const inputValue = Number(inputStringWithoutUnit);

if(Number.isNaN(inputValue)) {
setValue(value);
setErrorMessage("숫자만 입력 가능합니다");
} else {
setValue(inputValue);
setErrorMessage(validate(inputValue));
}

handleChange(inputValue);
}

function valueString(): string {
if (value === 0) return "";
return `${value}${unitString}`;
}

return (
<StyledInputWrapper>
<StyledNumberInput
placeholder={placeholder}
hasError={!!errorMessage}
maxValue={maxValue}
handleChange={handleChange}
unitString={unitString}
{...props}
>
<StyledInput
placeholder={placeholder}
onChange={onValueChange}
value={valueString()}
/>
<Typography>{`/ ${maxValue}${unitString}`}</Typography>
</StyledNumberInput>
{errorMessage && <FormError>{errorMessage}</FormError>}
</StyledInputWrapper>
);
};

export default ItemNumberInput;

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/* eslint-disable react/prop-types */
import { ChangeEvent, InputHTMLAttributes } from "react"

import styled, { css } from "styled-components";

import FormError from "../FormError";

interface TextInputProps
extends InputHTMLAttributes<HTMLInputElement> {
placeholder: string
disabled: boolean
value: string
errorMessage: string
handleChange: (value: string) => void
}


const errorBorderStyle = css`
border-color: ${({ theme }) => theme.colors.RED[600]};
`;

const disabledStyle = css`
background-color: ${({ theme }) => theme.colors.GRAY[100]};
border-color: ${({ theme }) => theme.colors.GRAY[200]};
`;

const StyledInput = styled.input<TextInputProps & { hasError: boolean }>`
display: block;
width: 100%;
padding: 8px 12px 8px 12px;
outline: none;
border: 1px solid ${({ theme }) => theme.colors.GRAY[200]};
border-radius: 4px;
gap: 8px;
font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD};
font-size: 16px;
line-height: 20px;
font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR};
color: ${({ theme }) => theme.colors.BLACK};
background-color: ${({ theme }) => theme.colors.WHITE};
&:focus {
border-color: ${({ theme, hasError, disabled }) =>
!hasError && !disabled && theme.colors.PRIMARY};
}
&:hover:not(:focus) {
border-color: ${({ theme, hasError, disabled }) =>
!hasError && !disabled && theme.colors.GRAY[300]};
}
&::placeholder {
color: ${({ theme }) => theme.colors.GRAY[200]};
}
${({ disabled }) => disabled && disabledStyle}
${({ hasError }) => hasError && errorBorderStyle}
`;

const StyledInputWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`
Comment on lines +56 to +60
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlexWrapper로 대체 가능!


const TextInput: React.FC<TextInputProps> = ({
placeholder = "",
value = "",
errorMessage = "",
disabled = false,
handleChange = () => {},
...props
}) => {
const onValueChange = (e: ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
handleChange(inputValue);
}

return (
<StyledInputWrapper>
<StyledInput
placeholder={placeholder}
hasError={!!errorMessage}
disabled={disabled}
value={value}
errorMessage={errorMessage}
handleChange={handleChange}
onChange={onValueChange}
{...props}
/>
{errorMessage && <FormError>{errorMessage}</FormError>}
</StyledInputWrapper>
);
};

export default TextInput;

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import AsyncBoundary from "@sparcs-clubs/web/common/components/AsyncBoundary";
import FlexWrapper from "@sparcs-clubs/web/common/components/FlexWrapper";
import FoldableSectionTitle from "@sparcs-clubs/web/common/components/FoldableSectionTitle";
import MoreDetailTitle from "@sparcs-clubs/web/common/components/MoreDetailTitle";
import ClubListGrid from "@sparcs-clubs/web/features/clubs/components/ClubListGrid";
import useGetMyClub from "@sparcs-clubs/web/features/my/clubs/service/useGetMyClub";

const MyClubFrame = () => {
const { data, isLoading, isError } = useGetMyClub();

return (
<FoldableSectionTitle
title="나의 동아리"
>
<FlexWrapper direction="column" gap={20} >
<MoreDetailTitle
title="2024년 봄학기"
moreDetail="전체 보기"
moreDetailPath="/my/clubs"
/>
<AsyncBoundary isLoading={isLoading} isError={isError}>
<ClubListGrid clubList={data?.semesters[0].clubs ?? []}/>
</AsyncBoundary>
</FlexWrapper>
</FoldableSectionTitle>
);
}

export default MyClubFrame;
Loading