├── .npmrc ├── apps ├── web │ ├── tamagui.config.ts │ ├── .eslintrc.js │ ├── app │ │ ├── favicon.ico │ │ ├── api │ │ │ ├── notifications │ │ │ │ ├── notification.d.ts │ │ │ │ └── route.ts │ │ │ ├── swagger │ │ │ │ └── route.ts │ │ │ ├── hello │ │ │ │ └── route.ts │ │ │ ├── post │ │ │ │ ├── route.ts │ │ │ │ └── [postId] │ │ │ │ │ └── route.ts │ │ │ └── EASHook │ │ │ │ └── route.ts │ │ ├── (app) │ │ │ ├── myPage │ │ │ │ ├── (topTab) │ │ │ │ │ ├── likes │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── layout.tsx │ │ │ │ └── setting │ │ │ │ │ └── page.tsx │ │ │ ├── @modal │ │ │ │ ├── default.tsx │ │ │ │ └── (...)notification │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── clientBoundary.tsx │ │ │ ├── post │ │ │ │ └── [postId] │ │ │ │ │ ├── clientBoundary.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── notification │ │ │ │ ├── page.tsx │ │ │ │ └── clientBoundary.tsx │ │ │ ├── clientBoundary.tsx │ │ │ ├── chat │ │ │ │ └── page.tsx │ │ │ ├── provider.tsx │ │ │ └── crew │ │ │ │ └── page.tsx │ │ ├── (private) │ │ │ └── swagger │ │ │ │ ├── swaggerDocument.tsx │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── reset.css │ ├── .env.example │ ├── .svgrrc.js │ ├── cypress │ │ ├── fixtures │ │ │ └── example.json │ │ ├── e2e │ │ │ └── index.cy.ts │ │ └── support │ │ │ ├── e2e.ts │ │ │ └── commands.ts │ ├── next-env.d.ts │ ├── cypress.config.ts │ ├── tsconfig.json │ ├── swagger.config.ts │ ├── public │ │ ├── vercel.svg │ │ ├── next.svg │ │ └── data │ │ │ └── notifications.json │ ├── README.md │ ├── package.json │ ├── service │ │ ├── getBaseUrl.ts │ │ └── httpStatus.ts │ └── next.config.js └── native │ ├── .eslintrc.js │ ├── app.config.js │ ├── .env.example │ ├── metro.config.js │ ├── assets │ ├── images │ │ ├── icon.png │ │ ├── splash.png │ │ ├── favicon.png │ │ └── adaptive-icon.png │ └── fonts │ │ └── SpaceMono-Regular.ttf │ ├── e2e │ ├── utils │ │ ├── common.ts │ │ ├── setupTests.ts │ │ └── openApp.ts │ ├── src │ │ └── home.test.ts │ └── jest.config.js │ ├── .svgrrc.js │ ├── app │ ├── setting.tsx │ ├── (tabs) │ │ ├── myPage │ │ │ ├── likes.tsx │ │ │ ├── index.tsx │ │ │ └── _layout.tsx │ │ ├── home │ │ │ ├── index.tsx │ │ │ ├── _layout.tsx │ │ │ └── post │ │ │ │ └── [postId].tsx │ │ ├── chat.tsx │ │ ├── _layout.tsx │ │ └── crew.tsx │ ├── [...missing].tsx │ ├── index.tsx │ ├── modal.tsx │ ├── +html.tsx │ └── _layout.tsx │ ├── .gitignore │ ├── tsconfig.json │ ├── babel.config.js │ ├── metro.config.ts │ ├── eas.json │ ├── app.config.ts │ ├── utils │ └── push-notification │ │ └── registerForPushNotificationAsync.ts │ ├── app.json │ ├── README.md │ ├── .detoxrc.js │ └── package.json ├── screenshot.png ├── packages ├── api │ ├── src │ │ ├── index.ts │ │ ├── getAndWriteOpenApi.ts │ │ ├── orvalConfig.ts │ │ ├── mutator.ts │ │ └── __generated__ │ │ │ ├── APISpecification.generated.ts │ │ │ └── openapi.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── ui │ ├── .eslintrc.js │ ├── .svgrrc.js │ ├── README.md │ ├── src │ │ ├── config │ │ │ └── constant.ts │ │ ├── tamagui.config.ts │ │ ├── block │ │ │ ├── windowing │ │ │ │ ├── index.tsx │ │ │ │ ├── shared.ts │ │ │ │ └── index.web.tsx │ │ │ ├── image │ │ │ │ ├── shared.ts │ │ │ │ ├── index.web.tsx │ │ │ │ └── index.tsx │ │ │ ├── carousel │ │ │ │ ├── index.tsx │ │ │ │ ├── shared.tsx │ │ │ │ └── index.web.tsx │ │ │ ├── appHeader │ │ │ │ ├── index.tsx │ │ │ │ ├── shared.ts │ │ │ │ └── index.web.tsx │ │ │ ├── externalLink.tsx │ │ │ └── bottomNavigation.web.tsx │ │ ├── button.tsx │ │ ├── store │ │ │ ├── mmkv.ts │ │ │ └── userInfoState.ts │ │ ├── assets │ │ │ ├── chevron-left.svg │ │ │ ├── heart.svg │ │ │ ├── myPage.svg │ │ │ ├── location.svg │ │ │ ├── home.svg │ │ │ ├── chat.svg │ │ │ ├── crew.svg │ │ │ ├── alarm.svg │ │ │ └── setting.svg │ │ ├── template │ │ │ ├── wip.tsx │ │ │ ├── chat │ │ │ │ ├── index.web.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── shared.tsx │ │ │ ├── myPage │ │ │ │ ├── index │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── topTabHeader.tsx │ │ │ │ └── setting.tsx │ │ │ ├── post │ │ │ │ └── [postId] │ │ │ │ │ ├── index.web.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── shared.tsx │ │ │ ├── notificationList.tsx │ │ │ └── home │ │ │ │ └── index.tsx │ │ ├── layout │ │ │ ├── appHeader │ │ │ │ ├── home.tsx │ │ │ │ └── myPage.tsx │ │ │ ├── tabBar │ │ │ │ ├── icons.tsx │ │ │ │ ├── icons.web.tsx │ │ │ │ └── shared.tsx │ │ │ └── toptab │ │ │ │ └── index.web.tsx │ │ ├── hook │ │ │ ├── useAppState.ts │ │ │ ├── useOnlineManager.ts │ │ │ └── useRefetchOnFocus │ │ │ │ ├── index.ts │ │ │ │ └── index.web.ts │ │ └── HOC │ │ │ └── withRecoilSync.tsx │ ├── tsconfig.json │ ├── turbo │ │ └── generators │ │ │ ├── templates │ │ │ ├── serverComponent.hbs │ │ │ └── clientComponent.hbs │ │ │ └── config.ts │ ├── package.json │ └── index.tsx ├── tsconfig │ ├── package.json │ ├── react-library.json │ ├── api.json │ ├── base.json │ └── nextjs.json └── eslint-config-custom │ ├── package.json │ └── index.js ├── .husky └── pre-commit ├── .eslintrc.js ├── .prettierignore ├── @types ├── process.d.ts ├── tamagui.d.ts ├── react-native-svg-transformer.d.ts ├── eas.d.ts └── service-worker.d.ts ├── .prettierrc.js ├── .github └── dependabot.yml ├── turbo.json ├── package.json ├── .gitignore ├── LICENSE └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | -------------------------------------------------------------------------------- /apps/web/tamagui.config.ts: -------------------------------------------------------------------------------- 1 | export { tamaguiConfig as default } from 'ui'; 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mononoke-choi/bandi/HEAD/screenshot.png -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './__generated__/APISpecification.generated'; 2 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['custom'], 3 | root: true, 4 | }; 5 | -------------------------------------------------------------------------------- /apps/native/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['custom'], 3 | root: true, 4 | }; 5 | -------------------------------------------------------------------------------- /apps/web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mononoke-choi/bandi/HEAD/apps/web/app/favicon.ico -------------------------------------------------------------------------------- /packages/api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['custom'], 3 | root: true, 4 | }; 5 | -------------------------------------------------------------------------------- /packages/ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['custom'], 3 | root: true, 4 | }; 5 | -------------------------------------------------------------------------------- /apps/native/app.config.js: -------------------------------------------------------------------------------- 1 | require('ts-node/register'); 2 | module.exports = require('./app.config.ts'); 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn run lint 5 | yarn run prettier 6 | -------------------------------------------------------------------------------- /apps/native/.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_API_END_POINT=http://localhost:3000 2 | EXPO_PUBLIC_PROFILE=development 3 | -------------------------------------------------------------------------------- /apps/native/metro.config.js: -------------------------------------------------------------------------------- 1 | require('ts-node/register'); 2 | module.exports = require('./metro.config.ts'); 3 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | IGNORE_TS_CONFIG_PATHS=true 2 | TAMAGUI_TARGET=web 3 | TAMAGUI_DISABLE_WARN_DYNAMIC_LOAD=1 4 | -------------------------------------------------------------------------------- /apps/native/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mononoke-choi/bandi/HEAD/apps/native/assets/images/icon.png -------------------------------------------------------------------------------- /apps/native/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mononoke-choi/bandi/HEAD/apps/native/assets/images/splash.png -------------------------------------------------------------------------------- /apps/native/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mononoke-choi/bandi/HEAD/apps/native/assets/images/favicon.png -------------------------------------------------------------------------------- /apps/native/e2e/utils/common.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (timeout: number) => 2 | new Promise(resolve => setTimeout(resolve, timeout)); 3 | -------------------------------------------------------------------------------- /apps/web/.svgrrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | icon: true, 3 | ref: true, 4 | replaceAttrValues: { '#fff': '{props.color}' }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/native/.svgrrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | icon: true, 3 | ref: true, 4 | replaceAttrValues: { '#fff': '{props.color}' }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/native/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mononoke-choi/bandi/HEAD/apps/native/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /packages/ui/.svgrrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | icon: true, 3 | ref: true, 4 | replaceAttrValues: { '#fff': '{props.color}' }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/native/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mononoke-choi/bandi/HEAD/apps/native/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /apps/web/app/api/notifications/notification.d.ts: -------------------------------------------------------------------------------- 1 | export type Notification = { 2 | date: string; 3 | title: string; 4 | img: string; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/native/app/setting.tsx: -------------------------------------------------------------------------------- 1 | import { SettingTemplate } from 'ui'; 2 | 3 | export default function Setting() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "devDependencies": {} 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['custom'], 3 | root: true, 4 | settings: { 5 | next: { 6 | rootDir: ['apps/*/'], 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/native/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 3 | # The following patterns were generated by expo-cli 4 | 5 | expo-env.d.ts 6 | # @end expo-cli -------------------------------------------------------------------------------- /apps/web/app/(app)/myPage/(topTab)/likes/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { Wip } from 'ui'; 5 | 6 | export default function Page() { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | .bundle 3 | coverage 4 | ios 5 | android 6 | dist 7 | .vercel 8 | .next 9 | .expo* 10 | __generated__ 11 | .tamagui 12 | -------------------------------------------------------------------------------- /@types/process.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | EXPO_PUBLIC_PROFILE: 'production' | 'development' | 'preview'; 4 | SECRET_WEBHOOK_KEY: string; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/(app)/myPage/(topTab)/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { MyPageTemplate } from 'ui'; 5 | 6 | export default function Page() { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/README.md: -------------------------------------------------------------------------------- 1 | # [Ui] Available commands 2 | 3 | - `build`: Build UI package for production 4 | - `dev`: Start development watch mode 5 | - `lint`: Lints the code using ESLint 6 | - `clean`: Clear build outputs 7 | -------------------------------------------------------------------------------- /packages/ui/src/config/constant.ts: -------------------------------------------------------------------------------- 1 | export const STORE_KEY = 'recoil-sync'; 2 | 3 | export const ACTIVE_TINT_COLOR = '#00d592'; 4 | 5 | export const HEADER_EDGE_SPACE_TOKEN = 15; 6 | export const HEADER_ICON_SIZE = 24; 7 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["dist", "build", "node_modules"], 3 | "extends": "tsconfig/react-library.json", 4 | "include": [".", "../../@types/**/*", "../../apps/web/.next/types/**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/(app)/@modal/default.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | 3 | // eslint-disable-next-line lodash/prefer-constant 4 | const Default: FC = () => { 5 | return null; 6 | }; 7 | 8 | export default Default; 9 | -------------------------------------------------------------------------------- /apps/native/e2e/utils/setupTests.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import 'dayjs/locale/ko'; 3 | import { openApp } from './openApp'; 4 | 5 | dayjs.locale('ko'); 6 | 7 | beforeEach(async () => { 8 | await openApp(); 9 | }); 10 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/ui/src/tamagui.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@tamagui/config'; 2 | import { createTamagui } from 'tamagui'; 3 | 4 | export const tamaguiConfig = createTamagui(config); 5 | 6 | export type TamaguiConfig = typeof tamaguiConfig; 7 | -------------------------------------------------------------------------------- /@types/tamagui.d.ts: -------------------------------------------------------------------------------- 1 | import type { TamaguiConfig } from 'ui'; 2 | 3 | declare module 'tamagui' { 4 | // overrides TamaguiCustomConfig so your custom types 5 | // work everywhere you import `tamagui` 6 | type TamaguiCustomConfig = TamaguiConfig; 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | overrides: [ 4 | { 5 | files: '*.svg', 6 | options: { 7 | parser: 'html', 8 | }, 9 | }, 10 | ], 11 | singleQuote: true, 12 | trailingComma: 'all', 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/cypress/e2e/index.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Home screen', () => { 2 | it('"Home" title should be visible in app header component', () => { 3 | cy.visit('/'); 4 | cy.get('[data-testid="appHeader"]').should('have.text', 'Home'); 5 | }); 6 | }); 7 | 8 | export {}; 9 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "types": ["next/types/global"] 5 | }, 6 | "exclude": ["build", "dist", "node_modules"], 7 | "extends": "tsconfig/api", 8 | "include": [".", "../../@types/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:3000', 6 | setupNodeEvents(on, config) { 7 | // implement node event listeners here 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/ui/src/block/windowing/index.tsx: -------------------------------------------------------------------------------- 1 | import { FlashList } from '@shopify/flash-list'; 2 | import React from 'react'; 3 | 4 | import { WindowingProps } from './shared'; 5 | 6 | export function Windowing(props: WindowingProps) { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/src/block/image/shared.ts: -------------------------------------------------------------------------------- 1 | import { default as NextImage } from 'next/image'; 2 | import { ComponentProps } from 'react'; 3 | import { Image as TamaguiImage } from 'tamagui'; 4 | 5 | export interface ImageProps { 6 | native?: ComponentProps; 7 | web?: ComponentProps; 8 | } 9 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "lib": ["ES2015", "DOM"], 6 | "module": "ESNext", 7 | "target": "ESNext" 8 | }, 9 | "display": "React Library", 10 | "extends": "./base.json" 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/(private)/swagger/swaggerDocument.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | // eslint-disable-next-line import/named 5 | import SwaggerUI, { SwaggerUIProps } from 'swagger-ui-react'; 6 | 7 | export default function SwaggerDocument({ spec }: SwaggerUIProps) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /apps/native/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "types": ["detox", "jest", "node"] 5 | }, 6 | "extends": "expo/tsconfig.base", 7 | "include": [ 8 | "**/*.ts", 9 | "**/*.tsx", 10 | ".expo/types/**/*.ts", 11 | "expo-env.d.ts", 12 | "../../@types/**/*" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/app/(app)/@modal/(...)notification/page.tsx: -------------------------------------------------------------------------------- 1 | import 'server-only'; 2 | import { getApiNotifications } from 'api'; 3 | 4 | import ClientBoundary from './clientBoundary'; 5 | 6 | export default async function Page() { 7 | const data = await getApiNotifications(); 8 | 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /packages/tsconfig/api.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "lib": ["DOM", "ESNext"], 6 | "noEmit": true, 7 | "resolveJsonModule": true, 8 | "target": "ESNext" 9 | }, 10 | "display": "api", 11 | "extends": "./base.json" 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [{ "name": "next" }] 4 | }, 5 | "exclude": ["node_modules"], 6 | "extends": "tsconfig/nextjs.json", 7 | "include": [ 8 | "**/*.ts", 9 | "**/*.tsx", 10 | "next-env.d.ts", 11 | "../../@types/**/*", 12 | ".next/types/**/*.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/src/button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { ComponentProps } from "react"; 4 | import "client-only"; 5 | import { Button as TamaguiButton } from "tamagui"; 6 | 7 | 8 | export function Button({children, ...props}: ComponentProps) { 9 | return {children}; 10 | } 11 | -------------------------------------------------------------------------------- /packages/ui/src/store/mmkv.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import { MMKV } from 'react-native-mmkv'; 3 | 4 | import { STORE_KEY } from '../config/constant'; 5 | 6 | export const MMKVStorage = new MMKV({ 7 | id: STORE_KEY, 8 | ...Platform.select({ 9 | native: { encryptionKey: '' }, 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /packages/ui/src/assets/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/native/app/(tabs)/myPage/likes.tsx: -------------------------------------------------------------------------------- 1 | import { useScrollProps } from '@bacons/expo-router-top-tabs'; 2 | import { Animated } from 'react-native'; 3 | import { Wip } from 'ui'; 4 | 5 | export default function Likes() { 6 | const scroll = useScrollProps(); 7 | 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/native/app/[...missing].tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from 'expo-router'; 2 | import { YStack, Text } from 'tamagui'; 3 | 4 | export default function NotFoundScreen() { 5 | return ( 6 | 7 | 8 | Not Found 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/src/template/wip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { H1, Text, YStack } from 'tamagui'; 3 | 4 | export function Wip() { 5 | return ( 6 | 7 |

WIP

8 | Bandi is a work in progress 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/(app)/post/[postId]/clientBoundary.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { AppHeader, AppHeaderProps, HeaderBackButton } from 'ui'; 5 | 6 | const HeaderLeft: AppHeaderProps['headerRight'] = () => ( 7 | 8 | ); 9 | 10 | export default function ClientBoundary() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/src/assets/heart.svg: -------------------------------------------------------------------------------- 1 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /packages/ui/src/block/carousel/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import React from 'react'; 5 | // eslint-disable-next-line import/default 6 | import Swiper from 'react-native-swiper'; 7 | 8 | import { CarouselProps } from './shared'; 9 | 10 | export function Carousel({ children, native }: CarouselProps) { 11 | return {children}; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/app/(app)/page.tsx: -------------------------------------------------------------------------------- 1 | import 'server-only'; 2 | import { getApiPost } from 'api'; 3 | import { HomeTemplate } from 'ui'; 4 | 5 | import ClientBoundary from './clientBoundary'; 6 | 7 | export default async function Home() { 8 | const data = await getApiPost(); 9 | 10 | return ( 11 | <> 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/ui/src/assets/myPage.svg: -------------------------------------------------------------------------------- 1 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /@types/react-native-svg-transformer.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | // eslint-disable-next-line import/no-namespace 3 | import * as React from 'react'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | 6 | export type ExtendedSVGProps = SvgProps & { 7 | // add your additional custom props 8 | }; 9 | 10 | const content: React.FC; 11 | export default content; 12 | } 13 | -------------------------------------------------------------------------------- /apps/native/app/(tabs)/myPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useScrollProps } from '@bacons/expo-router-top-tabs'; 2 | import { Animated } from 'react-native'; 3 | import { MyPageTemplate } from 'ui'; 4 | 5 | export default function Index() { 6 | const scroll = useScrollProps(); 7 | 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/api/src/getAndWriteOpenApi.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | fetch(`http://localhost:3000/api/swagger`) 5 | .then(res => res.text()) 6 | .then(res => 7 | writeFileSync(join(process.cwd(), '/src/__generated__/openapi.json'), res, { 8 | encoding: 'utf8', 9 | }), 10 | ) 11 | .catch(error => { 12 | console.error(error); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/ui/src/assets/location.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /packages/ui/src/block/appHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import { AppHeaderProps, HeaderBackButtonProps } from './shared'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 4 | export function AppHeader(_: AppHeaderProps) { 5 | return null; 6 | } 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | export function HeaderBackButton(_: HeaderBackButtonProps) { 10 | return null; 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/src/layout/appHeader/home.tsx: -------------------------------------------------------------------------------- 1 | import { Bell } from '@tamagui/lucide-icons'; 2 | import React from 'react'; 3 | import { Link } from 'solito/link'; 4 | 5 | import { HEADER_ICON_SIZE } from '../../config/constant'; 6 | 7 | export function HomeHeaderRight() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/swagger.config.ts: -------------------------------------------------------------------------------- 1 | import { createSwaggerSpec } from 'next-swagger-doc'; 2 | 3 | export const swaggerConfig = { 4 | apiFolder: 'app/api', 5 | definition: { 6 | info: { 7 | description: '[openapi.json](/api/swagger)', 8 | title: `Bandi Swagger`, 9 | version: '0.1.0', 10 | }, 11 | openapi: '3.0.0', 12 | }, 13 | schemaFolders: [], 14 | } as Parameters[0]; 15 | -------------------------------------------------------------------------------- /packages/ui/turbo/generators/templates/serverComponent.hbs: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { ReactNode } from "react"; 4 | import { Stack } from "tamagui"; 5 | 6 | interface {{ pascalCase name }}Props { 7 | children?: ReactNode; 8 | } 9 | 10 | export function {{ pascalCase name }}({ children }: {{ pascalCase name }}Props) { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/ui/src/assets/home.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /packages/ui/src/hook/useAppState.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { AppState, AppStateStatus } from 'react-native'; 3 | 4 | export const useAppState = ( 5 | onAppStateChange: (appState: AppStateStatus) => void, 6 | ) => { 7 | useEffect(() => { 8 | const subscription = AppState.addEventListener('change', onAppStateChange); 9 | 10 | return () => subscription.remove(); 11 | }, [onAppStateChange]); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/ui/turbo/generators/templates/clientComponent.hbs: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import "client-only"; 4 | import { ReactNode } from "react"; 5 | import { Stack } from "tamagui"; 6 | 7 | interface {{ pascalCase name }}Props { 8 | children?: ReactNode; 9 | } 10 | 11 | export function {{ pascalCase name }}({ children }: {{ pascalCase name }}Props) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { BottomNavigation } from 'ui'; 3 | 4 | import Provider from './provider'; 5 | 6 | export default function RootLayout({ 7 | modal, 8 | children, 9 | }: { 10 | children: ReactNode; 11 | modal: ReactNode; 12 | }) { 13 | return ( 14 | 15 | {children} 16 | 17 | {modal} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/(app)/notification/page.tsx: -------------------------------------------------------------------------------- 1 | import 'server-only'; 2 | 3 | import { getApiNotifications } from 'api'; 4 | import { NotificationListTemplate } from 'ui'; 5 | 6 | import ClientBoundary from './clientBoundary'; 7 | 8 | export default async function Page() { 9 | const data = await getApiNotifications(); 10 | 11 | return ( 12 | <> 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/ui/src/assets/chat.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /packages/ui/src/block/image/index.web.tsx: -------------------------------------------------------------------------------- 1 | import { default as NextImage } from 'next/image'; 2 | import React, { forwardRef } from 'react'; 3 | 4 | import { ImageProps } from './shared'; 5 | 6 | export const Image = forwardRef( 7 | function Image({ web }, ref) { 8 | if (!web) { 9 | throw Error('You must pass web prop into Image component!'); 10 | } 11 | 12 | return ; 13 | }, 14 | ); 15 | -------------------------------------------------------------------------------- /apps/native/e2e/src/home.test.ts: -------------------------------------------------------------------------------- 1 | import { by, element, expect } from 'detox'; 2 | 3 | describe('Home screen', () => { 4 | it('"Sign In" button should be visible', async () => { 5 | await expect(element(by.id('signInButton'))).toBeVisible(); 6 | }); 7 | 8 | it('Show screen has bottom navigation after tapping "Sign In"', async () => { 9 | await element(by.id('signInButton')).tap(); 10 | await expect(element(by.id('homeTabScreen'))).toBeVisible(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/ui/src/assets/crew.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /packages/ui/src/layout/appHeader/myPage.tsx: -------------------------------------------------------------------------------- 1 | import { Settings } from '@tamagui/lucide-icons'; 2 | import React from 'react'; 3 | import { Link } from 'solito/link'; 4 | 5 | import { HEADER_ICON_SIZE } from '../../config/constant'; 6 | 7 | export function MyPageHeaderRight() { 8 | return ( 9 | 10 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/native/app/(tabs)/home/index.tsx: -------------------------------------------------------------------------------- 1 | import type { GetApiPostResult } from 'api'; 2 | import { getApiPost } from 'api'; 3 | import { useEffect, useState } from 'react'; 4 | import { HomeTemplate } from 'ui'; 5 | 6 | export default function Home() { 7 | const [data, setData] = useState(); 8 | 9 | useEffect(function fetchOnDidMount() { 10 | getApiPost().then(res => { 11 | setData(res); 12 | }); 13 | }, []); 14 | 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for npm 4 | - package-ecosystem: 'npm' 5 | # Look for `package.json` and `lock` files in the `root` directory 6 | directory: '/' 7 | # Check the npm registry for updates every day 8 | schedule: 9 | interval: 'weekly' 10 | time: '11:00' 11 | timezone: 'Asia/Seoul' 12 | reviewers: 13 | - 'mononoke-choi' 14 | # Raise all npm pull requests with an assignee 15 | assignees: 16 | - 'mononoke-choi' 17 | -------------------------------------------------------------------------------- /apps/native/babel.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line turbo/no-undeclared-env-vars 2 | process.env.TAMAGUI_TARGET = 'native'; 3 | 4 | module.exports = function (api) { 5 | api.cache(true); 6 | return { 7 | plugins: [ 8 | 'expo-router/babel', 9 | [ 10 | 'transform-inline-environment-variables', 11 | { 12 | include: ['TAMAGUI_TARGET'], 13 | }, 14 | ], 15 | 'react-native-reanimated/plugin', 16 | ], 17 | presets: ['babel-preset-expo'], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/ui/src/block/carousel/shared.tsx: -------------------------------------------------------------------------------- 1 | import useEmblaCarousel from 'embla-carousel-react'; 2 | import { ComponentProps, HTMLAttributes, ReactNode } from 'react'; 3 | // eslint-disable-next-line import/default 4 | import Swiper from 'react-native-swiper'; 5 | 6 | export interface CarouselProps { 7 | children: ReactNode; 8 | native?: ComponentProps; 9 | web?: { 10 | carouselProps?: Parameters; 11 | containerStyle?: HTMLAttributes['style']; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/src/layout/tabBar/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getSharedHomeTabIconOptions, 3 | getSharedCrewTabBarIconOptions, 4 | getSharedChatTabBarIconOptions, 5 | getSharedMyPageTabBarIconOptions, 6 | } from './shared'; 7 | 8 | export const getHomeTabIconOptions = getSharedHomeTabIconOptions; 9 | export const getCrewTabBarIconOptions = getSharedCrewTabBarIconOptions; 10 | export const getChatTabBarIconOptions = getSharedChatTabBarIconOptions; 11 | export const getMyPageTabBarIconOptions = getSharedMyPageTabBarIconOptions; 12 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "globalEnv": ["NODE_ENV", "EAS_BUILD_PLATFORM"], 5 | "pipeline": { 6 | "build": { 7 | "dependsOn": ["^build"], 8 | "env": ["NODE_ENV", "EAS_BUILD_PLATFORM"], 9 | "outputs": [".next/**", "!.next/cache/**"] 10 | }, 11 | "clean": { 12 | "cache": false 13 | }, 14 | "dev": { 15 | "cache": false, 16 | "persistent": true 17 | }, 18 | "lint": {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/(app)/post/[postId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getApiPostPostId } from 'api'; 2 | import React from 'react'; 3 | import { PostIdTemplate } from 'ui'; 4 | 5 | import ClientBoundary from './clientBoundary'; 6 | 7 | interface PageProps { 8 | params: Record; 9 | } 10 | 11 | export default async function Page(props: PageProps) { 12 | const data = await getApiPostPostId(props.params?.postId); 13 | 14 | return ( 15 | <> 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/native/e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@jest/types').Config.InitialOptions} */ 2 | module.exports = { 3 | rootDir: '..', 4 | testMatch: ['/e2e/**/*.test.(js|ts)'], 5 | testTimeout: 120000, 6 | maxWorkers: 1, 7 | globalSetup: 'detox/runners/jest/globalSetup', 8 | globalTeardown: 'detox/runners/jest/globalTeardown', 9 | reporters: ['detox/runners/jest/reporter'], 10 | testEnvironment: 'detox/runners/jest/testEnvironment', 11 | setupFilesAfterEnv: ['/e2e/utils/setupTests.ts'], 12 | verbose: true, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/ui/src/assets/alarm.svg: -------------------------------------------------------------------------------- 1 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web/app/(private)/swagger/page.tsx: -------------------------------------------------------------------------------- 1 | import 'server-only'; 2 | import 'swagger-ui-react/swagger-ui.css'; 3 | import { Metadata } from 'next'; 4 | import { createSwaggerSpec } from 'next-swagger-doc'; 5 | 6 | import { swaggerConfig } from '../../../swagger.config'; 7 | 8 | import SwaggerDocument from './swaggerDocument'; 9 | 10 | export default function Page() { 11 | const spec = createSwaggerSpec(swaggerConfig); 12 | 13 | return ; 14 | } 15 | 16 | export const metadata: Metadata = { 17 | title: 'Swagger', 18 | }; 19 | -------------------------------------------------------------------------------- /packages/ui/src/block/image/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import { View } from 'react-native'; 3 | import { Image as TamaguiImage } from 'tamagui'; 4 | 5 | import { ImageProps } from './shared'; 6 | 7 | export const Image = forwardRef(function Image( 8 | { native }, 9 | ref, 10 | ) { 11 | if (!native) { 12 | throw Error('You must pass native prop into Image component!'); 13 | } 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/web/app/api/swagger/route.ts: -------------------------------------------------------------------------------- 1 | import { createSwaggerSpec } from 'next-swagger-doc'; 2 | 3 | import { HTTP_STATUS } from '../../../service/httpStatus'; 4 | import { swaggerConfig } from '../../../swagger.config'; 5 | 6 | export async function GET() { 7 | try { 8 | const swaggerSpec = createSwaggerSpec(swaggerConfig); 9 | 10 | return new Response(JSON.stringify(swaggerSpec), { 11 | status: HTTP_STATUS.OK, 12 | }); 13 | } catch (error) { 14 | return new Response(error as BodyInit, { status: HTTP_STATUS.BAD_REQUEST }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/app/(app)/notification/clientBoundary.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { Text } from 'tamagui'; 5 | import { HeaderBackButton, AppHeader, AppHeaderProps } from 'ui'; 6 | 7 | const Title: AppHeaderProps['title'] = styles => ( 8 | Notification 9 | ); 10 | const HeaderLeft: AppHeaderProps['headerRight'] = () => ( 11 | 12 | ); 13 | 14 | export default function ClientBoundary() { 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/orvalConfig.ts: -------------------------------------------------------------------------------- 1 | import defineConfig from 'orval'; 2 | 3 | defineConfig({ 4 | hooks: { 5 | afterAllFilesWrite: 'eslint --fix', 6 | }, 7 | input: { 8 | target: 'src/__generated__/openapi.json', 9 | }, 10 | output: { 11 | mode: 'single', 12 | override: { 13 | mutator: { 14 | name: 'customClient', 15 | path: 'src/mutator.ts', 16 | }, 17 | requestOptions: true, 18 | useDates: true, 19 | }, 20 | prettier: true, 21 | target: 'src/__generated__/APISpecification.generated.ts', 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/ui/src/template/chat/index.web.tsx: -------------------------------------------------------------------------------- 1 | import { getApiHello, GetApiHello200 } from 'api'; 2 | import React, { useEffect, useState } from 'react'; 3 | 4 | import { SharedChatTemplate, SharedChatIndexTemplateProps } from './shared'; 5 | 6 | export function ChatIndexTemplate({ children }: SharedChatIndexTemplateProps) { 7 | const [response, setResponse] = useState(); 8 | 9 | useEffect(function fetchOnDidMount() { 10 | getApiHello().then(res => { 11 | setResponse(res); 12 | }); 13 | }, []); 14 | 15 | return {children}; 16 | } 17 | -------------------------------------------------------------------------------- /packages/ui/src/template/myPage/index/index.tsx: -------------------------------------------------------------------------------- 1 | import { times, map } from 'lodash'; 2 | import React from 'react'; 3 | import { Text, XStack, YStack } from 'tamagui'; 4 | 5 | export function MyPageTemplate() { 6 | return ( 7 | 8 | {map(times(20), (_, index) => ( 9 | 10 | 11 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Commodi, 12 | enim? 13 | 14 | 15 | ))} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/src/block/appHeader/shared.ts: -------------------------------------------------------------------------------- 1 | import React, { ComponentProps, ReactNode } from 'react'; 2 | import { Text } from 'tamagui'; 3 | 4 | export interface AppHeaderProps { 5 | title?: string | { (props: ComponentProps): ReactNode }; 6 | headerLeft?: (props: { 7 | tintColor?: string; 8 | pressColor?: string; 9 | pressOpacity?: number; 10 | }) => React.ReactNode; 11 | headerRight?: (props: { 12 | tintColor?: string; 13 | pressColor?: string; 14 | pressOpacity?: number; 15 | }) => React.ReactNode; 16 | } 17 | 18 | export interface HeaderBackButtonProps { 19 | fallbackUrl: string; 20 | } 21 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "plugins": [{ "name": "next" }], 7 | "allowJs": true, 8 | "declaration": false, 9 | "declarationMap": false, 10 | "incremental": true, 11 | "jsx": "preserve", 12 | "lib": ["dom", "dom.iterable", "esnext"], 13 | "module": "esnext", 14 | "noEmit": true, 15 | "resolveJsonModule": true, 16 | "strict": true, 17 | "target": "ESNext" 18 | }, 19 | "include": ["src", "next-env.d.ts"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/app/(app)/myPage/setting/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { Text } from 'tamagui'; 5 | import { 6 | AppHeader, 7 | AppHeaderProps, 8 | HeaderBackButton, 9 | SettingTemplate, 10 | } from 'ui'; 11 | 12 | const Title: AppHeaderProps['title'] = styles => ( 13 | Setting 14 | ); 15 | const HeaderLeft: AppHeaderProps['headerRight'] = () => ( 16 | 17 | ); 18 | 19 | export default function Page() { 20 | return ( 21 | <> 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@typescript-eslint/eslint-plugin": "^6.0.0", 8 | "@typescript-eslint/parser": "^6.0.0", 9 | "eslint-config-next": "^13.4.1", 10 | "eslint-config-turbo": "^1.9.3", 11 | "eslint-plugin-cypress": "^2.13.3", 12 | "eslint-plugin-import": "^2.27.5", 13 | "eslint-plugin-jsonc": "^2.9.0", 14 | "eslint-plugin-lodash": "^7.4.0", 15 | "eslint-plugin-react": "^7.32.2", 16 | "eslint-plugin-sort-keys-fix": "^1.1.2" 17 | }, 18 | "devDependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/app/(app)/clientBoundary.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { Bell } from '@tamagui/lucide-icons'; 5 | import { Link } from 'solito/link'; 6 | import { Text } from 'tamagui'; 7 | import { AppHeader, AppHeaderProps, HEADER_ICON_SIZE } from 'ui'; 8 | 9 | const Title: AppHeaderProps['title'] = styles => Home; 10 | const HeaderRight: AppHeaderProps['headerRight'] = () => ( 11 | 12 | 13 | 14 | ); 15 | 16 | export default function ClientBoundary() { 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/src/block/windowing/shared.ts: -------------------------------------------------------------------------------- 1 | import { FlashList } from '@shopify/flash-list'; 2 | import type { VirtualItem } from '@tanstack/react-virtual'; 3 | import { useWindowVirtualizer } from '@tanstack/react-virtual'; 4 | import { ComponentProps, ReactNode } from 'react'; 5 | 6 | export type WindowingProps = Pick< 7 | ComponentProps>, 8 | 'data' 9 | > & { 10 | native: Omit>, 'data'>; 11 | web: Parameters[0] & { 12 | renderItem: ( 13 | props: VirtualItem & { 14 | item: T; 15 | isLastItem: boolean; 16 | }, 17 | ) => ReactNode; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /apps/native/metro.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | 4 | module.exports = (() => { 5 | /** @type {import('expo/metro-config').MetroConfig} */ 6 | const config = getDefaultConfig(__dirname, { 7 | isCSSEnabled: true, 8 | }); 9 | 10 | const { transformer, resolver } = config; 11 | 12 | config.transformer = { 13 | ...transformer, 14 | babelTransformerPath: require.resolve('react-native-svg-transformer'), 15 | }; 16 | config.resolver = { 17 | ...resolver, 18 | assetExts: resolver.assetExts.filter((ext: string) => ext !== 'svg'), 19 | sourceExts: [...resolver.sourceExts, 'svg'], 20 | }; 21 | 22 | return config; 23 | })(); 24 | -------------------------------------------------------------------------------- /packages/ui/src/block/externalLink.tsx: -------------------------------------------------------------------------------- 1 | import { openBrowserAsync } from 'expo-web-browser'; 2 | import React, { ComponentProps } from 'react'; 3 | import { Platform } from 'react-native'; 4 | import { Link } from 'solito/link'; 5 | 6 | export function ExternalLink(props: ComponentProps) { 7 | return ( 8 | { 12 | if (Platform.OS !== 'web') { 13 | // Prevent the default behavior of linking to the default browser on native. 14 | e.preventDefault(); 15 | // Open the link in an in-app browser. 16 | openBrowserAsync(props.href as string); 17 | } 18 | }} 19 | /> 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/ui/src/template/chat/index.tsx: -------------------------------------------------------------------------------- 1 | import { getApiHello, GetApiHello200 } from 'api'; 2 | import React, { useCallback, useState } from 'react'; 3 | 4 | import { useRefetchOnFocus } from '../../hook/useRefetchOnFocus'; 5 | 6 | import { SharedChatTemplate, SharedChatIndexTemplateProps } from './shared'; 7 | 8 | export function ChatIndexTemplate({ children }: SharedChatIndexTemplateProps) { 9 | const [response, setResponse] = useState(); 10 | 11 | useRefetchOnFocus( 12 | useCallback(function fetchHelloApi() { 13 | getApiHello().then(res => { 14 | setResponse(res); 15 | }); 16 | }, []), 17 | true, 18 | ); 19 | 20 | return {children}; 21 | } 22 | -------------------------------------------------------------------------------- /apps/native/app/(tabs)/home/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from 'expo-router'; 2 | import React from 'react'; 3 | import { HomeHeaderRight } from 'ui'; 4 | 5 | export default function Layout() { 6 | return ( 7 | <> 8 | 9 | 14 | 21 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /apps/native/app/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'solito/link'; 2 | import { Stack, YStack, Text } from 'tamagui'; 3 | 4 | export default function Page() { 5 | return ( 6 | 7 | 8 | 🐶 9 | 10 | 11 | 12 | 20 | Sign In 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/native/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "base": {}, 4 | "development": { 5 | "android": { 6 | "gradleCommand": ":app:assembleDebug", 7 | "image": "latest", 8 | "withoutCredentials": true 9 | }, 10 | "developmentClient": true, 11 | "distribution": "internal", 12 | "extends": "base", 13 | "ios": { 14 | "image": "latest", 15 | "simulator": true 16 | } 17 | }, 18 | "preview": { 19 | "android": { 20 | "buildType": "apk" 21 | }, 22 | "distribution": "internal", 23 | "extends": "base" 24 | }, 25 | "production": { 26 | "extends": "base" 27 | } 28 | }, 29 | "cli": { 30 | "version": ">= 2.4.0" 31 | }, 32 | "submit": { 33 | "production": {} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import { ReactNode } from 'react'; 4 | import './reset.css'; 5 | 6 | const inter = Inter({ 7 | subsets: ['latin'], 8 | variable: '--f-fa', 9 | weight: ['400', '700', '800'], 10 | }); 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: ReactNode; 16 | modal: ReactNode; 17 | }) { 18 | return ( 19 | 20 | 24 | {children} 25 | 26 | 27 | ); 28 | } 29 | 30 | export const metadata: Metadata = { 31 | title: { 32 | default: 'Bandi', 33 | template: `%s | Bandi`, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /apps/native/app/(tabs)/home/post/[postId].tsx: -------------------------------------------------------------------------------- 1 | import { GetApiPostPostIdResult, getApiPostPostId } from 'api'; 2 | import { useLocalSearchParams } from 'expo-router'; 3 | import { useEffect, useState } from 'react'; 4 | import { PostIdTemplate } from 'ui'; 5 | 6 | export default function Home() { 7 | const { postId, sharedTransitionTag } = useLocalSearchParams(); 8 | const [data, setData] = useState(); 9 | 10 | useEffect(function fetchOnDidMount() { 11 | getApiPostPostId(postId ?? '').then(res => { 12 | setData(res); 13 | }); 14 | }, []); 15 | 16 | return ( 17 | data && ( 18 | 24 | ) 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "postinstall": "npx typesync", 5 | "build": "turbo run build", 6 | "dev": "turbo run dev", 7 | "lint": "turbo run lint", 8 | "clean": "turbo run clean", 9 | "postclean": "yarn", 10 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 11 | "check-deps": "check-dependency-version-consistency ." 12 | }, 13 | "devDependencies": { 14 | "@turbo/gen": "^1.9.7", 15 | "@types/eslint": "8.44.0", 16 | "@types/prettier": "^2.7.3", 17 | "check-dependency-version-consistency": "4.1.0", 18 | "eslint": "^8.45.0", 19 | "eslint-config-custom": "*", 20 | "prettier": "latest", 21 | "turbo": "latest" 22 | }, 23 | "name": "bandi", 24 | "packageManager": "yarn@1.22.19", 25 | "workspaces": [ 26 | "apps/*", 27 | "packages/*" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | .idea 20 | 21 | # code gen 22 | .tamagui 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # turbo 37 | .turbo 38 | 39 | # vercel 40 | .vercel 41 | 42 | # Expo 43 | .expo/ 44 | dist/ 45 | web-build/ 46 | ios 47 | android 48 | 49 | # Native 50 | *.orig.* 51 | *.jks 52 | *.p8 53 | *.p12 54 | *.key 55 | *.mobileprovision 56 | 57 | # Metro 58 | .metro-health-check* 59 | 60 | # typescript 61 | *.tsbuildinfo 62 | -------------------------------------------------------------------------------- /packages/ui/src/hook/useOnlineManager.ts: -------------------------------------------------------------------------------- 1 | import { addEventListener } from '@react-native-community/netinfo'; 2 | import isBoolean from 'lodash/isBoolean'; 3 | import { useEffect } from 'react'; 4 | 5 | interface UseOnlineManagerProps { 6 | onConnected?: () => void; 7 | onDisConnected?: () => void; 8 | } 9 | 10 | export const useOnlineManager = (callbacks: UseOnlineManagerProps) => { 11 | useEffect( 12 | function navigateToOfflineScreen() { 13 | const unsubscribe = addEventListener(state => { 14 | if (isBoolean(state.isConnected)) { 15 | if (state.isConnected) { 16 | callbacks.onConnected?.(); 17 | } else { 18 | callbacks.onDisConnected?.(); 19 | } 20 | } 21 | }); 22 | 23 | return () => { 24 | unsubscribe(); 25 | }; 26 | }, 27 | [callbacks], 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /apps/native/app/(tabs)/chat.tsx: -------------------------------------------------------------------------------- 1 | import { map } from 'lodash'; 2 | import { Stack, Text } from 'tamagui'; 3 | import { Carousel, ChatIndexTemplate } from 'ui'; 4 | 5 | export default function Chat() { 6 | return ( 7 | 8 | 15 | {map([1, 2, 3], (item, index) => ( 16 | 23 | 24 | Slide {++index} 25 | 26 | 27 | ))} 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/native/app/(tabs)/myPage/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { TopTabs } from '@bacons/expo-router-top-tabs'; 2 | import { ACTIVE_TINT_COLOR, MyPageTopTabHeaderTemplate } from 'ui'; 3 | 4 | export const unstable_settings = { 5 | initialRouteName: 'index', 6 | }; 7 | 8 | export default function Layout() { 9 | return ( 10 | 17 | 18 | 19 | 20 | 26 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.0", 4 | "main": "./dist/index.js", 5 | "types": "./dist/index.d.ts", 6 | "license": "MIT", 7 | "scripts": { 8 | "writeOpenapi": "ts-node -r tsconfig-paths/register src/getAndWriteOpenApi.ts", 9 | "postwriteOpenapi": "prettier src/__generated__/openapi.json --write", 10 | "dev": "EXPO_PUBLIC_PROFILE=development orval --config src/orvalConfig.ts && tsup src/index.ts --dts --watch", 11 | "build": "EXPO_PUBLIC_PROFILE=production orval --config src/orvalConfig.ts && tsup src/index.ts --dts --minify", 12 | "lint": "eslint . --fix", 13 | "clean": "rm -rf dist node_modules" 14 | }, 15 | "devDependencies": { 16 | "eslint-config-custom": "*", 17 | "ts-node": "^10.9.1", 18 | "tsconfig": "*", 19 | "tsup": "^7.1.0" 20 | }, 21 | "dependencies": { 22 | "orval": "^6.16.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/ui/src/store/userInfoState.ts: -------------------------------------------------------------------------------- 1 | import { nullable, object, string, bool, optional } from '@recoiljs/refine'; 2 | import { atom } from 'recoil'; 3 | import { syncEffect } from 'recoil-sync'; 4 | 5 | import { STORE_KEY } from '../config/constant'; 6 | 7 | type UserInfoState = { 8 | auth: boolean; 9 | userID?: string; 10 | userName?: string; 11 | userPhoneNumber?: string; 12 | }; 13 | 14 | const userInfoState = atom({ 15 | default: { 16 | auth: false, 17 | }, 18 | effects: [ 19 | syncEffect({ 20 | refine: nullable( 21 | object({ 22 | auth: bool(), 23 | userID: optional(string()), 24 | userName: optional(string()), 25 | userPhoneNumber: optional(string()), 26 | }), 27 | ), 28 | storeKey: STORE_KEY, 29 | }), 30 | ], 31 | key: 'userInfo', 32 | }); 33 | 34 | export { userInfoState }; 35 | -------------------------------------------------------------------------------- /packages/ui/src/template/post/[postId]/index.web.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { replace } from 'lodash'; 5 | import React from 'react'; 6 | import { Stack } from 'tamagui'; 7 | 8 | import { Image } from '../../../block/image'; 9 | 10 | import { PostIdTemplateProps, PostDetail } from './shared'; 11 | 12 | export function PostIdTemplate({ 13 | data: { img, meta, title, description }, 14 | }: PostIdTemplateProps) { 15 | return ( 16 | <> 17 | 18 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/api/README.md: -------------------------------------------------------------------------------- 1 | # [Api] Available commands 2 | 3 | - `writeOpenapi`: Write openapi.json file from next.js local server 4 | - `postwriteOpenapi`: Format the code using ESLint 5 | - `dev`: Generate API from openapi.json for development 6 | - `build`: Generate API from openapi.json for production 7 | - `lint`: Lints the code using ESLint 8 | - `clean`: Clear build outputs 9 | 10 | # Swagger UI 11 | After running local server, if you type [http://localhost:3000/swagger](http://localhost:3000/swagger) URL in 12 | browser you could see swagger page 13 | 14 | # Troubleshooting 15 | 16 |
17 | [ERROR] Could not resolve "./__generated__/APISpecification.generated" 18 |

19 | 20 | This `api` package generates code from route handler of next.js, 21 | so you'd better run next.js development server before you generate codes 22 | 23 | ```shell 24 | yarn dev 25 | yarn workspace api generate:orval:dev 26 | ``` 27 | 28 |

29 |
30 | -------------------------------------------------------------------------------- /packages/ui/src/template/post/[postId]/index.tsx: -------------------------------------------------------------------------------- 1 | import { replace } from 'lodash'; 2 | import React from 'react'; 3 | import { Image } from 'react-native'; 4 | import Animated from 'react-native-reanimated'; 5 | import { useWindowDimensions } from 'tamagui'; 6 | 7 | import { PostIdTemplateProps, PostDetail } from './shared'; 8 | 9 | const AnimatedImage = Animated.createAnimatedComponent(Image); 10 | 11 | export function PostIdTemplate({ 12 | data: { img, title, description, meta }, 13 | native, 14 | }: PostIdTemplateProps) { 15 | const { width } = useWindowDimensions(); 16 | 17 | return ( 18 | <> 19 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/native/app/modal.tsx: -------------------------------------------------------------------------------- 1 | import { getApiNotifications, GetApiNotifications200Item } from 'api'; 2 | import { Stack } from 'expo-router'; 3 | import { StatusBar } from 'expo-status-bar'; 4 | import { useEffect, useState } from 'react'; 5 | import { Platform } from 'react-native'; 6 | import { SafeAreaView } from 'react-native-safe-area-context'; 7 | import { NotificationListTemplate } from 'ui'; 8 | 9 | export default function ModalScreen() { 10 | const [data, setData] = useState([]); 11 | 12 | useEffect(function getNotificationList() { 13 | (async () => { 14 | setData(await getApiNotifications()); 15 | })(); 16 | }, []); 17 | 18 | return ( 19 | <> 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/src/layout/tabBar/icons.web.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getSharedHomeTabIconOptions, 3 | getSharedCrewTabBarIconOptions, 4 | getSharedChatTabBarIconOptions, 5 | getSharedMyPageTabBarIconOptions, 6 | } from './shared'; 7 | import type { TabBarIcon } from './shared'; 8 | 9 | export const getHomeTabIconOptions: TabBarIcon = () => ({ 10 | ...getSharedHomeTabIconOptions(), 11 | activation: segment => segment === '', 12 | href: '/', 13 | }); 14 | export const getCrewTabBarIconOptions: TabBarIcon = () => ({ 15 | ...getSharedCrewTabBarIconOptions(), 16 | activation: segment => segment === 'crew', 17 | href: '/crew', 18 | }); 19 | export const getChatTabBarIconOptions: TabBarIcon = () => ({ 20 | ...getSharedChatTabBarIconOptions(), 21 | activation: segment => segment === 'chat', 22 | href: '/chat', 23 | }); 24 | export const getMyPageTabBarIconOptions: TabBarIcon = () => ({ 25 | ...getSharedMyPageTabBarIconOptions(), 26 | activation: segment => segment === 'myPage', 27 | href: '/myPage', 28 | }); 29 | -------------------------------------------------------------------------------- /apps/web/app/api/hello/route.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_STATUS } from '../../../service/httpStatus'; 2 | 3 | /** 4 | * @swagger 5 | * paths: 6 | * /api/hello: 7 | * get: 8 | * tags: 9 | * - sample 10 | * summary: greeting 11 | * description: > 12 | * say hello 13 | * responses: 14 | * 200: 15 | * description: OK 16 | * content: 17 | * application/json: 18 | * schema: 19 | * type: object 20 | * properties: 21 | * message: 22 | * type: string 23 | * createdAt: 24 | * type: string 25 | * required: 26 | * - message 27 | * - createdAt 28 | */ 29 | export async function GET() { 30 | return new Response( 31 | JSON.stringify({ createdAt: Date(), message: 'Hello Bandi!' }), 32 | { 33 | status: HTTP_STATUS.OK, 34 | }, 35 | ); 36 | } 37 | 38 | export const runtime = 'edge'; 39 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # [Web] Available commands 2 | 3 | - `dev`: Start Next.js in development mode. 4 | - `build`: Build the application for production usage. 5 | - `start`: Start a Next.js production server. 6 | - `cypress:open`: 7 | - `lint`: Lints the code using ESLint 8 | - `clean`: Clear build outputs 9 | 10 | ## React-navigation and Next.js router 11 | 12 | if we render react-navigation on the web it works fine. 13 | 14 | but we can't use many things built in Next.js router system, so we will render app header and bottom navigation manually. 15 | 16 | check out the files below and change them to suit your design 17 | ```typescript 18 | - packages/ui/src/block/appHeader.web.tsx 19 | - packages/ui/src/block/bottomNavigation.web.tsx 20 | ``` 21 | 22 | > ⚠️ Next.js recommend moving Client Components to the leaves of your component tree where possible. 23 | > we are using client component in most parts because Tamagui has beta support for RSC at this time. 24 | 25 | > [View transition API is not supported in next/navigation currently](https://github.com/vercel/next.js/discussions/46300) 26 | -------------------------------------------------------------------------------- /packages/ui/src/assets/setting.svg: -------------------------------------------------------------------------------- 1 | 8 | 16 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aiden.Choi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/ui/src/hook/useRefetchOnFocus/index.ts: -------------------------------------------------------------------------------- 1 | import { useFocusEffect } from 'expo-router'; 2 | import { useCallback, useEffect, useRef } from 'react'; 3 | 4 | /** 5 | * This hook will call the provided refetch function when the screen has 6 | * focus again. 7 | * 8 | * @example 9 | * ```js 10 | * useReFetchOnFocus( 11 | * useCallback(function fetchHelloApi() { 12 | * getApiHello().then(res => { 13 | * setResponse(res); 14 | * }); 15 | * }, []), 16 | * true, 17 | * ); 18 | * ``` 19 | * 20 | * if you need some detail example how to use this hook please check this out 21 | */ 22 | 23 | export const useRefetchOnFocus = (refetch: () => void, onDidMount = true) => { 24 | const firstTimeRef = useRef(true); 25 | 26 | useEffect( 27 | function fetchOnDidMount() { 28 | if (onDidMount) { 29 | refetch(); 30 | } 31 | }, 32 | [refetch], 33 | ); 34 | 35 | useFocusEffect( 36 | useCallback(() => { 37 | if (firstTimeRef.current) { 38 | firstTimeRef.current = false; 39 | return; 40 | } 41 | 42 | refetch(); 43 | }, [refetch]), 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /apps/web/app/(app)/myPage/(topTab)/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { Settings } from '@tamagui/lucide-icons'; 5 | import { ReactNode } from 'react'; 6 | import { Link } from 'solito/link'; 7 | import { Text } from 'tamagui'; 8 | import { 9 | AppHeader, 10 | AppHeaderProps, 11 | HEADER_ICON_SIZE, 12 | MyPageTopTabHeaderTemplate, 13 | TopTab, 14 | } from 'ui'; 15 | 16 | interface LayoutProps { 17 | children: ReactNode; 18 | } 19 | 20 | const Title: AppHeaderProps['title'] = styles => ( 21 | MyPage 22 | ); 23 | const HeaderLeft: AppHeaderProps['headerRight'] = () => ( 24 | 25 | 26 | 27 | ); 28 | 29 | export default function Layout({ children }: LayoutProps) { 30 | return ( 31 | <> 32 | 33 | 34 | 40 | {children} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "cypress:open": "cypress open", 10 | "lint": "next lint", 11 | "clean": "rm -rf .next .tamagui node_modules" 12 | }, 13 | "dependencies": { 14 | "@svgr/webpack": "^8.0.1", 15 | "@tamagui/next-plugin": "^1.41.1", 16 | "@tamagui/next-theme": "^1.41.1", 17 | "eslint-config-custom": "*", 18 | "next": "13.4.10", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "safe-compare": "^1.1.4", 22 | "tamagui": "^1.41.1", 23 | "ui": "*" 24 | }, 25 | "devDependencies": { 26 | "@next/bundle-analyzer": "^13.4.9", 27 | "@types/eslint": "8.44.0", 28 | "@types/node": "^20.4.2", 29 | "@types/react": "18.2.15", 30 | "@types/react-dom": "18.2.7", 31 | "@types/safe-compare": "^1.1.0", 32 | "@types/swagger-ui-react": "^4.18.0", 33 | "cypress": "^12.17.1", 34 | "eslint-config-custom": "*", 35 | "next-swagger-doc": "^0.4.0", 36 | "swagger-ui-react": "^5.1.0", 37 | "tsconfig": "*", 38 | "typescript": "^5.1.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/native/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigContext, ExpoConfig } from '@expo/config'; 2 | import * as _ from 'lodash'; 3 | 4 | const config = ({ 5 | config, 6 | }: Omit & { config: ExpoConfig }): ExpoConfig => { 7 | const APP_ENV = process.env.EXPO_PUBLIC_PROFILE ?? 'development'; 8 | const isProductionEnv = APP_ENV === 'production'; 9 | const addEnvSuffix = (value: string | undefined) => { 10 | if (value) { 11 | return isProductionEnv ? value : value + '.' + APP_ENV; 12 | } 13 | }; 14 | 15 | const override: Partial = { 16 | android: { 17 | package: addEnvSuffix(config.android?.package), 18 | }, 19 | ios: { 20 | bundleIdentifier: addEnvSuffix(config.ios?.bundleIdentifier), 21 | }, 22 | name: addEnvSuffix(config.name), 23 | plugins: [ 24 | [ 25 | '@config-plugins/detox', 26 | { 27 | subdomains: 28 | process.env.EAS_BUILD_PROFILE === 'development' || !isProductionEnv 29 | ? '*' 30 | : ['10.0.2.2', 'localhost'], 31 | }, 32 | ], 33 | ], 34 | }; 35 | 36 | return _.merge>(config, override); 37 | }; 38 | 39 | export default config; 40 | -------------------------------------------------------------------------------- /packages/ui/src/hook/useRefetchOnFocus/index.web.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | /** 4 | * This hook will call the provided refetch function when the screen has 5 | * focus again. 6 | * 7 | * @example 8 | * ```js 9 | * useReFetchOnFocus( 10 | * useCallback(function fetchHelloApi() { 11 | * getApiHello().then(res => { 12 | * setResponse(res); 13 | * }); 14 | * }, []), 15 | * true, 16 | * ); 17 | * ``` 18 | * 19 | * if you need some detail example how to use this hook please check this out 20 | */ 21 | 22 | export const useRefetchOnFocus = (refetch: () => void, onDidMount = true) => { 23 | const firstTimeRef = useRef(true); 24 | 25 | useEffect( 26 | function fetchOnDidMount() { 27 | if (onDidMount) { 28 | refetch(); 29 | } 30 | }, 31 | [refetch], 32 | ); 33 | 34 | useEffect(() => { 35 | const onFocus = () => { 36 | if (firstTimeRef.current) { 37 | firstTimeRef.current = false; 38 | return; 39 | } 40 | refetch(); 41 | }; 42 | 43 | window.addEventListener('focus', onFocus); 44 | onFocus(); 45 | 46 | return () => { 47 | window.removeEventListener('focus', onFocus); 48 | }; 49 | }, []); 50 | }; 51 | -------------------------------------------------------------------------------- /packages/ui/src/HOC/withRecoilSync.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { DefaultValue } from 'recoil'; 3 | import type { RecoilSyncOptions } from 'recoil-sync'; 4 | import { RecoilSync } from 'recoil-sync'; 5 | 6 | import { STORE_KEY } from '../config/constant'; 7 | import { MMKVStorage } from '../store/mmkv'; 8 | 9 | export const WithRecoilSync: FC = ({ 10 | children, 11 | ...restRecoilSyncProps 12 | }) => { 13 | return ( 14 | { 17 | if (typeof window === 'undefined') { 18 | return new DefaultValue(); 19 | } 20 | 21 | const gainedValue = MMKVStorage.getString(itemKey); 22 | 23 | if (gainedValue) { 24 | return JSON.parse(gainedValue); 25 | } else { 26 | return new DefaultValue(); 27 | } 28 | }} 29 | write={({ diff }) => { 30 | for (const [key, value] of diff) { 31 | try { 32 | MMKVStorage.set(key, JSON.stringify(value)); 33 | } catch (error) { 34 | console.error(error); 35 | } 36 | } 37 | }} 38 | {...restRecoilSyncProps} 39 | > 40 | {children} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /apps/web/app/(app)/chat/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { map } from 'lodash'; 5 | import React, { ComponentProps } from 'react'; 6 | import { Stack, Text } from 'tamagui'; 7 | import { AppHeader, AppHeaderProps, Carousel, ChatIndexTemplate } from 'ui'; 8 | 9 | const CAROUSEL_OPTION: ComponentProps['web'] = { 10 | carouselProps: [{ loop: true }], 11 | containerStyle: { 12 | maxHeight: '160px', 13 | }, 14 | }; 15 | 16 | const Title: AppHeaderProps['title'] = styles => Chat; 17 | export default function Page() { 18 | return ( 19 | 20 | 21 | 22 | {map([1, 2, 3], (item, index) => ( 23 | 34 | 35 | Slide {++index} 36 | 37 | 38 | ))} 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /apps/web/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/src/template/post/[postId]/shared.tsx: -------------------------------------------------------------------------------- 1 | import { GetApiPostPostId200 } from 'api'; 2 | import React from 'react'; 3 | import { Heading, YStack, Text, XStack } from 'tamagui'; 4 | 5 | export interface PostIdTemplateProps { 6 | data: GetApiPostPostId200; 7 | native?: { 8 | sharedTransitionTag: string; 9 | }; 10 | web?: unknown; 11 | } 12 | 13 | export function PostDetail({ 14 | title, 15 | description, 16 | meta: { location, createdAt }, 17 | }: Omit) { 18 | return ( 19 | 20 | 21 | 28 | {title} 29 | 30 | 31 | 32 | {location} 33 | 34 | 35 | {createdAt.toLocaleString()} 36 | 37 | 38 | 39 | 40 | {description} 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /packages/api/src/mutator.ts: -------------------------------------------------------------------------------- 1 | import { getBaseUrl } from 'web/service/getBaseUrl'; 2 | 3 | type CustomClient = { 4 | url: string; 5 | method: 'get' | 'post' | 'put' | 'delete' | 'patch'; 6 | /* eslint-disable @typescript-eslint/no-explicit-any */ 7 | params?: Record; 8 | headers?: HeadersInit; 9 | data?: BodyType; 10 | signal?: AbortSignal; 11 | }; 12 | 13 | const customClient = async ( 14 | { url, method, params, headers, signal, data }: CustomClient, 15 | options?: RequestInit, 16 | ): Promise => { 17 | const searchParams = params ? `?${new URLSearchParams(params)}` : ''; 18 | const response = await fetch(getBaseUrl() + url + searchParams, { 19 | headers: { 20 | ...headers, 21 | }, 22 | method, 23 | signal, 24 | ...(data ? { body: JSON.stringify(data) } : {}), 25 | ...options, 26 | }); 27 | 28 | if (response.ok) { 29 | return await response.json(); 30 | } else { 31 | const parsedJSON = await response.json(); 32 | 33 | throw new Error(`[NetworkError] ${JSON.stringify(parsedJSON)}`, { 34 | cause: { status: response.status, ...parsedJSON }, 35 | }); 36 | } 37 | }; 38 | 39 | export { customClient }; 40 | 41 | export type ErrorType = { 42 | cause: { 43 | status: number; 44 | } & ErrorData; 45 | }; 46 | export type BodyType = BodyData; 47 | -------------------------------------------------------------------------------- /apps/native/utils/push-notification/registerForPushNotificationAsync.ts: -------------------------------------------------------------------------------- 1 | import { isDevice } from 'expo-device'; 2 | import { 3 | setNotificationChannelAsync, 4 | AndroidImportance, 5 | getPermissionsAsync, 6 | requestPermissionsAsync, 7 | getExpoPushTokenAsync, 8 | } from 'expo-notifications'; 9 | import { Platform } from 'react-native'; 10 | 11 | async function registerForPushNotificationsAsync() { 12 | let token; 13 | 14 | if (Platform.OS === 'android') { 15 | await setNotificationChannelAsync('default', { 16 | importance: AndroidImportance.MAX, 17 | lightColor: '#FF231F7C', 18 | name: 'default', 19 | vibrationPattern: [0, 250, 250, 250], 20 | }); 21 | } 22 | 23 | if (isDevice) { 24 | const { status: existingStatus } = await getPermissionsAsync(); 25 | let finalStatus = existingStatus; 26 | if (existingStatus !== 'granted') { 27 | const { status } = await requestPermissionsAsync(); 28 | finalStatus = status; 29 | } 30 | if (finalStatus !== 'granted') { 31 | alert('it was failed to get push notification permission'); 32 | return; 33 | } 34 | token = (await getExpoPushTokenAsync()).data; 35 | console.log(token); 36 | } else { 37 | alert('push notification only could work on physical devices'); 38 | } 39 | 40 | return token; 41 | } 42 | 43 | export { registerForPushNotificationsAsync }; 44 | -------------------------------------------------------------------------------- /packages/ui/src/template/myPage/setting.tsx: -------------------------------------------------------------------------------- 1 | import { FlashList } from '@shopify/flash-list'; 2 | import React from 'react'; 3 | import { XStack, Text, Switch, YStack, useTheme } from 'tamagui'; 4 | 5 | const DATA = [ 6 | { 7 | description: 'we will send you informative notification', 8 | title: 'Push notification', 9 | }, 10 | ]; 11 | 12 | export function SettingTemplate() { 13 | const theme = useTheme(); 14 | 15 | return ( 16 | ( 22 | 29 | 30 | 36 | {item.title} 37 | 38 | 39 | {item.description} 40 | 41 | 42 | 43 | 44 | 45 | 46 | )} 47 | estimatedItemSize={100} 48 | /> 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/app/api/notifications/route.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | import { HTTP_STATUS } from '../../../service/httpStatus'; 5 | 6 | import { Notification } from './notification'; 7 | 8 | /** 9 | * @swagger 10 | * paths: 11 | * /api/notifications: 12 | * get: 13 | * tags: 14 | * - config 15 | * summary: config 16 | * description: > 17 | * recent notification list 18 | * responses: 19 | * 200: 20 | * description: OK 21 | * content: 22 | * application/json: 23 | * schema: 24 | * type: array 25 | * items: 26 | * type: object 27 | * properties: 28 | * title: 29 | * type: string 30 | * date: 31 | * type: string 32 | * img: 33 | * type: string 34 | * required: 35 | * - title 36 | * - date 37 | * - img 38 | */ 39 | export async function GET() { 40 | const dataDir = join(process.cwd(), '/public/data/notifications.json'); 41 | const jsonString = readFileSync(dataDir, 'utf8'); 42 | const { data } = JSON.parse(jsonString) as { 43 | data: Notification[]; 44 | }; 45 | 46 | return new Response(JSON.stringify(data), { 47 | status: HTTP_STATUS.OK, 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /apps/web/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /apps/web/service/getBaseUrl.ts: -------------------------------------------------------------------------------- 1 | // todo resolve turbo env eslint errors 2 | export const getBaseUrl = () => { 3 | // eslint-disable-next-line turbo/no-undeclared-env-vars 4 | const runtimeEnv = process.env.TAMAGUI_TARGET; 5 | // eslint-disable-next-line turbo/no-undeclared-env-vars 6 | const profile = process.env.EXPO_PUBLIC_PROFILE; 7 | // eslint-disable-next-line turbo/no-undeclared-env-vars 8 | const localHost = `http://localhost:${process.env.PORT ?? 3000}`; 9 | // eslint-disable-next-line turbo/no-undeclared-env-vars 10 | const VercelUrl = process.env.VERCEL_URL; 11 | 12 | if (runtimeEnv === 'web') { 13 | if (VercelUrl) { 14 | // if you build api package on cloud server, they must be pass their platform specific built in environment variables 15 | return `https://${VercelUrl}`; 16 | } else { 17 | // otherwise, we would guess runtime environment as local development 18 | // eslint-disable-next-line turbo/no-undeclared-env-vars 19 | return localHost; 20 | } 21 | } else { 22 | // Native is similar as above 23 | if (profile === 'production') { 24 | return `https://.vercel.app`; 25 | } else { 26 | return localHost; 27 | } 28 | } 29 | 30 | // If you want to divide into more stages, try the values below! 31 | // Vercel: NEXT_PUBLIC_VERCEL_ENV = production, preview, development 32 | // EAS: EAS_BUILD_PROFILE = production, preview, development (It's up to you how to name it in eas.json file) 33 | }; 34 | -------------------------------------------------------------------------------- /packages/ui/src/template/chat/shared.tsx: -------------------------------------------------------------------------------- 1 | import { GetApiHello200 } from 'api'; 2 | import React, { ReactNode } from 'react'; 3 | import { Text, YStack } from 'tamagui'; 4 | 5 | type SharedChatTemplateProps = { 6 | children?: ReactNode; 7 | } & Partial; 8 | 9 | export type SharedChatIndexTemplateProps = { 10 | children?: ReactNode; 11 | }; 12 | 13 | export function SharedChatTemplate({ 14 | message, 15 | createdAt, 16 | children, 17 | }: SharedChatTemplateProps) { 18 | return ( 19 | <> 20 | {children} 21 | 31 | 37 | 🌱 Fresh data 🌱 38 | 39 | 40 | Response time:{' '} 41 | 42 | {new Date(createdAt ?? '').toLocaleTimeString()} 43 | 44 | 45 | 46 | Message:{' '} 47 | 48 | {message} 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /packages/ui/src/template/myPage/index/topTabHeader.tsx: -------------------------------------------------------------------------------- 1 | import { MapPin } from '@tamagui/lucide-icons'; 2 | import React from 'react'; 3 | import { Text, Stack, XStack, YStack } from 'tamagui'; 4 | 5 | import { Image } from '../../../block/image'; 6 | 7 | const PROFILE_IMAGE_SIZE = 54; 8 | export function MyPageTopTabHeaderTemplate() { 9 | return ( 10 | 11 | 12 | 13 | 29 | 30 | 31 | 32 | mononoke.choi 33 | 34 | 35 | 41 | 42 | seoul 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /apps/native/app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from 'expo-router'; 2 | import { useTheme } from 'tamagui'; 3 | import { 4 | ACTIVE_TINT_COLOR, 5 | HEADER_EDGE_SPACE_TOKEN, 6 | MyPageHeaderRight, 7 | getChatTabBarIconOptions, 8 | getCrewTabBarIconOptions, 9 | getHomeTabIconOptions, 10 | getMyPageTabBarIconOptions, 11 | } from 'ui'; 12 | 13 | export const unstable_settings = { 14 | initialRouteName: 'home', 15 | }; 16 | 17 | export default function TabLayout() { 18 | const theme = useTheme(); 19 | 20 | return ( 21 | 36 | 42 | 48 | 54 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "sideEffects": [ 7 | "*.css" 8 | ], 9 | "license": "MIT", 10 | "scripts": { 11 | "lint": "eslint . --fix", 12 | "clean": "rm -rf dist node_modules", 13 | "generate:component": "turbo gen react-component" 14 | }, 15 | "devDependencies": { 16 | "@types/lodash": "^4.14.195", 17 | "@types/ms": "^0.7.31", 18 | "@types/react": "18.2.15", 19 | "@types/react-dom": "18.2.7", 20 | "@types/react-native-swiper": "^1.5.12", 21 | "client-only": "latest", 22 | "eslint-config-custom": "*", 23 | "react": "18.2.0", 24 | "react-native": "0.72.3", 25 | "server-only": "latest", 26 | "tsconfig": "*", 27 | "typescript": "^5.1.3" 28 | }, 29 | "dependencies": { 30 | "@react-native-community/netinfo": "9.3.10", 31 | "@shopify/flash-list": "1.4.3", 32 | "@tamagui/animations-react-native": "^1.41.1", 33 | "@tamagui/babel-plugin": "^1.41.1", 34 | "@tamagui/config": "^1.41.1", 35 | "@tamagui/font-inter": "^1.41.1", 36 | "@tamagui/lucide-icons": "^1.41.1", 37 | "@tamagui/themes": "^1.41.1", 38 | "@tanstack/react-virtual": "3.0.0-beta.54", 39 | "dayjs": "^1.11.9", 40 | "embla-carousel-react": "^8.0.0-rc11", 41 | "lodash": "^4.17.21", 42 | "ms": "^2.1.3", 43 | "react-hook-form": "^7.45.1", 44 | "react-native-mmkv": "^2.10.1", 45 | "react-native-reanimated": "^3.3.0", 46 | "react-native-svg": "13.9.0", 47 | "react-native-swiper": "^1.6.0", 48 | "recoil": "^0.7.7", 49 | "recoil-sync": "^0.2.0", 50 | "solito": "^4.0.0-alpha.5", 51 | "tamagui": "^1.41.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/native/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "android": { 4 | "adaptiveIcon": { 5 | "backgroundColor": "#4C7AED", 6 | "foregroundImage": "./assets/images/adaptive-icon.png" 7 | }, 8 | "package": "com.bandi" 9 | }, 10 | "androidStatusBar": { 11 | "backgroundColor": "#ffffff", 12 | "barStyle": "dark-content", 13 | "hidden": false 14 | }, 15 | "assetBundlePatterns": ["**/*"], 16 | "backgroundColor": "#ffffff", 17 | "description": "New RN starter kit", 18 | "experiments": { 19 | "tsconfigPaths": true, 20 | "typedRoutes": true 21 | }, 22 | "icon": "./assets/images/icon.png", 23 | "ios": { 24 | "bundleIdentifier": "com.bandi", 25 | "infoPlist": { 26 | "RCTAsyncStorageExcludeFromBackup": true, 27 | "UIBackgroundModes": ["remote-notification"] 28 | }, 29 | "supportsTablet": true 30 | }, 31 | "name": "Bandi", 32 | "orientation": "portrait", 33 | "plugins": [ 34 | "expo-router", 35 | "expo-notifications", 36 | [ 37 | "expo-router", 38 | { 39 | "asyncRoutes": "development", 40 | "origin": ".vercel.app" 41 | } 42 | ] 43 | ], 44 | "primaryColor": "#ffffff", 45 | "privacy": "public", 46 | "scheme": "bandi", 47 | "slug": "bandi", 48 | "splash": { 49 | "backgroundColor": "#4C7AED", 50 | "image": "./assets/images/splash.png", 51 | "resizeMode": "contain" 52 | }, 53 | "userInterfaceStyle": "automatic", 54 | "version": "1.0.0", 55 | "web": { 56 | "bundler": "metro", 57 | "favicon": "./assets/images/favicon.png", 58 | "output": "static" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/ui/src/layout/tabBar/shared.tsx: -------------------------------------------------------------------------------- 1 | import type { IconProps } from '@tamagui/helpers-icon'; 2 | import { Home, MessageCircle, User, Users } from '@tamagui/lucide-icons'; 3 | import { Tabs } from 'expo-router'; 4 | import React, { NamedExoticComponent } from 'react'; 5 | import { useTheme } from 'tamagui'; 6 | 7 | import { ACTIVE_TINT_COLOR } from '../../config/constant'; 8 | 9 | type TabBarIconReturn = Required< 10 | Pick< 11 | NonNullable[0]['options']>, 12 | 'tabBarIcon' | 'title' 13 | > 14 | > & { 15 | href?: string; 16 | activation?: (segment: string) => boolean; 17 | }; 18 | export type TabBarIcon = () => TabBarIconReturn; 19 | 20 | function getTabBarIcon(SVG: NamedExoticComponent) { 21 | return function TabBarIcon({ 22 | size, 23 | focused, 24 | }: Omit[0], 'color'>) { 25 | const theme = useTheme(); 26 | 27 | return ( 28 | 33 | ); 34 | }; 35 | } 36 | 37 | export const getSharedHomeTabIconOptions: TabBarIcon = () => { 38 | return { 39 | tabBarIcon: getTabBarIcon(Home), 40 | title: 'Home', 41 | }; 42 | }; 43 | 44 | export const getSharedCrewTabBarIconOptions: TabBarIcon = () => { 45 | return { 46 | tabBarIcon: getTabBarIcon(Users), 47 | title: 'Crew', 48 | }; 49 | }; 50 | 51 | export const getSharedChatTabBarIconOptions: TabBarIcon = () => { 52 | return { 53 | tabBarIcon: getTabBarIcon(MessageCircle), 54 | title: 'Chat', 55 | }; 56 | }; 57 | 58 | export const getSharedMyPageTabBarIconOptions: TabBarIcon = () => { 59 | return { 60 | tabBarIcon: getTabBarIcon(User), 61 | title: 'MyPage', 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /packages/ui/src/block/windowing/index.web.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'client-only'; 4 | import { useWindowVirtualizer } from '@tanstack/react-virtual'; 5 | import { map } from 'lodash'; 6 | import React, { Fragment } from 'react'; 7 | import { getTokens, getVariable } from 'tamagui'; 8 | import { WindowingProps } from './shared'; 9 | import { Variable } from '@tamagui/web'; 10 | 11 | export function Windowing(props: WindowingProps) { 12 | const rowVirtualizer = useWindowVirtualizer({ 13 | ...props.web, 14 | paddingStart: (getTokens({ prefixed: true }).size['$4.5'] as Variable).val, 15 | }); 16 | 17 | return map(rowVirtualizer.getVirtualItems(), virtualRow => { 18 | const isLastItem = virtualRow.index === props.web.count - 1; 19 | 20 | if (!props.data) { 21 | return null; 22 | } 23 | 24 | return ( 25 | 26 | {props.web.renderItem({ 27 | ...virtualRow, 28 | ...(props.data && { item: props.data[virtualRow.index] }), 29 | isLastItem, 30 | })} 31 | {isLastItem && ( 32 |
48 | )} 49 | 50 | ); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /packages/ui/turbo/generators/config.ts: -------------------------------------------------------------------------------- 1 | import { PlopTypes } from '@turbo/gen'; 2 | 3 | // Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation 4 | 5 | export default function generator(plop: PlopTypes.NodePlopAPI): void { 6 | // A simple generator to add a new React component to the internal UI library 7 | plop.setGenerator('react-component', { 8 | actions(data) { 9 | const actions = []; 10 | 11 | if (!data) { 12 | throw 'something went wrong'; 13 | } 14 | 15 | if (data.componentType === 'server') { 16 | actions.push({ 17 | path: 'src/{{camelCase name}}.tsx', 18 | templateFile: 'templates/serverComponent.hbs', 19 | type: 'add', 20 | }); 21 | actions.push({ 22 | path: 'index.tsx', 23 | pattern: /(\/\/ server component exports)/g, 24 | template: 'export * from "./src/{{camelCase name}}";', 25 | type: 'append', 26 | }); 27 | } else { 28 | actions.push({ 29 | path: 'src/{{camelCase name}}.tsx', 30 | templateFile: 'templates/clientComponent.hbs', 31 | type: 'add', 32 | }); 33 | actions.push({ 34 | path: 'index.tsx', 35 | pattern: /(\/\/ client component exports)/g, 36 | template: 'export * from "./src/{{camelCase name}}";', 37 | type: 'append', 38 | }); 39 | } 40 | 41 | return actions; 42 | }, 43 | description: 'Adds a new react component', 44 | prompts: [ 45 | { 46 | message: 'What is the name of the component?', 47 | name: 'name', 48 | type: 'input', 49 | }, 50 | { 51 | choices: ['server', 'client'], 52 | name: 'componentType', 53 | type: 'list', 54 | }, 55 | ], 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /apps/native/README.md: -------------------------------------------------------------------------------- 1 | # [Native] Available commands 2 | 3 | - `dev`: Run start script 4 | - `start`: Starts the development server with ios simulator 5 | - `prebuild`: Generate native source code 6 | - `start:all`: Starts the development server with ios, android simulator 7 | - `ios:build:local`: Build ios app on local environment 8 | - `ios:build:eas`: Build ios app on EAS 9 | - `android:build:local`: Build android app on local environment 10 | - `android:build:eas`: Build android app on EAS 11 | - `eas:resignADHOC`: Codesign an existing .ipa for iOS to a new ad hoc provisioning profile. 12 | - `detox:android:debug:build`: Build android development environment e2e app 13 | - `detox:android:debug:test`: Run e2e android development environment 14 | - `detox:android:release:build`: Build android production environment e2e app 15 | - `detox:android:release:test`: Run e2e android production environment 16 | - `detox:ios:debug:build`: Build ios development environment e2e app 17 | - `detox:ios:debug:test`: Run e2e ios development environment 18 | - `detox:ios:release:build`: Build ios production environment e2e app 19 | - `detox:ios:release:test`: Run e2e ios production environment 20 | - `lint`: Lints the code using ESLint 21 | - `clean`: Clear build outputs 22 | 23 | # Troubleshooting 24 | 25 |
26 | Only Android is failed to request localhost 27 |

28 | 29 | Use ADB reverse to bind an emulator port to a port on your computer. 30 | 31 | ```shell 32 | adb reverse tcp:3000 tcp:3000 33 | ``` 34 | 35 |

36 |
37 | 38 |
39 | The transition movement of post thumbnail is unnatural on native 40 |

41 | 42 | We are using Shared Element Transitions powered by React Native Reanimated. 43 | and It is an experimental feature, not recommended for production use yet. 44 | we're just trying it out! 45 | 46 |

47 |
48 | -------------------------------------------------------------------------------- /apps/native/e2e/utils/openApp.ts: -------------------------------------------------------------------------------- 1 | import { AppJSONConfig } from '@expo/config/build/Config.types'; 2 | import appJSON from '../../app.json'; 3 | import { sleep } from './common'; 4 | import { resolveConfig } from 'detox/internals'; 5 | 6 | async function openApp() { 7 | const { configurationName } = await resolveConfig(); 8 | const [platform, target] = configurationName.split('.') ?? ['', '']; 9 | 10 | if (target === 'debug') { 11 | return await openAppForDebugBuild(platform); 12 | } else { 13 | return await device.launchApp({ 14 | newInstance: true, 15 | }); 16 | } 17 | } 18 | 19 | async function openAppForDebugBuild(platform: string) { 20 | const deepLinkUrl = process.env.EXPO_USE_UPDATES 21 | ? // Testing latest published EAS update for the test_debug channel 22 | getDeepLinkUrl(getLatestUpdateUrl()) 23 | : // Local testing with packager 24 | getDeepLinkUrl(getDevLauncherPackagerUrl(platform)); 25 | 26 | if (platform === 'ios') { 27 | await device.launchApp({ 28 | newInstance: true, 29 | }); 30 | sleep(3000); 31 | await device.openURL({ 32 | url: deepLinkUrl, 33 | }); 34 | } else { 35 | await device.launchApp({ 36 | newInstance: true, 37 | url: deepLinkUrl, 38 | }); 39 | } 40 | 41 | await sleep(3000); 42 | } 43 | 44 | const getDeepLinkUrl = (url: string | number | boolean) => 45 | `${appJSON.expo.name}://expo-development-client/?url=${encodeURIComponent( 46 | url, 47 | )}`; 48 | 49 | const getDevLauncherPackagerUrl = (platform: string) => 50 | `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`; 51 | 52 | const getLatestUpdateUrl = () => 53 | `https://u.expo.dev/${getAppId()}?channel-name=test_debug&disableOnboarding=1`; 54 | 55 | const getAppId = () => 56 | (appJSON as AppJSONConfig)?.expo?.extra?.eas?.projectId ?? ''; 57 | 58 | export { openApp }; 59 | -------------------------------------------------------------------------------- /packages/ui/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars,import/no-namespace 3 | import * as React from 'react'; 4 | 5 | export * from './src/tamagui.config'; 6 | export * from './src/config/constant'; 7 | 8 | // server component exports 9 | 10 | // client component exports 11 | export * from './src/button'; 12 | 13 | // block 14 | export * from './src/block/carousel/index'; 15 | export * from './src/block/carousel/shared'; 16 | export * from './src/block/image/index'; 17 | export * from './src/block/image/shared'; 18 | export * from './src/block/windowing/index'; 19 | export * from './src/block/windowing/shared'; 20 | export * from './src/block/appHeader/index'; 21 | export * from './src/block/appHeader/shared'; 22 | export * from './src/block/bottomNavigation.web'; 23 | export * from './src/block/externalLink'; 24 | 25 | // HOC 26 | export * from './src/HOC/withRecoilSync'; 27 | 28 | // hook 29 | export * from './src/hook/useAppState'; 30 | export * from './src/hook/useOnlineManager'; 31 | export * from './src/hook/useRefetchOnFocus'; 32 | 33 | // layout 34 | export * from './src/layout/appHeader/home'; 35 | export * from './src/layout/appHeader/myPage'; 36 | export * from './src/layout/tabBar/icons'; 37 | export * from './src/layout/tabBar/shared'; 38 | export * from './src/layout/toptab/index.web'; 39 | 40 | // store 41 | export * from './src/store/mmkv'; 42 | export * from './src/store/userInfoState'; 43 | 44 | // template 45 | export * from './src/template/chat/index'; 46 | export * from './src/template/chat/shared'; 47 | export * from './src/template/home/index'; 48 | export * from './src/template/myPage/index/index'; 49 | export * from './src/template/myPage/index/topTabHeader'; 50 | export * from './src/template/myPage/setting'; 51 | export * from './src/template/post/[postId]/index'; 52 | export * from './src/template/post/[postId]/shared'; 53 | export * from './src/template/notificationList'; 54 | export * from './src/template/wip'; 55 | -------------------------------------------------------------------------------- /apps/native/app/+html.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollViewStyleReset } from 'expo-router/html'; 2 | 3 | // This file is web-only and used to configure the root HTML for every 4 | // web page during static rendering. 5 | // The contents of this function only run in Node.js environments and 6 | // do not have access to the DOM or browser APIs. 7 | export default function Root({ children }: { children: React.ReactNode }) { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | {/* 15 | This viewport disables scaling which makes the mobile website act more like a native app. 16 | However this does reduce built-in accessibility. If you want to enable scaling, use this instead: 17 | 18 | */} 19 | 23 | {/* 24 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 25 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 26 | */} 27 | 28 | 29 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 30 |