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

feat: add gdsc-grass component #27

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as NextImage from 'next/image';
import '../public/global.css';

// new `next/image` support
const OriginalNextImage = NextImage.default;
Expand Down
10 changes: 10 additions & 0 deletions public/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*,
*:before,
*:after {
font-family: Pretendard;
box-sizing: border-box;
}

body {
margin: 0;
}
16 changes: 16 additions & 0 deletions src/components/github-contest/Grass/Grass.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { style } from '@vanilla-extract/css';

export const grassWrapperStyle = style({
position: 'relative',
maxWidth: '1060px',
display: 'flex',
flexDirection: 'column',
backgroundColor: 'transparent',
margin: '0 10px',
});

// TODO: style, 반응형, constant
export const grassInfoStyle = style({
fontSize: '18px',
color: '#D6D6D6',
});
22 changes: 22 additions & 0 deletions src/components/github-contest/Grass/Grass.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';

import Grass, { GrassPropsType } from './Grass';

// dummy data
const dummyData = [] as any[];
for (let i = 1; i <= 365; i++) {
const dayObj = { date: new Date(new Date().setDate(i)), count: i % 5 };
dummyData.push(dayObj);
}

export default {
title: 'github-contest/Grass',
component: Grass,
} as ComponentMeta<typeof Grass>;

const Template: ComponentStory<typeof Grass> = ({ data }: GrassPropsType) => (
<Grass data={data} />
);

export const Default = Template.bind({});
Default.args = { data: dummyData };
66 changes: 66 additions & 0 deletions src/components/github-contest/Grass/Grass.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCallback, useState } from 'react';

import { CountType } from '@/constants/github';
import GrassGraph, { GrassGraphDataType } from './GrassGraph';
import GrassTip, { GrassTipDataType } from './GrassTip';
import GrassExplain from './GrassExplain';

import * as styles from './Grass.css';

export interface GrassPropsType {
data: GrassGraphDataType[];
}

const Grass = ({ data }: GrassPropsType) => {
const [grassTipData, setGrassTipData] = useState<GrassTipDataType>({
x: 0,
y: 0,
infoText: '',
isHover: false,
});

const totalCount = data.reduce((acc, cur) => {
return acc + cur.count;
}, 0);

const onMouseEnter = useCallback(
(x?: number, y?: number, infoText?: string) => {
if (x === undefined || y === undefined || infoText === undefined) {
return;
}

setGrassTipData((prev) => ({
...prev,
x,
y,
infoText,
isHover: true,
}));
},
[],
);

const onMouseLeave = useCallback(() => {
setGrassTipData((prev) => ({
...prev,
isHover: false,
}));
}, []);

return (
<div className={styles.grassWrapperStyle}>
<div
className={styles.grassInfoStyle}
>{`${totalCount} ${CountType}s in the last year`}</div>
<GrassGraph
data={data}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
<GrassExplain />
<GrassTip grassTipData={grassTipData} />
</div>
);
};

export default Grass;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { style } from '@vanilla-extract/css';

// TODO: style(사이즈), 반응형, constant
export const explainWrapper = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
fontSize: '18px',
color: '#D6D6D6',
paddingLeft: '40px',
marginTop: '3px',
});

export const explainLinkStyle = style({
textDecoration: 'none',
color: '#D6D6D6',

':hover': {
color: '#4286F5',
},
});

export const colorExampleWrapperStyle = style({
display: 'flex',
alignItems: 'center',
});

export const colorExampleStyle = style({
width: '91px',
height: '15px',
margin: '0 8px',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import GrassExplain from './GrassExplain';

export default {
title: 'github-contest/Grass/GrassExplain',
component: GrassExplain,
};

export const Default = GrassExplain;
33 changes: 33 additions & 0 deletions src/components/github-contest/Grass/GrassExplain/GrassExplain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Link from 'next/link';
import GrassGraphItem, {
GrassLevelType,
} from '@/components/github-contest/Grass/GrassGraph/GrassGraphItem';
import * as styles from './GrassExplain.css';

const GrassExplain = () => {
const levels = [0, 1, 2, 3, 4] as GrassLevelType[];

// TODO: 커밋 개수 가져오는 로직 설명 링크 추가 (노션 or new page)
const explainURL = '/';

return (
<div className={styles.explainWrapper}>
<Link className={styles.explainLinkStyle} href={explainURL}>
Learn how we count commits
</Link>
<div className={styles.colorExampleWrapperStyle}>
<span>Less</span>
<svg className={styles.colorExampleStyle}>
{levels.map((level) => {
return (
<GrassGraphItem x={level * 19} y={0} level={level} key={level} />
);
})}
</svg>
<span>More</span>
</div>
</div>
);
};

export default GrassExplain;
4 changes: 4 additions & 0 deletions src/components/github-contest/Grass/GrassExplain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import GrassExplain from './GrassExplain';

export * from './GrassExplain';
export default GrassExplain;
22 changes: 22 additions & 0 deletions src/components/github-contest/Grass/GrassGraph/GrassGraph.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { style } from '@vanilla-extract/css';

// TODO: stlye(font size), constant, 반응형
export const grassGraphFontStyle = style({
fontSize: '18px',
fill: '#D6D6D6',
});

export const grassGraphBaseStyle = style({
position: 'relative',
width: '100%',
overflowX: 'scroll',

'::-webkit-scrollbar': {
height: '0px',
},
});

export const grassGraphSvgStyle = style({
minWidth: '1060px',
height: '160px',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';

import GrassGraph, { GrassGraphPropsType } from './GrassGraph';

// dummy data
const dummyData = [] as any[];
for (let i = 1; i <= 365; i++) {
const dayObj = { date: new Date(new Date().setDate(i)), count: i % 5 };
dummyData.push(dayObj);
}

export default {
title: 'github-contest/Grass/GrassGraph',
component: GrassGraph,
} as ComponentMeta<typeof GrassGraph>;

const Template: ComponentStory<typeof GrassGraph> = ({
data,
}: GrassGraphPropsType) => <GrassGraph data={data} />;

export const Default = Template.bind({});
Default.args = { data: dummyData };
115 changes: 115 additions & 0 deletions src/components/github-contest/Grass/GrassGraph/GrassGraph.tsx

Choose a reason for hiding this comment

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

ㅋㅋㅋ와 이거 그래프 구현하기 꽤 어렵겠다 생각했는데,, 역시,, 👍 이강현이 이강현했네요

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { DayInfo, MonthInfo, MonthTxtTrans } from '@/constants/date';
import { CountType } from '@/constants/github';
import GrassGraphItem from './GrassGraphItem';
import * as styles from './GrassGraph.css';

export interface GrassGraphDataType {
date: Date;
count: number;
}

export interface GrassGraphPropsType {
data: GrassGraphDataType[];
onMouseEnter?: (x?: number, y?: number, infoText?: string) => void;
onMouseLeave?: () => void;
}

function getLevel(count: number) {
if (count === 0) return 0;
if (count <= 2) return 1;
if (count <= 5) return 2;
if (count <= 8) return 3;
if (count >= 9) return 4;
return 0;
}

// TODO: 디자인에 따라 rectX, rectY 이동값 반영
const GrassGraph = ({
data,
onMouseEnter,
onMouseLeave,
}: GrassGraphPropsType) => {
const grass = () => {
let rectX = 45;

const monthArea = [] as JSX.Element[]; // 상단의 월 표시
const dayArea = [] as JSX.Element[]; // 좌측의 요일 표시

const grassGraphArea = data?.map(({ date, count }, idx) => {
const thisYear = date.getFullYear();
const thisMonth = date.getMonth();
const thisDate = date.getDate();
const thisDay = date.getDay();

// 텍스트 (2 commits on January 19, 2023)
const dateInfo = `${MonthTxtTrans[thisMonth]} ${thisDate}, ${thisYear}`;
const countInfo =
count > 1 ? `${count} ${CountType}s` : `${count} ${CountType}`; // 복수 처리
const infoText = `${countInfo} on ${dateInfo}`;

// 위치
if (thisDay === 0 && idx !== 0) {
rectX += 19; // 일요일 나올 때마다 우로 이동
}
const rectY = 30 + thisDay * 19;
const monthY = 20;
const dayY = rectY + 10;

// 색상 기준 레벨
const level = getLevel(count);
if ((thisDay === 1 || thisDay === 3 || thisDay === 5) && idx < 7) {
dayArea.push(
<text
x={0}
y={dayY}
key={thisDay}
className={styles.grassGraphFontStyle}
>
{DayInfo[thisDay]}
</text>,
);
}

if (thisDate === 1) {
monthArea.push(
<text
x={rectX}
y={monthY}
key={thisMonth}
className={styles.grassGraphFontStyle}
>
{MonthInfo[thisMonth]}
</text>,
);
}

return (
<GrassGraphItem
infoText={infoText}
x={rectX}
y={rectY}
key={dateInfo}
level={level}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
);
});

return (
<>
<g>{dayArea}</g>
<g>{monthArea}</g>
<g>{grassGraphArea}</g>
</>
);
};

return (
<div className={styles.grassGraphBaseStyle}>
<svg className={styles.grassGraphSvgStyle}>{grass()}</svg>
</div>
);
};

export default GrassGraph;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { style, styleVariants } from '@vanilla-extract/css';

export const base = style({
outline: '1px solid rgba(27, 31, 35, 0.06);',
outlineOffset: '-1px',
borderRadius: '2px',
});

// TODO: constant
export const grassGraphItemStyle = styleVariants({
'0': [base, { fill: '#EDEDED' }],
'1': [base, { fill: '#AAFFFF' }],
'2': [base, { fill: '#58C4C4' }],
'3': [base, { fill: '#00A4CA' }],
'4': [base, { fill: '#00688F' }],
});
Loading