$30 off During Our Annual Pro Sale. View Details »

React Mid Game

React Mid Game

Radoslav Stankov

October 25, 2023
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. React Native
    Midgame

    View Slide

  2. !

    View Slide

  3. Radoslav Stankov
    @rstankov rstankov.com

    View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. " Side project

    View Slide

  8. View Slide

  9. - implement a mobile app #
    - one developer (me) $
    - support iOS % / Android &
    - ship it as fast as possible '

    View Slide

  10. Going native (swift/kotlin) wasn't an option

    View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. ( backed by Google
    ) uses Dart
    * cross platform
    + compiles to native machine code
    , custom components
    - battery included

    View Slide

  16. View Slide

  17. . backed by Facebook
    / uses Javascript (or TypeScript)
    0 uses React
    1 cross platform
    2 minimal
    3 native and JS bridge
    4 native components

    View Slide

  18. React Native in 2019 was 5
    React Native in 2021 was 6
    React Native in 2022 was 7
    React Native in 2023 still is 7

    View Slide

  19. View Slide

  20. React Native
    is fast enough
    has a rich ecosystem
    Flutter
    is faster
    has more batteries included
    I have done 3 React Native apps previously
    I know React/TypeScript/GraphQL extremely well
    I don't know Dart
    I don't know Flutter

    View Slide

  21. source: https://refactoring.fm/p/how-to-choose-technology

    View Slide

  22. View Slide

  23. View Slide

  24. View Slide

  25. Expo in 2020 was ... 8
    Expo in 2020 was ... 9
    Expo in 2022 was ... 7
    Expo in 2023 is ...:

    View Slide

  26. View Slide

  27. # Tech Stack

    View Slide

  28. View Slide

  29. Architecture

    View Slide

  30. View Slide

  31. View Slide

  32. View Slide

  33. 0 Make common operations easy
    4 Static type-safety
    1 Isolate dependencies
    + Extensibility and reusability

    View Slide

  34. 0 Make common operations easy
    4 Static type-safety
    1 Isolate dependencies
    + Extensibility and reusability

    View Slide

  35. 2
    1 3
    Support Components Screens

    View Slide

  36. Support
    Components
    Screens

    View Slide

  37. 1) Support 2) Components 3) Screens

    View Slide

  38. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  39. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx
    1 Support
    Components
    Pages
    2
    3

    View Slide

  40. View Slide

  41. 3
    Screens

    View Slide

  42. View Slide

  43. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  44. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  45. {
    "expo": {
    "name": "Angry Building",
    "version": "1.48.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
    "image": "./assets/splash.png",
    "resizeMode": "contain",
    "backgroundColor": "#252629"
    }
    },
    // ...
    }

    View Slide

  46. View Slide

  47. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  48. View Slide

  49. @react-navigation/native

    View Slide

  50. View Slide

  51. View Slide

  52. ... I still don't use it, because it was released fairly recently ;
    ... however the way the app is setup, transition should be easy <

    View Slide

  53. Overlay

    View Slide

  54. Overlay
    Tab Navigation

    View Slide

  55. Overlay
    Tab Navigation
    Stack Navigation
    Title

    View Slide

  56. Overlay
    Tab Navigation
    Stack Navigation
    Title
    Screen

    View Slide

  57. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  58. import screens from 'src/screens';
    import { NavigationContainer } from '@react-navigation/native';
    import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
    import { createStackNavigator } from '@react-navigation/stack';
    Sentry.init();
    const RootStack = createStackNavigator();
    export default function App() {
    return (















    View Slide

  59. export default function App() {
    return (
















    );
    }

    View Slide

  60. const Tab = createBottomTabNavigator();
    function TabsScreen() {
    return (

    name="buildingTab"
    component={Screen}
    initialParams={{ initialRoute: screens.home.name }}
    options={/* ... */}
    />
    name="apartmentTab"
    component={Screen}
    initialParams={{ initialRoute: screens.apartment.name }}
    options={/* ... */}
    />
    name="bulletinBoardTab"
    component={Screen}
    initialParams={{ initialRoute: screens.bulletinBoard.name }}
    options={/* ... */}
    />
    name="cashReserveTab"
    component={Screen}
    initialParams={{ initialRoute: screens.cashReserve.name }}

    View Slide

  61. />
    name="apartmentTab"
    component={Screen}
    initialParams={{ initialRoute: screens.apartment.name }}
    options={/* ... */}
    />
    name="bulletinBoardTab"
    component={Screen}
    initialParams={{ initialRoute: screens.bulletinBoard.name }}
    options={/* ... */}
    />
    name="cashReserveTab"
    component={Screen}
    initialParams={{ initialRoute: screens.cashReserve.name }}
    options={/* ... */}
    />
    name="issuesTab"
    component={Screen}
    initialParams={{ initialRoute: screens.issues.name }}
    options={/* ... */}
    />

    );
    }

    View Slide

  62. const ScreenStack = createStackNavigator();
    function Screen(props) {
    const initialRoute = props.route.params.initialRoute;
    return (




















    );
    }

    View Slide

  63. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  64. import apartment from './apartment';
    import home from './home';
    // ...
    export default {
    apartment,
    home,
    // ...
    };

    View Slide

  65. Screen States
    Layout

    View Slide

  66. Loading State
    Screen States
    Layout

    View Slide

  67. Loading State
    Error State
    Screen States
    Layout

    View Slide

  68. Loading State
    Not Found Error
    Server Error
    Authorization
    Error
    Authentication
    Error
    Error State
    Screen States
    Layout

    View Slide

  69. Loading State
    Not Found Error
    Server Error
    Authorization
    Error
    Authentication
    Error
    Error State
    Loaded State
    Screen States
    Layout

    View Slide

  70. Loading State
    Not Found Error
    Server Error
    Authorization
    Error
    Authentication
    Error
    Error State
    Loaded State
    render
    Screen States
    Layout

    View Slide

  71. interface IOptions {
    name: string;
    query?: DocumentNode;
    queryVariables?: object | ((params: P) => object);
    queryRefreshOnShow?: boolean;
    component: IComponent;
    type?: keyof typeof layouts;
    headerTitle?: ITranslation;
    background: IBackground;
    }
    interface IScreen {
    name: string;
    options: StackNavigationOptions;
    component: any;
    }
    export default function createScreen(options: IOptions): IScreen
    // ...
    }

    View Slide

  72. screens/[name]/index.ts (createScreen) -> screens/index.ts -> app.ts

    View Slide

  73. export default createScreen({
    name: 'home',
    type: 'plain',
    query: QUERY,
    background: 'black',
    component({ data, fetchMore, refetch }) {
    usePushNotificationsRegister();
    usePushNotificationHandle(data.viewer);
    return (


    refetch={refetch}
    fetchMore={fetchMore}
    building={data.building}
    />


    );
    },
    });

    View Slide

  74. ▾ src/
    ▾ screens/
    ▾ home/
    ▸ Status/
    background.png
    BuildingApartmentsList.tsx
    Header.tsx
    index.tsx
    Query.ts
    SelectEntrancePicker.tsx

    View Slide

  75. http://graphql.org/

    View Slide

  76. View Slide

  77. graphql-codegen --config codegen.yml

    View Slide

  78. components/ApartmentStatus/Fragment.tsx
    import { gql } from '@apollo/client';
    export default gql`
    fragment IApartmentStatusFragment on Apartment {
    id
    number
    name
    overdueAmount
    }
    `;

    View Slide

  79. types/grahpql.ts
    export type IApartmentStatusFragment = {
    __typename: 'Apartment';
    id: string;
    number?: string | null;
    name?: string | null;
    overdueAmount: number;
    };

    View Slide

  80. import { IApartmentStatusFragment } from '~/types/graphql';

    View Slide

  81. import { gql } from '@apollo/client';
    import IHomeScreenStatusFragment from './Status/Fragment';
    import IApartmentStatusFragment from 'src/components/ApartmentStatus/Fragment';
    export default gql`
    query IHomeScreen($cursor: String) {
    building {
    id
    name
    apartments(first: 100, after: $cursor) {
    nodes {
    id
    name
    floor
    isViewerSelected
    ...IApartmentStatusFragment
    }
    pageInfo {
    hasNextPage
    endCursor
    }
    }
    ...IHomeScreenStatusFragment
    }
    }
    ${IHomeScreenStatusFragment}
    ${IApartmentStatusFragment}
    `;
    screens/home/Query.ts

    View Slide

  82. Screen
    Component Component
    Component
    Component
    Component
    Component

    View Slide

  83. Query
    Fragment Fragment
    Fragment
    Fragment
    Fragment
    Fragment

    View Slide

  84. View Slide

  85. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  86. import routes from '~/routes';

    routes.back;
    routes.home;
    routes.bulletinBoardPostUpdate(post);

    View Slide


  87. View Slide

  88. import { useNavigation } from '@react-navigation/native';
    import screens from 'src/screens';
    type IScreenName = keyof typeof screens;
    export type IRoute =
    | { transition: 'goBack' }
    | { screen: IScreenName; params?: IParams; root?: boolean }
    | {
    screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab';
    params?: IParams;
    root: true;
    };
    const routes = {
    back: { transition: 'goBack' } as IRoute,
    home: { root: true, screen: 'buildingTab' } as IRoute,
    // ...
    bulletinBoardPostUpdate(post: { id: string }) {
    return {
    screen: 'bulletinBoardPostUpdate',
    params: { post },
    } as IRoute;
    },
    };

    View Slide

  89. import { useNavigation } from '@react-navigation/native';
    import screens from 'src/screens';
    type IScreenName = keyof typeof screens;
    export type IRoute =
    | { transition: 'goBack' }
    | { screen: IScreenName; params?: IParams; root?: boolean }
    | {
    screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab';
    params?: IParams;
    root: true;
    };
    const routes = {
    back: { transition: 'goBack' } as IRoute,
    home: { root: true, screen: 'buildingTab' } as IRoute,
    // ...
    bulletinBoardPostUpdate(post: { id: string }) {
    return {
    screen: 'bulletinBoardPostUpdate',
    params: { post },
    } as IRoute;
    },
    };

    View Slide

  90. import { useNavigation } from '@react-navigation/native';
    import screens from 'src/screens';
    type IScreenName = keyof typeof screens;
    export type IRoute =
    | { transition: 'goBack' }
    | { screen: IScreenName; params?: IParams; root?: boolean }
    | {
    screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab';
    params?: IParams;
    root: true;
    };
    const routes = {
    back: { transition: 'goBack' } as IRoute,
    home: { root: true, screen: 'buildingTab' } as IRoute,
    // ...
    bulletinBoardPostUpdate(post: { id: string }) {
    return {
    screen: 'bulletinBoardPostUpdate',
    params: { post },
    } as IRoute;
    },
    };

    View Slide

  91. import { useNavigation } from '@react-navigation/native';
    import screens from 'src/screens';
    type IScreenName = keyof typeof screens;
    export type IRoute =
    | { transition: 'goBack' }
    | { screen: IScreenName; params?: IParams; root?: boolean }
    | {
    screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab';
    params?: IParams;
    root: true;
    };
    const routes = {
    back: { transition: 'goBack' } as IRoute,
    home: { root: true, screen: 'buildingTab' } as IRoute,
    // ...
    bulletinBoardPostUpdate(post: { id: string }) {
    return {
    screen: 'bulletinBoardPostUpdate',
    params: { post },
    } as IRoute;
    },
    };

    View Slide

  92. | { screen: IScreenName; params?: IParams; root?: boolean }
    | {
    screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab';
    params?: IParams;
    root: true;
    };
    const routes = {
    back: { transition: 'goBack' } as IRoute,
    home: { root: true, screen: 'buildingTab' } as IRoute,
    // ...
    bulletinBoardPostUpdate(post: { id: string }) {
    return {
    screen: 'bulletinBoardPostUpdate',
    params: { post },
    } as IRoute;
    },
    };
    export default routes;
    export interface INavigation {
    navigate: (screen: string, params?: IParams) => void;
    goBack: VoidFunction;
    }

    View Slide

  93. | { screen: IScreenName; params?: IParams; root?: boolean }
    | {
    screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab';
    params?: IParams;
    root: true;
    };
    const routes = {
    back: { transition: 'goBack' } as IRoute,
    home: { root: true, screen: 'buildingTab' } as IRoute,
    // ...
    bulletinBoardPostUpdate(post: { id: string }) {
    return {
    screen: 'bulletinBoardPostUpdate',
    params: { post },
    } as IRoute;
    },
    };
    export default routes;
    export interface INavigation {
    navigate: (screen: string, params?: IParams) => void;
    goBack: VoidFunction;
    }

    View Slide

  94. import { useNavigation } from '@react-navigation/native';
    import screens from 'src/screens';
    type IScreenName = keyof typeof screens;
    export type IRoute =
    | { transition: 'goBack' }
    | { screen: IScreenName; params?: IParams; root?: boolean }
    | {
    screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab';
    params?: IParams;
    root: true;
    };
    const routes = {
    back: { transition: 'goBack' } as IRoute,
    home: { root: true, screen: 'buildingTab' } as IRoute,
    // ...
    bulletinBoardPostUpdate(post: { id: string }) {
    return {
    screen: 'bulletinBoardPostUpdate',
    params: { post },
    } as IRoute;
    },
    };

    View Slide

  95. export interface INavigation {
    navigate: (screen: string, params?: IParams) => void;
    goBack: VoidFunction;
    }
    export function navigate(navigation: INavigation, to: IRoute) {
    if ('transition' in to) {
    navigation.goBack();
    } else if (to.root) {
    navigation.navigate('Main', to);
    } else {
    navigation.navigate(to.screen, to.params);
    }
    }
    export function useNavigate() {
    const navigation: any = useNavigation();
    const fn = React.useCallback(
    (to: IRoute) => {
    navigate(navigation, to);
    },
    [navigation],
    );
    return fn;
    }

    View Slide

  96. import routes, { useNavigate } from '~/routes';
    export function MyComponent() {
    const navigate = useNavigate();
    const onPress = () => {
    navigate(routes.home);
    }
    return (

    Visit product

    );
    }

    View Slide

  97. 2
    Components

    View Slide

  98. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  99. View Slide

  100. If component is used in more than 1 screens
    then goes to src/components

    View Slide

  101. = generic components
    > domain components

    View Slide

  102. [Domain][Name][Type]

    View Slide

  103. ? Component as directory
    components/
    PublicComponent/
    PrivateSubComponent/
    Fragment.ts
    Mutation.ts
    index.ts
    styles.ts
    utils.ts

    View Slide

  104. ? Component as directory
    components/
    PublicComponent/
    PrivateSubComponent/
    Fragment.ts
    Mutation.ts
    index.ts
    styles.ts
    utils.ts

    View Slide

  105. Text

    View Slide

  106. to={routes.profile(profile)} />

    View Slide

  107. to={routes.profile(profile)} />
    onPress={onClickReturnsPromise} />

    View Slide

  108. to={routes.profile(profile)} />
    onPress={onClickReturnsPromise} />
    onPress={onClickReturnsPromise}
    confirm="Are you sure?"
    requireLogin={true} />

    View Slide

  109. to={routes.profile(profile)} />
    onPress={onClickReturnsPromise} />
    onPress={onClickReturnsPromise}
    confirm="Are you sure?"
    requireLogin={true} />
    mutation={MUTATION}
    input={input}
    onMutate={onMutate} />

    View Slide





  110. View Slide

  111. View Slide

  112. {...}
    {...}

    View Slide

  113. {...}
    {...}

    View Slide

  114. 1
    Support

    View Slide

  115. View Slide

  116. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  117. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  118. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  119. import { format } from 'date-fns';
    export function formatDateTime(date: string) {
    return format(date, 'H:mm A · MMM D, YYYY');
    }
    utils/date.ts

    View Slide

  120. moment date.ts Component Page

    View Slide

  121. date.ts Component Page
    date-fns

    View Slide

  122. utils/
    external/
    Intercom/
    OneSignal/
    Segment/
    Sentry/

    View Slide

  123. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  124. useGraphQLFragment()
    useViewier()
    useIsLoggedIn()

    View Slide

  125. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  126. @ A B

    View Slide

  127. i18next

    View Slide

  128. ▾ src/
    ▾ translations/
    bg.json
    en.json
    es.json
    index.ts

    View Slide

  129. import i18n from 'i18next';
    import captureError from 'src/utils/captureError';
    import { isProduction } from 'src/config';
    import bg from './bg.json';
    import en from './en.json';
    import es from './es.json';
    // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio
    i18n.init({
    resources: {
    bg: {
    translation: bg,
    },
    en: {
    translation: en,
    },
    es: {
    translation: es,
    },
    },
    lng: 'bg',
    saveMissing: true,
    missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => {
    if (isProduction) {
    captureError(`Key not found t('${key}')`);

    View Slide

  130. import i18n from 'i18next';
    import captureError from 'src/utils/captureError';
    import { isProduction } from 'src/config';
    import bg from './bg.json';
    import en from './en.json';
    import es from './es.json';
    // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio
    i18n.init({
    resources: {
    bg: {
    translation: bg,
    },
    en: {
    translation: en,
    },
    es: {
    translation: es,
    },
    },
    lng: 'bg',
    saveMissing: true,
    missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => {
    if (isProduction) {
    captureError(`Key not found t('${key}')`);

    View Slide

  131. import i18n from 'i18next';
    import captureError from 'src/utils/captureError';
    import { isProduction } from 'src/config';
    import bg from './bg.json';
    import en from './en.json';
    import es from './es.json';
    // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio
    i18n.init({
    resources: {
    bg: {
    translation: bg,
    },
    en: {
    translation: en,
    },
    es: {
    translation: es,
    },
    },
    lng: 'bg',
    saveMissing: true,
    missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => {
    if (isProduction) {
    captureError(`Key not found t('${key}')`);

    View Slide

  132. // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio
    i18n.init({
    resources: {
    bg: {
    translation: bg,
    },
    en: {
    translation: en,
    },
    es: {
    translation: es,
    },
    },
    lng: 'bg',
    saveMissing: true,
    missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => {
    if (isProduction) {
    captureError(`Key not found t('${key}')`);
    return key;
    }
    throw new Error(`Key not found t('${key}')`);
    },
    compatibilityJSON: 'v3',
    });

    View Slide

  133. // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio
    i18n.init({
    resources: {
    bg: {
    translation: bg,
    },
    en: {
    translation: en,
    },
    es: {
    translation: es,
    },
    },
    lng: 'bg',
    saveMissing: true,
    missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => {
    if (isProduction) {
    captureError(`Key not found t('${key}')`);
    return key;
    }
    throw new Error(`Key not found t('${key}')`);
    },
    compatibilityJSON: 'v3',
    });

    View Slide

  134. // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio
    i18n.init({
    resources: {
    bg: {
    translation: bg,
    },
    en: {
    translation: en,
    },
    es: {
    translation: es,
    },
    },
    lng: 'bg',
    saveMissing: true,
    missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => {
    if (isProduction) {
    captureError(`Key not found t('${key}')`);
    return key;
    }
    throw new Error(`Key not found t('${key}')`);
    },
    compatibilityJSON: 'v3',
    });

    View Slide

  135. captureError(`Key not found t('${key}')`);
    return key;
    }
    throw new Error(`Key not found t('${key}')`);
    },
    compatibilityJSON: 'v3',
    });
    export type ITranslation = keyof typeof bg;
    export type ILanguage = 'bg' | 'en' | 'es';
    export default i18n.t as (key: keyof typeof bg, interpolations?: any) => string;
    export function changeLanguage(newLanguage: ILanguage) {
    if (i18n.language === newLanguage) {
    return;
    }
    return i18n.changeLanguage(newLanguage);
    }

    View Slide

  136. export type ITranslation = keyof typeof bg;
    export type ILanguage = 'bg' | 'en' | 'es';
    export default i18n.t as (key: keyof typeof bg, interpolations?: any) => string;
    export function changeLanguage(newLanguage: ILanguage) {
    if (i18n.language === newLanguage) {
    return;
    }
    return i18n.changeLanguage(newLanguage);
    }

    View Slide

  137. ▾ src/
    ▸ components/
    ▸ hooks/
    ▸ screens/
    ▸ styles/
    ▸ translations/
    ▸ types/
    ▸ utils/
    config.ts
    routes.ts
    app.json
    App.tsx

    View Slide

  138. import * as React from 'react';
    import { View, StyleSheet } from 'react-native';
    import s from 'src/styles';
    import Logo from 'src/components/Logo';
    export default React.memo(function Ribbon() {
    return (
    <>






    >
    );
    });
    const styles = StyleSheet.create({
    container: {
    backgroundColor: s.colors.black,
    },
    view: {
    marginTop: 10,
    borderTopLeftRadius: s.radius,
    borderTopRightRadius: s.radius,
    width: '100%',

    View Slide

  139. import * as React from 'react';
    import { View, StyleSheet } from 'react-native';
    import s from 'src/styles';
    import Logo from 'src/components/Logo';
    export default React.memo(function Ribbon() {
    return (
    <>






    >
    );
    });
    const styles = StyleSheet.create({
    container: {
    backgroundColor: s.colors.black,
    },
    view: {
    marginTop: 10,
    borderTopLeftRadius: s.radius,
    borderTopRightRadius: s.radius,
    width: '100%',

    View Slide

  140. import * as React from 'react';
    import { View, StyleSheet } from 'react-native';
    import s from 'src/styles';
    import Logo from 'src/components/Logo';
    export default React.memo(function Ribbon() {
    return (
    <>






    >
    );
    });
    const styles = StyleSheet.create({
    container: {
    backgroundColor: s.colors.black,
    },
    view: {
    marginTop: 10,
    borderTopLeftRadius: s.radius,
    borderTopRightRadius: s.radius,
    width: '100%',

    View Slide

  141. });
    const styles = StyleSheet.create({
    container: {
    backgroundColor: s.colors.black,
    },
    view: {
    marginTop: 10,
    borderTopLeftRadius: s.radius,
    borderTopRightRadius: s.radius,
    width: '100%',
    height: s.radius * 2,
    marginBottom: -s.radius,
    backgroundColor: s.colors.white,
    },
    topBackground: {
    position: 'absolute',
    top: -500,
    height: 500,
    right: 0,
    left: 0,
    backgroundColor: s.colors.black,
    alignItems: 'center',
    justifyContent: 'flex-end',
    paddingBottom: s.spacing.l,
    },
    });

    View Slide

  142. const styles = StyleSheet.create({
    container: {
    backgroundColor: s.colors.black,
    },
    view: {
    marginTop: 10,
    borderTopLeftRadius: s.radius,
    borderTopRightRadius: s.radius,
    width: '100%',
    height: s.radius * 2,
    marginBottom: -s.radius,
    backgroundColor: s.colors.white,
    },
    topBackground: {
    position: 'absolute',
    top: -500,
    height: 500,
    right: 0,
    left: 0,
    backgroundColor: s.colors.black,
    alignItems: 'center',
    justifyContent: 'flex-end',
    paddingBottom: s.spacing.l,
    },
    });

    View Slide




  143. View Slide




  144. View Slide

  145. import variables from 'src/styles';
    export type ISpacing = keyof typeof spacing;
    export interface ISpacingProps {
    marginBottom?: ISpacing | null;
    marginHorizontal?: ISpacing | null;
    marginLeft?: ISpacing | null;
    marginRight?: ISpacing | null;
    marginTop?: ISpacing | null;
    marginVertical?: ISpacing | null;
    margin?: ISpacing | null;
    padding?: ISpacing | null;
    paddingBottom?: ISpacing | null;
    paddingHorizontal?: ISpacing | null;
    paddingLeft?: ISpacing | null;
    paddingRight?: ISpacing | null;
    paddingTop?: ISpacing | null;
    paddingVertical?: ISpacing | null;
    }
    export function spacingStyle(props: ISpacingProps, style: any = {}) {
    if (props.marginTop) { style.marginTop = variables.spacing[props.marginTop]; }
    // ...
    return style;
    }

    View Slide

  146. import { spacingStyle, ISpacing } from '~/styles/spacing';
    interface IProps extends ISpacingProps {
    someProps: any,
    // ...
    }
    function MyComponent(props) {
    // ...
    return (

    {/* ... */}

    );
    }

    View Slide

  147. View Slide