-
Notifications
You must be signed in to change notification settings - Fork 1
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
base: main
Are you sure you want to change the base?
Changes from all commits
1fa9cfc
6bdd2bc
b05fa9e
ed43eee
3d2bdfd
aaba970
f0ba04a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> | ||
) | ||
} | ||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* eslint-disable react/no-children-prop */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이거는 왜 생긴 파일인가요?