├── .env.example ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── preview.yml │ ├── production.yml │ ├── update.yml │ └── validate.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── STYLEGUIDE.md ├── __tests__ └── components │ ├── buttons │ ├── Avatar.test.tsx │ ├── BackNavigationButton.test.tsx │ ├── ClearButton.test.tsx │ ├── DayButton.test.tsx │ ├── FeedbackButton.test.tsx │ ├── FeedbackRatingButton.test.tsx │ ├── FilterButton.test.tsx │ ├── IconButton.test.tsx │ ├── PrimaryButton.test.tsx │ ├── SocialShareButton.test.tsx │ ├── StyledSwitch.test.tsx │ ├── SubmitFeedBackButton.test.tsx │ └── ViewAllButton.test.tsx │ ├── cards │ ├── CallForSpeakersCard.test.tsx │ ├── OrganizersCard.test.tsx │ ├── SessionCard.test.tsx │ └── SponsorsCard.test.tsx │ ├── common │ ├── Row.test.tsx │ └── StyledText.test.tsx │ ├── container │ └── MainContainer.test.tsx │ ├── headers │ ├── HeaderActionRight.test.tsx │ ├── HeaderRight.test.tsx │ └── MainHeader.test.tsx │ ├── lists │ ├── FeedList.test.tsx │ ├── SessionsList.test.tsx │ ├── SessionsListVertical.test.tsx │ └── SpeakersList.test.tsx │ └── player │ └── VideoPlayer.test.tsx ├── app.json ├── app ├── (app) │ ├── [profile].tsx │ ├── _layout.tsx │ ├── feedback.tsx │ ├── home │ │ ├── _layout.tsx │ │ ├── about.tsx │ │ ├── feed │ │ │ ├── _layout.tsx │ │ │ ├── index.tsx │ │ │ └── share.tsx │ │ ├── main.tsx │ │ └── sessions.tsx │ ├── index.tsx │ ├── session │ │ ├── [session].tsx │ │ └── _layout.tsx │ └── speakers.tsx └── _layout.tsx ├── assets ├── artworks │ ├── Arrow.tsx │ ├── CogIcon.tsx │ ├── FeedBackBanner.tsx │ ├── GoogleIcon.tsx │ ├── ListSeparator.tsx │ ├── Logo.tsx │ ├── LogoDark.tsx │ ├── StarIcon.tsx │ └── Vector.tsx ├── fonts │ ├── Montserrat-Bold.ttf │ ├── Montserrat-Light.ttf │ ├── Montserrat-Medium.ttf │ ├── Montserrat-Regular.ttf │ ├── Montserrat-SemiBold.ttf │ ├── Roboto-Medium.ttf │ └── Rubik-Light.ttf └── images │ ├── about.jpg │ ├── banner.png │ ├── bannerDark.png │ ├── bannerLight.png │ ├── confetti.gif │ ├── favicon.png │ ├── icon.png │ ├── speaker-dark.png │ ├── speaker-light.png │ └── splash.png ├── babel.config.js ├── bottomsheet └── bottom-sheet.tsx ├── components ├── buttons │ ├── Avatar.tsx │ ├── BackNavigationButton.tsx │ ├── ClearButton.tsx │ ├── DayButton.tsx │ ├── FeedBackRatingButton.tsx │ ├── FeedbackButton.tsx │ ├── FilterButton.tsx │ ├── IconButton.tsx │ ├── PrimaryButton.tsx │ ├── SocialShareButton.tsx │ ├── StyledSwitch.tsx │ ├── SubmitFeedbackButton.tsx │ └── ViewAllButton.tsx ├── cards │ ├── CallForSpeakersCard.tsx │ ├── OrganizerCard.tsx │ ├── OrganizersCard.tsx │ ├── SessionCard.tsx │ ├── SpeakerCard.tsx │ └── SponsorsCard.tsx ├── common │ ├── Row.tsx │ ├── Space.tsx │ └── StyledText.tsx ├── container │ ├── BottomSheetContainer.tsx │ └── MainContainer.tsx ├── headers │ ├── HeaderActionRight.tsx │ ├── HeaderRight.tsx │ └── MainHeader.tsx ├── lists │ ├── FeedList.tsx │ ├── SessionsList.tsx │ ├── SessionsListVertical.tsx │ └── SpeakersList.tsx ├── modals │ ├── FeedbackSentModal.tsx │ ├── FilterModal.tsx │ └── GoogleSignInModal.tsx └── player │ └── VideoPlayer.tsx ├── config ├── constants.ts ├── theme.ts └── typography.ts ├── context └── auth.tsx ├── eas.json ├── global.d.ts ├── jest.setupFilesAfterEnv.ts ├── metro.config.js ├── mock ├── feed.ts ├── organizers.ts ├── organizingTeam.ts ├── schedule.ts ├── sessions.ts ├── speakers.ts └── sponsors.ts ├── package.json ├── services └── api │ ├── axios.ts │ ├── index.ts │ └── react-query.ts ├── tsconfig.json ├── util ├── helpers.ts ├── test-utils.tsx └── time-travel.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_API_AUTHORIZATION_KEY="some key" 2 | EXPO_PUBLIC_BASE_URL="https://api.droidcon.co.ke/v1" 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | extends: ['@react-native-community', 'plugin:@typescript-eslint/recommended', 'prettier'], 4 | rules: { 5 | '@typescript-eslint/array-type': ['error', { default: 'generic' }], // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/array-type.md 6 | '@typescript-eslint/consistent-type-imports': 'error', // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-imports.md 7 | 'react/no-unescaped-entities': 'off', // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-unescaped-entities.md 8 | 'react/react-in-jsx-scope': 'off', // https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/react-in-jsx-scope.md 9 | 'react/no-unstable-nested-components': ['error', { allowAsProps: true }], // https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unstable-nested-components.md 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] This change requires a documentation update 13 | 14 | # How Has This Been Tested? 15 | 16 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 17 | 18 | - [ ] Test A 19 | - [ ] Test B 20 | 21 | # Checklist 22 | 23 | - [ ] My code follows the style guidelines of this project 24 | - [ ] I have performed a self-review of my own code 25 | - [ ] I have commented my code, particularly in hard-to-understand areas 26 | - [ ] I have made corresponding changes to the documentation 27 | - [ ] My changes generate no new warnings 28 | - [ ] I have added tests that prove my fix is effective or that my feature works 29 | - [ ] New and existing unit tests pass locally with my changes 30 | - [ ] Any dependent changes have been merged and published in downstream modules 31 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | name: preview 2 | on: pull_request 3 | 4 | jobs: 5 | update: 6 | name: EAS Update 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | steps: 12 | - name: Check for EXPO_TOKEN 13 | run: | 14 | if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then 15 | echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" 16 | exit 1 17 | fi 18 | 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version-file: '.nvmrc' 26 | cache: yarn 27 | 28 | - name: Setup EAS 29 | uses: expo/expo-github-action@v8 30 | with: 31 | eas-version: latest 32 | token: ${{ secrets.EXPO_TOKEN }} 33 | 34 | - name: Install dependencies 35 | run: yarn install --frozen-lockfile 36 | 37 | - name: Create preview 38 | uses: expo/expo-github-action/preview@v8 39 | with: 40 | command: eas update --auto 41 | -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | name: production build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | production: 9 | name: EAS Build 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version-file: '.nvmrc' 20 | cache: yarn 21 | 22 | - name: Setup EAS 23 | uses: expo/expo-github-action@v8 24 | with: 25 | eas-version: latest 26 | token: ${{ secrets.EXPO_TOKEN }} 27 | 28 | - name: Install dependencies 29 | run: yarn install --frozen-lockfile 30 | 31 | - name: Build on EAS 32 | if: ${{ github.ref == 'refs/heads/main' }} 33 | run: eas build --platform all --profile production --non-interactive --clear-cache 34 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: update 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | 7 | jobs: 8 | update: 9 | name: EAS Update 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check for EXPO_TOKEN 13 | run: | 14 | if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then 15 | echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" 16 | exit 1 17 | fi 18 | 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version-file: '.nvmrc' 26 | cache: yarn 27 | 28 | - name: Setup EAS 29 | uses: expo/expo-github-action@v8 30 | with: 31 | eas-version: latest 32 | token: ${{ secrets.EXPO_TOKEN }} 33 | 34 | - name: Install dependencies 35 | run: yarn install --frozen-lockfile 36 | 37 | - name: Publish update 38 | run: eas update --auto 39 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | branches: 8 | - '**' 9 | 10 | jobs: 11 | validate: 12 | runs-on: ubuntu-latest 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | steps: 19 | - name: Git Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version-file: '.nvmrc' 26 | cache: 'yarn' 27 | 28 | - name: Install Dependencies 29 | run: yarn install --frozen-lockfile 30 | 31 | - name: Validate 32 | run: yarn validate 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | .vscode/snipsnap.code-snippets 17 | .idea/ 18 | android/ 19 | ios/ 20 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 21 | # The following patterns were generated by expo-cli 22 | 23 | expo-env.d.ts 24 | # @end expo-cli 25 | 26 | # env 27 | .env*.local 28 | .env*.development 29 | .env 30 | 31 | # debug files 32 | yarn-debug.* 33 | yarn-error.* 34 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.17.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | .vscode/snipsnap.code-snippets 17 | 18 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 19 | # The following patterns were generated by expo-cli 20 | 21 | expo-env.d.ts 22 | # @end expo-cli 23 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | singleQuote: true, 4 | printWidth: 120, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "naumovs.color-highlight", 4 | "usernamehw.errorlens", 5 | "dbaeumer.vscode-eslint", 6 | "vincaslt.highlight-matching-tag", 7 | "leizongmin.node-module-intellisense", 8 | "christian-kohler.npm-intellisense", 9 | "esbenp.prettier-vscode", 10 | "wayou.vscode-todo-highlight", 11 | "shardulm94.trailing-spaces", 12 | "Gruntfuggly.todo-tree" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.insertFinalNewline": true, 4 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true, 7 | "source.organizeImports": true 8 | }, 9 | "javascript.preferences.importModuleSpecifier": "relative", 10 | "typescript.preferences.importModuleSpecifier": "relative", 11 | "typescript.tsdk": "node_modules/typescript/lib", 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | } 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # DroidconKe-RN Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ### Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | Demonstrating empathy and kindness toward other people 14 | 15 | Being respectful of differing opinions, viewpoints, and experiences 16 | 17 | Giving and gracefully accepting constructive feedback 18 | 19 | Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 20 | 21 | Focusing on what is best not just for us as individuals, but for the overall community 22 | 23 | Examples of unacceptable behavior include: 24 | 25 | The use of sexualized language or imagery, and sexual attention or advances of any kind 26 | 27 | Trolling, insulting or derogatory comments, and personal or political attacks 28 | 29 | Public or private harassment 30 | 31 | Publishing others' private information, such as a physical or email address, without their explicit permission 32 | 33 | Other conduct which could reasonably be considered inappropriate in a professional setting 34 | 35 | ### Enforcement Responsibilities 36 | 37 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 38 | 39 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 40 | 41 | ### Scope 42 | 43 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 44 | 45 | ### Enforcement 46 | 47 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Android254](https://twitter.com/254androiddevs). All complaints will be reviewed and investigated promptly and fairly. 48 | 49 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 50 | 51 | ### Enforcement Guidelines 52 | 53 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 54 | 55 | 1. Correction 56 | 57 | Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 58 | 59 | Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 60 | 61 | 2. Warning 62 | 63 | Community Impact: A violation through a single incident or series of actions. 64 | 65 | Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 66 | 67 | 3. Temporary Ban 68 | 69 | Community Impact: A serious violation of community standards, including sustained inappropriate behavior. 70 | 71 | Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 72 | 73 | 4. Permanent Ban 74 | 75 | Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 76 | 77 | Consequence: A permanent ban from any sort of public interaction within the community. 78 | 79 | ### Attribution 80 | 81 | This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at . 82 | 83 | Community Impact Guidelines were inspired by Mozilla's code of conduct enforcement ladder. 84 | 85 | For answers to common questions about this code of conduct, see the FAQ at . Translations are available at . 86 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to DroidconKe-RN App 2 | 3 | You can contribute to the DroidconKe-RN app by beta testing or submitting code. 4 | If you plan to make a contribution please do so through [our detailed contribution workflow.](#contribution-steps). You can also join us on Slack to discuss ideas. 5 | 6 | When submitting code, please make every effort to follow existing [conventions and style](https://github.com/droidconKE/droidconKE2023ReactNative/blob/main/STYLEGUIDE.md) in order to keep the code as readable as possible. 7 | 8 | Please note we have a [code of conduct](https://github.com/droidconKE/droidconKE2023ReactNative/blob/main/CODE_OF_CONDUCT.md), follow it in all your interactions with the project. 9 | 10 | ## Submitting a PR 11 | 12 | - For every PR there should be an accompanying [issue](https://github.com/droidconKE/droidconKE2023ReactNative/issues) which the PR solves 13 | 14 | - The PR itself should only contain code which is the solution for the given issue 15 | 16 | - If you are a first time contributor check if there is a [good first issue](https://github.com/droidconKE/droidconKE2023ReactNative/labels/good%20first%20issue) for you 17 | 18 |
19 | 20 | ## Contribution steps 21 | 22 | 1. Fork this repository to your own repositiry. 23 | 24 | 2. Clone the forked repository to your local machine. 25 | 26 | 3. Run it locally with our [guide](#how-to-run-locally) 27 | 28 | 4. Create your feature branch: `git checkout -b feature-my-new-feature` 29 | 30 | 5. Make changes to the project. 31 | 32 | 6. Test your changes. Write tests for every component you implement. 33 | 34 | 7. Commit your changes: `git commit -m 'Add some feature'` 35 | 36 | 8. Push to the branch: `git push origin feature-my-new-feature` 37 | 38 | 9. Submit a pull request :D 39 | 40 |

41 | 42 | ## How To Run locally? 43 | 44 | ### Install 45 | 46 | To install the project, navigate to the directory and run: 47 | 48 | - `yarn add --global expo-cli` 49 | - `yarn` 50 | 51 | ### Setup 52 | 53 | - Create `.env.development` file and copy the contents from `.env.example` 54 | - To run the app locally you need to fill in the actual values after copying the `.env.example` file to `.env.development` 55 | - Add Google Client ID to your `.env.development`. Follow [https://docs.expo.dev/guides/authentication/#google](https://docs.expo.dev/guides/authentication/#google) on how to get a Google Client ID 56 | 57 | ### Run 58 | 59 | To run the project, run the following commands: 60 | 61 | - `npm run android` 62 | - `npm run ios` 63 | 64 |
65 | 66 | ## Financial contributions 67 | 68 | We also welcome financial contributions. It helps us to grow better and faster. 69 | 70 | ## License 71 | 72 | By contributing your code, you agree to license your contribution under the terms of the [MIT License](https://github.com/droidconKE/droidconKE2023ReactNative/blob/main/LICENSE) license. 73 | 74 | All files are released with the MIT License license. 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 droidconKE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # droidconKE2023ReactNative 2 | 3 | React Native App for Kenya's 4th Android Developer Conference - droidcon to be held in Nairobi from November 8-10th 2023. 4 | 5 | ## Run the published app on your phone 6 | 7 | Download the [Expo Go](https://expo.dev/client) Android / iOS app and scan this QR code to run the app on your phone. 8 | 9 | ### Production app 10 | 11 | 12 | 13 | ### Development app 14 | 15 | 16 | 17 | # Tech Stack 18 | 19 | - Expo - [https://docs.expo.dev/](https://docs.expo.dev/) 20 | - Typescript - [https://www.typescriptlang.org/](https://www.typescriptlang.org/) 21 | 22 | # Dependencies 23 | 24 | # Features 25 | 26 | - Authentication 27 | - Home 28 | - About 29 | - Sponsors 30 | - Sessions 31 | - Speakers 32 | - Feed 33 | - Feedback 34 | 35 | # Designs 36 | 37 | This is the link to app designs: 38 | Light Theme: [https://xd.adobe.com/view/dd5d0245-b92b-4678-9d4a-48b3a6f48191-880e/](https://xd.adobe.com/view/dd5d0245-b92b-4678-9d4a-48b3a6f48191-880e/) 39 | Dark Theme: [https://xd.adobe.com/view/5ec235b6-c3c6-49a9-b783-1f1303deb1a8-0b91/](https://xd.adobe.com/view/5ec235b6-c3c6-49a9-b783-1f1303deb1a8-0b91/) 40 | 41 | ## Contributing 42 | 43 | Contributions are always welcome! 44 | 45 | See [`CONTRIBUTING.md`](CONTRIBUTING.md) for ways to get started. 46 | -------------------------------------------------------------------------------- /STYLEGUIDE.md: -------------------------------------------------------------------------------- 1 | # Style Guide for Droidconke-RN app 2 | 3 | Here are the style rules to follow: 4 | 5 | ## #1 Be consistent with the rest of the codebase 6 | 7 | This is the number one rule and should help determine what to do in most cases. 8 | 9 | ## #2 Respect Prettier and Linter rules 10 | 11 | We use a linter and prettier to automatically help you make style guide decisions easy. 12 | 13 | ## #3 File Name 14 | 15 | Generally file names are PascalCase if they are components or classes, and camelCase otherwise. Filenames' extension must be .tsx for component files and .ts otherwise. 16 | 17 | ## #4 Respect Google JavaScript style guide 18 | 19 | The style guide accessible 20 | [here](https://google.github.io/styleguide/jsguide.html) should be 21 | respected. However, if a rule is not consistent with the rest of the codebase, 22 | rule #1 takes precedence. Same thing goes with any of the above rules taking precedence over this rule. 23 | 24 | ## #5 Follow these grammar rules 25 | 26 | - Functions descriptions have to start with a verb using the third person of the 27 | singular. 28 | - _Ex: `/\*\* Tests the validity of the input. _/`\* 29 | - Inline comments within procedures should always use the imperative. 30 | - _Ex: `// Check whether the value is true.`_ 31 | - Acronyms have to be uppercased in comments. 32 | - _Ex: `// IP, DOM, CORS, URL...`_ 33 | - _Exception: Identity Provider = IdP_ 34 | - Acronyms have to be capitalized (but not uppercased) in variable names. 35 | - _Ex: `redirectUrl()`, `signInIdp()`_ 36 | - Never use login/log in in comments. Use “sign-in” if it’s a noun, “sign in” if 37 | it’s a verb. The same goes for the variable name. Never use `login`; always use 38 | `signIn`. 39 | - _Ex: `// The sign-in method.`_ 40 | - _Ex: `// Signs in the user.`_ 41 | - Always start an inline comment with a capital (unless referring to the name of 42 | a variable/function), and end it with a period. 43 | - _Ex: `// This is a valid inline comment.`_ 44 | -------------------------------------------------------------------------------- /__tests__/components/buttons/Avatar.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import Avatar from '../../../components/buttons/Avatar'; 4 | 5 | jest.mock('@react-navigation/native', () => ({ 6 | useTheme: jest.fn().mockReturnValue({ 7 | colors: { 8 | primary: 'red', 9 | background: 'white', 10 | }, 11 | }), 12 | })); 13 | 14 | describe('', () => { 15 | it('renders avatar component', () => { 16 | render(); 17 | expect(screen.getByTestId('avatar')).toBeTruthy(); 18 | }); 19 | 20 | it('renders expected border color when bordered boolean value changes', () => { 21 | render(); 22 | expect(screen.getByTestId('avatar')).toHaveStyle({ 23 | borderColor: 'red', 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/components/buttons/BackNavigationButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | 4 | import BackNavigationButton from '../../../components/buttons/BackNavigationButton'; 5 | 6 | describe('', () => { 7 | it('Renders back navigation button component', () => { 8 | render( console.log('back')} />); 9 | expect(screen.getByText('Go back')).toBeDefined(); 10 | }); 11 | it('calls the function provided by onPress prop after pressing the button', () => { 12 | const onPress = jest.fn(); 13 | render(); 14 | fireEvent.press(screen.getByText('Go back')); 15 | 16 | expect(onPress).toHaveBeenCalledTimes(1); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /__tests__/components/buttons/ClearButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import ClearButton from '../../../components/buttons/ClearButton'; 4 | 5 | describe('', () => { 6 | it('renders clearbutton component', () => { 7 | render( console.log('Powered off')} label="Power off" iconName="poweroff" />); 8 | expect(screen.getByTestId('clearButton')).toBeTruthy(); 9 | }); 10 | 11 | it('renders label powered off', () => { 12 | render( console.log('Powered off')} label="Power off" iconName="poweroff" />); 13 | expect(screen.getByText('Power off')).toBeDefined(); 14 | }); 15 | 16 | it('calls the function provided by onPress prop after pressing the button', () => { 17 | const onPress = jest.fn(); 18 | render(); 19 | fireEvent.press(screen.getByText('Power off')); 20 | 21 | expect(onPress).toHaveBeenCalledTimes(1); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/components/buttons/DayButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import DayButton from '../../../components/buttons/DayButton'; 4 | 5 | jest.mock('@react-navigation/native', () => ({ 6 | useTheme: jest.fn().mockReturnValue({ 7 | colors: { 8 | tertiary: 'red', 9 | tertiaryTint: 'blue', 10 | bg: 'orange', 11 | text: 'green', 12 | }, 13 | }), 14 | })); 15 | 16 | describe('', () => { 17 | const onPress = jest.fn(); 18 | 19 | it('renders daybutton component', () => { 20 | render( console.log('pressed')} date={''} day={''} dateInfull="2022-11-16" />); 21 | }); 22 | 23 | it('renders date', () => { 24 | render( 25 | console.log('pressed')} date={'16th'} day={'Day 1'} dateInfull="2022-11-16" />, 26 | ); 27 | expect(screen.getByText('16th')).toBeDefined(); 28 | }); 29 | 30 | it('renders day', () => { 31 | render( 32 | console.log('pressed')} date={'17th'} day={'Day 2'} dateInfull="2022-11-17" />, 33 | ); 34 | expect(screen.getByText('Day 2')).toBeDefined(); 35 | }); 36 | 37 | it('renders different color when button is selected', () => { 38 | render(); 39 | expect(screen.getByTestId('dayButton')).toHaveStyle({ 40 | backgroundColor: 'red', 41 | }); 42 | expect(screen.getByTestId('date')).toHaveStyle({ 43 | color: 'orange', 44 | }); 45 | expect(screen.getByTestId('day')).toHaveStyle({ 46 | color: 'orange', 47 | }); 48 | }); 49 | 50 | it('fires onPress function when pressed', () => { 51 | render(); 52 | fireEvent.press(screen.getByTestId('dayButton')); 53 | 54 | expect(onPress).toHaveBeenCalledTimes(1); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /__tests__/components/buttons/FeedbackButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import FeedbackButton from '../../../components/buttons/FeedbackButton'; 4 | 5 | describe('', () => { 6 | it('Renders feedback button component', () => { 7 | render( console.log('Feedback gotten succesfully')} />); 8 | expect(screen.getByText('Feedback')).toBeDefined(); 9 | }); 10 | 11 | it('calls the function provided by onPress prop after pressing the button', () => { 12 | const onPress = jest.fn(); 13 | render(); 14 | fireEvent.press(screen.getByText('Feedback')); 15 | 16 | expect(onPress).toHaveBeenCalledTimes(1); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /__tests__/components/buttons/FeedbackRatingButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | 4 | import FeedBackRatingButton from '../../../components/buttons/FeedBackRatingButton'; 5 | 6 | describe('', () => { 7 | it('Renders feedback rating button component', () => { 8 | render( 9 | console.log('Feedback gotten succesfully')} 12 | testID="btn1" 13 | onSelected={true} 14 | />, 15 | ); 16 | expect(screen.getByText('Ok')).toBeDefined(); 17 | }); 18 | 19 | it('calls the function provided by onPress prop after pressing the button', () => { 20 | const onPress = jest.fn(); 21 | render(); 22 | fireEvent.press(screen.getByText('Ok')); 23 | 24 | expect(onPress).toHaveBeenCalledTimes(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/components/buttons/FilterButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import FilterButton from '../../../components/buttons/FilterButton'; 4 | 5 | describe('FilterButton', () => { 6 | it('renders button with the correct label', () => { 7 | const { getByText } = render( 8 | console.log('clicked')} selected={false} />, 9 | ); 10 | const buttonLabel = getByText('Filter'); 11 | expect(buttonLabel).toBeTruthy(); 12 | }); 13 | 14 | it('calls the onPress function when the button is pressed', () => { 15 | const onPressMock = jest.fn(); 16 | const { getByTestId } = render(); 17 | const button = getByTestId('filterButton'); 18 | fireEvent.press(button); 19 | expect(onPressMock).toHaveBeenCalledTimes(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/components/buttons/IconButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import IconButton from '../../../components/buttons/IconButton'; 4 | 5 | jest.mock('@react-navigation/native', () => ({ 6 | useTheme: jest.fn().mockReturnValue({ 7 | colors: { 8 | primary: 'red', 9 | text: 'blue', 10 | }, 11 | }), 12 | })); 13 | 14 | describe('', () => { 15 | const onPress = jest.fn(); 16 | 17 | it('renders iconButton component', () => { 18 | render( console.log('Powered off')} name="power-off" />); 19 | expect(screen.getByTestId('iconButton')).toBeTruthy(); 20 | }); 21 | 22 | it('calls the function provided by onPress prop after pressing the button', () => { 23 | const { getByTestId } = render(); 24 | const button = getByTestId('iconButton'); 25 | fireEvent.press(button); 26 | expect(onPress).toHaveBeenCalledTimes(1); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /__tests__/components/buttons/PrimaryButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import PrimaryButton from '../../../components/buttons/PrimaryButton'; 4 | 5 | describe('', () => { 6 | it('renders button with the correct label', () => { 7 | const { getByText } = render( console.log('clicked')} label="Click Me" />); 8 | const buttonLabel = getByText('Click Me'); 9 | expect(buttonLabel).toBeTruthy(); 10 | }); 11 | 12 | it('calls the onPress function when the button is pressed', () => { 13 | const onPressMock = jest.fn(); 14 | const { getByTestId } = render(); 15 | const button = getByTestId('primaryButton'); 16 | fireEvent.press(button); 17 | expect(onPressMock).toHaveBeenCalledTimes(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/components/buttons/SocialShareButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | import SocialShareButton from '../../../components/buttons/SocialShareButton'; 3 | 4 | jest.mock('@react-navigation/native', () => ({ 5 | useTheme: jest.fn().mockReturnValue({ 6 | colors: { 7 | background: 'red', 8 | }, 9 | }), 10 | })); 11 | 12 | describe('', () => { 13 | const onPress = jest.fn(); 14 | 15 | it('renders', () => { 16 | render(); 17 | expect(screen.getByText('share')).toBeDefined(); 18 | }); 19 | 20 | it('shows title share', () => { 21 | render(); 22 | expect(screen.getByTestId('socialShareButtonText')).toHaveTextContent('share'); 23 | }); 24 | 25 | it('Triggers HandlePress button ', () => { 26 | render(); 27 | fireEvent.press(screen.getByText('share')); 28 | expect(onPress).toHaveBeenCalledTimes(1); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/components/buttons/StyledSwitch.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, fireEvent, render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import StyledSwitch from '../../../components/buttons/StyledSwitch'; 4 | import { setup, teardown, travel } from '../../../util/time-travel'; 5 | 6 | beforeEach(setup); 7 | afterEach(teardown); 8 | 9 | describe('', () => { 10 | const onPress = jest.fn(); 11 | 12 | it('renders StyledSwitch component', () => { 13 | render( 14 | , 27 | ); 28 | expect(screen.getByTestId('styled-switch')).toBeDefined(); 29 | }); 30 | 31 | it('renders blue trackColor when value is false', () => { 32 | render( 33 | , 46 | ); 47 | expect(screen.getByTestId('track')).toHaveStyle({ 48 | backgroundColor: 'blue', 49 | }); 50 | }); 51 | 52 | it('renders red trackColor when value is true', () => { 53 | render( 54 | , 67 | ); 68 | expect(screen.getByTestId('track')).toHaveStyle({ 69 | backgroundColor: 'red', 70 | }); 71 | }); 72 | 73 | it('renders purple thumbColor', () => { 74 | render( 75 | , 88 | ); 89 | expect(screen.getByTestId('thumb')).toHaveStyle({ 90 | backgroundColor: 'purple', 91 | }); 92 | }); 93 | 94 | it('renders orange icon color when value is true', () => { 95 | render( 96 | , 109 | ); 110 | const icon = screen.getByTestId('icon'); 111 | 112 | expect(icon.props.color).toBe('orange'); 113 | }); 114 | 115 | it('renders maroon icon color when value is false', () => { 116 | render( 117 | , 130 | ); 131 | const icon = screen.getByTestId('icon'); 132 | 133 | expect(icon.props.color).toBe('maroon'); 134 | }); 135 | 136 | it('fires onValueChange function when toggled', () => { 137 | render( 138 | , 151 | ); 152 | expect(screen.getByTestId('track')).toHaveStyle({ 153 | backgroundColor: 'blue', 154 | }); 155 | fireEvent.press(screen.getByTestId('styled-switch')); 156 | act(() => travel(500)); 157 | expect(onPress).toHaveBeenCalledTimes(1); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /__tests__/components/buttons/SubmitFeedBackButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | 4 | import SubmitFeedbackButton from '../../../components/buttons/SubmitFeedbackButton'; 5 | 6 | describe('', () => { 7 | it('Renders submit feedback button component', () => { 8 | render( 9 | { 11 | console.log('hello'); 12 | }} 13 | text="SUBMIT FEEDBACK" 14 | />, 15 | ); 16 | expect(screen.getByText('SUBMIT FEEDBACK')).toBeDefined(); 17 | }); 18 | it('calls the function provided by onPress prop after pressing the button', () => { 19 | const onPress = jest.fn(); 20 | render(); 21 | fireEvent.press(screen.getByText('SUBMIT FEEDBACK')); 22 | 23 | expect(onPress).toHaveBeenCalledTimes(1); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/components/buttons/ViewAllButton.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | import ViewAllButton from '../../../components/buttons/ViewAllButton'; 3 | 4 | describe('', () => { 5 | it('renders ViewAllButton component', () => { 6 | render( console.log('pressed')} label="+80" />); 7 | expect(screen.getByTestId('viewAllButton')).toBeTruthy(); 8 | }); 9 | 10 | it('Tests that label +80 is shown', () => { 11 | render( console.log('pressed')} label="+80" />); 12 | expect(screen.getByText('+80')).toBeDefined(); 13 | }); 14 | 15 | it('Tests that onPress function works', () => { 16 | const onPress = jest.fn(); 17 | render(); 18 | fireEvent.press(screen.getByText('+80')); 19 | 20 | expect(onPress).toHaveBeenCalledTimes(1); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/components/cards/CallForSpeakersCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | 4 | import CallForSpeakersCard from '../../../components/cards/CallForSpeakersCard'; 5 | 6 | jest.mock('@expo/vector-icons', () => ({ 7 | AntDesign: 'AntDesign', 8 | })); 9 | 10 | jest.mock('@react-navigation/native', () => ({ 11 | useTheme: jest.fn().mockReturnValue({ colors: { tint: '#00E2C3' } }), 12 | })); 13 | 14 | jest.mock('../../../assets/artworks/Vector', () => 'Vector'); 15 | 16 | describe('', () => { 17 | it('renders CallForSpeakersCard component', () => { 18 | const { getByText, getByTestId } = render(); 19 | 20 | const title = getByText('Call for Speakers'); 21 | const smallText = getByText('Apply to be a speaker'); 22 | const vector = getByTestId('vector'); 23 | const caretrightIcon = getByTestId('caretrightIcon'); 24 | 25 | expect(title).toBeTruthy(); 26 | expect(smallText).toBeTruthy(); 27 | expect(vector).toBeTruthy(); 28 | expect(caretrightIcon).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/components/cards/OrganizersCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react-native'; 2 | import OrganizersCard from '../../../components/cards/OrganizersCard'; 3 | import { Organizers } from '../../../mock/organizers'; 4 | 5 | describe('OrganizersCard', () => { 6 | it('renders the component', () => { 7 | const { getByText } = render(); 8 | 9 | const card = getByText('Organised by:'); 10 | expect(card).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/components/cards/SessionCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react-native'; 2 | 3 | import SessionCard from '../../../components/cards/SessionCard'; 4 | 5 | const schedule = { 6 | id: 122, 7 | title: 'Registration & Check-In', 8 | description: 'Registration & Check-In', 9 | slug: '', 10 | session_format: '', 11 | session_level: '', 12 | session_image: null, 13 | backgroundColor: '#89609E', 14 | borderColor: '#89609E', 15 | is_serviceSession: true, 16 | is_keynote: false, 17 | is_bookmarked: true, 18 | start_date_time: '2022-11-16 08:30:00', 19 | start_time: '08:30:00', 20 | end_date_time: '2022-11-16 10:00:00', 21 | end_time: '10:00:00', 22 | speakers: [], 23 | rooms: [ 24 | { 25 | title: 'Mekatilili', 26 | id: 1, 27 | }, 28 | ], 29 | }; 30 | 31 | const session = { 32 | title: 'The Apache Way: Doing Community like Apache', 33 | description: 34 | 'The Apache Way - collaborative, consensus-driven, vendor-neutral\nsoftware development - emphasizes the community over the code, and yet\nhas somehow produced some of the most successful software on the\nplanet, from the Apache web server to Spark, Hadoop, OpenOffice,\nCassandra, Kafka, and many many others. In this talk, I’ll cover what\nthe Apache Way is, and why it’s been so successful over the past 25\nyears.', 35 | slug: 'the-apache-way-doing-community-like-apache-1668083244', 36 | session_format: 'Session', 37 | session_level: 'Intermediate', 38 | is_keynote: true, 39 | session_image: 40 | 'https://res.cloudinary.com/droidconke/image/upload/v1668083311/prod/upload/sessions/pwt9pnojtmng8bymxkls.png', 41 | speakers: [ 42 | { 43 | name: 'Rich Bowen', 44 | tagline: 'Principal Evangelist, Open Source at AWS', 45 | biography: 46 | "Rich has been working on Open Source since before we called it that.\nHe's a member, and currently serving as a director, at the Apache\nSoftware Foundation. He currently works in the Open Source Strategy and\nMarketing team at AWS as an Open Source Advocate. Rich was born in\nTenwek, and grew up in Kericho and Nairobi before moving to the United\nStates.\n\nI am very excited about getting back to Nairobi. It has been 32 years\n(!!!) since I've been home, and I know that so much has changed. I'm\nlooking forward to seeing home with new eyes.\n\nI was born at Tenwek, down in Bomet, and mostly grew up in Kericho, and\nattended St Andrews School in Turi. We then moved to Nairobi when I was\nin my early teens.", 47 | avatar: 48 | 'https://res.cloudinary.com/droidconke/image/upload/v1668083245/prod/upload/speakers/bupmjcd4ddtbcbq0yyqn.jpg', 49 | twitter: 'https://twitter.com/rbowen', 50 | facebook: 'https://twitter.com/rbowen', 51 | linkedin: 'https://twitter.com/rbowen', 52 | instagram: 'https://twitter.com/rbowen', 53 | blog: 'https://twitter.com/rbowen', 54 | company_website: 'https://twitter.com/rbowen', 55 | }, 56 | ], 57 | }; 58 | 59 | jest.mock('expo-router', () => ({ 60 | useRouter: jest.fn(), 61 | })); 62 | 63 | describe('', () => { 64 | const onPress = jest.fn(); 65 | 66 | it('renders SessionCard component', () => { 67 | render(); 68 | expect(screen.getByTestId('card')).toBeDefined(); 69 | }); 70 | 71 | it('renders session details', () => { 72 | const { getByText } = render(); 73 | expect(getByText('The Apache Way: Doing Community like Apache')).toBeDefined(); 74 | }); 75 | 76 | it('fires onPress function when pressed', () => { 77 | render(); 78 | fireEvent.press(screen.getByText('The Apache Way: Doing Community like Apache')); 79 | 80 | expect(onPress).toHaveBeenCalledTimes(1); 81 | }); 82 | 83 | it('renders SessionCardOnSessions when you pass sessions to screen prop', () => { 84 | render( 85 | , 93 | ); 94 | expect(screen.getByTestId('card-sessions')).toBeDefined(); 95 | }); 96 | 97 | it('renders SessionCardList when you pass sessions and list to screen prop', () => { 98 | render( 99 | , 107 | ); 108 | expect(screen.getByTestId('card-list')).toBeDefined(); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /__tests__/components/cards/SponsorsCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react-native'; 2 | import SponsorsCard from '../../../components/cards/SponsorsCard'; 3 | import { Sponsors } from '../../../mock/sponsors'; 4 | 5 | describe('SponsorsCard', () => { 6 | it('renders the component', () => { 7 | const { getByText } = render(); 8 | 9 | const card = getByText('Sponsors'); 10 | expect(card).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/components/common/Row.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | 4 | import { View } from 'react-native'; 5 | import Row from '../../../components/common/Row'; 6 | 7 | describe('', () => { 8 | const children = ; 9 | 10 | it('renders Row component', () => { 11 | render(); 12 | expect(screen.getByTestId('row')).toBeTruthy(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/components/common/StyledText.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | 4 | import StyledText from '../../../components/common/StyledText'; 5 | 6 | describe('', () => { 7 | it('renders StyledText component', () => { 8 | render(); 9 | expect(screen.getByText('Styled Text')).toBeTruthy(); 10 | }); 11 | 12 | it('renders correct style with title prop', () => { 13 | render(); 14 | expect(screen.getByText('Styled Text')).toHaveStyle({ 15 | fontSize: 16, 16 | fontFamily: 'montserratRegular', 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/components/container/MainContainer.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | 4 | import MainContainer from '../../../components/container/MainContainer'; 5 | 6 | describe('', () => { 7 | const children = <>; 8 | 9 | it('renders MainContainer component', () => { 10 | render(); 11 | expect(screen.getByTestId('main-container')).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/components/headers/HeaderActionRight.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react-native'; 2 | import HeaderActionRight from '../../../components/headers/HeaderActionRight'; 3 | 4 | jest.mock('expo-router', () => ({ 5 | useRouter: jest.fn(), 6 | })); 7 | 8 | describe('', () => { 9 | it('renders HeaderActionRight component.', () => { 10 | render( 11 | console.log('collapsed')} 14 | handlePress={() => console.log('open filter modal')} 15 | />, 16 | ); 17 | expect(screen.getByTestId('headerActionRight')).toBeTruthy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/components/headers/HeaderRight.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react-native'; 2 | import HeaderRight from '../../../components/headers/HeaderRight'; 3 | 4 | jest.mock('expo-router', () => ({ 5 | __esModule: true, 6 | useRouter: jest.fn(), 7 | })); 8 | 9 | describe('', () => { 10 | it('renders HeaderRight component', () => { 11 | render(); 12 | expect(screen.getByTestId('headerRight')).toBeDefined(); 13 | expect(screen.getByText('Feedback')).toBeDefined(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/components/headers/MainHeader.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react-native'; 2 | import MainHeader from '../../../components/headers/MainHeader'; 3 | 4 | jest.mock('@react-navigation/native', () => ({ 5 | useTheme: jest.fn().mockReturnValue({ 6 | dark: true, 7 | }), 8 | })); 9 | 10 | describe('', () => { 11 | it('renders MainHeader component', () => { 12 | render(); 13 | expect(screen.getByTestId('mainHeader')).toBeDefined(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/components/lists/FeedList.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import FeedList from '../../../components/lists/FeedList'; 4 | import { Feed as FeedData } from '../../../mock/feed'; 5 | 6 | jest.mock('expo-router'); 7 | 8 | describe('FeedList', () => { 9 | it('renders the feed list correctly', () => { 10 | const { getAllByTestId } = render(); 11 | 12 | // Ensure that all feed items are rendered 13 | const feedItems = getAllByTestId('feedItem'); 14 | expect(feedItems.length).toBe(FeedData.data.length); 15 | }); 16 | 17 | it('renders the feed item with the correct data', () => { 18 | const { getByText } = render(); 19 | 20 | // Check if each feed item is rendered with the correct data 21 | FeedData.data.forEach((feed) => { 22 | const feedItem = getByText(feed.body); 23 | expect(feedItem).toBeTruthy(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/components/lists/SessionsList.test.tsx: -------------------------------------------------------------------------------- 1 | import SessionsList from '../../../components/lists/SessionsList'; 2 | import { Sessions } from '../../../mock/sessions'; 3 | import { render } from '../../../util/test-utils'; 4 | 5 | jest.mock('expo-router'); 6 | 7 | describe('SessionsList', () => { 8 | it('renders the sessions list correctly', () => { 9 | const { getByText } = render(); 10 | 11 | expect(getByText('Sessions')).toBeDefined(); 12 | 13 | const sessionCount = (Sessions.meta.paginator.count - 5).toString(); 14 | 15 | expect(getByText(`+${sessionCount}`)).toBeDefined(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /__tests__/components/lists/SessionsListVertical.test.tsx: -------------------------------------------------------------------------------- 1 | import SessionsListVertical from '../../../components/lists/SessionsListVertical'; 2 | import { Schedule } from '../../../mock/schedule'; 3 | import { render, screen } from '../../../util/test-utils'; 4 | 5 | jest.mock('expo-router'); 6 | 7 | describe('SessionsListVertical', () => { 8 | it('renders SessionsListVertical component', () => { 9 | render(); 10 | expect(screen.getByTestId('sessions-list-vertical')).toBeDefined(); 11 | }); 12 | it('Renders card component when you pass prop variant card', () => { 13 | render(); 14 | expect(screen.getAllByTestId('card-sessions')).toBeDefined(); 15 | }); 16 | 17 | it('Renders list item component when you pass prop variant list', () => { 18 | render(); 19 | expect(screen.getAllByTestId('card-list')).toBeDefined(); 20 | }); 21 | 22 | it('Shows bookmarked content when prop bookmark is true', () => { 23 | const { getAllByText } = render( 24 | , 25 | ); 26 | expect(getAllByText('A guide to Abstract writing - Hannah Olukuye')).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /__tests__/components/lists/SpeakersList.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react-native'; 2 | import SpeakersList from '../../../components/lists/SpeakersList'; 3 | import { Speakers } from '../../../mock/speakers'; 4 | 5 | jest.mock('expo-router'); 6 | 7 | describe('SpeakersList', () => { 8 | it('renders the SpeakersList correctly', () => { 9 | const { getByText } = render(); 10 | 11 | expect(getByText('Speakers')).toBeDefined(); 12 | 13 | const speakersCount = (Speakers.meta.paginator.count - 5).toString(); 14 | 15 | expect(getByText(`+${speakersCount}`)).toBeDefined(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /__tests__/components/player/VideoPlayer.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react-native'; 2 | import React from 'react'; 3 | import VideoPlayer from '../../../components/player/VideoPlayer'; 4 | 5 | describe('VideoPlayer', () => { 6 | test('renders video player component', () => { 7 | const { getByTestId } = render(); 8 | const videoPlayer = getByTestId('video-player'); 9 | expect(videoPlayer).toBeTruthy(); 10 | }); 11 | 12 | test('renders video component', () => { 13 | const { getByTestId } = render(); 14 | const videoComponent = getByTestId('video-component'); 15 | expect(videoComponent).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "userInterfaceStyle": "automatic", 4 | "web": { 5 | "bundler": "metro" 6 | }, 7 | "name": "DroidconKe ReactNative", 8 | "slug": "droidconke-reactnative", 9 | "scheme": "droidconreactnative", 10 | "plugins": [ 11 | "expo-router", 12 | [ 13 | "expo-build-properties", 14 | { 15 | "android": { 16 | "enableProguardInReleaseBuilds": true, 17 | "enableShrinkResourcesInReleaseBuilds": true 18 | } 19 | } 20 | ] 21 | ], 22 | "experiments": { 23 | "typedRoutes": true 24 | }, 25 | "android": { 26 | "icon": "./assets/images/icon.png", 27 | "splash": { 28 | "image": "./assets/images/splash.png", 29 | "resizeMode": "cover", 30 | "backgroundColor": "#ffffff", 31 | "dark": { 32 | "image": "./assets/images/splash.png", 33 | "resizeMode": "contain", 34 | "backgroundColor": "#000000" 35 | } 36 | }, 37 | "intentFilters": [ 38 | { 39 | "action": "VIEW", 40 | "autoVerify": true, 41 | "data": [ 42 | { 43 | "scheme": "https", 44 | "host": "*.ke.droidcon.reactnative.io", 45 | "pathPrefix": "/records" 46 | } 47 | ], 48 | "category": ["BROWSABLE", "DEFAULT"] 49 | } 50 | ], 51 | "package": "com.ke.droidcon.reactnative" 52 | }, 53 | "ios": { 54 | "icon": "./assets/images/icon.png", 55 | "splash": { 56 | "image": "./assets/images/splash.png", 57 | "resizeMode": "cover", 58 | "backgroundColor": "#ffffff", 59 | "dark": { 60 | "image": "./assets/images/splash.png", 61 | "resizeMode": "contain", 62 | "backgroundColor": "#000000" 63 | } 64 | }, 65 | "bundleIdentifier": "com.ke.droidcon.reactnative" 66 | }, 67 | "extra": { 68 | "router": { 69 | "origin": false 70 | }, 71 | "eas": { 72 | "projectId": "6ad9531f-f082-4bfb-9213-272fdc94f01e" 73 | } 74 | }, 75 | "owner": "reactdevske-reactnative", 76 | "runtimeVersion": { 77 | "policy": "appVersion" 78 | }, 79 | "updates": { 80 | "url": "https://u.expo.dev/6ad9531f-f082-4bfb-9213-272fdc94f01e" 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/(app)/[profile].tsx: -------------------------------------------------------------------------------- 1 | import { AntDesign, FontAwesome5 } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import { Image } from 'expo-image'; 4 | import * as Linking from 'expo-linking'; 5 | import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; 6 | import React, { useEffect, useState } from 'react'; 7 | import { Dimensions, ImageBackground, Pressable, ScrollView, StyleSheet, View } from 'react-native'; 8 | import Row from '../../components/common/Row'; 9 | import Space from '../../components/common/Space'; 10 | import StyledText from '../../components/common/StyledText'; 11 | import MainContainer from '../../components/container/MainContainer'; 12 | import { usePrefetchedEventData } from '../../services/api'; 13 | import { getTwitterHandle } from '../../util/helpers'; 14 | 15 | type Profile = { 16 | image?: string; 17 | bio?: string; 18 | twitter_handle?: string | null; 19 | tagline?: string; 20 | name?: string; 21 | }; 22 | 23 | const { height } = Dimensions.get('window'); 24 | 25 | const Speaker = () => { 26 | const { colors, dark } = useTheme(); 27 | const router = useRouter(); 28 | const { name, type } = useLocalSearchParams(); 29 | const [details, setDetails] = useState({}); 30 | 31 | const { speakers, organizingTeam } = usePrefetchedEventData(); 32 | 33 | useEffect(() => { 34 | if (type === 'speaker') { 35 | const speaker = speakers?.data.filter((x) => x.name === name)[0]; 36 | setDetails({ 37 | image: speaker?.avatar, 38 | bio: speaker?.biography, 39 | twitter_handle: speaker?.twitter, 40 | tagline: speaker?.tagline, 41 | name: speaker?.name, 42 | }); 43 | } else if (type === 'organizer') { 44 | const organizer = organizingTeam?.data.filter((x) => x.name === name)[0]; 45 | setDetails({ 46 | image: organizer?.photo, 47 | bio: organizer?.bio, 48 | twitter_handle: organizer?.twitter_handle, 49 | tagline: organizer?.tagline, 50 | name: organizer?.name, 51 | }); 52 | } 53 | }, [name, organizingTeam?.data, speakers?.data, type]); 54 | 55 | const handleTwitterProfile = (link: string | null | undefined) => { 56 | if (link) { 57 | Linking.openURL(`${link}`); 58 | } 59 | }; 60 | 61 | return ( 62 | 63 | ( 68 | router.back()} /> 69 | ), 70 | }} 71 | /> 72 | 73 | 80 | 81 | 82 | 83 | 89 | 90 | 91 | 92 | {type === 'speaker' && } 93 | 94 | {type === 'speaker' ? 'Speaker' : 'Organizer'} 95 | 96 | 97 | 98 | {name} 99 | 100 | 101 | 102 | 103 | 104 | 105 | {details?.tagline} 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | Bio 116 | 117 | 118 | 119 | 120 | 121 | {details?.bio} 122 | 123 | 124 | 125 | 126 | 127 | 128 | Twitter Handle 129 | 130 | handleTwitterProfile(details?.twitter_handle)} 133 | > 134 | 135 | 136 | 137 | {details?.twitter_handle ? getTwitterHandle(details?.twitter_handle) : 'N/A'} 138 | 139 | 140 | 141 | 142 | 143 | ); 144 | }; 145 | 146 | const styles = StyleSheet.create({ 147 | main: { 148 | flex: 1, 149 | width: '100%', 150 | }, 151 | banner: { 152 | height: height / 5, 153 | width: '100%', 154 | }, 155 | centered: { 156 | width: '100%', 157 | alignItems: 'center', 158 | }, 159 | wrapper: { 160 | width: '100%', 161 | top: -40, 162 | paddingHorizontal: 20, 163 | }, 164 | avatar: { 165 | height: 100, 166 | width: 100, 167 | borderRadius: 60, 168 | alignItems: 'center', 169 | top: -50, 170 | }, 171 | text: { 172 | textAlign: 'center', 173 | }, 174 | row: { 175 | flexDirection: 'row', 176 | alignItems: 'center', 177 | gap: 5, 178 | }, 179 | image: { 180 | height: 103, 181 | width: 103, 182 | borderWidth: 2, 183 | borderRadius: 60, 184 | }, 185 | info: { 186 | top: -40, 187 | alignItems: 'center', 188 | }, 189 | contentText: { 190 | lineHeight: 18, 191 | }, 192 | socialLink: { 193 | width: '100%', 194 | borderTopWidth: 1, 195 | padding: 20, 196 | marginBottom: 20, 197 | }, 198 | button: { 199 | flexDirection: 'row', 200 | alignItems: 'center', 201 | justifyContent: 'space-evenly', 202 | paddingHorizontal: 12, 203 | paddingVertical: 8, 204 | borderRadius: 8, 205 | borderWidth: 1, 206 | }, 207 | taglineContainer: { 208 | width: '70%', 209 | }, 210 | }); 211 | 212 | export default Speaker; 213 | -------------------------------------------------------------------------------- /app/(app)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { Stack } from 'expo-router'; 3 | import React from 'react'; 4 | 5 | export default () => { 6 | const { colors } = useTheme(); 7 | return ( 8 | 9 | 15 | 24 | 34 | 45 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /app/(app)/feedback.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { useMutation } from '@tanstack/react-query'; 3 | import React, { useState } from 'react'; 4 | import { ImageBackground, StyleSheet, View } from 'react-native'; 5 | import { TextInput } from 'react-native-gesture-handler'; 6 | import FeedBackRatingButton from '../../components/buttons/FeedBackRatingButton'; 7 | import SubmitFeedbackButton from '../../components/buttons/SubmitFeedbackButton'; 8 | import Space from '../../components/common/Space'; 9 | import StyledText from '../../components/common/StyledText'; 10 | import MainContainer from '../../components/container/MainContainer'; 11 | import FeedbackSentModal from '../../components/modals/FeedbackSentModal'; 12 | import { typography } from '../../config/typography'; 13 | import { sendFeedback } from '../../services/api'; 14 | 15 | export type TypeRatingStates = { 16 | icon: string; 17 | text: string; 18 | value: number; 19 | }; 20 | 21 | const Feedback = () => { 22 | const { colors, dark } = useTheme(); 23 | const [showModal, setShowModal] = useState(false); 24 | const [selectedRating, setSelectedRating] = useState(2); 25 | const [description, setDescription] = useState(''); 26 | const ratingStates: Array = [ 27 | { icon: '😔', text: 'Bad', value: 0 }, 28 | { icon: '😐', text: 'Okay', value: 1 }, 29 | { icon: '😊', text: 'Great', value: 2 }, 30 | ]; 31 | const openModal = () => { 32 | setShowModal(true); 33 | }; 34 | 35 | const { mutate } = useMutation({ 36 | mutationFn: () => sendFeedback(description, selectedRating), 37 | onSuccess: () => { 38 | openModal(); 39 | }, 40 | onError: (error) => { 41 | console.error(error); 42 | //TODO: implement UI to handle error 43 | }, 44 | }); 45 | 46 | return ( 47 | 48 | 49 | 54 | 55 | 56 | 62 | Your feedback helps us improve 63 | 64 | 65 | 66 | 67 | How is/was the event 68 | 69 | 70 | {ratingStates.map((rating, index) => { 71 | return ( 72 | 78 | ); 79 | })} 80 | 81 | 82 | 83 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | ); 101 | }; 102 | 103 | const styles = StyleSheet.create({ 104 | main: { 105 | flex: 1, 106 | width: '100%', 107 | }, 108 | feedBackBanner: { 109 | height: 179, 110 | flex: 1, 111 | width: '100%', 112 | }, 113 | feedBackForm: { 114 | flex: 1, 115 | paddingTop: 17, 116 | borderRadius: 10, 117 | borderWidth: 1, 118 | }, 119 | feedBackFormContainer: { 120 | flex: 1, 121 | paddingVertical: 10, 122 | paddingHorizontal: 16, 123 | }, 124 | feedBackFormLabel: { 125 | textAlign: 'center', 126 | paddingBottom: 30, 127 | }, 128 | feedBackFormRatingContainer: { 129 | flex: 1, 130 | flexDirection: 'row', 131 | justifyContent: 'space-evenly', 132 | paddingBottom: 27, 133 | }, 134 | FeedBackFormTitle: { 135 | textAlign: 'center', 136 | }, 137 | feedbackInput: { 138 | paddingLeft: 20, 139 | paddingTop: 12, 140 | minHeight: 115, 141 | textAlignVertical: 'top', 142 | borderRadius: 7, 143 | borderWidth: 1, 144 | fontFamily: typography.primary.light, 145 | }, 146 | }); 147 | 148 | export default Feedback; 149 | -------------------------------------------------------------------------------- /app/(app)/home/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { AntDesign, Ionicons } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import { Tabs } from 'expo-router'; 4 | import React from 'react'; 5 | import CogIcon from '../../../assets/artworks/CogIcon'; 6 | import HeaderRight from '../../../components/headers/HeaderRight'; 7 | import MainHeader from '../../../components/headers/MainHeader'; 8 | 9 | export default () => { 10 | const { colors } = useTheme(); 11 | 12 | return ( 13 | 33 | ( 37 | 38 | ), 39 | tabBarLabel: 'Home', 40 | headerTintColor: colors.text, 41 | headerStyle: { 42 | backgroundColor: colors.background, 43 | }, 44 | headerTitleAlign: 'left', 45 | headerTitle: () => , 46 | headerRight: () => , 47 | }} 48 | /> 49 | ( 53 | 54 | ), 55 | tabBarLabel: 'Feed', 56 | headerTintColor: colors.text, 57 | headerStyle: { 58 | backgroundColor: colors.background, 59 | }, 60 | headerTitleAlign: 'left', 61 | headerTitle: () => , 62 | headerRight: () => , 63 | }} 64 | /> 65 | ( 69 | 70 | ), 71 | tabBarLabel: 'Sessions', 72 | headerTintColor: colors.text, 73 | headerStyle: { 74 | backgroundColor: colors.background, 75 | }, 76 | headerTitleAlign: 'left', 77 | headerTitle: () => , 78 | }} 79 | /> 80 | ( 84 | 85 | ), 86 | tabBarLabel: 'About', 87 | headerTintColor: colors.text, 88 | headerStyle: { 89 | backgroundColor: colors.background, 90 | }, 91 | headerTitleAlign: 'left', 92 | headerTitle: () => , 93 | }} 94 | /> 95 | 96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /app/(app)/home/about.tsx: -------------------------------------------------------------------------------- 1 | import { FlashList } from '@shopify/flash-list'; 2 | import { Image } from 'expo-image'; 3 | import { Stack, useRouter } from 'expo-router'; 4 | import React from 'react'; 5 | import { StyleSheet, View } from 'react-native'; 6 | import OrganizerCard from '../../../components/cards/OrganizerCard'; 7 | import OrganizersCard from '../../../components/cards/OrganizersCard'; 8 | import Space from '../../../components/common/Space'; 9 | import StyledText from '../../../components/common/StyledText'; 10 | import MainContainer from '../../../components/container/MainContainer'; 11 | import HeaderRight from '../../../components/headers/HeaderRight'; 12 | import { WIDE_BLURHASH } from '../../../config/constants'; 13 | import type { OrganizingTeamMember } from '../../../global'; 14 | import { usePrefetchedEventData } from '../../../services/api'; 15 | 16 | const About = () => { 17 | const router = useRouter(); 18 | 19 | const { organizers, organizingTeam } = usePrefetchedEventData(); 20 | 21 | const organizingIndividuals = organizingTeam?.data.filter((item: OrganizingTeamMember) => item.type === 'individual'); 22 | 23 | return ( 24 | 25 | , 28 | animation: 'none', 29 | }} 30 | /> 31 | 32 | 38 | 39 | 40 | 41 | About 42 | 43 | 44 | 45 | 46 | 47 | Droidcon is a global conference focused on the engineering of Android applications. Droidcon provides a 48 | forum for developers to network with other developers, share techniques, announce apps and products, and to 49 | learn and teach. 50 | 51 | 52 | 53 | 54 | 55 | This two-day developer focused gathering will be held in Nairobi Kenya on November 8th to 10th 2023 and will 56 | be the largest of its kind in Africa. 57 | 58 | 59 | 60 | 61 | 62 | It will have workshops and codelabs focused on the building of Android applications and will give 63 | participants an excellent chance to learn about the local Android development ecosystem, opportunities and 64 | services as well as meet the engineers and companies who work on them. 65 | 66 | 67 | 68 | 69 | 70 | Organizing Team 71 | 72 | 73 | 74 | 75 | 76 | 77 | ( 81 | 86 | router.push({ pathname: `/${item.name}`, params: { name: item.name, type: 'organizer' } }) 87 | } 88 | /> 89 | )} 90 | keyExtractor={(item: OrganizingTeamMember, index: number) => index.toString()} 91 | estimatedItemSize={50} 92 | /> 93 | 94 | 95 | 96 | {organizers && } 97 | 98 | 99 | 100 | ); 101 | }; 102 | 103 | export default About; 104 | 105 | const styles = StyleSheet.create({ 106 | container: { 107 | flex: 1, 108 | width: '100%', 109 | }, 110 | image: { 111 | width: '100%', 112 | height: 225, 113 | }, 114 | content: { 115 | padding: 16, 116 | }, 117 | listContainer: { 118 | width: '100%', 119 | height: '100%', 120 | paddingHorizontal: 12, 121 | }, 122 | }); 123 | -------------------------------------------------------------------------------- /app/(app)/home/feed/_layout.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-imports */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | import { Slot } from 'expo-router'; 4 | import React from 'react'; 5 | 6 | export const unstable_settings = { 7 | initialRouteName: 'index', 8 | }; 9 | 10 | export default function Layout() { 11 | if (typeof window === 'undefined') return ; 12 | const { BottomSheet } = 13 | require('../../../../bottomsheet/bottom-sheet') as typeof import('../../../../bottomsheet/bottom-sheet'); 14 | 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /app/(app)/home/feed/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | import React from 'react'; 4 | import { ActivityIndicator, StyleSheet, View } from 'react-native'; 5 | import StyledText from '../../../../components/common/StyledText'; 6 | import MainContainer from '../../../../components/container/MainContainer'; 7 | import FeedList from '../../../../components/lists/FeedList'; 8 | import { getEventFeed } from '../../../../services/api'; 9 | 10 | export default function Page() { 11 | const { colors } = useTheme(); 12 | 13 | const { isLoading, data } = useQuery({ queryKey: ['feed'], queryFn: getEventFeed }); 14 | 15 | if (isLoading) { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | return ( 26 | 27 | {data?.data && data?.data.length < 1 ? ( 28 | 29 | This feed is empty for now. 30 | 31 | ) : ( 32 | {data && } 33 | )} 34 | 35 | ); 36 | } 37 | 38 | const styles = StyleSheet.create({ 39 | main: { 40 | flex: 1, 41 | paddingHorizontal: 0, 42 | }, 43 | centered: { 44 | flex: 1, 45 | alignItems: 'center', 46 | justifyContent: 'center', 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /app/(app)/home/feed/share.tsx: -------------------------------------------------------------------------------- 1 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import { Link } from 'expo-router'; 4 | import React from 'react'; 5 | import { Alert, StyleSheet, View } from 'react-native'; 6 | import SocialShareButton from '../../../../components/buttons/SocialShareButton'; 7 | import Row from '../../../../components/common/Row'; 8 | import StyledText from '../../../../components/common/StyledText'; 9 | import BottomSheetContainer from '../../../../components/container/BottomSheetContainer'; 10 | 11 | // TODO: implement share to social media 12 | /** 13 | * TASKS: 14 | * - buttons should share to respective social media platforms. 15 | */ 16 | 17 | export default function Share() { 18 | const { colors } = useTheme(); 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | Share 26 | 27 | 28 | CANCEL 29 | 30 | 31 | 32 | 33 | 34 | { 38 | Alert.alert('Twitter pressed'); 39 | }} 40 | /> 41 | { 45 | Alert.alert('Facebook pressed'); 46 | }} 47 | /> 48 | 49 | 50 | { 54 | Alert.alert('Whatsapp pressed'); 55 | }} 56 | /> 57 | { 61 | Alert.alert('Telegram pressed'); 62 | }} 63 | /> 64 | 65 | 66 | 67 | ); 68 | } 69 | 70 | const styles = StyleSheet.create({ 71 | main: { 72 | padding: 16, 73 | }, 74 | gap: { 75 | width: 8, 76 | }, 77 | content: { 78 | flex: 1, 79 | marginTop: 16, 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /app/(app)/home/main.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React, { useLayoutEffect } from 'react'; 3 | import { ActivityIndicator, StyleSheet, View } from 'react-native'; 4 | import OrganizersCard from '../../../components/cards/OrganizersCard'; 5 | import SponsorsCard from '../../../components/cards/SponsorsCard'; 6 | import Space from '../../../components/common/Space'; 7 | import MainContainer from '../../../components/container/MainContainer'; 8 | import SessionsList from '../../../components/lists/SessionsList'; 9 | import SpeakersList from '../../../components/lists/SpeakersList'; 10 | import VideoPlayer from '../../../components/player/VideoPlayer'; 11 | import { prefetchEvent, usePrefetchedEventData } from '../../../services/api'; 12 | 13 | const Main = () => { 14 | const { colors } = useTheme(); 15 | 16 | useLayoutEffect(() => { 17 | prefetchEvent(); 18 | }, []); 19 | 20 | const { sessions, speakers, sponsors, organizers } = usePrefetchedEventData(); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {sessions && sessions.data.length > 0 ? ( 34 | 35 | ) : ( 36 | 37 | )} 38 | 39 | 40 | 41 | {speakers && speakers.data.length > 0 ? ( 42 | 43 | ) : ( 44 | 45 | )} 46 | 47 | {sponsors && sponsors.data.length > 1 && ( 48 | <> 49 | 50 | 51 | 52 | 53 | )} 54 | 55 | {organizers && } 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default Main; 64 | 65 | const styles = StyleSheet.create({ 66 | main: { 67 | width: '100%', 68 | alignItems: 'center', 69 | }, 70 | section: { 71 | flex: 1, 72 | }, 73 | image: { 74 | height: 175, 75 | width: '100%', 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /app/(app)/home/sessions.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { Stack } from 'expo-router'; 3 | import React, { useState } from 'react'; 4 | import { StyleSheet, View } from 'react-native'; 5 | import DayButton from '../../../components/buttons/DayButton'; 6 | import CustomSwitch from '../../../components/buttons/StyledSwitch'; 7 | import Row from '../../../components/common/Row'; 8 | import Space from '../../../components/common/Space'; 9 | import StyledText from '../../../components/common/StyledText'; 10 | import MainContainer from '../../../components/container/MainContainer'; 11 | import HeaderActionRight from '../../../components/headers/HeaderActionRight'; 12 | import SessionsListVertical from '../../../components/lists/SessionsListVertical'; 13 | import FilterModal from '../../../components/modals/FilterModal'; 14 | import type { IDateForDayButton, SessionForSchedule } from '../../../global'; 15 | import { usePrefetchedEventData } from '../../../services/api'; 16 | import { getDaysFromSchedule } from '../../../util/helpers'; 17 | 18 | const Sessions = () => { 19 | const { colors } = useTheme(); 20 | 21 | const [showsBookmarked, setShowsBookmarked] = useState(false); 22 | const [listVisible, setListVisible] = useState(true); 23 | 24 | const { schedule: scheduleData } = usePrefetchedEventData(); 25 | 26 | const dates = scheduleData && getDaysFromSchedule(scheduleData); 27 | const [selectedDate, setSelectedDate] = useState((dates && dates[0]?.key) ?? ''); 28 | 29 | const toggleSwitch = () => setShowsBookmarked((previousState) => !previousState); 30 | const toggleView = () => setListVisible((previousState) => !previousState); 31 | 32 | const handleDayButtonPress = (dayButtonKey: string) => setSelectedDate(dayButtonKey); 33 | 34 | const handleBookMark = (id: number) => { 35 | console.log(id); 36 | // TODO: bookmark a session here 37 | }; 38 | 39 | const [filterModalVisible, setFilterModalVisible] = useState(false); 40 | 41 | const showFilterModal = () => { 42 | setFilterModalVisible(true); 43 | }; 44 | 45 | const handleFilter = () => { 46 | // TODO: handle filter sessions here 47 | }; 48 | 49 | return ( 50 | 51 | ( 54 | 55 | ), 56 | }} 57 | /> 58 | 59 | 60 | 61 | 62 | 63 | {dates?.map((item: IDateForDayButton) => ( 64 | 65 | 72 | 73 | 74 | ))} 75 | 76 | 77 | 90 | 91 | 92 | {showsBookmarked ? 'My Sessions' : 'All Sessions'} 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | item?.is_bookmarked === true) 108 | : scheduleData?.data[selectedDate] 109 | } 110 | /> 111 | 112 | 113 | 114 | setFilterModalVisible(false)} 117 | onFilter={handleFilter} 118 | /> 119 | 120 | 121 | ); 122 | }; 123 | 124 | export default Sessions; 125 | 126 | const styles = StyleSheet.create({ 127 | main: { 128 | flex: 1, 129 | width: '100%', 130 | }, 131 | paddingMain: { 132 | paddingHorizontal: 15, 133 | }, 134 | dayButton: { 135 | borderRadius: 5, 136 | paddingHorizontal: 5, 137 | paddingVertical: 8, 138 | }, 139 | row: { 140 | alignItems: 'center', 141 | }, 142 | column: { 143 | flexDirection: 'column', 144 | }, 145 | separator: { 146 | borderWidth: 1, 147 | width: '100%', 148 | }, 149 | }); 150 | -------------------------------------------------------------------------------- /app/(app)/index.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect, useRootNavigationState } from 'expo-router'; 2 | 3 | export default function Page() { 4 | const rootNavigationState = useRootNavigationState(); 5 | 6 | if (!rootNavigationState?.key) return null; 7 | 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /app/(app)/session/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { Stack } from 'expo-router'; 3 | 4 | export default () => { 5 | const { colors } = useTheme(); 6 | 7 | return ( 8 | 9 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /app/(app)/speakers.tsx: -------------------------------------------------------------------------------- 1 | import { AntDesign } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import { FlashList } from '@shopify/flash-list'; 4 | import { Stack, useRouter } from 'expo-router'; 5 | import React from 'react'; 6 | import { StyleSheet, View } from 'react-native'; 7 | import SpeakerCard from '../../components/cards/SpeakerCard'; 8 | import Space from '../../components/common/Space'; 9 | import MainContainer from '../../components/container/MainContainer'; 10 | import type { ISessions, ISpeaker, Session, Speaker } from '../../global'; 11 | import { usePrefetchedEventData } from '../../services/api'; 12 | 13 | interface SpeakerItem extends Speaker { 14 | sessions: Array; 15 | } 16 | 17 | const getSpeakerSessions = (sessions: ISessions, speakers: ISpeaker) => { 18 | const speakerArray = [...speakers?.data] as Array; 19 | 20 | speakerArray.map((speaker) => { 21 | speaker.sessions = sessions?.data.filter((session) => 22 | session.speakers.some((_speaker) => _speaker.name === speaker.name), 23 | ); 24 | }); 25 | return speakerArray; 26 | }; 27 | 28 | const SpeakersPage = () => { 29 | const { colors } = useTheme(); 30 | const router = useRouter(); 31 | 32 | const { sessions, speakers } = usePrefetchedEventData(); 33 | 34 | const data = (sessions && speakers && getSpeakerSessions(sessions, speakers)) ?? []; 35 | 36 | return ( 37 | 38 | router.back()} />, 42 | }} 43 | /> 44 | 45 | 46 | 47 | 48 | 49 | } 52 | keyExtractor={(_, index: number) => index.toString()} 53 | numColumns={2} 54 | estimatedItemSize={100} 55 | /> 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default SpeakersPage; 64 | 65 | const styles = StyleSheet.create({ 66 | main: { 67 | flex: 1, 68 | paddingHorizontal: 10, 69 | }, 70 | listContainer: { 71 | flex: 1, 72 | width: '100%', 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; 2 | import { QueryClientProvider } from '@tanstack/react-query'; 3 | import * as Font from 'expo-font'; 4 | import { Slot, SplashScreen } from 'expo-router'; 5 | import React, { useEffect, useLayoutEffect, useState } from 'react'; 6 | import type { ColorSchemeName } from 'react-native'; 7 | import { Appearance } from 'react-native'; 8 | import { theme_colors } from '../config/theme'; 9 | import { customFontsToLoad } from '../config/typography'; 10 | import { queryClient } from '../services/api/react-query'; 11 | 12 | type Theme = { 13 | mode: ColorSchemeName; 14 | system: boolean; 15 | }; 16 | 17 | SplashScreen.preventAutoHideAsync(); 18 | 19 | export default () => { 20 | const [isReady, setIsReady] = useState(false); 21 | const [theme, setTheme] = useState({ mode: Appearance.getColorScheme() }); 22 | 23 | useEffect(() => { 24 | const updateTheme = (newTheme: Theme) => { 25 | let mode: ColorSchemeName; 26 | if (!newTheme) { 27 | mode = theme.mode === 'dark' ? 'light' : 'dark'; 28 | newTheme = { mode, system: false }; 29 | } else { 30 | if (newTheme.system) { 31 | mode = Appearance.getColorScheme(); 32 | newTheme = { ...newTheme, mode }; 33 | } else { 34 | newTheme = { ...newTheme, system: false }; 35 | } 36 | } 37 | setTheme(newTheme); 38 | }; 39 | 40 | // if the theme of the device changes, update the theme 41 | Appearance.addChangeListener(({ colorScheme }) => { 42 | updateTheme({ mode: colorScheme, system: true }); 43 | setTheme({ mode: colorScheme }); 44 | }); 45 | }, [theme.mode]); 46 | 47 | const _lightTheme = { 48 | ...DefaultTheme, 49 | colors: { 50 | ...DefaultTheme.colors, 51 | ...theme_colors.light, 52 | }, 53 | }; 54 | 55 | const _darkTheme = { 56 | ...DarkTheme, 57 | colors: { 58 | ...DarkTheme.colors, 59 | ...theme_colors.dark, 60 | }, 61 | }; 62 | 63 | useEffect(() => { 64 | async function prepare() { 65 | try { 66 | await Font.loadAsync(customFontsToLoad); 67 | } catch (e) { 68 | console.warn(e); 69 | } finally { 70 | setIsReady(true); 71 | } 72 | } 73 | 74 | prepare(); 75 | }, []); 76 | 77 | useLayoutEffect(() => { 78 | if (isReady) { 79 | setTimeout(() => { 80 | SplashScreen.hideAsync(); 81 | }, 3000); 82 | } 83 | }, [isReady]); 84 | 85 | if (!isReady) return null; 86 | 87 | return ( 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /assets/artworks/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Svg, { G, Path } from 'react-native-svg'; 3 | import type { ISvgProps } from '../../global'; 4 | 5 | const Arrow = (props: ISvgProps) => ( 6 | 7 | 8 | 9 | 14 | 15 | 16 | ); 17 | 18 | export default Arrow; 19 | -------------------------------------------------------------------------------- /assets/artworks/CogIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Svg, { G, Path } from 'react-native-svg'; 3 | import type { ISvgProps } from '../../global'; 4 | 5 | const CogDark = (props: ISvgProps) => ( 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | ); 18 | 19 | export default CogDark; 20 | -------------------------------------------------------------------------------- /assets/artworks/FeedBackBanner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import { StyleSheet, View } from 'react-native'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { ClipPath, Defs, G, Path } from 'react-native-svg'; 6 | 7 | export interface ISvgProps extends SvgProps { 8 | xmlns?: string; 9 | xmlnsXlink?: string; 10 | xmlSpace?: string; 11 | } 12 | 13 | const FeedBackBanner = (props: ISvgProps) => { 14 | const { colors } = useTheme(); 15 | const originalWidth = 412; 16 | const originalHeight = 179; 17 | const aspectRatio = originalWidth / originalHeight; 18 | return ( 19 | 20 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | const styles = (aspectRatio: number) => { 63 | return StyleSheet.create({ 64 | svgContainer: { 65 | width: '100%', 66 | aspectRatio, 67 | }, 68 | }); 69 | }; 70 | 71 | export default FeedBackBanner; 72 | -------------------------------------------------------------------------------- /assets/artworks/GoogleIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Svg, { ClipPath, Defs, Path, Use } from 'react-native-svg'; 3 | import type { ISvgProps } from '../../global'; 4 | 5 | const GoogleIcon = (props: ISvgProps) => ( 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | export default GoogleIcon; 24 | -------------------------------------------------------------------------------- /assets/artworks/ListSeparator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Ellipse, G, Line } from 'react-native-svg'; 3 | import type { ISvgProps } from '../../global'; 4 | 5 | const SessionListSeparator = (props: ISvgProps) => ( 6 | 7 | 8 | 17 | 27 | 28 | 29 | ); 30 | 31 | export default SessionListSeparator; 32 | -------------------------------------------------------------------------------- /assets/artworks/Logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Svg, { ClipPath, Defs, G, Path } from 'react-native-svg'; 3 | import type { ISvgProps } from '../../global'; 4 | 5 | const Logo = (props: ISvgProps) => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 24 | 29 | 34 | 35 | 40 | 45 | 50 | 55 | 60 | 61 | 62 | 63 | ); 64 | 65 | export default Logo; 66 | -------------------------------------------------------------------------------- /assets/artworks/LogoDark.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Svg, { ClipPath, Defs, G, Path, Text, TSpan } from 'react-native-svg'; 3 | import type { ISvgProps } from '../../global'; 4 | 5 | const LogoDark = (props: ISvgProps) => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 24 | 29 | 34 | 35 | 40 | 45 | 50 | 55 | 62 | 63 | {'K'} 64 | 65 | {'e.'} 66 | 67 | 68 | 69 | 70 | ); 71 | 72 | export default LogoDark; 73 | -------------------------------------------------------------------------------- /assets/artworks/StarIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Svg, { G, Path } from 'react-native-svg'; 3 | import type { ISvgProps } from '../../global'; 4 | 5 | const StarIcon = (props: ISvgProps) => ( 6 | 14 | 15 | 16 | 17 | 22 | 28 | 29 | 30 | 31 | ); 32 | 33 | export default StarIcon; 34 | -------------------------------------------------------------------------------- /assets/artworks/Vector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Svg, { Circle, Defs, G, LinearGradient, Path, Rect, Stop } from 'react-native-svg'; 3 | import type { ISvgProps } from '../../global'; 4 | 5 | const Vector = (props: ISvgProps) => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | export default Vector; 51 | -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/fonts/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/fonts/Montserrat-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/fonts/Montserrat-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/fonts/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/fonts/Montserrat-SemiBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Rubik-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/fonts/Rubik-Light.ttf -------------------------------------------------------------------------------- /assets/images/about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/about.jpg -------------------------------------------------------------------------------- /assets/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/banner.png -------------------------------------------------------------------------------- /assets/images/bannerDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/bannerDark.png -------------------------------------------------------------------------------- /assets/images/bannerLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/bannerLight.png -------------------------------------------------------------------------------- /assets/images/confetti.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/confetti.gif -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/speaker-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/speaker-dark.png -------------------------------------------------------------------------------- /assets/images/speaker-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/speaker-light.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidconKE/droidconKE2023ReactNative/d312ffbd0a1016e37b55850ffd2e211762d4e641/assets/images/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['react-native-reanimated/plugin', 'expo-router/babel'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /bottomsheet/bottom-sheet.tsx: -------------------------------------------------------------------------------- 1 | import type { EventMapBase, NavigationState } from '@react-navigation/native'; 2 | import type { BottomSheetNavigationOptions } from '@th3rdwave/react-navigation-bottom-sheet'; 3 | import { createBottomSheetNavigator } from '@th3rdwave/react-navigation-bottom-sheet'; 4 | 5 | import { withLayoutContext } from 'expo-router'; 6 | 7 | const { Navigator } = createBottomSheetNavigator(); 8 | 9 | export const BottomSheet = withLayoutContext< 10 | BottomSheetNavigationOptions, 11 | typeof Navigator, 12 | NavigationState, 13 | EventMapBase 14 | >(Navigator); 15 | -------------------------------------------------------------------------------- /components/buttons/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import { Image, StyleSheet } from 'react-native'; 4 | 5 | type AvatarProps = { 6 | bordered?: boolean; 7 | }; 8 | 9 | /** 10 | * @param param bordered - will add a border to the child image 11 | * TODO: add image prop that will be used as the child image 12 | */ 13 | 14 | const Avatar = ({ bordered }: AvatarProps) => { 15 | const { colors } = useTheme(); 16 | return ( 17 | 23 | ); 24 | }; 25 | 26 | export default Avatar; 27 | 28 | const styles = StyleSheet.create({ 29 | avatar: { 30 | width: 40, 31 | height: 40, 32 | borderRadius: 20, 33 | borderWidth: 1, 34 | alignItems: 'center', 35 | justifyContent: 'center', 36 | marginLeft: 8, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /components/buttons/BackNavigationButton.tsx: -------------------------------------------------------------------------------- 1 | import { AntDesign } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import React from 'react'; 4 | import { Pressable, StyleSheet } from 'react-native'; 5 | import StyledText from '../common/StyledText'; 6 | 7 | type BackNavigationButtonProps = { 8 | text: string; 9 | onPress: () => void; 10 | }; 11 | 12 | const BackNavigationButton = ({ text, onPress }: BackNavigationButtonProps) => { 13 | const { colors } = useTheme(); 14 | return ( 15 | 16 | 17 | 18 | {text} 19 | 20 | 21 | ); 22 | }; 23 | 24 | const styles = StyleSheet.create({ 25 | headerTitle: { 26 | paddingLeft: 23, 27 | flexDirection: 'row', 28 | columnGap: 22, 29 | position: 'absolute', 30 | width: '100%', 31 | paddingTop: 26, 32 | paddingBottom: 20, 33 | }, 34 | }); 35 | 36 | export default BackNavigationButton; 37 | -------------------------------------------------------------------------------- /components/buttons/ClearButton.tsx: -------------------------------------------------------------------------------- 1 | import { AntDesign } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import React from 'react'; 4 | import { StyleSheet, TouchableOpacity, View } from 'react-native'; 5 | import StyledText from '../common/StyledText'; 6 | 7 | type Props = { 8 | onPress: () => void; 9 | label: string; 10 | iconName: keyof typeof AntDesign.glyphMap; 11 | }; 12 | 13 | const ClearButton = ({ onPress, label, iconName }: Props) => { 14 | const { colors } = useTheme(); 15 | return ( 16 | 17 | 18 | {label} 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default ClearButton; 27 | 28 | const styles = StyleSheet.create({ 29 | button: { 30 | flexDirection: 'row', 31 | alignItems: 'center', 32 | justifyContent: 'space-between', 33 | paddingHorizontal: 8, 34 | height: 36, 35 | borderRadius: 8, 36 | }, 37 | spacer: { 38 | width: 8, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /components/buttons/DayButton.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { Pressable, StyleSheet } from 'react-native'; 3 | import Space from '../common/Space'; 4 | import StyledText from '../common/StyledText'; 5 | 6 | type DayButtonProps = { 7 | handlePress: (key: string) => void; 8 | date: string; 9 | day: string; 10 | dateInfull: string; 11 | selected?: boolean; 12 | }; 13 | 14 | /** 15 | * @returns DayButton component 16 | * @param handlePress: () => void 17 | * @param date: string 18 | * @param day: string 19 | * @param dateInfull: string; 20 | * @param selected: boolean 21 | */ 22 | 23 | const DayButton = (props: DayButtonProps) => { 24 | const { handlePress, date, day, dateInfull, selected } = props; 25 | 26 | const { colors } = useTheme(); 27 | return ( 28 | handlePress(dateInfull)} 30 | style={[styles.dayButton, { backgroundColor: selected ? colors.tertiary : colors.tertiaryTint }]} 31 | testID="dayButton" 32 | > 33 | 34 | {date} 35 | 36 | 37 | 38 | {day} 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default DayButton; 45 | 46 | const styles = StyleSheet.create({ 47 | dayButton: { 48 | borderRadius: 5, 49 | paddingHorizontal: 5, 50 | paddingVertical: 8, 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /components/buttons/FeedBackRatingButton.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import { Pressable, StyleSheet, Text } from 'react-native'; 4 | import type { TypeRatingStates } from '../../app/(app)/feedback'; 5 | import StyledText from '../common/StyledText'; 6 | 7 | type Props = { 8 | onPress: (value: number) => void; 9 | rating: TypeRatingStates; 10 | onSelected: boolean; 11 | testID?: string; 12 | }; 13 | 14 | const FeedBackRatingButton = ({ onPress, rating, onSelected }: Props) => { 15 | const { colors } = useTheme(); 16 | const { icon, text, value } = rating; 17 | 18 | return ( 19 | onPress(value)} 26 | > 27 | {icon} 28 | 29 | {text} 30 | 31 | 32 | ); 33 | }; 34 | const styles = StyleSheet.create({ 35 | formRatingText: { 36 | fontSize: 30, 37 | }, 38 | pressableEmoji: { 39 | minWidth: 67, 40 | minHeight: 67, 41 | justifyContent: 'center', 42 | alignItems: 'center', 43 | borderRadius: 4, 44 | borderWidth: 1, 45 | }, 46 | }); 47 | 48 | export default FeedBackRatingButton; 49 | -------------------------------------------------------------------------------- /components/buttons/FeedbackButton.tsx: -------------------------------------------------------------------------------- 1 | import { Feather } from '@expo/vector-icons'; 2 | import React from 'react'; 3 | import { StyleSheet, TouchableOpacity, View } from 'react-native'; 4 | import Arrow from '../../assets/artworks/Arrow'; 5 | import StyledText from '../common/StyledText'; 6 | 7 | type Props = { 8 | onPress: () => void; 9 | testID?: string; 10 | }; 11 | 12 | const FeedbackButton = ({ onPress, testID }: Props) => { 13 | return ( 14 | 15 | 16 | 17 | Feedback 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default FeedbackButton; 25 | 26 | const styles = StyleSheet.create({ 27 | button: { 28 | backgroundColor: '#00e2c350', 29 | flexDirection: 'row', 30 | alignItems: 'center', 31 | justifyContent: 'space-between', 32 | paddingHorizontal: 8, 33 | height: 36, 34 | borderRadius: 8, 35 | }, 36 | spacer: { 37 | width: 8, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /components/buttons/FilterButton.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import type { StyleProp, ViewStyle } from 'react-native'; 4 | import { Pressable, StyleSheet } from 'react-native'; 5 | import StyledText from '../common/StyledText'; 6 | 7 | type ButtonProps = { 8 | label: string; 9 | isCenter?: boolean; 10 | onPress: () => void; 11 | selected: boolean; 12 | style?: StyleProp; 13 | }; 14 | 15 | const FilterButton = ({ label, isCenter, onPress, selected, style }: ButtonProps) => { 16 | const { colors, dark } = useTheme(); 17 | 18 | return ( 19 | 29 | 30 | {label} 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default FilterButton; 37 | 38 | const styles = StyleSheet.create({ 39 | button: { 40 | flex: 1, 41 | alignItems: 'center', 42 | height: '100%', 43 | justifyContent: 'center', 44 | }, 45 | centerButton: { 46 | borderLeftWidth: 1, 47 | borderRightWidth: 1, 48 | borderRadius: 0, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /components/buttons/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import { MaterialIcons } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import React from 'react'; 4 | import { StyleSheet, TouchableOpacity } from 'react-native'; 5 | 6 | type Props = { 7 | onPress: () => void; 8 | name: keyof typeof MaterialIcons.glyphMap; 9 | isActive: boolean; 10 | }; 11 | 12 | const IconButton = ({ onPress, name, isActive }: Props) => { 13 | const { colors } = useTheme(); 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default IconButton; 22 | 23 | const styles = StyleSheet.create({ 24 | button: { 25 | alignItems: 'center', 26 | justifyContent: 'center', 27 | paddingHorizontal: 8, 28 | height: 36, 29 | borderRadius: 8, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /components/buttons/PrimaryButton.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import { StyleSheet, TouchableOpacity } from 'react-native'; 4 | import StyledText from '../common/StyledText'; 5 | 6 | type PrimaryButtonProps = { 7 | onPress: () => void; 8 | label: string; 9 | }; 10 | 11 | const PrimaryButton = ({ onPress, label }: PrimaryButtonProps) => { 12 | const { colors } = useTheme(); 13 | return ( 14 | 19 | 20 | {label} 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default PrimaryButton; 27 | 28 | const styles = StyleSheet.create({ 29 | button: { 30 | width: '100%', 31 | alignItems: 'center', 32 | justifyContent: 'center', 33 | height: 45, 34 | borderRadius: 8, 35 | }, 36 | buttonText: { 37 | textTransform: 'uppercase', 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /components/buttons/SocialShareButton.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesome } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import React from 'react'; 4 | import { StyleSheet, TouchableOpacity, View } from 'react-native'; 5 | import StyledText from '../common/StyledText'; 6 | 7 | type ButtonProps = { 8 | handlePress: () => void; 9 | title: string; 10 | iconName: keyof typeof FontAwesome.glyphMap; 11 | }; 12 | 13 | const SocialShareButton = ({ handlePress, title, iconName }: ButtonProps) => { 14 | const { colors } = useTheme(); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | {title} 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default SocialShareButton; 28 | 29 | const styles = StyleSheet.create({ 30 | button: { 31 | flexDirection: 'row', 32 | alignItems: 'center', 33 | justifyContent: 'space-evenly', 34 | padding: 8, 35 | height: 48, 36 | width: 150, 37 | borderRadius: 8, 38 | borderColor: '#00e2c3', 39 | borderWidth: 1, 40 | marginBottom: 8, 41 | }, 42 | gap: { 43 | width: 8, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /components/buttons/StyledSwitch.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native'; 3 | import StarIcon from '../../assets/artworks/StarIcon'; 4 | 5 | type StyledSwitchProps = { 6 | trackColor: { 7 | false: string; 8 | true: string; 9 | }; 10 | thumbColor: string; 11 | iconColor: { 12 | false: string; 13 | true: string; 14 | }; 15 | onValueChange: () => void; 16 | value: boolean; 17 | }; 18 | const StyledSwitch = (props: StyledSwitchProps) => { 19 | const { trackColor, thumbColor, iconColor, onValueChange, value } = props; 20 | 21 | const [switchAnimation] = useState(new Animated.Value(0)); 22 | 23 | const handleSwitch = () => { 24 | onValueChange(); 25 | Animated.spring(switchAnimation, { 26 | toValue: value ? 0 : 1, 27 | useNativeDriver: false, 28 | }).start(); 29 | }; 30 | 31 | const translateX = switchAnimation.interpolate({ 32 | inputRange: [0, 1], 33 | outputRange: [0, 30], 34 | }); 35 | 36 | return ( 37 | 38 | 47 | 57 | 63 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default StyledSwitch; 70 | 71 | const styles = StyleSheet.create({ 72 | track: { 73 | width: 55, 74 | height: 20, 75 | borderRadius: 30, 76 | justifyContent: 'center', 77 | }, 78 | thumb: { 79 | width: 25, 80 | height: 25, 81 | borderRadius: 12.5, 82 | alignItems: 'center', 83 | justifyContent: 'center', 84 | elevation: 1, 85 | shadowOpacity: 0.1, 86 | shadowRadius: 1, 87 | shadowOffset: { 88 | height: 0, 89 | width: 0, 90 | }, 91 | }, 92 | }); 93 | -------------------------------------------------------------------------------- /components/buttons/SubmitFeedbackButton.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import { Pressable, StyleSheet } from 'react-native'; 4 | import StyledText from '../common/StyledText'; 5 | 6 | type Props = { 7 | text: string; 8 | handlePress: () => void; 9 | }; 10 | 11 | const SubmitFeedbackButton = ({ handlePress, text }: Props) => { 12 | const { colors } = useTheme(); 13 | return ( 14 | 15 | 16 | {text} 17 | 18 | 19 | ); 20 | }; 21 | 22 | const styles = StyleSheet.create({ 23 | pressableSubmit: { 24 | justifyContent: 'center', 25 | alignItems: 'center', 26 | borderRadius: 10, 27 | height: 45, 28 | }, 29 | }); 30 | 31 | export default SubmitFeedbackButton; 32 | -------------------------------------------------------------------------------- /components/buttons/ViewAllButton.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { StyleSheet, TouchableOpacity, View } from 'react-native'; 3 | import StyledText from '../common/StyledText'; 4 | 5 | type ViewAllButtonProps = { 6 | onPress: () => void; 7 | label: string; 8 | }; 9 | 10 | /** 11 | * @returns Button component. 12 | * @param onPress: function - Navigate to the screeen to view all. 13 | * @param label: string - Label showing on the button. 14 | */ 15 | 16 | const ViewAllButton = ({ onPress, label }: ViewAllButtonProps) => { 17 | const { colors } = useTheme(); 18 | 19 | const tallyContainerTint = { 20 | backgroundColor: colors.secondaryTint, 21 | }; 22 | 23 | return ( 24 | 25 | 26 | View All 27 | 28 | 29 | 30 | 31 | {label} 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default ViewAllButton; 39 | 40 | const styles = StyleSheet.create({ 41 | row: { 42 | flexDirection: 'row', 43 | alignItems: 'center', 44 | justifyContent: 'space-between', 45 | }, 46 | tallyContainer: { 47 | borderRadius: 11, 48 | paddingVertical: 5, 49 | paddingHorizontal: 10, 50 | }, 51 | gap: { 52 | width: 8, 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /components/cards/CallForSpeakersCard.tsx: -------------------------------------------------------------------------------- 1 | import { AntDesign } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import React from 'react'; 4 | import { StyleSheet, View } from 'react-native'; 5 | import Vector from '../../assets/artworks/Vector'; 6 | import Row from '../common/Row'; 7 | import StyledText from '../common/StyledText'; 8 | 9 | const CallForSpeakersCard = () => { 10 | const { colors } = useTheme(); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | Call for Speakers 19 | 20 | 21 | Apply to be a speaker 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default CallForSpeakersCard; 31 | 32 | const styles = StyleSheet.create({ 33 | card: { 34 | width: '90%', 35 | borderRadius: 8, 36 | paddingHorizontal: 18, 37 | paddingVertical: 24, 38 | }, 39 | title: { 40 | color: '#fff', 41 | }, 42 | smallText: { 43 | color: '#000', 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /components/cards/OrganizerCard.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { Image } from 'expo-image'; 3 | import { memo } from 'react'; 4 | import { Pressable, StyleSheet, View } from 'react-native'; 5 | import { truncate } from '../../util/helpers'; 6 | import StyledText from '../common/StyledText'; 7 | 8 | type OrganizerCardProps = { 9 | name: string; 10 | photo: string; 11 | tagline?: string; 12 | handlePress: () => void; 13 | }; 14 | 15 | const OrganizerCard = ({ name, photo, tagline, handlePress }: OrganizerCardProps) => { 16 | const { colors } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {name} 27 | 28 | 29 | 30 | {tagline && ( 31 | 32 | {truncate(50, tagline)} 33 | 34 | )} 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default memo(OrganizerCard); 41 | 42 | const styles = StyleSheet.create({ 43 | item: { 44 | marginHorizontal: 8, 45 | marginBottom: 8, 46 | alignItems: 'center', 47 | width: 108, 48 | height: 200, 49 | paddingVertical: 4, 50 | }, 51 | pressable: { 52 | width: '100%', 53 | height: 100, 54 | borderRadius: 12, 55 | borderWidth: 2, 56 | }, 57 | textContainer: { 58 | flex: 1, 59 | alignItems: 'center', 60 | paddingHorizontal: 4, 61 | }, 62 | avatar: { 63 | width: '100%', 64 | height: '100%', 65 | borderRadius: 10, 66 | }, 67 | title: { 68 | marginTop: 4, 69 | height: 40, 70 | textAlign: 'center', 71 | }, 72 | name: { 73 | textAlign: 'center', 74 | }, 75 | description: { 76 | flex: 1, 77 | textAlign: 'center', 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /components/cards/OrganizersCard.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { Image } from 'expo-image'; 3 | import React from 'react'; 4 | import { StyleSheet, View } from 'react-native'; 5 | import type { IOrganizers } from '../../global'; 6 | import Space from '../common/Space'; 7 | import StyledText from '../common/StyledText'; 8 | 9 | type Props = { 10 | organizers: IOrganizers; 11 | }; 12 | 13 | const OrganizersCard = ({ organizers }: Props) => { 14 | const { colors } = useTheme(); 15 | 16 | const renderOrganizers = () => { 17 | return organizers?.data.map((organizer, index) => ( 18 | 19 | )); 20 | }; 21 | 22 | return ( 23 | 24 | 25 | Organised by: 26 | 27 | 28 | {renderOrganizers()} 29 | 30 | ); 31 | }; 32 | 33 | export default OrganizersCard; 34 | 35 | const styles = StyleSheet.create({ 36 | card: { 37 | flex: 1, 38 | width: '90%', 39 | alignItems: 'center', 40 | justifyContent: 'center', 41 | paddingHorizontal: 8, 42 | paddingVertical: 16, 43 | borderRadius: 16, 44 | }, 45 | row: { 46 | flexDirection: 'row', 47 | alignItems: 'center', 48 | justifyContent: 'space-evenly', 49 | flexWrap: 'wrap', 50 | marginVertical: 4, 51 | }, 52 | logo: { 53 | width: '30%', 54 | height: 60, 55 | marginHorizontal: 4, 56 | marginVertical: 4, 57 | borderRadius: 8, 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /components/cards/SpeakerCard.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { Image } from 'expo-image'; 3 | import { Link, useRouter } from 'expo-router'; 4 | import React from 'react'; 5 | import { Dimensions, StyleSheet, TouchableOpacity, View } from 'react-native'; 6 | import type { Session, Speaker } from '../../global'; 7 | import { truncate } from '../../util/helpers'; 8 | import Space from '../common/Space'; 9 | import StyledText from '../common/StyledText'; 10 | 11 | const { width } = Dimensions.get('window'); 12 | 13 | interface SpeakerCardProps extends Speaker { 14 | sessions: Array; 15 | } 16 | 17 | const SpeakerCard = (props: SpeakerCardProps) => { 18 | const { colors } = useTheme(); 19 | const router = useRouter(); 20 | 21 | const { name, avatar, tagline, sessions } = props; 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {name} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {truncate(60, tagline)} 41 | 42 | 43 | 44 | 45 | 46 | {sessions.length > 0 && ( 47 | 50 | router.push({ 51 | pathname: `/session/${sessions[0]?.slug}`, 52 | params: { slug: sessions[0]?.slug ?? '' }, 53 | }) 54 | } 55 | testID="sessionButton" 56 | > 57 | 58 | Session 59 | 60 | 61 | )} 62 | 63 | 64 | ); 65 | }; 66 | 67 | export default SpeakerCard; 68 | 69 | const styles = StyleSheet.create({ 70 | card: { 71 | width: width / 2 - 24, 72 | height: 280, 73 | borderRadius: 10, 74 | paddingVertical: 16, 75 | paddingHorizontal: 8, 76 | margin: 8, 77 | alignItems: 'center', 78 | }, 79 | image: { 80 | width: 108, 81 | height: 108, 82 | borderRadius: 10, 83 | borderWidth: 2, 84 | }, 85 | details: { 86 | width: '100%', 87 | flex: 1, 88 | alignItems: 'center', 89 | }, 90 | tagline: { 91 | width: '100%', 92 | flex: 1, 93 | height: 40, 94 | }, 95 | text: { 96 | textAlign: 'center', 97 | }, 98 | bottom: { 99 | width: '100%', 100 | alignItems: 'center', 101 | flex: 1, 102 | }, 103 | button: { 104 | width: '90%', 105 | alignItems: 'center', 106 | justifyContent: 'center', 107 | height: 45, 108 | borderRadius: 8, 109 | borderWidth: 2, 110 | }, 111 | buttonText: { 112 | textTransform: 'uppercase', 113 | }, 114 | }); 115 | -------------------------------------------------------------------------------- /components/cards/SponsorsCard.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { Image } from 'expo-image'; 3 | import React from 'react'; 4 | import { StyleSheet, View } from 'react-native'; 5 | import type { ISponsor, ISponsors } from '../../global'; 6 | import Space from '../common/Space'; 7 | import StyledText from '../common/StyledText'; 8 | 9 | type Props = { 10 | sponsors: ISponsors; 11 | }; 12 | 13 | const SponsorsCard = ({ sponsors }: Props) => { 14 | const { colors } = useTheme(); 15 | 16 | const renderSponsors = (sponsorType: string) => { 17 | return sponsors?.data 18 | .filter((sponsor: ISponsor) => sponsor.sponsor_type === sponsorType) 19 | .map((sponsor, index) => ( 20 | 27 | )); 28 | }; 29 | 30 | return ( 31 | 32 | 33 | Sponsors 34 | 35 | 36 | {renderSponsors('platinum')} 37 | {renderSponsors('gold')} 38 | {renderSponsors('silver')} 39 | {renderSponsors('bronze')} 40 | {renderSponsors('startup')} 41 | {renderSponsors('swag')} 42 | 43 | ); 44 | }; 45 | 46 | export default SponsorsCard; 47 | 48 | const styles = StyleSheet.create({ 49 | card: { 50 | flex: 1, 51 | width: '90%', 52 | alignItems: 'center', 53 | justifyContent: 'center', 54 | paddingHorizontal: 8, 55 | paddingVertical: 16, 56 | borderRadius: 16, 57 | gap: 4, 58 | }, 59 | sponsorRow: { 60 | flexDirection: 'row', 61 | alignItems: 'center', 62 | justifyContent: 'space-evenly', 63 | flexWrap: 'wrap', 64 | }, 65 | goldLogo: { 66 | width: '50%', 67 | height: 60, 68 | marginHorizontal: 4, 69 | borderRadius: 8, 70 | }, 71 | logo: { 72 | width: '30%', 73 | height: 60, 74 | marginHorizontal: 4, 75 | borderRadius: 8, 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /components/common/Row.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { StyleProp, ViewStyle } from 'react-native'; 3 | import { StyleSheet, View } from 'react-native'; 4 | 5 | type RowProps = { 6 | children: React.ReactNode; 7 | style?: StyleProp; 8 | }; 9 | 10 | const Row = ({ children, style, testID = 'row' }: RowProps & View['props']) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export default Row; 19 | 20 | const styles = StyleSheet.create({ 21 | row: { 22 | flexDirection: 'row', 23 | justifyContent: 'space-between', 24 | alignItems: 'center', 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /components/common/Space.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | 4 | type SpaceProps = { 5 | size: number; 6 | horizontal?: boolean; 7 | }; 8 | 9 | const Space = ({ size, horizontal = false }: SpaceProps) => { 10 | const horizontalStyle = { width: size }; 11 | const verticalStyle = { height: size }; 12 | 13 | return ; 14 | }; 15 | 16 | export default Space; 17 | -------------------------------------------------------------------------------- /components/common/StyledText.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import type { StyleProp, TextStyle } from 'react-native'; 4 | import { Text as NativeText, StyleSheet } from 'react-native'; 5 | import { typography } from '../../config/typography'; 6 | 7 | type StyledTextProps = { 8 | children: React.ReactNode; 9 | size?: 'xs' | 'sm' | 'base' | 'md' | 'lg' | 'xl'; 10 | font?: 'bold' | 'regular' | 'medium' | 'semiBold' | 'light'; 11 | variant?: 'text' | 'link' | 'primary' | 'secondary'; 12 | style?: StyleProp; 13 | }; 14 | 15 | /** 16 | * @returns Text component 17 | * @param children: React.ReactNode - text 18 | * @param size: xs | sm | md | lg 19 | * @param fonts: bold | regular | semibold | light 20 | * @param variant: text | link 21 | * @param style: StyleProp - custom style 22 | */ 23 | 24 | const StyledText = ({ 25 | style, 26 | size = 'md', 27 | font = 'regular', 28 | variant = 'text', 29 | ...rest 30 | }: StyledTextProps & NativeText['props']) => { 31 | const { colors } = useTheme(); 32 | 33 | const { primary } = typography; 34 | 35 | const sizes: Record, number> = { 36 | xs: 10, 37 | sm: 12, 38 | base: 14, 39 | md: 16, 40 | lg: 18, 41 | xl: 22, 42 | }; 43 | const fonts: Record, string> = { 44 | bold: primary.bold, 45 | regular: primary.regular, 46 | medium: primary.medium, 47 | semiBold: primary.semiBold, 48 | light: primary.light, 49 | }; 50 | const variants: Record, string> = { 51 | text: colors.text, 52 | link: colors.link, 53 | primary: colors.primary, 54 | secondary: colors.secondary, 55 | }; 56 | 57 | return ( 58 | 62 | ); 63 | }; 64 | 65 | export default StyledText; 66 | -------------------------------------------------------------------------------- /components/container/BottomSheetContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import type { StyleProp, ViewStyle } from 'react-native'; 4 | import { StyleSheet, View } from 'react-native'; 5 | 6 | type BottomSheetContainerProps = { 7 | children: React.ReactNode; 8 | style?: StyleProp; 9 | }; 10 | 11 | const BottomSheetContainer = (props: BottomSheetContainerProps) => { 12 | const { children, style } = props; 13 | 14 | const { colors } = useTheme(); 15 | 16 | const flattenStyle = StyleSheet.flatten([styles.container, { backgroundColor: colors.bg }]); 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | const styles = StyleSheet.create({ 26 | container: { 27 | flex: 1, 28 | }, 29 | main: { 30 | flex: 1, 31 | padding: 8, 32 | marginHorizontal: 'auto', 33 | minHeight: 150, 34 | }, 35 | }); 36 | 37 | export default BottomSheetContainer; 38 | -------------------------------------------------------------------------------- /components/container/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import type { StatusBarProps } from 'expo-status-bar'; 3 | import { StatusBar } from 'expo-status-bar'; 4 | import React from 'react'; 5 | import type { KeyboardAvoidingViewProps, ScrollViewProps, StyleProp, ViewStyle } from 'react-native'; 6 | import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, View } from 'react-native'; 7 | import type { Edge, SafeAreaViewProps } from 'react-native-safe-area-context'; 8 | import { SafeAreaProvider, SafeAreaView, initialWindowMetrics } from 'react-native-safe-area-context'; 9 | 10 | interface BaseScreenProps { 11 | children?: React.ReactNode; 12 | SafeAreaViewProps?: SafeAreaViewProps; 13 | StatusBarProps?: StatusBarProps; 14 | keyboardOffset?: number; 15 | KeyboardAvoidingViewProps?: KeyboardAvoidingViewProps; 16 | safeAreaEdges?: Array; 17 | style?: StyleProp; 18 | } 19 | 20 | interface FixedScreenProps extends BaseScreenProps { 21 | preset?: 'fixed'; 22 | } 23 | 24 | interface ScrollScreenProps extends BaseScreenProps { 25 | preset: 'scroll'; 26 | keyboardShouldPersistTaps?: 'handled' | 'always' | 'never'; 27 | ScrollViewProps?: ScrollViewProps; 28 | } 29 | 30 | export type ScreenProps = ScrollScreenProps | FixedScreenProps; 31 | 32 | const isIos = Platform.OS === 'ios'; 33 | 34 | function isNonScrolling(preset?: ScreenProps['preset']) { 35 | return !preset || preset === 'fixed'; 36 | } 37 | 38 | function ScreenWithoutScrolling(props: ScreenProps) { 39 | const { children, ...rest } = props; 40 | return ( 41 | 42 | {children} 43 | 44 | ); 45 | } 46 | 47 | function ScreenWithScrolling(props: ScreenProps) { 48 | const { children, keyboardShouldPersistTaps = 'handled', ScrollViewProps, ...rest } = props as ScrollScreenProps; 49 | return ( 50 | 51 | 57 | {children} 58 | 59 | 60 | ); 61 | } 62 | 63 | const MainContainer = (props: ScreenProps) => { 64 | const { colors, dark } = useTheme(); 65 | 66 | const { 67 | SafeAreaViewProps, 68 | StatusBarProps, 69 | safeAreaEdges, 70 | keyboardOffset = 0, 71 | KeyboardAvoidingViewProps, 72 | style, 73 | } = props; 74 | 75 | const backgroundColor = dark ? colors.bg : colors.background; 76 | 77 | const statusBarStyle = dark ? 'light' : 'dark'; 78 | 79 | const Wrapper = safeAreaEdges?.length ? SafeAreaView : View; 80 | 81 | const wrapperStyles = StyleSheet.compose(styles.container, style); 82 | 83 | return ( 84 | 85 | 86 | 87 | 88 | 94 | {isNonScrolling(props.preset) ? : } 95 | 96 | 97 | 98 | ); 99 | }; 100 | 101 | const styles = StyleSheet.create({ 102 | container: { 103 | flex: 1, 104 | }, 105 | containerStyle: { 106 | width: '100%', 107 | alignItems: 'center', 108 | }, 109 | keyboard: { 110 | flex: 1, 111 | }, 112 | }); 113 | 114 | export default MainContainer; 115 | -------------------------------------------------------------------------------- /components/headers/HeaderActionRight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import ClearButton from '../buttons/ClearButton'; 4 | import IconButton from '../buttons/IconButton'; 5 | import Row from '../common/Row'; 6 | 7 | type HeaderActionRightProps = { 8 | handlePress: () => void; 9 | toggleView: () => void; 10 | listVisible: boolean; 11 | }; 12 | 13 | const HeaderActionRight = ({ toggleView, listVisible, handlePress }: HeaderActionRightProps) => { 14 | // TODO: open filter modal functionality 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default HeaderActionRight; 29 | 30 | const styles = StyleSheet.create({ 31 | row: { 32 | marginHorizontal: 16, 33 | }, 34 | gap: { 35 | width: 8, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /components/headers/HeaderRight.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'expo-router'; 2 | import React from 'react'; 3 | import { StyleSheet } from 'react-native'; 4 | import FeedbackButton from '../buttons/FeedbackButton'; 5 | import Row from '../common/Row'; 6 | 7 | const HeaderRight = () => { 8 | const router = useRouter(); 9 | 10 | return ( 11 | 12 | router.push('/feedback')} /> 13 | 14 | ); 15 | }; 16 | 17 | export default HeaderRight; 18 | 19 | const styles = StyleSheet.create({ 20 | row: { 21 | marginHorizontal: 16, 22 | marginBottom: 8, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /components/headers/MainHeader.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import { StyleSheet, View } from 'react-native'; 4 | import Logo from '../../assets/artworks/Logo'; 5 | import LogoDark from '../../assets/artworks/LogoDark'; 6 | 7 | const MainHeader = () => { 8 | const { dark } = useTheme(); 9 | 10 | return ( 11 | 12 | {dark ? : } 13 | 14 | ); 15 | }; 16 | 17 | export default MainHeader; 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | marginRight: 16, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /components/lists/FeedList.tsx: -------------------------------------------------------------------------------- 1 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import dayjs from 'dayjs'; 4 | import relativeTime from 'dayjs/plugin/relativeTime'; 5 | import { Image } from 'expo-image'; 6 | import { Link } from 'expo-router'; 7 | import React from 'react'; 8 | import { Dimensions, StyleSheet, View } from 'react-native'; 9 | import { FlatList } from 'react-native-gesture-handler'; 10 | import { blurhash } from '../../config/constants'; 11 | import type { Feed, IFeed } from '../../global'; 12 | import Row from '../common/Row'; 13 | import StyledText from '../common/StyledText'; 14 | 15 | interface FeedListItemProps { 16 | item: Feed; 17 | } 18 | 19 | type Props = { 20 | feed: IFeed; 21 | }; 22 | 23 | dayjs.extend(relativeTime); 24 | const { height } = Dimensions.get('screen'); 25 | 26 | const FeedListItem = ({ item }: FeedListItemProps) => { 27 | const { colors } = useTheme(); 28 | return ( 29 | 30 | 31 | {item.body} 32 | 33 | 34 | 41 | 42 | 43 | 44 | 49 | 50 | 51 | Share 52 | 53 | 54 | 55 | 56 | 57 | {dayjs(item.created_at).fromNow()} 58 | 59 | 60 | ); 61 | }; 62 | 63 | const FeedList = ({ feed }: Props) => { 64 | const recentFirst = (data: Array) => { 65 | return data?.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); 66 | }; 67 | 68 | return ( 69 | 70 | index.toString()} 74 | renderItem={({ item }: { item: Feed }) => { 75 | return ; 76 | }} 77 | /> 78 | 79 | ); 80 | }; 81 | 82 | export default FeedList; 83 | 84 | const styles = StyleSheet.create({ 85 | main: { 86 | paddingBottom: 20, 87 | }, 88 | feed: { 89 | marginVertical: 8, 90 | borderBottomWidth: 1, 91 | padding: 12, 92 | }, 93 | image: { 94 | height: height * 0.23, 95 | borderRadius: 8, 96 | }, 97 | imageContainer: { 98 | borderRadius: 8, 99 | marginVertical: 8, 100 | overflow: 'hidden', 101 | }, 102 | share: { 103 | display: 'flex', 104 | flexDirection: 'row', 105 | alignItems: 'center', 106 | paddingVertical: 8, 107 | }, 108 | shareLabel: { 109 | marginRight: 6, 110 | }, 111 | }); 112 | -------------------------------------------------------------------------------- /components/lists/SessionsList.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { FlashList } from '@shopify/flash-list'; 3 | import { useRouter } from 'expo-router'; 4 | import React from 'react'; 5 | import { StyleSheet, View } from 'react-native'; 6 | import type { ISessions, Session } from '../../global'; 7 | import { usePrefetchedEventData } from '../../services/api'; 8 | import { getSessionTimeAndLocation } from '../../util/helpers'; 9 | import ViewAllButton from '../buttons/ViewAllButton'; 10 | import SessionCard from '../cards/SessionCard'; 11 | import Row from '../common/Row'; 12 | import Space from '../common/Space'; 13 | import StyledText from '../common/StyledText'; 14 | 15 | type Props = { 16 | sessions: ISessions; 17 | }; 18 | 19 | const SessionsList = ({ sessions }: Props) => { 20 | const { colors } = useTheme(); 21 | const router = useRouter(); 22 | const data = sessions?.data.slice(0, 5); // filter sessions to 5 23 | const sessionCount = (sessions?.meta.paginator.count - 5).toString(); 24 | const { schedule } = usePrefetchedEventData(); 25 | 26 | return ( 27 | 28 | 29 | 30 | Sessions 31 | 32 | router.push('/home/sessions')} label={`+${sessionCount}`} /> 33 | 34 | 35 | 36 | 37 | 38 | ( 41 | router.push({ pathname: `/session/${item.slug}`, params: { slug: item.slug } })} 43 | item={item} 44 | screen="home" 45 | time={(schedule && getSessionTimeAndLocation(item.slug, schedule)) ?? ''} 46 | /> 47 | )} 48 | keyExtractor={(item: Session, index: number) => index.toString()} 49 | horizontal 50 | showsHorizontalScrollIndicator={false} 51 | estimatedItemSize={25} 52 | /> 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default SessionsList; 59 | 60 | const styles = StyleSheet.create({ 61 | list: { 62 | flex: 1, 63 | paddingHorizontal: 16, 64 | }, 65 | listContainer: { 66 | flex: 1, 67 | width: '100%', 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /components/lists/SessionsListVertical.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { useRouter } from 'expo-router'; 3 | import React from 'react'; 4 | import type { ListRenderItemInfo } from 'react-native'; 5 | import { FlatList, StyleSheet, View } from 'react-native'; 6 | import SessionListSeparator from '../../assets/artworks/ListSeparator'; 7 | import type { SessionForSchedule } from '../../global'; 8 | import { usePrefetchedEventData } from '../../services/api'; 9 | import { getSessionTimeAndLocation } from '../../util/helpers'; 10 | import SessionCard from '../cards/SessionCard'; 11 | import Row from '../common/Row'; 12 | import Space from '../common/Space'; 13 | import StyledText from '../common/StyledText'; 14 | 15 | interface SessionListVerticalProps { 16 | variant?: 'card' | 'list'; 17 | bookmarked: boolean; 18 | handleBookMark?: (id: number) => void; 19 | sessions?: Array; 20 | } 21 | 22 | /** 23 | * @param variant: 'card' | 'list' 24 | * @param bookmarked: boolean 25 | * @param handleBookMark: () => void 26 | * @param sessions: Array of type Sessions 27 | */ 28 | const SessionsListVertical = ({ 29 | variant = 'card', 30 | bookmarked = false, 31 | sessions, 32 | handleBookMark, 33 | }: SessionListVerticalProps) => { 34 | const { colors } = useTheme(); 35 | const router = useRouter(); 36 | const { schedule } = usePrefetchedEventData(); 37 | 38 | if (sessions === undefined) { 39 | return ( 40 | 41 | 42 | No sessions available. 43 | 44 | 45 | ); 46 | } 47 | 48 | const renderComponentItem = ({ item, index }: ListRenderItemInfo) => { 49 | return ( 50 | 51 | router.push({ pathname: `/session/${item.slug}`, params: { slug: item.slug } })} 53 | item={item} 54 | handleBookMark={() => { 55 | if (handleBookMark) { 56 | handleBookMark(item.id); 57 | } 58 | }} 59 | screen={'sessions'} 60 | variant={variant === 'card' ? 'card' : 'list'} 61 | time={(schedule && getSessionTimeAndLocation(item.slug, schedule)) ?? ''} 62 | /> 63 | {index !== sessions.length - 1 ? ( 64 | variant === 'card' ? ( 65 | 66 | ) : ( 67 | 68 | 69 | 70 | ) 71 | ) : ( 72 | 73 | )} 74 | 75 | ); 76 | }; 77 | 78 | return ( 79 | 80 | 81 | 82 | {bookmarked === true ? 'My Sessions' : 'All Sessions'} 83 | 84 | 85 | 86 | 87 | item.slug + item.id} 91 | /> 92 | 93 | ); 94 | }; 95 | 96 | export default SessionsListVertical; 97 | 98 | const styles = StyleSheet.create({ 99 | list: { 100 | flex: 1, 101 | paddingHorizontal: 15, 102 | }, 103 | listHolder: { 104 | marginTop: 20, 105 | paddingLeft: 20, 106 | paddingRight: 20, 107 | }, 108 | cardContainer: { marginLeft: 50, marginVertical: 6 }, 109 | }); 110 | -------------------------------------------------------------------------------- /components/lists/SpeakersList.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { FlashList } from '@shopify/flash-list'; 3 | import { useRouter } from 'expo-router'; 4 | import React from 'react'; 5 | import { Dimensions, StyleSheet, View } from 'react-native'; 6 | import type { ISpeaker, Speaker } from '../../global'; 7 | import ViewAllButton from '../buttons/ViewAllButton'; 8 | import OrganizerCard from '../cards/OrganizerCard'; 9 | import Row from '../common/Row'; 10 | import Space from '../common/Space'; 11 | import StyledText from '../common/StyledText'; 12 | 13 | const { width } = Dimensions.get('window'); 14 | 15 | type Props = { 16 | speakers: ISpeaker; 17 | }; 18 | 19 | const SpeakersList = ({ speakers }: Props) => { 20 | const { colors } = useTheme(); 21 | const router = useRouter(); 22 | const data = speakers?.data.slice(0, 15); // filter speakers to 15 23 | const speakersCount = (speakers?.meta.paginator.count - 5).toString(); 24 | 25 | return ( 26 | 27 | 28 | 29 | Speakers 30 | 31 | router.push('/speakers')} label={`+${speakersCount}`} /> 32 | 33 | 34 | 35 | 36 | 37 | ( 40 | 44 | router.push({ pathname: `/${item.name}`, params: { name: item.name, type: 'speaker' } }) 45 | } 46 | /> 47 | )} 48 | keyExtractor={(item: Speaker, index: number) => index.toString()} 49 | horizontal 50 | showsHorizontalScrollIndicator={false} 51 | estimatedItemSize={25} 52 | /> 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default SpeakersList; 59 | 60 | const styles = StyleSheet.create({ 61 | list: { 62 | flex: 1, 63 | }, 64 | listContainer: { 65 | flex: 1, 66 | height: 200, 67 | width: width - 32, 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /components/modals/FeedbackSentModal.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import { Image } from 'expo-image'; 3 | import { useRouter } from 'expo-router'; 4 | import React from 'react'; 5 | import { Modal, Pressable, StyleSheet, View } from 'react-native'; 6 | import Space from '../common/Space'; 7 | import StyledText from '../common/StyledText'; 8 | 9 | const FeedbackSentModal = (props: { showModal: boolean }) => { 10 | const { colors } = useTheme(); 11 | const router = useRouter(); 12 | 13 | const { showModal } = props; 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | Thank you for your feedback 21 | 22 | { 25 | router.push('/home'); 26 | }} 27 | > 28 | OKAY 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | const styles = StyleSheet.create({ 37 | confetti: { 38 | width: 166, 39 | height: 166, 40 | justifyContent: 'center', 41 | flex: 1, 42 | }, 43 | modalView: { 44 | justifyContent: 'center', 45 | flex: 1, 46 | alignItems: 'center', 47 | }, 48 | pressable: { 49 | marginTop: 26, 50 | justifyContent: 'center', 51 | alignItems: 'center', 52 | borderRadius: 10, 53 | height: 45, 54 | width: '100%', 55 | }, 56 | pressableSubmitText: { 57 | color: 'white', 58 | }, 59 | container: { 60 | opacity: 1, 61 | width: '90%', 62 | borderRadius: 10, 63 | minHeight: 367, 64 | paddingHorizontal: 100, 65 | paddingTop: 45, 66 | alignItems: 'center', 67 | }, 68 | }); 69 | 70 | export default FeedbackSentModal; 71 | -------------------------------------------------------------------------------- /components/modals/GoogleSignInModal.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import { Modal, Pressable, StyleSheet, TouchableOpacity, View } from 'react-native'; 4 | import GoogleIcon from '../../assets/artworks/GoogleIcon'; 5 | import { typography } from '../../config/typography'; 6 | import { useAuth } from '../../context/auth'; 7 | import Row from '../common/Row'; 8 | import StyledText from '../common/StyledText'; 9 | 10 | type GoogleSignInModalProps = { 11 | visible: boolean; 12 | onClose: () => void; 13 | }; 14 | 15 | const GoogleSignInModal = ({ visible, onClose }: GoogleSignInModalProps) => { 16 | const { signInWithGoogle } = useAuth(); 17 | const { colors } = useTheme(); 18 | const { primary, secondary } = typography; 19 | 20 | const handleSignIn = () => { 21 | signInWithGoogle(); 22 | onClose(); 23 | }; 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 39 | CANCEL 40 | 41 | 42 | 43 | 44 | 45 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Sign in with Google 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default GoogleSignInModal; 69 | 70 | const styles = StyleSheet.create({ 71 | centeredView: { 72 | flex: 1, 73 | justifyContent: 'center', 74 | alignItems: 'center', 75 | backgroundColor: 'rgba(0, 0, 0, 0.8)', 76 | }, 77 | header: { 78 | width: '100%', 79 | flexDirection: 'row', 80 | justifyContent: 'space-between', 81 | alignItems: 'center', 82 | paddingVertical: 8, 83 | }, 84 | modalView: { 85 | width: '90%', 86 | height: '50%', 87 | borderRadius: 12, 88 | paddingHorizontal: 12, 89 | paddingVertical: 20, 90 | alignItems: 'center', 91 | }, 92 | modalContent: { 93 | flex: 1, 94 | width: '100%', 95 | justifyContent: 'center', 96 | alignItems: 'center', 97 | }, 98 | button: { 99 | width: '80%', 100 | borderRadius: 4, 101 | justifyContent: 'center', 102 | borderWidth: 1, 103 | }, 104 | buttonIcon: { 105 | padding: 12, 106 | justifyContent: 'center', 107 | alignItems: 'center', 108 | }, 109 | buttonText: { 110 | flex: 1, 111 | paddingHorizontal: 12, 112 | paddingVertical: 16, 113 | }, 114 | }); 115 | -------------------------------------------------------------------------------- /components/player/VideoPlayer.tsx: -------------------------------------------------------------------------------- 1 | import { Ionicons } from '@expo/vector-icons'; 2 | import { useTheme } from '@react-navigation/native'; 3 | import type { VideoProps } from 'expo-av'; 4 | import { ResizeMode, Video } from 'expo-av'; 5 | import { Image } from 'expo-image'; 6 | import React, { useRef, useState } from 'react'; 7 | import { Dimensions, Pressable, StyleSheet, View } from 'react-native'; 8 | import { VIDEO_URL } from '../../config/constants'; 9 | 10 | const { width } = Dimensions.get('window'); 11 | 12 | type VideoPlayerRef = Video | null; 13 | 14 | const VideoPlayer = () => { 15 | const [status, setStatus] = useState({} as VideoProps['status']); 16 | 17 | const { colors } = useTheme(); 18 | 19 | const video = useRef(null); 20 | 21 | return ( 22 | 23 | 59 | ); 60 | }; 61 | 62 | export default VideoPlayer; 63 | 64 | const styles = StyleSheet.create({ 65 | container: { 66 | flex: 1, 67 | justifyContent: 'center', 68 | borderRadius: 12, 69 | width: width - 32, 70 | }, 71 | video: { 72 | alignSelf: 'center', 73 | width: '100%', 74 | height: 180, 75 | borderRadius: 12, 76 | }, 77 | image: { 78 | height: 180, 79 | width: '100%', 80 | borderRadius: 12, 81 | }, 82 | button: { 83 | position: 'absolute', 84 | top: 10, 85 | right: 10, 86 | width: 60, 87 | height: 60, 88 | borderRadius: 30, 89 | backgroundColor: 'rgba(0,0,0,0.5)', 90 | justifyContent: 'center', 91 | alignItems: 'center', 92 | }, 93 | }); 94 | -------------------------------------------------------------------------------- /config/constants.ts: -------------------------------------------------------------------------------- 1 | export const blurhash = 2 | '|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7azayj[ayj[j[ayofayayayj[fQj[ayayj[ayfjj[j[ayjuayj['; 3 | 4 | export const VIDEO_URL = 'https://droidcon.co.ke/video/DroidconKe_2019_Highlight_Reel_HD.mp4'; 5 | export const WIDE_BLURHASH = 'LHF$LwM,IS%P.mIvsit9_1IXRiog'; 6 | 7 | export const EVENT_SLUG = 'droidconke-2023-137'; 8 | export const ORG_SLUG = 'droidcon-ke-645'; 9 | -------------------------------------------------------------------------------- /config/theme.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeColors } from '@react-navigation/native'; 2 | 3 | export const theme_colors: ThemeColors = { 4 | light: { 5 | primary: '#000CEB', 6 | secondary: '#707070', 7 | tertiary: '#FF6E4D', 8 | accent: '#7070702C', 9 | textLight: '#707070', 10 | tint: '#00E2C3', 11 | secondaryTint: 'rgba(0, 12, 235, 0.11)', 12 | tertiaryTint: 'rgba(125, 225, 195, 0.11)', 13 | modalTint: 'hsla(60, 3%, 12%, 0.52)', 14 | link: '#000CEB', 15 | bg: '#F5F5F5', 16 | background: '#FFFFFF', 17 | card: '#F5F5F5', 18 | bgInverse: '#20201E', 19 | whiteConstant: '#FFFFFF', 20 | iconSwitch: '#20201E', 21 | placeHolder: '#C3C3C3', 22 | borderColor: '#F5F5F5', 23 | borderColorSecondary: '#F5F5F5', 24 | assetAccent: '#000CEB', 25 | }, 26 | dark: { 27 | primary: '#00E2C3', 28 | secondary: '#707070', 29 | tertiary: '#FF6E4D', 30 | accent: '#7070702C', 31 | textLight: '#707070', 32 | tint: '#00E2C3', 33 | secondaryTint: '#f5f5f51c', 34 | tertiaryTint: 'rgba(125, 225, 195, 0.11)', 35 | modalTint: 'hsla(60, 3%, 12%, 0.52)', 36 | link: '#F5F5F5', 37 | bg: '#20201E', 38 | background: '#000000', 39 | card: '#000000', 40 | bgInverse: '#F5F5F5', 41 | whiteConstant: '#FFFFFF', 42 | iconSwitch: '#20201E', 43 | placeHolder: '#C3C3C3', 44 | borderColor: '#707070', 45 | borderColorSecondary: '#000000', 46 | assetAccent: '#000000', 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /config/typography.ts: -------------------------------------------------------------------------------- 1 | import type { Fonts, Typography } from '../global'; 2 | 3 | export const customFontsToLoad = { 4 | montserratBold: require('../assets/fonts/Montserrat-Bold.ttf'), 5 | montserratRegular: require('../assets/fonts/Montserrat-Regular.ttf'), 6 | montserratMedium: require('../assets/fonts/Montserrat-Medium.ttf'), 7 | montserratSemiBold: require('../assets/fonts/Montserrat-SemiBold.ttf'), 8 | montserratLight: require('../assets/fonts/Montserrat-Light.ttf'), 9 | robotoMedium: require('../assets/fonts/Roboto-Medium.ttf'), 10 | rubikLight: require('../assets/fonts/Rubik-Light.ttf'), 11 | }; 12 | 13 | export const fonts: Fonts = { 14 | montserrat: { 15 | bold: 'montserratBold', 16 | regular: 'montserratRegular', 17 | medium: 'montserratMedium', 18 | semiBold: 'montserratSemiBold', 19 | light: 'montserratLight', 20 | }, 21 | roboto: { 22 | medium: 'robotoMedium', 23 | }, 24 | rubik: { 25 | light: 'rubikLight', 26 | }, 27 | }; 28 | 29 | export const typography: Typography = { 30 | /** 31 | * The primary font. Used in most places. 32 | */ 33 | primary: fonts.montserrat, 34 | 35 | /** 36 | * An alternate font used for perhaps titles and stuff. 37 | * If not provided, use primary. 38 | */ 39 | secondary: fonts.roboto, 40 | /** 41 | * logo font 42 | */ 43 | logo: fonts.rubik, 44 | }; 45 | -------------------------------------------------------------------------------- /context/auth.tsx: -------------------------------------------------------------------------------- 1 | import { useAsyncStorage } from '@react-native-async-storage/async-storage'; 2 | import { createContext, useContext, useEffect, useState } from 'react'; 3 | 4 | type UserType = { 5 | id: string; 6 | name: string; 7 | email: string; 8 | photo?: string; 9 | }; 10 | 11 | type AuthContextType = { 12 | user: UserType | null; 13 | signInWithGoogle: () => Promise; 14 | signOut: () => Promise; 15 | }; 16 | 17 | const AuthContext = createContext({} as AuthContextType); 18 | 19 | export const useAuth = () => useContext(AuthContext); 20 | 21 | type ProviderProps = { 22 | children: React.ReactNode; 23 | }; 24 | 25 | export const AuthProvider = ({ children }: ProviderProps) => { 26 | const { getItem, setItem, removeItem } = useAsyncStorage('@user'); 27 | const [user, setUser] = useState(null); 28 | 29 | useEffect(() => { 30 | getItem().then((data) => { 31 | if (data) { 32 | setUser(JSON.parse(data)); 33 | } 34 | }); 35 | /** 36 | * Reason: The `useAsyncStorage` "experimental" hook is not memoized and always returns new functions 37 | * which causes an infinite loop when `getItem` is used as a dependency in `useEffect`. 38 | * Disabling the check here should be fine since we only want to fetch the data from async storage once 39 | * and there is no need to synchronize the data with our state. 40 | */ 41 | // eslint-disable-next-line react-hooks/exhaustive-deps 42 | }, []); 43 | 44 | return ( 45 | { 49 | try { 50 | // TODO: login with google implementation 51 | // await setItem(JSON.stringify(res)); - use setItem(from useAsyncStorage) to save user data 52 | // setUser(res); 53 | const dummyUser = { 54 | id: '1', 55 | name: 'John Doe', 56 | email: 'johndoe@email.com', 57 | photo: 'https://robohash.org/5759c92851e66680ae5723f5de4f7757?set=set4&bgset=bg2&size=400x400', 58 | }; 59 | await setItem(JSON.stringify(dummyUser)); 60 | setUser(dummyUser); 61 | } catch (error) { 62 | console.log(error); 63 | } 64 | }, 65 | signOut: async () => { 66 | try { 67 | await removeItem(); 68 | setUser(null); 69 | } catch (error) { 70 | console.log(error); 71 | } 72 | }, 73 | }} 74 | > 75 | {children} 76 | 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 5.6.0", 4 | "appVersionSource": "remote" 5 | }, 6 | "build": { 7 | "development": { 8 | "extends": "production", 9 | "developmentClient": true, 10 | "distribution": "internal", 11 | "channel": "development", 12 | "android": { 13 | "gradleCommand": ":app:assembleDebug" 14 | }, 15 | "ios": { 16 | "buildConfiguration": "Debug", 17 | "simulator": true 18 | } 19 | }, 20 | "development:device": { 21 | "extends": "development", 22 | "channel": "development-device", 23 | "developmentClient": true, 24 | "distribution": "internal", 25 | "ios": { 26 | "buildConfiguration": "Debug", 27 | "simulator": false 28 | } 29 | }, 30 | "preview": { 31 | "extends": "production", 32 | "distribution": "internal", 33 | "channel": "preview", 34 | "ios": { "simulator": true }, 35 | "android": { "buildType": "apk" }, 36 | "env": {} 37 | }, 38 | "preview:device": { 39 | "extends": "preview", 40 | "channel": "preview-device", 41 | "ios": { "simulator": false } 42 | }, 43 | "production": { 44 | "channel": "production", 45 | "autoIncrement": true, 46 | "env": {} 47 | } 48 | }, 49 | "submit": { 50 | "production": {} 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | import '@react-navigation/native'; 2 | import type { SvgProps } from 'react-native-svg'; 3 | 4 | declare module '@react-navigation/native' { 5 | export type ExtendedTheme = { 6 | dark: boolean; 7 | colors: { 8 | primary: string; 9 | secondary: string; 10 | tertiary: string; 11 | accent: string; 12 | textLight: string; 13 | tint: string; 14 | secondaryTint: string; 15 | tertiaryTint: string; 16 | modalTint: string; 17 | link: string; 18 | bg: string; 19 | background: string; 20 | text: string; 21 | border: string; 22 | card: string; 23 | notification: string; 24 | bgInverse: string; 25 | whiteConstant: string; 26 | iconSwitch: string; 27 | placeHolder: string; 28 | borderColor: string; 29 | assetAccent: string; 30 | borderColorSecondary: string; 31 | }; 32 | }; 33 | 34 | export type ThemeColors = { 35 | [key in 'light' | 'dark']: Omit; 36 | }; 37 | export function useTheme(): ExtendedTheme; 38 | } 39 | 40 | type Montserrat = { 41 | bold: string; 42 | regular: string; 43 | medium: string; 44 | semiBold: string; 45 | light: string; 46 | }; 47 | type Roboto = { 48 | medium: string; 49 | }; 50 | type Rubik = { 51 | light: string; 52 | }; 53 | 54 | export type Fonts = { 55 | montserrat: Montserrat; 56 | roboto: Roboto; 57 | rubik: Rubik; 58 | }; 59 | 60 | export type Typography = { 61 | primary: Montserrat; 62 | secondary: Roboto; 63 | logo: Rubik; 64 | }; 65 | 66 | export interface Feed { 67 | title: string; 68 | body: string; 69 | topic: string; 70 | url: string; 71 | image: string; 72 | created_at: string; 73 | } 74 | 75 | export interface IFeed { 76 | data: Array; 77 | meta: Meta; 78 | } 79 | 80 | export interface IOrganizer { 81 | id: number; 82 | name: string; 83 | email: string; 84 | description: string; 85 | facebook: string; 86 | twitter: string; 87 | instagram: null | string; 88 | logo: string; 89 | slug: string; 90 | status: string; 91 | created_at: string; 92 | upcoming_events_count: number; 93 | total_events_count: number; 94 | } 95 | 96 | export interface IOrganizers { 97 | data: Array; 98 | meta: Meta; 99 | } 100 | 101 | export interface ISchedule { 102 | data: { 103 | [key: string]: Array; 104 | }; 105 | } 106 | 107 | export interface Session { 108 | title: string; 109 | description: string; 110 | slug: string; 111 | session_format: string; 112 | session_level: string; 113 | is_keynote: boolean; 114 | session_image: string | null; 115 | speakers: Array; 116 | } 117 | 118 | export interface Room { 119 | title: string; 120 | id: number; 121 | } 122 | 123 | export interface SessionForSchedule extends Session { 124 | id: number; 125 | backgroundColor: string; 126 | borderColor: string; 127 | is_serviceSession: boolean; 128 | is_bookmarked: boolean; 129 | start_date_time: string; 130 | start_time: string; 131 | end_date_time: string; 132 | end_time: string; 133 | rooms: Array; 134 | } 135 | 136 | export interface ISessions { 137 | data: Array; 138 | meta: Meta; 139 | } 140 | 141 | export interface ScheduleSession { 142 | data: SessionForSchedule; 143 | } 144 | 145 | export interface Speaker { 146 | name: string; 147 | tagline: string; 148 | biography: string; 149 | avatar: string; 150 | twitter?: string | null; 151 | facebook?: string | null; 152 | linkedin?: string | null; 153 | instagram?: string | null; 154 | blog?: string | null; 155 | company_website?: string | null; 156 | } 157 | 158 | export interface ISpeaker { 159 | data: Array; 160 | meta: Meta; 161 | } 162 | 163 | export interface ISponsor { 164 | name: string; 165 | tagline: string; 166 | link: string; 167 | sponsor_type: string; 168 | logo: string; 169 | created_at: string; 170 | } 171 | 172 | export interface ISponsors { 173 | data: Array; 174 | } 175 | 176 | export interface Meta { 177 | paginator: Paginator; 178 | } 179 | 180 | export interface Paginator { 181 | count: number; 182 | per_page: string; 183 | current_page: number; 184 | next_page: string | null; 185 | has_more_pages: boolean; 186 | next_page_url: string | null; 187 | previous_page_url: string | null; 188 | } 189 | 190 | export interface ISvgProps extends SvgProps { 191 | xmlns?: string; 192 | xmlnsXlink?: string; 193 | xmlSpace?: string; 194 | } 195 | 196 | export interface IDateForDayButton { 197 | day: string; 198 | date: string; 199 | key: string; 200 | } 201 | 202 | export type OrganizingTeamMember = { 203 | name: string; 204 | tagline: string; 205 | link: string; 206 | type: string; 207 | bio: string; 208 | twitter_handle: string; 209 | designation: string; 210 | photo: string; 211 | created_at: string; 212 | }; 213 | export interface IOrganizingTeam { 214 | data: Array; 215 | } 216 | 217 | export interface IProfile extends OrganizingTeamMember, Speaker {} 218 | -------------------------------------------------------------------------------- /jest.setupFilesAfterEnv.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-native/extend-expect'; 2 | jest.mock('expo-font'); 3 | jest.mock('expo-asset'); 4 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const { getDefaultConfig } = require('expo/metro-config'); 4 | 5 | module.exports = getDefaultConfig(__dirname); 6 | -------------------------------------------------------------------------------- /mock/feed.ts: -------------------------------------------------------------------------------- 1 | import type { IFeed } from '../global'; 2 | 3 | export const Feed: IFeed = { 4 | data: [ 5 | { 6 | title: 'Juma escorts GDE to UK', 7 | body: 'This 4th in-person event will include several tech communities from the East African Region and continental members. Participants will have an excellent chance to learn about Android development and opportunities and to network with Android experts in the ecosystem.', 8 | topic: 'droidconke-2022-281', 9 | url: 'https://droidcon.co.ke/', 10 | image: 11 | 'https://res.cloudinary.com/droidconke/image/upload/v1684788565/prod/upload/event/feeds/r0gtvnh2qtfy0rnkoxwq.png', 12 | created_at: '2023-05-22 23:49:26', 13 | }, 14 | { 15 | title: 'droidconKe Ticket sales', 16 | body: 'This 4th in-person event will include several tech communities from the East African Region and continental members. Participants will have an excellent chance to learn about Android development and opportunities and to network with Android expert in the ecosystem.', 17 | topic: 'droidconke-2022-281', 18 | url: 'https://droidcon.co.ke/', 19 | image: 20 | 'https://res.cloudinary.com/droidconke/image/upload/v1684788565/prod/upload/event/feeds/r0gtvnh2qtfy0rnkoxwq.png', 21 | created_at: '2023-05-22 23:40:48', 22 | }, 23 | ], 24 | meta: { 25 | paginator: { 26 | count: 2, 27 | per_page: '10', 28 | current_page: 1, 29 | next_page: null, 30 | has_more_pages: false, 31 | next_page_url: null, 32 | previous_page_url: null, 33 | }, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /mock/organizers.ts: -------------------------------------------------------------------------------- 1 | import type { IOrganizers } from '../global'; 2 | 3 | export const Organizers: IOrganizers = { 4 | data: [ 5 | { 6 | id: 2, 7 | name: 'DevsKE', 8 | email: 'connect@devs.info.ke', 9 | description: 'DevsKE is a Kenyan dev communities & software developer directory.', 10 | facebook: 'devsinfoKE', 11 | twitter: 'devsinfoKE', 12 | instagram: null, 13 | logo: 'https://res.cloudinary.com/droidconke/image/upload/v1657570286/prod/upload/logo/qpt3oto32djwvhgapqnp.png', 14 | slug: 'devske-663', 15 | status: 'active', 16 | created_at: '2022-07-11 23:11:26', 17 | upcoming_events_count: 0, 18 | total_events_count: 0, 19 | }, 20 | { 21 | id: 1, 22 | name: 'Droidcon Kenya', 23 | email: 'droidconke@gmail.com', 24 | description: 'Largest Android Focused Developer conference in Africa.', 25 | facebook: 'droidconke', 26 | twitter: 'droidconke', 27 | instagram: 'droidconke', 28 | logo: 'https://res.cloudinary.com/droidconke/image/upload/v1657570135/prod/upload/logo/pgeealuobwzkoxsjiqgx.jpg', 29 | slug: 'droidcon-ke-645', 30 | status: 'active', 31 | created_at: '2021-09-10 16:26:32', 32 | upcoming_events_count: 0, 33 | total_events_count: 1, 34 | }, 35 | ], 36 | meta: { 37 | paginator: { 38 | count: 2, 39 | per_page: '30', 40 | current_page: 1, 41 | next_page: null, 42 | has_more_pages: false, 43 | next_page_url: null, 44 | previous_page_url: null, 45 | }, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /mock/sponsors.ts: -------------------------------------------------------------------------------- 1 | import type { ISponsors } from '../global'; 2 | 3 | export const Sponsors: ISponsors = { 4 | data: [ 5 | { 6 | name: 'JetBrains', 7 | tagline: 'Essential tools for software developers and teams', 8 | link: 'https://www.jetbrains.com/', 9 | sponsor_type: 'silver', 10 | logo: 'https://res.cloudinary.com/droidconke/image/upload/v1665999057/prod/upload/event_sponsors/qkonqiszttkohsyv7tfz.svg', 11 | created_at: '2022-10-17 12:30:58', 12 | }, 13 | { 14 | name: 'Google', 15 | tagline: 'Google Developers', 16 | link: 'https://developers.google.com/', 17 | sponsor_type: 'gold', 18 | logo: 'https://res.cloudinary.com/droidconke/image/upload/v1668068727/prod/upload/event_sponsors/fhdott8jnkc8hjhu0qb2.png', 19 | created_at: '2022-11-10 11:25:27', 20 | }, 21 | { 22 | name: 'United States International University - Africa, Nairobi, Kenya', 23 | tagline: 'Education to take you places', 24 | link: 'https://www.usiu.ac.ke/', 25 | sponsor_type: 'silver', 26 | logo: 'https://res.cloudinary.com/droidconke/image/upload/v1668113594/prod/upload/event_sponsors/docnxfcg28zi8ixl1jde.png', 27 | created_at: '2022-11-10 23:53:14', 28 | }, 29 | { 30 | name: 'Ryggs Kitchen', 31 | tagline: 'Endless taste, guaranteed', 32 | link: 'https://twitter.com/ryggs_kitchen', 33 | sponsor_type: 'bronze', 34 | logo: 'https://res.cloudinary.com/droidconke/image/upload/v1668411370/prod/upload/event_sponsors/cybbvwsznqb8ebo8kabp.png', 35 | created_at: '2022-11-14 10:36:11', 36 | }, 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "expo start", 4 | "android": "expo start --android", 5 | "ios": "expo start --ios", 6 | "web": "expo start --web", 7 | "lint": "eslint --report-unused-disable-directives --ignore-path .gitignore --ext js,jsx,ts,tsx .", 8 | "prettier:format": "prettier --write .", 9 | "prettier:check": "prettier --check .", 10 | "typecheck": "tsc --build", 11 | "validate": "concurrently --kill-others-on-fail -g -p \"[{name}]\" -n \"lint,prettier,typecheck,test\" \"npm:lint\" \"npm:prettier:check\" \"npm:typecheck\" \"npm:test\"", 12 | "test": "jest", 13 | "test:watch": "jest --watch" 14 | }, 15 | "dependencies": { 16 | "@gorhom/bottom-sheet": "4.5.1", 17 | "@react-native-async-storage/async-storage": "1.18.2", 18 | "@react-navigation/native": "6.1.7", 19 | "@shopify/flash-list": "1.4.3", 20 | "@tanstack/react-query": "^5.0.0", 21 | "@th3rdwave/react-navigation-bottom-sheet": "0.2.7", 22 | "@types/react": "~18.2.14", 23 | "axios": "^1.5.1", 24 | "dayjs": "^1.11.10", 25 | "expo": "^49.0.0", 26 | "expo-av": "~13.4.1", 27 | "expo-build-properties": "~0.8.3", 28 | "expo-constants": "~14.4.2", 29 | "expo-font": "~11.4.0", 30 | "expo-image": "~1.3.4", 31 | "expo-linking": "~5.0.2", 32 | "expo-router": "2.0.9", 33 | "expo-splash-screen": "~0.20.5", 34 | "expo-status-bar": "~1.6.0", 35 | "expo-updates": "~0.18.17", 36 | "jest": "^29.2.1", 37 | "jest-expo": "49.0.0", 38 | "react": "18.2.0", 39 | "react-dom": "18.2.0", 40 | "react-native": "0.72.6", 41 | "react-native-gesture-handler": "~2.12.0", 42 | "react-native-reanimated": "~3.3.0", 43 | "react-native-safe-area-context": "4.6.3", 44 | "react-native-screens": "~3.22.0", 45 | "react-native-svg": "13.9.0", 46 | "react-native-web": "~0.19.6", 47 | "react-query": "^3.39.3", 48 | "typescript": "^5.1.3" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.20.0", 52 | "@react-native-community/eslint-config": "^3.2.0", 53 | "@testing-library/jest-native": "5.4.3", 54 | "@testing-library/react-native": "12.3.0", 55 | "@types/jest": "29.5.5", 56 | "@types/react-test-renderer": "18.0.1", 57 | "@typescript-eslint/eslint-plugin": "6.7.0", 58 | "concurrently": "8.2.1", 59 | "eslint": "8.49.0", 60 | "eslint-config-prettier": "9.0.0", 61 | "eslint-plugin-jest": "^27.4.0", 62 | "eslint-plugin-prettier": "^5.0.0", 63 | "jest-date-mock": "^1.0.8", 64 | "prettier": "3.0.3", 65 | "prettier-plugin-organize-imports": "3.2.3" 66 | }, 67 | "name": "ke.droidcon.reactnative", 68 | "version": "1.0.0", 69 | "main": "expo-router/entry", 70 | "private": true, 71 | "jest": { 72 | "preset": "jest-expo", 73 | "setupFilesAfterEnv": [ 74 | "/jest.setupFilesAfterEnv.ts", 75 | "jest-date-mock" 76 | ], 77 | "transformIgnorePatterns": [ 78 | "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /services/api/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const axiosInstance = axios.create({ 4 | baseURL: process.env.EXPO_PUBLIC_BASE_URL, 5 | headers: { 6 | 'Api-Authorization-Key': process.env.EXPO_PUBLIC_API_AUTHORIZATION_KEY, 7 | Accept: 'application/json', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /services/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { UseQueryOptions } from '@tanstack/react-query'; 2 | import { useQueries } from '@tanstack/react-query'; 3 | import { EVENT_SLUG, ORG_SLUG } from '../../config/constants'; 4 | import type { 5 | IFeed, 6 | IOrganizers, 7 | IOrganizingTeam, 8 | ISchedule, 9 | ISessions, 10 | ISpeaker, 11 | ISponsors, 12 | ScheduleSession, 13 | } from '../../global'; 14 | import { axiosInstance } from './axios'; 15 | import { queryClient } from './react-query'; 16 | 17 | export const getSessions = async (count: number) => { 18 | const { data } = await axiosInstance.get(`/events/${EVENT_SLUG}/sessions`, { 19 | params: { per_page: count }, 20 | }); 21 | return data; 22 | }; 23 | 24 | export const getSpeakers = async (count: number) => { 25 | const { data } = await axiosInstance.get(`/events/${EVENT_SLUG}/speakers`, { 26 | params: { per_page: count }, 27 | }); 28 | return data; 29 | }; 30 | 31 | export const getSponsors = async () => { 32 | const { data } = await axiosInstance.get(`/events/${EVENT_SLUG}/sponsors`); 33 | return data; 34 | }; 35 | 36 | export const getOrganizers = async () => { 37 | const { data } = await axiosInstance.get(`/organizers`); 38 | return data; 39 | }; 40 | 41 | export const getOrganizingTeam = async () => { 42 | const { data } = await axiosInstance.get(`/organizers/${ORG_SLUG}/team`); 43 | return data; 44 | }; 45 | 46 | export const getEventFeed = async () => { 47 | const { data } = await axiosInstance.get(`/events/${EVENT_SLUG}/feeds`); 48 | return data; 49 | }; 50 | 51 | export const getEventSchedule = async () => { 52 | const { data } = await axiosInstance.get(`/events/${EVENT_SLUG}/schedule`, { 53 | params: { grouped: true }, 54 | }); 55 | return data; 56 | }; 57 | 58 | export const sendFeedback = async (feedback: string, rating: number) => { 59 | const _feedback = { feedback, rating }; 60 | const res = await axiosInstance.post(`/events/${EVENT_SLUG}/feedback`, _feedback); 61 | return res; 62 | }; 63 | 64 | export const getSessionBySlug = async (slug: string | undefined) => { 65 | if (typeof slug === 'undefined') return null; 66 | const { data } = await axiosInstance.get(`/events/${EVENT_SLUG}/sessions/${slug}`); 67 | return data; 68 | }; 69 | 70 | const _queries = [ 71 | { 72 | queryKey: ['sessions', 50], 73 | queryFn: () => getSessions(50), 74 | }, 75 | { 76 | queryKey: ['speakers', 60], 77 | queryFn: () => getSpeakers(60), 78 | }, 79 | { 80 | queryKey: ['sponsors'], 81 | queryFn: () => getSponsors(), 82 | }, 83 | { 84 | queryKey: ['organizers'], 85 | queryFn: () => getOrganizers(), 86 | }, 87 | { 88 | queryKey: ['schedule'], 89 | queryFn: () => getEventSchedule(), 90 | }, 91 | { 92 | queryKey: ['organizingTeam'], 93 | queryFn: () => getOrganizingTeam(), 94 | }, 95 | ] as const; 96 | 97 | export const prefetchEvent = async () => { 98 | _queries.forEach(async (query) => { 99 | await queryClient.prefetchQuery( 100 | query as UseQueryOptions, 101 | ); 102 | }); 103 | }; 104 | 105 | export const usePrefetchedEventData = () => { 106 | const queries = useQueries({ 107 | queries: _queries, 108 | }); 109 | 110 | const _isLoading = queries.map((query) => query.isLoading).some((isLoading) => isLoading); 111 | const _isRefetching = queries.map((query) => query.isRefetching).some((isRefetching) => isRefetching); 112 | 113 | const refetch = async () => Promise.all(queries.map((query) => query.refetch())); 114 | 115 | const sessions = queries[0]?.data; 116 | const speakers = queries[1]?.data; 117 | const sponsors = queries[2]?.data; 118 | const organizers = queries[3]?.data; 119 | const schedule = queries[4]?.data; 120 | const organizingTeam = queries[5]?.data; 121 | 122 | return { 123 | sessions, 124 | speakers, 125 | sponsors, 126 | organizers, 127 | schedule, 128 | organizingTeam, 129 | isLoading: _isLoading, 130 | isRefetching: _isRefetching, 131 | refetch, 132 | }; 133 | }; 134 | -------------------------------------------------------------------------------- /services/api/react-query.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query'; 2 | 3 | /** 4 | * a react-query client with stale time set to "Infinity" so its never stale 5 | */ 6 | export const queryClient = new QueryClient({ 7 | defaultOptions: { 8 | queries: { staleTime: Infinity }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "isolatedModules": true, 6 | "noUncheckedIndexedAccess": true, 7 | "forceConsistentCasingInFileNames": true 8 | }, 9 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /util/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { IDateForDayButton, ISchedule, Room, SessionForSchedule } from '../global'; 2 | 3 | /** 4 | * a function to truncate text and add ellipsis 5 | * @param limit number of characters to truncate 6 | * @param text text to truncate 7 | * @returns truncated text 8 | */ 9 | export const truncate = (limit: number, text?: string) => { 10 | if (text && text.length > limit) { 11 | return `${text.substring(0, limit)}...`; 12 | } else { 13 | return text; 14 | } 15 | }; 16 | 17 | /** 18 | * a function that gets the start time and room.title of a session from the schedule.data array 19 | * @param slug slug of session 20 | * @returns start time and room.title 21 | * @example getSessionTimeAndLocation('session-1') // returns @ 9:00 | Room 1 22 | */ 23 | export const getSessionTimeAndLocation = (slug: string, schedule: ISchedule) => { 24 | for (const key in schedule?.data) { 25 | const sessionData = schedule?.data[key]; 26 | const session = sessionData?.find((item: SessionForSchedule) => item.slug === slug); 27 | if (session) { 28 | const startTime = session.start_time.split(':').slice(0, 2).join(':'); 29 | return `@ ${startTime} | Room ${session?.rooms[0]?.title}`; 30 | } 31 | } 32 | return ''; 33 | }; 34 | 35 | /** 36 | * a function that gets the start time, end time, and room.title of a session from the schedule.data array 37 | * @param _slug slug of session 38 | * @returns start time, end time, and room.title 39 | * @example getSessionTimesAndLocation('session-1') // returns 9:00 AM - 10:00 AM | Room 1 40 | */ 41 | export const getSessionTimesAndLocation = (_slug: string, schedule: ISchedule) => { 42 | for (const key in schedule?.data) { 43 | const sessionData = schedule?.data[key]; 44 | const sessionItem = sessionData?.find((item: SessionForSchedule) => item.slug === _slug); 45 | 46 | if (sessionItem) { 47 | // convert time to 12 hour format and hh:mm aa 48 | const startTime = new Date(sessionItem.start_date_time).toLocaleTimeString('en-US', { 49 | hour: 'numeric', 50 | minute: 'numeric', 51 | hour12: true, 52 | }); 53 | const endTime = new Date(sessionItem.end_date_time).toLocaleTimeString('en-US', { 54 | hour: 'numeric', 55 | minute: 'numeric', 56 | hour12: true, 57 | }); 58 | 59 | return `${startTime} - ${endTime} | Room ${sessionItem?.rooms[0]?.title}`; 60 | } 61 | } 62 | return ''; 63 | }; 64 | 65 | /** 66 | * a function that gets the start time and returns it start time in am format 67 | * @param startTime 68 | * @returns start time 69 | * @example getSessionTime('12:30:00') // returns 12:30 PM 70 | */ 71 | export const getSessionTime = (startTime: string) => { 72 | const [hours, minutes] = startTime.split(':').map(Number); 73 | 74 | let formattedHours; 75 | let period; 76 | if (hours !== undefined) { 77 | formattedHours = hours % 12; 78 | if (formattedHours === 0) { 79 | formattedHours = 12; // 0 should be displayed as 12 pm 80 | } 81 | 82 | period = hours < 12 ? 'am' : 'pm'; 83 | } 84 | 85 | const formattedMinutes = String(minutes).padStart(2, '0'); 86 | 87 | return `${formattedHours}:${formattedMinutes} ${period}`; 88 | }; 89 | 90 | /** 91 | * function that returns twitter_handle of speaker given the twitter url 92 | * @param url twitter url 93 | * @returns twitter_handle 94 | * @example getTwitterHandle('https://twitter.com/kharioki') // returns kharioki 95 | */ 96 | export const getTwitterHandle = (url?: string) => { 97 | const regex = /twitter\.com\/([A-Za-z0-9_]+)/; 98 | const match = url?.match(regex); 99 | 100 | if (match && match[1]) { 101 | return match[1]; 102 | } else { 103 | return 'N/A'; 104 | } 105 | }; 106 | 107 | /** 108 | * function that returns day, date and key of a given session 109 | * @param schedule ISchedule 110 | * @returns Array 111 | * @example 112 | */ 113 | export const getDaysFromSchedule = (schedule: ISchedule): Array => { 114 | const keys = Object.keys(schedule?.data); 115 | const datesToSave = keys.map((key, index) => { 116 | const date = new Date(key).getDate(); 117 | const dateWithSuffix = getSuffixForDate(date); 118 | return { 119 | day: `Day ${index + 1}`, 120 | date: `${date}${dateWithSuffix}`, 121 | key, 122 | }; 123 | }); 124 | return datesToSave; 125 | }; 126 | 127 | /** 128 | * function that returns suffix for a date 129 | * @param date number 130 | * @returns Array 131 | * @example 2 returns 2nd 132 | */ 133 | const getSuffixForDate = (date: number) => { 134 | const suffix = date > 0 ? ['th', 'st', 'nd', 'rd'][(date > 3 && date < 21) || date % 10 > 3 ? 0 : date % 10] : ''; 135 | return suffix; 136 | }; 137 | 138 | /** 139 | * a function that gets the start time, end time, and room.title of a session from the schedule.data array 140 | * @param start_date_time start time of a session 141 | * @param end_date_time end time of a session 142 | * @param room array of rooms 143 | * @returns start time, end time, and room.title 144 | * @example getScheduleTimesAndLocation('session-1') // returns 9:00 AM - 10:00 AM | Room 1 145 | */ 146 | export const getScheduleTimeAndLocation = (start_date_time: string, end_date_time: string, room?: Room) => { 147 | // convert time to 12 hour format and hh:mm aa 148 | const startTime = new Date(start_date_time).toLocaleTimeString('en-US', { 149 | hour: 'numeric', 150 | minute: 'numeric', 151 | hour12: true, 152 | }); 153 | const endTime = new Date(end_date_time).toLocaleTimeString('en-US', { 154 | hour: 'numeric', 155 | minute: 'numeric', 156 | hour12: true, 157 | }); 158 | 159 | return `${startTime} - ${endTime} ${room !== undefined ? `| Room ${room.title}` : ''}`; 160 | }; 161 | -------------------------------------------------------------------------------- /util/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import { DefaultTheme, ThemeProvider } from '@react-navigation/native'; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import type { RenderOptions } from '@testing-library/react-native'; 4 | import { render } from '@testing-library/react-native'; 5 | import type { ReactElement } from 'react'; 6 | import React from 'react'; 7 | 8 | const queryClient = new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | retry: false, 12 | staleTime: Infinity, 13 | }, 14 | }, 15 | }); 16 | 17 | const TestProviders = ({ children }: { children: React.ReactNode }) => { 18 | const theme = { 19 | ...DefaultTheme, 20 | }; 21 | 22 | return ( 23 | 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | const customRender = (ui: ReactElement, options?: Omit) => 30 | render(ui, { wrapper: TestProviders, ...options }); 31 | 32 | export * from '@testing-library/react-native'; 33 | export { customRender as render }; 34 | -------------------------------------------------------------------------------- /util/time-travel.ts: -------------------------------------------------------------------------------- 1 | import { advanceBy, advanceTo, clear } from 'jest-date-mock'; 2 | 3 | const FRAME_TIME = 10; 4 | 5 | function advanceOneFrame() { 6 | advanceBy(FRAME_TIME); 7 | jest.advanceTimersByTime(FRAME_TIME); 8 | } 9 | 10 | /** 11 | * Setup tests for time travel (start date) 12 | */ 13 | export function setup(startDate = '') { 14 | jest.useFakeTimers(); 15 | advanceTo(new Date(startDate)); 16 | } 17 | 18 | /** 19 | * Travel a specific amount of time (in ms) inside a test 20 | */ 21 | export function travel(time = FRAME_TIME) { 22 | let framesToRun = time / FRAME_TIME; 23 | 24 | while (framesToRun > 0) { 25 | advanceOneFrame(); 26 | framesToRun--; 27 | } 28 | } 29 | 30 | /** 31 | * End test with time travel 32 | */ 33 | export function teardown() { 34 | clear(); 35 | jest.useRealTimers(); 36 | } 37 | --------------------------------------------------------------------------------