Skip to content

Commit

Permalink
feat(community): 마이 페이지 추가 (#38)
Browse files Browse the repository at this point in the history
* feat(community): add userCard component

* feat(community): add userList component

* feat(community): add user page

* fix(community): rename getUser function

* feat(design-system): add box-shadow tokens

* refactor(community): rename MyPage and related components for clarity

* Update apps/community/src/app/my/page.tsx

* feat(community): add list card component and implement edit functionality

* feat(design-system): add outline variant to button component

* feat(community): update MyInfoCard styles and button variants

* feat(community): refactor MyInfoCard and improve edit functionality

* feat(design-system): enhance input component styles for disabled and focus states

* refactor(community): update export method of MyInfoCardList component

* feat(community): split MyInfoCardList into two components

---------

Co-authored-by: JaeguJaegu <[email protected]>
Co-authored-by: Gwansik Kim <[email protected]>
  • Loading branch information
3 people authored Dec 8, 2024
1 parent b96af6c commit f20df13
Show file tree
Hide file tree
Showing 15 changed files with 336 additions and 3 deletions.
10 changes: 10 additions & 0 deletions apps/community/src/app/api/mock/my/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const myProfile = {
name: '강민하',
phone: '010-0000-0000',
email: '[email protected]',
role: '학부생',
major: '컴퓨터공학부',
id: '201912000',
};

export { myProfile };
5 changes: 5 additions & 0 deletions apps/community/src/app/api/mock/my/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { myProfile } from './data';

export function GET() {
return Response.json({ data: myProfile });
}
34 changes: 34 additions & 0 deletions apps/community/src/app/my/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { MyInfoEditableProfileCard } from '~/components/my/my-info-editable-profile-card';
import { MyInfoProfileCard } from '~/components/my/my-info-profile-card';
import { PageHeader } from '~/components/page-header';
import { getMyProfile } from './remotes';

// TODO: for mocking but will be replaced with a proper solution later
export const dynamic = 'force-dynamic';

export default async function MyPage() {
const { data } = await getMyProfile();

const userDetails = [
{ title: '이름', value: data.name },
{ title: '학번', value: data.id },
{ title: '구분', value: data.role },
{ title: '전공', value: data.major },
];

const userEditableDetails = [
{ title: '전화번호', value: data.phone },
{ title: '이메일', value: data.email },
];

return (
<>
<PageHeader
title="회원 정보"
description="등록한 회원 정보를 확인할 수 있어요."
/>
<MyInfoProfileCard data={userDetails} />
<MyInfoEditableProfileCard data={userEditableDetails} />
</>
);
}
17 changes: 17 additions & 0 deletions apps/community/src/app/my/remotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MOCK_END_POINT } from '~/constants/api';
import { http } from '~/utils/http';

interface MyProfile {
id: string;
name: string;
phone: string;
email: string;
role: string;
major: string;
}

function getMyProfile() {
return http.get<MyProfile>(MOCK_END_POINT.MY_PROFILE);
}

export { type MyProfile, getMyProfile };
87 changes: 87 additions & 0 deletions apps/community/src/components/my/my-info-card.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { screen, themeVars } from '@aics-client/design-system/styles';
import { style, styleVariants } from '@vanilla-extract/css';

const cardWrapper = style({
display: 'flex',
flexDirection: 'column',
gap: themeVars.spacing.lg,
marginBottom: themeVars.spacing.xl,
padding: themeVars.spacing.xl,
border: `1px solid ${themeVars.color.gray300}`,
borderRadius: themeVars.borderRadius.xl,
boxShadow: themeVars.boxShadow.md,
});

const cardTitle = style({
marginBottom: themeVars.spacing.lg,
fontSize: themeVars.fontSize.xl,
fontWeight: themeVars.fontWeight.bold,
});

const cardContent = styleVariants({
default: {
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: themeVars.spacing.lg,
},
singleColumn: {
display: 'grid',
gridTemplateColumns: '1fr',
gap: themeVars.spacing.lg,
},
});

const field = style({
margin: '1.5rem 0',
});

const fieldTitle = style({
width: '6rem',
fontSize: themeVars.fontSize.lg,
fontWeight: themeVars.fontWeight.semibold,
});

const editFieldWrapper = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
});

const editFieldContent = style({
display: 'flex',
flexDirection: 'column',
gap: themeVars.spacing.lg,

...screen.md({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}),
});

const editField = style({
width: '15rem',

...screen.md({
width: '20rem',
}),
});

const buttonWrapper = style({
display: 'flex',
margin: '1rem 0',
gap: themeVars.spacing.sm,
});

export {
cardWrapper,
cardTitle,
cardContent,
field,
fieldTitle,
editFieldWrapper,
editFieldContent,
editField,
buttonWrapper,
};
91 changes: 91 additions & 0 deletions apps/community/src/components/my/my-info-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use client';

import { Button, Input } from '@aics-client/design-system';
import { useState } from 'react';
import * as styles from '~/components/my/my-info-card.css';

interface MyInfoCardProps {
title: string;
layout?: 'default' | 'singleColumn';
children: React.ReactNode;
}

interface MyInfoFieldProps {
title: string;
value: string;
}

interface MyInfoEditableFieldProps extends MyInfoFieldProps {
onSave?: (value: string) => void;
}

function MyInfoCard({ title, children, layout = 'default' }: MyInfoCardProps) {
return (
<div className={styles.cardWrapper}>
<h2 className={styles.cardTitle}>{title}</h2>
<div className={styles.cardContent[layout]}>{children}</div>
</div>
);
}

function MyInfoField({ title, value }: MyInfoFieldProps) {
return (
<div>
<h3 className={styles.fieldTitle}>{title}</h3>
<p className={styles.field}>{value}</p>
</div>
);
}

function MyInfoEditableField({
title,
value,
onSave,
}: MyInfoEditableFieldProps) {
const [isEditing, setIsEditing] = useState(false);
const [currentValue, setCurrentValue] = useState(value);

const handleEditToggle = () => setIsEditing((prev) => !prev);
const handleSave = () => {
setIsEditing(false);
onSave?.(currentValue);
};

return (
<div className={styles.editFieldWrapper}>
<div className={styles.editFieldContent}>
<h3 className={styles.fieldTitle}>{title}</h3>
<Input
className={styles.editField}
type="text"
value={currentValue}
placeholder={value}
onChange={(e) => setCurrentValue(e.target.value)}
disabled={!isEditing}
/>
</div>

<div className={styles.buttonWrapper}>
{isEditing ? (
<>
<Button size="sm" color="outline" onClick={handleSave}>
저장
</Button>
<Button size="sm" color="outline" onClick={handleEditToggle}>
취소
</Button>
</>
) : (
<Button size="sm" color="outline" onClick={handleEditToggle}>
변경
</Button>
)}
</div>
</div>
);
}

MyInfoCard.Field = MyInfoField;
MyInfoCard.EditableField = MyInfoEditableField;

export { MyInfoCard };
32 changes: 32 additions & 0 deletions apps/community/src/components/my/my-info-editable-profile-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import { MyInfoCard } from '~/components/my/my-info-card';

interface Props {
data: {
title: string;
value: string;
}[];
}

function MyInfoEditableProfileCard({ data }: Props) {
const handleSave = (field: string, value: string) => {
// TODO: 필요한 서버 API 호출 로직 추가
console.log(`${field} 저장: ${value}`);
};

return (
<MyInfoCard title="기본 정보" layout="singleColumn">
{data.map((detail) => (
<MyInfoCard.EditableField
key={detail.title}
title={detail.title}
value={detail.value}
onSave={(value) => handleSave(detail.title, value)}
/>
))}
</MyInfoCard>
);
}

export { MyInfoEditableProfileCard };
26 changes: 26 additions & 0 deletions apps/community/src/components/my/my-info-profile-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import { MyInfoCard } from '~/components/my/my-info-card';

interface Props {
data: {
title: string;
value: string;
}[];
}

function MyInfoProfileCard({ data }: Props) {
return (
<MyInfoCard title="내 프로필" layout="default">
{data.map((detail) => (
<MyInfoCard.Field
key={detail.title}
title={detail.title}
value={detail.value}
/>
))}
</MyInfoCard>
);
}

export { MyInfoProfileCard };
1 change: 1 addition & 0 deletions apps/community/src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const MOCK_END_POINT = {
PROFESSORS: `${MOCK_BASE_URL}/professors`,
CLUB: `${MOCK_BASE_URL}/about/club`,
CONTACT: `${MOCK_BASE_URL}/about/contact`,
MY_PROFILE: `${MOCK_BASE_URL}/my`,
BOARD: `${MOCK_BASE_URL}/board`,
BOARD_DETAIL: `${MOCK_BASE_URL}/board/detail`,
} as const;
Expand Down
8 changes: 8 additions & 0 deletions packages/design-system/src/components/button/button.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const buttonVariants = recipe({
warning: {
backgroundColor: themeVars.color.warning,
},
outline: {
backgroundColor: themeVars.color.white,
color: themeVars.color.black,
border: `1px solid ${themeVars.color.gray300}`,
':hover': {
backgroundColor: themeVars.color.gray50,
},
},
},
size: {
sm: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ export const Warning: Story = {
},
};

export const Outline: Story = {
args: {
color: 'outline',
size: 'md',
children: 'Button',
},
};

export const Small: Story = {
args: {
size: 'sm',
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Spinner from '../spinner/spinner';
import { buttonVariants } from './button.css';

interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
color?: 'primary' | 'secondary' | 'danger' | 'warning';
color?: 'primary' | 'secondary' | 'danger' | 'warning' | 'outline';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
Expand Down
9 changes: 7 additions & 2 deletions packages/design-system/src/components/input/input.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,20 @@ export const input = recipe({
},

':disabled': {
backgroundColor: themeVars.color.gray100,
opacity: themeVars.opacity[80],
cursor: 'not-allowed',
},

':focus-visible': {
outline: 'none',
boxShadow: `0 0 0 2px ${themeVars.color.white}, 0 0 0 4px ${themeVars.color.gray400}`,
},
},
variants: {
variant: {
primary: {
border: `1px solid ${themeVars.color.gray200}`,
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
boxShadow: themeVars.boxShadow.sm,
},
ghost: {
border: 'none',
Expand Down
2 changes: 2 additions & 0 deletions packages/design-system/src/styles/theme.css.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createTheme } from '@vanilla-extract/css';
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
import border from './tokens/border';
import boxShadow from './tokens/box-shadow';
import color from './tokens/color';
import container from './tokens/container';
import display from './tokens/display';
Expand All @@ -21,6 +22,7 @@ const tokens = {
// presets
container: container,
spacing: spacing,
boxShadow: boxShadow,
};

const properties = defineProperties({
Expand Down
Loading

0 comments on commit f20df13

Please sign in to comment.