Skip to content

Commit

Permalink
chore(utils): utils package
Browse files Browse the repository at this point in the history
  • Loading branch information
stakbucks committed Nov 3, 2024
1 parent 0712b23 commit 8798965
Show file tree
Hide file tree
Showing 41 changed files with 717 additions and 64 deletions.
2 changes: 1 addition & 1 deletion apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@bottlesteam/ui": "workspace:*",
"@bottlesteam/server-utils": "workspace:*",
"@bottlesteam/utils": "workspace:*",
"@tanstack/react-query": "^5.51.21",
"cookies-next": "^4.2.1",
"es-toolkit": "^1.26.1",
Expand Down
43 changes: 43 additions & 0 deletions apps/demo/src/app/bottle/[id]/BottleDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client';

import { UserInformation } from '@/components/common/user-information';
import { Bottle } from '@/models/bottle';
import { Asset, FixedBottomCTAButton, spacings } from '@bottlesteam/ui';
import { useUserAgent } from '@bottlesteam/utils';

export function BottleDetail({ bottleDetail: user }: { bottleDetail: Bottle }) {
const userAgent = useUserAgent();

const handleInstall = () => {
if (!userAgent.isMobile) {
alert('모바일에서만 설치 가능합니다.');
return;
}
if (userAgent.isAndroid) {
window.location.href = 'intent://main#Intent;scheme=bottle;package=com.team.bottles;end';
}
if (userAgent.isIOS) {
// TODO: add iOS scheme
alert('iOS 대응 예정...ㅠ');
}
};

return (
<>
<UserInformation hasCTAButton>
<UserInformation.BasicInformationArea
likeMessage={user.likeMessage}
userImageUrl={user.userImageUrl}
age={user.age}
userName={user.userName}
/>
<UserInformation.IntroductionCard title={`${user.userName}님이 보내는 편지`} introduction={user.introduction} />
<UserInformation.SelectedProfile profile={user.profileSelect} />
</UserInformation>
<FixedBottomCTAButton variant="one" onClick={handleInstall}>
<Asset type="icon-letter" style={{ marginRight: spacings.xs }} />
보틀 설치하고 대화 시작하기
</FixedBottomCTAButton>
</>
);
}
16 changes: 16 additions & 0 deletions apps/demo/src/app/bottle/[id]/HeaderArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client';

import { Header } from '@bottlesteam/ui';
import { useRouter } from 'next/navigation';

export function HeaderArea() {
const router = useRouter();

return (
<Header
onGoBack={() => {
router.back();
}}
/>
);
}
11 changes: 11 additions & 0 deletions apps/demo/src/app/bottle/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ReactNode } from 'react';
import { HeaderArea } from './HeaderArea';

export default function BottleLayout({ children }: { children: ReactNode }) {
return (
<>
<HeaderArea />
{children}
</>
);
}
16 changes: 16 additions & 0 deletions apps/demo/src/app/bottle/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Bottle } from '@/models/bottle';
import { GET, getServerSideTokens, createInit } from '@/server';
import { UserAgentProvider } from '@bottlesteam/utils';
import { BottleDetail } from './BottleDetail';

export default async function BottlePage({ params: { id } }: { params: { id: number } }) {
const tokens = getServerSideTokens();

const bottleDetail = await GET<Bottle>(`/api/v1/bottles/${id}`, tokens, createInit(tokens.accessToken));

return (
<UserAgentProvider>
<BottleDetail bottleDetail={bottleDetail} />
</UserAgentProvider>
);
}
10 changes: 9 additions & 1 deletion apps/demo/src/app/bottles/Bottles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BottleCard } from '@/components/common/bottle-card';
import { Fallback } from '@/components/common/fallback';
import { Layout, spacings } from '@bottlesteam/ui';
import { pick } from 'es-toolkit';
import { useRouter } from 'next/navigation';
import { RandomBottlesQuery, UserInfo } from './page';

interface Props {
Expand All @@ -13,14 +14,21 @@ interface Props {
}

export function Bottles({ bottles: { randomBottles }, userInfo }: Props) {
const router = useRouter();

return (
<Layout.Contents>
{randomBottles.length > 0 ? (
<>
<Layout.Title>{`${userInfo.name}님에게\n추천하는 분들이에요!`}</Layout.Title>
<section style={{ marginTop: spacings.xxl, display: 'flex', flexDirection: 'column', gap: spacings.md }}>
{randomBottles.map(bottle => (
<BottleCard key={bottle.id} onClick={() => {}}>
<BottleCard
key={bottle.id}
onClick={() => {
router.push(`/bottle/${bottle.id}`);
}}
>
<BottleCard.TimeTag>{bottle.expiredAt}</BottleCard.TimeTag>
<BottleCard.Introduction>{bottle.introduction[0]?.answer}</BottleCard.Introduction>
<BottleCard.UserInformation
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/app/bottles/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RecommendationBottlePreview } from '@/models/bottle';
import { getServerSideTokens, GET, createInit } from '@bottlesteam/server-utils';
import { GET, getServerSideTokens, createInit } from '@/server';
import { Bottles } from './Bottles';

export interface RandomBottlesQuery {
Expand Down
15 changes: 14 additions & 1 deletion apps/demo/src/components/common/avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,20 @@ export function Avatar({ size: _size, blur, ...props }: AvatarProps) {
return (
<div className={containerStyle({ size: _size })}>
{props.src != null ? (
<Image priority {...props} alt="user profile image" fill objectFit="cover" className={avatarStyle({ blur })} />
<>
<Image
priority
{...props}
alt="user profile image"
fill
objectFit="cover"
className={avatarStyle({ blur })}
/>
<div
aria-hidden
style={{ width: '100%', height: '100%', position: 'absolute', top: 0, left: 0, zIndex: 2 }}
/>
</>
) : (
<Placeholder size={size} />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client';

import { OverlayProvider } from 'overlay-kit';
import { ReactNode } from 'react';

export function OverlayClientPatch({ children }: { children: ReactNode }) {
return <OverlayProvider>{children}</OverlayProvider>;
}
65 changes: 65 additions & 0 deletions apps/demo/src/components/common/selected-profile/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ProfileSelect } from '@/models/profile';
import { Card, Chip, Paragraph, spacings } from '@bottlesteam/ui';
import type { ReactNode } from 'react';
import { chipWrapper, informationContainer, selectedProfileBlockStyle } from './selectedProfileStyle.css';

interface Props {
profile: ProfileSelect;
items: ({
basicInformation,
personalities,
hobbies,
}: {
basicInformation: string[];
personalities: string[];
hobbies: string[];
}) => ReactNode;
}

export function SelectedProfileImpl({
profile: {
job,
mbti,
region: { city },
smoking,
alcohol,
keyword,
height,
interest: { culture, sports, entertainment, etc },
},
items,
}: Props) {
const basicInformation = [job, mbti, city, `${height}cm`, smoking, alcohol];
const personalities = keyword;
const hobbies = [
...Object.values(culture),
...Object.values(sports),
...Object.values(entertainment),
...Object.values(etc),
];

return (
<Card style={{ marginBottom: spacings.xl }}>
<div className={informationContainer}>{items({ basicInformation, personalities, hobbies })}</div>
</Card>
);
}

export function SelectedProfileBlock({ type, values }: { type: string; values: (string | number)[] }) {
return (
<div className={selectedProfileBlockStyle}>
<Paragraph typography="st2" color="neutral600">
{type}
</Paragraph>
<div className={chipWrapper}>
{values.map(value => (
<Chip key={value}>{value}</Chip>
))}
</div>
</div>
);
}

export const SelectedProfile = Object.assign(SelectedProfileImpl, {
Item: SelectedProfileBlock,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { spacings } from '@bottlesteam/ui';
import { style } from '@vanilla-extract/css';

export const selectedProfileBlockStyle = style({
display: 'flex',
flexDirection: 'column',
gap: spacings.sm,
});

export const informationContainer = style({
display: 'flex',
flexDirection: 'column',
gap: spacings.xl,
});

export const chipWrapper = style({
display: 'flex',
rowGap: spacings.sm,
columnGap: spacings.xs,
flexWrap: 'wrap',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { CurrentUser, OtherUser } from '@/models/user';
import { Asset, Bubble, Paragraph, spacings } from '@bottlesteam/ui';
import { Avatar } from '../avatar';
import { basicInformationAreaStyle, nameAndAgeContainerStyle } from './userInformationStyle.css';

type Props = (
| Pick<CurrentUser, 'imageUrl' | 'age' | 'userName'>
| Pick<OtherUser, 'userImageUrl' | 'age' | 'userName'>
) & { likeMessage?: string };

function isCurrentUser(props: Props): props is Pick<CurrentUser, 'imageUrl' | 'age' | 'userName'> {
return 'imageUrl' in props;
}

export function BasicInformationArea(props: Props) {
return (
<section className={basicInformationAreaStyle}>
{props.likeMessage && <Bubble style={{ marginBottom: spacings.sm }}>{props.likeMessage}</Bubble>}
<Avatar src={isCurrentUser(props) ? props.imageUrl : props.userImageUrl} size="lg" />
<div className={nameAndAgeContainerStyle}>
<Paragraph typography="st1" color="neutral900">
{props.userName}
</Paragraph>
<Asset type="icon-vertical-bar" />
<Paragraph typography="bo" color="neutral900">
{props.age}
</Paragraph>
</div>
</section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Introduction } from '@/models/introduction';
import { User } from '@/models/user';
import { Card, Paragraph, spacings } from '@bottlesteam/ui';
import { blockStyle, introductionStyle } from './userInformationStyle.css';

interface Props {
introduction: User['introduction'];
title: string;
}

export function IntroductionCard(props: Props) {
return (
<Card style={{ marginBottom: spacings.sm }}>
<Paragraph typography="st1" color="black100">
{props.title}
</Paragraph>
<div className={introductionStyle}>
{props.introduction.length > 0 ? (
props.introduction.map(field => <IntroductionBlock key={field.question} field={field} />)
) : (
<Paragraph typography="bo" color="neutral900">
아직 자기소개를 작성하지 않았아요.
</Paragraph>
)}
</div>
</Card>
);
}

function IntroductionBlock({ field: { answer } }: { field: Introduction[number] }) {
return (
<div className={blockStyle}>
<Paragraph typography="bo" color="neutral900">
{answer}
</Paragraph>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { ProfileSelect } from '@/models/profile';
import { SelectedProfile } from '../selected-profile';

interface Props {
profile: ProfileSelect;
}

export function UserInformationSelectedProfile({ profile }: Props) {
return (
<SelectedProfile
profile={profile}
items={({ basicInformation, personalities, hobbies }) => (
<>
<SelectedProfile.Item type="기본 정보" values={basicInformation} />
<SelectedProfile.Item type="나의 성격은" values={personalities} />
<SelectedProfile.Item type="내가 푹 빠진 취미는" values={hobbies} />
</>
)}
/>
);
}
20 changes: 20 additions & 0 deletions apps/demo/src/components/common/user-information/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ReactNode } from 'react';
import { BasicInformationArea } from './BasicInformationArea';
import { IntroductionCard } from './IntroductionCard';
import { UserInformationSelectedProfile } from './SelectedProfile';
import { layoutStyle } from './userInformationStyle.css';

interface RootProps {
children: ReactNode;
hasCTAButton: boolean;
}

function UserInformationRoot({ children, hasCTAButton }: RootProps) {
return <div className={layoutStyle({ hasCTAButton })}>{children}</div>;
}

export const UserInformation = Object.assign(UserInformationRoot, {
BasicInformationArea,
IntroductionCard,
SelectedProfile: UserInformationSelectedProfile,
});
Loading

0 comments on commit 8798965

Please sign in to comment.