Skip to content

Commit

Permalink
feat(demo): demo app for advertisement (#79)
Browse files Browse the repository at this point in the history
* chore(demo): init demo

* feat(demo): main page

* feat(demo): kakao login

* feat(server-utils): server utils package

* feat(ui): add header, layout component

* feat(demo): bottles page

* feat(bottle, demo): scroll restoration

* fix(bottle): disable image drag

* chore(utils): utils package
  • Loading branch information
stakbucks authored Nov 3, 2024
1 parent 00c9d7a commit 1c37ec8
Show file tree
Hide file tree
Showing 104 changed files with 2,459 additions and 18 deletions.
3 changes: 3 additions & 0 deletions apps/bottle/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ const withVanillaExtract = createVanillaExtractPlugin();

/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
scrollRestoration: true,
},
images: {
remotePatterns: [
{
Expand Down
15 changes: 14 additions & 1 deletion apps/bottle/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
9 changes: 9 additions & 0 deletions apps/demo/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ['@bottlesteam/eslint-config/next.js', 'eslint-config-vitest-globals'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: true,
},
};
36 changes: 36 additions & 0 deletions apps/demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
36 changes: 36 additions & 0 deletions apps/demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
26 changes: 26 additions & 0 deletions apps/demo/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
const withVanillaExtract = createVanillaExtractPlugin();

/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
scrollRestoration: true,
},
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'bottles-bucket.s3.ap-northeast-2.amazonaws.com',
},
],
},
webpack: config => {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
};

export default withVanillaExtract(nextConfig);
48 changes: 48 additions & 0 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@bottlesteam/demo",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@bottlesteam/ui": "workspace:*",
"@bottlesteam/utils": "workspace:*",
"@tanstack/react-query": "^5.51.21",
"cookies-next": "^4.2.1",
"es-toolkit": "^1.26.1",
"next": "14.2.4",
"overlay-kit": "^1.3.0",
"react": "^18.3.1",
"react-dom": "^18"
},
"devDependencies": {
"@bottlesteam/eslint-config": "workspace:*",
"@bottlesteam/typescript-config": "workspace:*",
"@bottlesteam/vitest-config": "workspace:*",
"@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vanilla-extract/css": "^1.15.3",
"@vanilla-extract/next-plugin": "^2.4.3",
"@vanilla-extract/recipes": "^0.5.3",
"@vanilla-extract/vite-plugin": "^4.0.13",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8",
"eslint-config-next": "15.0.0-rc.0",
"eslint-config-vitest-globals": "^1.0.0",
"jsdom": "^24.1.1",
"typescript": "^5",
"vite-plugin-magical-svg": "^1.2.1",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^2.0.4",
"vitest-canvas-mock": "^0.3.3"
}
}
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>
);
}
50 changes: 50 additions & 0 deletions apps/demo/src/app/bottles/Bottles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import TelescopeImage from '@/assets/images/telescope.webp';
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 {
bottles: RandomBottlesQuery;
userInfo: UserInfo;
}

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={() => {
router.push(`/bottle/${bottle.id}`);
}}
>
<BottleCard.TimeTag>{bottle.expiredAt}</BottleCard.TimeTag>
<BottleCard.Introduction>{bottle.introduction[0]?.answer}</BottleCard.Introduction>
<BottleCard.UserInformation
{...pick(bottle, ['userName', 'age', 'mbti', 'userImageUrl', 'lastActivatedAt'])}
/>
</BottleCard>
))}
</section>
</>
) : (
<Fallback marginTop={94}>
<Fallback.Image src={TelescopeImage} alt="fallback image" />
<Fallback.Title>꼭 맞는 상대를 찾는 중이에요</Fallback.Title>
<Fallback.Subtitle>{`보틀은 ${userInfo.name}님과 케미가 통하는\n상대를 엄선해 추천드리고 있어요`}</Fallback.Subtitle>
</Fallback>
)}
</Layout.Contents>
);
}
7 changes: 7 additions & 0 deletions apps/demo/src/app/bottles/HeaderArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client';

import { Header } from '@bottlesteam/ui';

export function HeaderArea() {
return <Header onGoBack={() => {}} />;
}
13 changes: 13 additions & 0 deletions apps/demo/src/app/bottles/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Layout, Header } from '@bottlesteam/ui';
import type { ReactNode } from 'react';

export default function BottlesLayout({ children }: { children: ReactNode }) {
return (
<>
<Layout hasCTAButton={false}>
<Header />
{children}
</Layout>
</>
);
}
22 changes: 22 additions & 0 deletions apps/demo/src/app/bottles/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RecommendationBottlePreview } from '@/models/bottle';
import { GET, getServerSideTokens, createInit } from '@/server';
import { Bottles } from './Bottles';

export interface RandomBottlesQuery {
nextBottleLeftHours: number;
randomBottles: RecommendationBottlePreview[];
}
export interface UserInfo {
name: string;
}

export default async function BottlesPage() {
const tokens = getServerSideTokens();

const [bottles, userInfo] = await Promise.all([
GET<RandomBottlesQuery>(`/api/v2/bottles/random`, tokens, createInit(tokens.accessToken)),
GET<UserInfo>(`/api/v1/profile/info`, tokens, createInit(tokens.accessToken)),
]);

return <Bottles bottles={bottles} userInfo={userInfo} />;
}
Binary file added apps/demo/src/app/favicon.ico
Binary file not shown.
Loading

0 comments on commit 1c37ec8

Please sign in to comment.