diff --git a/packages/web/src/app/credits/page.tsx b/packages/web/src/app/credits/page.tsx new file mode 100644 index 0000000..f3e1604 --- /dev/null +++ b/packages/web/src/app/credits/page.tsx @@ -0,0 +1,65 @@ +"use client"; + +import React from "react"; + +import styled from "styled-components"; + +import FlexWrapper from "@sparcs-students/web/common/components/FlexWrapper"; +import FoldableSectionTitle from "@sparcs-students/web/common/components/FoldableSectionTitle"; +import PageHead from "@sparcs-students/web/common/components/PageHead"; + +import SectionTitle from "@sparcs-students/web/common/components/SectionTitle"; +import MemberCardSection from "@sparcs-students/web/features/credits/components/MemberCardSection"; +import credits from "@sparcs-students/web/features/credits/credits"; + +const CreditCardsFlexWrapper = styled(FlexWrapper)` + gap: 40px; + + @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) { + gap: 20px; + } +`; + +const ResponsiveMemberCardSectionWrapper = styled.div` + @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) { + margin-left: 16px; + } +`; + +const Credits: React.FC = () => { + const [toggleFold, setToggleFold] = React.useState(false); + return ( + + + {credits.map((credit, index) => ( + + {index === 0 ? ( + <> + {credit.semester} + + + + + ) : ( + { + setToggleFold(!toggleFold); + }} + > + + + + + )} + + ))} + + ); +}; +export default Credits; diff --git a/packages/web/src/assets/sparcs-orange.svg b/packages/web/src/assets/sparcs-orange.svg new file mode 100644 index 0000000..efff173 --- /dev/null +++ b/packages/web/src/assets/sparcs-orange.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/web/src/common/components/Card.tsx b/packages/web/src/common/components/Card.tsx index 9eaaf51..4ef72c0 100644 --- a/packages/web/src/common/components/Card.tsx +++ b/packages/web/src/common/components/Card.tsx @@ -1,19 +1,29 @@ "use client"; -import React from "react"; +import isPropValid from "@emotion/is-prop-valid"; import styled from "styled-components"; -const Card: React.FC = styled.div<{ +interface CardProps { outline?: boolean; -}>` + padding?: string; + gap?: number; +} + +const Card = styled.div.withConfig({ + shouldForwardProp: prop => isPropValid(prop), +})` display: flex; flex-direction: column; position: relative; - padding: 16px 20px; + align-self: stretch; + padding: ${({ padding }) => padding ?? `32px`}; + gap: ${({ gap }) => (gap ? `${gap}px` : "inherit")}; + 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}; border-radius: ${({ theme }) => theme.round.md}; diff --git a/packages/web/src/common/components/FlexWrapper.tsx b/packages/web/src/common/components/FlexWrapper.tsx new file mode 100644 index 0000000..ac7bad3 --- /dev/null +++ b/packages/web/src/common/components/FlexWrapper.tsx @@ -0,0 +1,22 @@ +import isPropValid from "@emotion/is-prop-valid"; +import styled from "styled-components"; + +interface FlexWrapperProps { + direction: "row" | "column"; + gap: number; + justify?: string; + padding?: string; +} + +const FlexWrapper = styled.div.withConfig({ + shouldForwardProp: prop => isPropValid(prop), +})` + display: flex; + position: relative; + flex-direction: ${({ direction }) => direction}; + gap: ${({ gap }) => `${gap}px`}; + justify-content: ${({ justify }) => justify ?? "flex-start"}; + padding: ${({ padding }) => padding ?? 0}; +`; + +export default FlexWrapper; diff --git a/packages/web/src/common/components/FoldableSectionTitle.tsx b/packages/web/src/common/components/FoldableSectionTitle.tsx index bb6cf14..e9e8453 100644 --- a/packages/web/src/common/components/FoldableSectionTitle.tsx +++ b/packages/web/src/common/components/FoldableSectionTitle.tsx @@ -1,38 +1,67 @@ "use client"; import React from "react"; + +import isPropValid from "@emotion/is-prop-valid"; import styled from "styled-components"; +import TextButton from "@sparcs-students/web/common/components/Buttons/TextButton"; import SectionTitle from "@sparcs-students/web/common/components/SectionTitle"; +import Icon from "@sparcs-students/web/common/components/Icon"; + +const FoldableSectionOuter = styled.div` + width: 100%; + max-width: calc(100vw + (100% - 100vw)); +`; const FoldableSectionTitleInner = styled.div` display: flex; align-items: center; justify-content: space-between; gap: 20px; - width: 100%; `; -export const MoreInfo = styled.div` - font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; - font-size: 14px; - line-height: 20px; - font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR}; - color: ${({ theme }) => theme.colors.BLACK}; - text-decoration-line: underline; - cursor: pointer; +const ChildrenOuter = styled.div.withConfig({ + shouldForwardProp: prop => isPropValid(prop), +})<{ margin?: string }>` + margin-top: ${({ margin }) => margin}; + margin-left: 24px; `; const FoldableSectionTitle: React.FC<{ + iconType?: string; title: string; - toggle: boolean; - toggleHandler: () => void; -}> = ({ title, toggle, toggleHandler }) => ( - - {title} - {toggle ? `접기` : `펼치기`} - -); + toggle?: boolean; + toggleHandler?: () => void; + children?: React.ReactNode; + childrenMargin?: string; +}> = ({ + iconType = null, + title, + toggle = null, + toggleHandler = null, + children = null, + childrenMargin = "40px", +}) => { + const [open, setOpen] = React.useState(true); + const openHandler = () => setOpen(!open); + + return ( + + + {iconType && } + {title} + + + {(toggle ?? open) && children && ( + {children} + )} + + ); +}; export default FoldableSectionTitle; diff --git a/packages/web/src/common/components/PageHead/_atomic/PageTitle.tsx b/packages/web/src/common/components/PageHead/_atomic/PageTitle.tsx new file mode 100644 index 0000000..baf18c1 --- /dev/null +++ b/packages/web/src/common/components/PageHead/_atomic/PageTitle.tsx @@ -0,0 +1,25 @@ +"use client"; + +import React from "react"; + +import styled from "styled-components"; + +const PageTitleInner = styled.div` + position: relative; + width: fit-content; + font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; + font-size: 30px; + line-height: 24px; + @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) { + font-size: 24px; + line-height: 36px; + } + font-weight: ${({ theme }) => theme.fonts.WEIGHT.SEMIBOLD}; + color: ${({ theme }) => theme.colors.BLACK}; +`; + +const PageTitle: React.FC = ({ + children =
, +}) => {children}; + +export default PageTitle; diff --git a/packages/web/src/common/components/PageHead/index.tsx b/packages/web/src/common/components/PageHead/index.tsx new file mode 100644 index 0000000..8745c45 --- /dev/null +++ b/packages/web/src/common/components/PageHead/index.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +import styled from "styled-components"; + +import PageTitle from "./_atomic/PageTitle"; + +// 주석 처리된 부분은 BreadCrumb 컴포넌트를 쓰게 된다면 다시 추가 +interface PageHeadProps { + // items: { name: string; path: string }[]; + title: string; + // enableLast?: boolean; + action?: React.ReactNode; +} + +const PageHeadWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 20px; + @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) { + gap: 12px; + } + align-items: flex-start; + align-self: stretch; +`; + +const TitleWrapper = styled.div` + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; + @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) { + flex-direction: column; + align-items: flex-start; + gap: 20px; + } +`; + +const PageHead: React.FC = ({ + // items, + title, + // enableLast = false, + action = null, +}) => ( + + {/* */} + + {title} + {action &&
{action}
} +
+
+); + +export default PageHead; diff --git a/packages/web/src/common/components/SectionTitle.tsx b/packages/web/src/common/components/SectionTitle.tsx index 7e60510..e89c33f 100644 --- a/packages/web/src/common/components/SectionTitle.tsx +++ b/packages/web/src/common/components/SectionTitle.tsx @@ -21,8 +21,9 @@ const IdentityBar = styled.div` const Title = styled.p<{ size: Size }>` font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; - font-size: ${({ size }) => (size === "sm" ? "20px" : "24px")}; - line-height: ${({ size }) => (size === "sm" ? "28px" : "32px")}; + font-size: ${({ size }) => + size === "sm" ? "20px" : "20px"}; // TODO: 반응형 사이즈 재확인 + line-height: ${({ size }) => (size === "sm" ? "24px" : "24px")}; font-weight: ${({ theme }) => theme.fonts.WEIGHT.MEDIUM}; color: ${({ theme }) => theme.colors.BLACK}; `; diff --git a/packages/web/src/constants/paths.ts b/packages/web/src/constants/paths.ts index 8668ca9..f2f268d 100644 --- a/packages/web/src/constants/paths.ts +++ b/packages/web/src/constants/paths.ts @@ -51,7 +51,7 @@ const paths = { }, ], }, - MADE_BY: { name: "만든 사람들", path: "/" }, + MADE_BY: { name: "만든 사람들", path: "/credits" }, LICENSE: { name: "라이센스", path: "/" }, TERMS_OF_SERVICE: { name: "이용 약관", path: "/" }, LOGIN: { name: "로그인", path: "/login" }, diff --git a/packages/web/src/features/credits/components/MemberCard.tsx b/packages/web/src/features/credits/components/MemberCard.tsx new file mode 100644 index 0000000..b810a5d --- /dev/null +++ b/packages/web/src/features/credits/components/MemberCard.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; + +import Image from "next/image"; +import styled from "styled-components"; + +import SparcsLogo from "@sparcs-students/web/assets/sparcs-orange.svg"; +import Card from "@sparcs-students/web/common/components/Card"; +import Typography from "@sparcs-students/web/common/components/Typography"; + +import type { Member } from "../credits"; + +const MemberWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: flex-end; + gap: 6px; +`; + +const MemberCard = ({ member }: { member: Member }) => { + const [displayText, setDisplayText] = useState(member.role); + + const handleMouseEnter = () => { + if (member.comment) { + setDisplayText(member.comment); + } + }; + + const handleMouseLeave = () => { + setDisplayText(member.role); + }; + + return ( + + + SPARCS Logo + + {member.nickname} + + + {member.name} + + + + {displayText} + + + ); +}; + +export default MemberCard; diff --git a/packages/web/src/features/credits/components/MemberCardSection.tsx b/packages/web/src/features/credits/components/MemberCardSection.tsx new file mode 100644 index 0000000..8e8a6a6 --- /dev/null +++ b/packages/web/src/features/credits/components/MemberCardSection.tsx @@ -0,0 +1,59 @@ +import React from "react"; + +import styled from "styled-components"; + +import { Member, SemesterCredit } from "../credits"; + +import MemberCard from "./MemberCard"; + +interface MemberCardSectionProps { + semesterCredit: SemesterCredit; + leftMargin?: number; +} + +const MemberCardWrapper = styled.div<{ leftMargin: number }>` + display: grid; + grid-gap: 20px; + grid-template-columns: repeat(5, 1fr); + margin-left: ${({ leftMargin }) => leftMargin}px; + + @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.xl}) { + grid-template-columns: repeat(4, 1fr); + } + + @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.lg}) { + grid-template-columns: repeat(3, 1fr); + } + + @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.md}) { + grid-template-columns: repeat(2, 1fr); + } + + // @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) { + // grid-template-columns: 1fr; + // } +`; + +const MemberCardSection: React.FC = ({ + semesterCredit, + leftMargin = 0, +}) => { + const compareMembers = (a: Member, b: Member) => { + if (a.roleType === b.roleType) { + return a.nickname.localeCompare(b.nickname); + } + return a.roleType - b.roleType; + }; + + return ( + + {semesterCredit.members + .sort((a, b) => compareMembers(a, b)) + .map(member => ( + + ))} + + ); +}; + +export default MemberCardSection; diff --git a/packages/web/src/features/credits/credits.ts b/packages/web/src/features/credits/credits.ts new file mode 100644 index 0000000..4e2f50d --- /dev/null +++ b/packages/web/src/features/credits/credits.ts @@ -0,0 +1,93 @@ +enum RoleType { + PM, + APM_FE, + APM_BE, + member, + intern, +} + +export interface Member { + nickname: string; + name: string; + role: string; + roleType: RoleType; + comment?: string; +} + +export interface SemesterCredit { + semester: string; + members: Member[]; +} + +const credits: SemesterCredit[] = [ + { + semester: "🍁 2024년 가을", + members: [ + { + nickname: "eel", + name: "최우정", + role: "PM", + roleType: RoleType.PM, + comment: "eel", + }, + { + nickname: "chacha", + name: "안채연", + role: "FE", + roleType: RoleType.member, + comment: "chacha", + }, + { + nickname: "casio", + name: "임가은", + role: "FE", + roleType: RoleType.member, + comment: "casio", + }, + { + nickname: "malloc", + name: "최지윤", + role: "FE", + roleType: RoleType.member, + comment: "malloc", + }, + { + nickname: "mingle", + name: "민지연", + role: "BE", + roleType: RoleType.member, + comment: "mingle", + }, + { + nickname: "gb", + name: "권혁원", + role: "BE", + roleType: RoleType.intern, + comment: "gb", + }, + { + nickname: "dudu", + name: "이연희", + role: "Designer", + roleType: RoleType.member, + comment: "dudu", + }, + { + nickname: "somato", + name: "장성원", + role: "Designer", + roleType: RoleType.member, + comment: "somato", + }, + { + nickname: "siwon", + name: "박정원", + role: "Designer", + roleType: RoleType.intern, + comment: "siwon", + }, + ], + }, +]; + +export default credits; diff --git a/packages/web/src/styles/fonts/googleFonts.ts b/packages/web/src/styles/fonts/googleFonts.ts index 8979bee..fed99a7 100644 --- a/packages/web/src/styles/fonts/googleFonts.ts +++ b/packages/web/src/styles/fonts/googleFonts.ts @@ -1,3 +1,4 @@ +import { Raleway } from "next/font/google"; import localFont from "next/font/local"; export const pretendard = localFont({ @@ -5,7 +6,13 @@ export const pretendard = localFont({ variable: "--next-font-family-pretendard", }); -export const raleway = localFont({ - src: "./local/RalewayVariable.woff2", +export const raleway = Raleway({ + subsets: ["latin"], + weight: ["700", "800"], // BOLD, EXTRABOLD only variable: "--next-font-family-raleway", }); + +export const nanumSquare = localFont({ + src: "./local/NanumSquare-ExtraBold.woff2", + variable: "--next-font-family-nanum-square", +}); diff --git a/packages/web/src/styles/fonts/local/NanumSquare-ExtraBold.woff2 b/packages/web/src/styles/fonts/local/NanumSquare-ExtraBold.woff2 new file mode 100644 index 0000000..71c8bd7 Binary files /dev/null and b/packages/web/src/styles/fonts/local/NanumSquare-ExtraBold.woff2 differ diff --git a/packages/web/src/styles/themes/colors.ts b/packages/web/src/styles/themes/colors.ts index be6d447..e9e6b73 100644 --- a/packages/web/src/styles/themes/colors.ts +++ b/packages/web/src/styles/themes/colors.ts @@ -27,6 +27,10 @@ const colors = { 200: "#F5A3A8", 700: "#B7202A", }, + SPARCS: { + main: "#EBA12A", + member: "#EBA12A66", + }, }; export default colors; diff --git a/packages/web/src/styles/themes/fonts.ts b/packages/web/src/styles/themes/fonts.ts index 6ca93dc..639f6b9 100644 --- a/packages/web/src/styles/themes/fonts.ts +++ b/packages/web/src/styles/themes/fonts.ts @@ -8,12 +8,14 @@ const fonts = { FAMILY: { PRETENDARD: "var(--next-font-family-pretendard)", RALEWAY: "var(--next-font-family-raleway)", + NANUM_SQUARE: "var(--next-font-family-nanum-square)", }, WEIGHT: { REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700, + EXTRABOLD: 800, }, };