├── .eas
└── workflows
│ └── staging_deploy.yml
├── .env.example
├── .eslintrc.js
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── codeql.yml
│ ├── prod_app_deployment.yml
│ ├── prod_web_deployment.yml
│ ├── staging_app_deployment.yml
│ └── staging_web_deployment.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierrc
├── .vscode
├── extensions.json
├── keybindings.json
└── settings.json
├── CHANGELOG.md
├── CLAUDE.md
├── Procfile
├── README.md
├── app.json
├── babel.config.js
├── buildprod.sh
├── config
└── nginx.conf.erb
├── deploy_frontend.sh
├── docs
├── docs
│ ├── capabilities.md
│ ├── faq.md
│ ├── index.md
│ ├── material.md
│ ├── ramadan-results.md
│ └── validation.md
└── mkdocs.yml
├── eas.json
├── metro.config.js
├── nativewind-env.d.ts
├── nginx.conf.sample
├── package-lock.json
├── package.json
├── public
├── icon-192x192.png
├── icon-256x256.png
├── icon-512x512.png
├── icons
│ ├── ansariWordDesktop.svg
│ ├── ansariWordMobile.svg
│ ├── compositeLogo.svg
│ ├── compositeLogoDark.svg
│ ├── compositeLogoMobile.svg
│ └── logo.svg
├── images
│ ├── background.png
│ ├── backgroundImage.svg
│ ├── backgroundRoundImage.svg
│ ├── darkBackgroundImage.svg
│ └── lightBackgroundImage.svg
├── index.html
├── logo-192_x_192.png
├── logo-256_x_256.png
├── logo-512_x_512.png
├── manifest.json
└── robots.txt
├── src
├── app
│ ├── (app)
│ │ ├── _layout.tsx
│ │ ├── chat
│ │ │ └── [threadId].tsx
│ │ ├── delete-account.tsx
│ │ ├── index.tsx
│ │ └── logout.tsx
│ ├── (public)
│ │ ├── 404.tsx
│ │ ├── _layout.tsx
│ │ ├── forgot-password.tsx
│ │ ├── login.tsx
│ │ ├── register.tsx
│ │ └── reset-password.tsx
│ ├── +html.tsx
│ ├── +not-found.tsx
│ ├── _layout.tsx
│ ├── share
│ │ ├── [shareThreadId].tsx
│ │ └── _layout.tsx
│ └── welcome
│ │ ├── _layout.tsx
│ │ └── index.tsx
├── assets
│ └── images
│ │ ├── background.png
│ │ ├── icon-dark.png
│ │ ├── icon.png
│ │ ├── splash-icon-dark.png
│ │ └── splash-icon.png
├── components
│ ├── ActionButtons.tsx
│ ├── AppUpdatePopup.tsx
│ ├── ConfirmationDialog.tsx
│ ├── Footer.tsx
│ ├── Header.tsx
│ ├── InfoPopup.tsx
│ ├── KeyboardHandler.tsx
│ ├── LanguageSelector.tsx
│ ├── LoadingScreen.tsx
│ ├── MaintenanceScreen.tsx
│ ├── RootContainer.tsx
│ ├── RootContainer.web.tsx
│ ├── RootImageBackground.tsx
│ ├── RootImageBackground.web.tsx
│ ├── StyledText.tsx
│ ├── Subscription.tsx
│ ├── TermsAndPrivacy.tsx
│ ├── Toast.tsx
│ ├── buttons
│ │ └── ENButton.tsx
│ ├── chat
│ │ ├── ChatContainer.tsx
│ │ ├── ChatInput.tsx
│ │ ├── MessageBubble.tsx
│ │ ├── MessageList.tsx
│ │ ├── ReactionButtons.tsx
│ │ └── ShareContainer.tsx
│ ├── index.ts
│ ├── menu
│ │ ├── MenuDrawer.tsx
│ │ ├── NameContainer.tsx
│ │ ├── SideMenuBody.tsx
│ │ └── index.ts
│ ├── prompts
│ │ ├── PromptCard.tsx
│ │ └── PromptList.tsx
│ ├── share
│ │ └── SharePopup.tsx
│ ├── svg
│ │ ├── AddIcon.tsx
│ │ ├── AnsariWordDesktopIcon.tsx
│ │ ├── AnsariWordMobileIcon.tsx
│ │ ├── ChallengeIcon.tsx
│ │ ├── ChatDeleteLineIcon.tsx
│ │ ├── ChatIcon.tsx
│ │ ├── CheckIcon.tsx
│ │ ├── CloseIcon.tsx
│ │ ├── CollapseIcon.tsx
│ │ ├── CopyIcon.tsx
│ │ ├── DarkIcon.tsx
│ │ ├── DeleteIcon.tsx
│ │ ├── DislikeIcon.tsx
│ │ ├── DoubleCheckIcon.tsx
│ │ ├── EditIcon.tsx
│ │ ├── ExpandIcon.tsx
│ │ ├── EyeIcon.tsx
│ │ ├── FlagIcon.tsx
│ │ ├── FullScreenExitIcon.tsx
│ │ ├── FullScreenIcon.tsx
│ │ ├── InfoIcon.tsx
│ │ ├── InformationGreenIcon.tsx
│ │ ├── InformationIcon.tsx
│ │ ├── Language18Icon.tsx
│ │ ├── Language24Icon.tsx
│ │ ├── LanguageGreenIcon.tsx
│ │ ├── LanguageIcon.tsx
│ │ ├── LightIcon.tsx
│ │ ├── LikeIcon.tsx
│ │ ├── LineIcon.tsx
│ │ ├── LoadingIcon.tsx
│ │ ├── LogoIcon.tsx
│ │ ├── LogoRoundIcon.tsx
│ │ ├── LogoTextIcon.tsx
│ │ ├── LogoutIcon.tsx
│ │ ├── MenuIcon.tsx
│ │ ├── MenuKebabIcon.tsx
│ │ ├── MessageLoaderIcon.tsx
│ │ ├── PaperPlaneLeftIcon.tsx
│ │ ├── PaperPlaneRightIcon.tsx
│ │ ├── PrayingIcon.tsx
│ │ ├── ReactNativeSvg.tsx
│ │ ├── RenameIcon.tsx
│ │ ├── RightArrowIcon.tsx
│ │ ├── ScrollToBottomIcon.tsx
│ │ ├── SelectTextIcon.tsx
│ │ ├── SendIcon.tsx
│ │ ├── SettingIcon.tsx
│ │ ├── ShareIcon.tsx
│ │ ├── StopIcon.tsx
│ │ ├── StopResponseIcon.tsx
│ │ ├── UserIcon.tsx
│ │ ├── WarningCircleIcon.tsx
│ │ └── index.ts
│ └── threads
│ │ ├── IconContainer.tsx
│ │ ├── ThreadCard.tsx
│ │ ├── ThreadsList.tsx
│ │ └── index.ts
├── constant
│ └── index.ts
├── env
│ └── index.ts
├── errors
│ ├── ApplicationError.ts
│ ├── ForbiddenError.ts
│ ├── NotFoundError.ts
│ ├── TokenRefreshError.ts
│ └── index.ts
├── global.css
├── hooks
│ ├── chat
│ │ ├── index.ts
│ │ ├── useChat.ts
│ │ ├── useFeedbackHandler.ts
│ │ ├── useFeedbackService.ts
│ │ └── useScrollManagement.ts
│ ├── index.ts
│ ├── useAuth.ts
│ ├── useDeleteAccount.ts
│ ├── useDirection.ts
│ ├── useGuest.ts
│ ├── useLogout.ts
│ ├── useScreenInfo.ts
│ ├── useToggleInfoPopup.ts
│ └── useTokenFromUrl.ts
├── i18n
│ ├── i18n.ts
│ ├── index.ts
│ ├── locales
│ │ ├── ar.json
│ │ ├── ar
│ │ │ ├── common.json
│ │ │ ├── feedback.json
│ │ │ ├── login.json
│ │ │ ├── prompts.json
│ │ │ └── register.json
│ │ ├── bs.json
│ │ ├── bs
│ │ │ ├── common.json
│ │ │ ├── feedback.json
│ │ │ ├── login.json
│ │ │ ├── prompts.json
│ │ │ └── register.json
│ │ ├── en.json
│ │ ├── en
│ │ │ ├── common.json
│ │ │ ├── feedback.json
│ │ │ ├── login.json
│ │ │ ├── prompts.json
│ │ │ └── register.json
│ │ ├── fr.json
│ │ ├── fr
│ │ │ ├── common.json
│ │ │ ├── feedback.json
│ │ │ ├── login.json
│ │ │ ├── prompts.json
│ │ │ └── register.json
│ │ ├── id.json
│ │ ├── id
│ │ │ ├── common.json
│ │ │ ├── feedback.json
│ │ │ ├── login.json
│ │ │ ├── prompts.json
│ │ │ └── register.json
│ │ ├── tml
│ │ │ ├── common.json
│ │ │ ├── feedback.json
│ │ │ ├── login.json
│ │ │ ├── prompts.json
│ │ │ └── register.json
│ │ ├── tur.json
│ │ ├── tur
│ │ │ ├── common.json
│ │ │ ├── feedback.json
│ │ │ ├── login.json
│ │ │ ├── prompts.json
│ │ │ └── register.json
│ │ ├── ur.json
│ │ └── ur
│ │ │ ├── common.json
│ │ │ ├── feedback.json
│ │ │ ├── login.json
│ │ │ ├── prompts.json
│ │ │ └── register.json
│ └── resources.ts
├── interfaces
│ ├── index.ts
│ └── message.ts
├── services
│ ├── ApiService.ts
│ ├── ChatService.ts
│ ├── FeedbackService.ts
│ ├── PromptsService.ts
│ ├── StorageService.ts
│ ├── UserService.ts
│ └── index.ts
├── store
│ ├── actions
│ │ ├── authActions.ts
│ │ ├── chatActions.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── slices
│ │ ├── authSlice.ts
│ │ ├── chatSlice.ts
│ │ ├── index.ts
│ │ ├── informationPopupSlice.ts
│ │ ├── inputFullModeSlice.ts
│ │ ├── reactionButtonsSlice.ts
│ │ ├── shareSlice.ts
│ │ ├── sideMenuSlice.ts
│ │ └── themeSlice.ts
│ ├── store.ts
│ └── types
│ │ ├── chatTypes.ts
│ │ └── index.ts
├── types
│ ├── index.ts
│ └── types.ts
├── utils
│ ├── getEnv.ts
│ ├── helpers.ts
│ ├── index.ts
│ ├── styles.ts
│ └── theme.ts
└── validation
│ ├── index.ts
│ ├── loginSchema.ts
│ └── registerSchema.ts
├── tailwind.config.js
├── tsconfig.json
└── vercel.json
/.eas/workflows/staging_deploy.yml:
--------------------------------------------------------------------------------
1 | name: Staging Deployment
2 |
3 | on:
4 | push:
5 | branches: ['develop']
6 |
7 | jobs:
8 | deploy:
9 | type: deploy
10 | name: Staging Deployment
11 | environment: preview
12 | params:
13 | prod: true
14 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | EXPO_PUBLIC_ENVIRONMENT=development
2 | EXPO_PUBLIC_SHARE_URL=http://localhost:8081
3 |
4 | EXPO_PUBLIC_API_V2_URL=https://staging-api.ansari.chat/api/v2
5 | EXPO_PUBLIC_API_TIMEOUT=60000
6 |
7 | EXPO_PUBLIC_SUBSCRIBE_URL=http://eepurl.com/iFCJaA
8 | EXPO_PUBLIC_FEEDBACK_EMAIL=feedback@ansari.chat
9 | EXPO_PUBLIC_COMPREHENSIVE_GUIDE_URL=https://docs.ansari.chat/capabilities/
10 | EXPO_PUBLIC_PRIVACY_URL=https://docs.ansari.chat/privacy/
11 | EXPO_PUBLIC_TERMS_URL=https://docs.ansari.chat/terms/
12 | EXPO_PUBLIC_ENABLE_SHARE=false
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // https://docs.expo.dev/guides/using-eslint/
2 | module.exports = {
3 | extends: ['expo', 'prettier'],
4 | plugins: ['prettier'],
5 | rules: {
6 | '@typescript-eslint/explicit-function-return-type': 'off',
7 | 'no-undef': 'off',
8 | '@typescript-eslint/no-require-imports': 'off',
9 | 'react/react-in-jsx-scope': 'off',
10 | camelcase: 'error',
11 | 'spaced-comment': 'error',
12 | quotes: ['error', 'single'],
13 | 'no-duplicate-imports': 'error',
14 | 'prettier/prettier': [
15 | 'error',
16 | {
17 | endOfLine: 'auto',
18 | },
19 | ],
20 | },
21 | ignorePatterns: ['/dist/*', '/capacitor/*', '/android/*', '/ios/*'],
22 | }
23 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 | *.png binary
--------------------------------------------------------------------------------
/.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 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
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 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.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 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/prod_web_deployment.yml:
--------------------------------------------------------------------------------
1 | name: Production Web Deployment
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | build-and-deploy:
9 | runs-on: ubuntu-latest
10 | environment: production
11 | env:
12 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
13 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
14 | steps:
15 | - name: 🏗 Setup repo
16 | uses: actions/checkout@v3
17 |
18 | - name: 🏗 Setup Node
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: 20.x
22 |
23 | - name: 📦 Install Vercel CLI
24 | run: npm install --global vercel@latest
25 |
26 | - name: 📦 Install dependencies
27 | run: npm ci
28 |
29 | - name: 🏗 Setup EAS
30 | uses: expo/expo-github-action@v8
31 | with:
32 | eas-version: latest
33 | token: ${{ secrets.EXPO_TOKEN }}
34 |
35 | - name: Pull Vercel Environment Information
36 | run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
37 |
38 | - name: 🏗 Pull in environment variables
39 | run: eas env:pull production
40 |
41 | - name: 🏗 Build
42 | run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
43 |
44 | - name: 🚀 Deploy to Vercel
45 | run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
46 |
--------------------------------------------------------------------------------
/.github/workflows/staging_app_deployment.yml:
--------------------------------------------------------------------------------
1 | name: Staging App Deployment
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 |
8 | env:
9 | EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
10 |
11 | jobs:
12 | continuously-deploy:
13 | runs-on: ubuntu-latest
14 | concurrency: continuous-deploy-fingerprint-${{ github.run_id }}
15 | permissions:
16 | contents: read # Allow checkout
17 | steps:
18 | - name: 🏗 Setup repo
19 | uses: actions/checkout@v4
20 |
21 | - name: 🏗 Setup Node
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: 18.x
25 |
26 | - name: 📦 Install dependencies
27 | run: npm ci
28 |
29 | - name: 🏗 Setup EAS
30 | uses: expo/expo-github-action@main
31 | with:
32 | eas-version: latest
33 | token: ${{ secrets.EXPO_TOKEN }}
34 |
35 | - name: Continuously Deploy
36 | uses: expo/expo-github-action/continuous-deploy-fingerprint@main
37 | with:
38 | profile: preview
39 | branch: preview
40 | environment: preview
41 |
--------------------------------------------------------------------------------
/.github/workflows/staging_web_deployment.yml:
--------------------------------------------------------------------------------
1 | name: Staging Web Deployment
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 | jobs:
8 | build-and-deploy:
9 | runs-on: ubuntu-latest
10 | environment: staging
11 | env:
12 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
13 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
14 | steps:
15 | - name: 🏗 Setup repo
16 | uses: actions/checkout@v3
17 |
18 | - name: 🏗 Setup Node
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: 20.x
22 |
23 | - name: 📦 Install Vercel CLI
24 | run: npm install --global vercel@latest
25 |
26 | - name: 📦 Install dependencies
27 | run: npm ci
28 |
29 | - name: 🏗 Setup EAS
30 | uses: expo/expo-github-action@v8
31 | with:
32 | eas-version: latest
33 | token: ${{ secrets.EXPO_TOKEN }}
34 |
35 | - name: Pull Vercel Environment Information
36 | run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
37 |
38 | - name: 🏗 Pull in environment variables
39 | run: eas env:pull preview
40 |
41 | - name: 🏗 Build
42 | run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
43 |
44 | - name: 🚀 Deploy to Vercel
45 | run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # Expo
9 | .expo/
10 | dist/
11 | web-build/
12 | expo-env.d.ts
13 |
14 | # Native
15 | ios
16 | android
17 | android-keystores
18 | google-service-account
19 | *.orig.*
20 | *.jks
21 | *.p8
22 | *.p12
23 | *.key
24 | *.mobileprovision
25 | credentials.json
26 |
27 | # Metro
28 | .metro-health-check*
29 |
30 | # testing
31 | /coverage
32 |
33 | # production
34 | /build
35 |
36 | # misc
37 | .DS_Store
38 | .env.local
39 | .env.development.local
40 | .env.test.local
41 | .env.production.local
42 | .history
43 | .env
44 |
45 | npm-debug.log*
46 | yarn-debug.log*
47 | yarn-error.log*
48 | node_modules
49 |
50 | .metro-health-check*
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run lint
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "printWidth": 120,
5 | "tabWidth": 2,
6 | "trailingComma": "all",
7 | "jsxSingleQuote": true,
8 | "bracketSpacing": true,
9 | "arrowParens": "always"
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Gruntfuggly.todo-tree"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/keybindings.json:
--------------------------------------------------------------------------------
1 | // Place your key bindings in this file to override the defaultsauto[]
2 | [
3 | {
4 | "key": "ctrl+shift+down",
5 | "command": "editor.action.copyLinesDownAction",
6 | "when": "editorTextFocus && !editorReadonly"
7 | },
8 | {
9 | "key": "ctrl+shift+alt+down",
10 | "command": "-editor.action.copyLinesDownAction",
11 | "when": "editorTextFocus && !editorReadonly"
12 | },
13 | {
14 | "key": "ctrl+shift+up",
15 | "command": "editor.action.copyLinesUpAction",
16 | "when": "editorTextFocus && !editorReadonly"
17 | },
18 | {
19 | "key": "ctrl+shift+alt+up",
20 | "command": "-editor.action.copyLinesUpAction",
21 | "when": "editorTextFocus && !editorReadonly"
22 | }
23 | ]
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.eol": "\n",
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "editor.formatOnSave": true,
5 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
6 | "[json]": {
7 | "editor.defaultFormatter": "esbenp.prettier-vscode",
8 | "editor.formatOnSave": true
9 | },
10 | "reactSnippets.settings.prettierEnabled": true,
11 | "prettier.jsxSingleQuote": true,
12 | "prettier.semi": false,
13 | "prettier.singleQuote": true,
14 | "prettier.trailingComma": "all",
15 | "prettier.useTabs": true,
16 | "prettier.vueIndentScriptAndStyle": true,
17 | "vs-code-prettier-eslint.prettierLast": true,
18 | "editor.columnSelection": false,
19 | "todo-tree.tree.showBadges": false,
20 | "todo-tree.tree.disableCompactFolders": true,
21 | "cSpell.words": ["endeavorpal", "Pressable", "reduxjs"]
22 | }
23 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Version 1.1.0
2 |
3 | ## New Feature
4 |
5 | - Add Français language.
6 | - Add "Stop" response.
7 | - Disable Pull to Refresh on mobile
8 | - Add greeting text on the homepage.
9 | - Change the mobile "Enter" button to add a new line.
10 |
11 | ## Bug fixes
12 |
13 | - Auto-scroll message while printing the response
14 |
15 | # Version 1.0.0
16 |
17 | Initial Release with core functionality, supporting English, العربية, Türkçe, اردو, Bosanski, Bahasa Indonesia
18 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bin/start-nginx-static
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "Ansari Chat",
4 | "owner": "ansari-project",
5 | "slug": "ansari-chat",
6 | "version": "1.0.0",
7 | "scheme": "ansarichat",
8 | "userInterfaceStyle": "automatic",
9 | "orientation": "portrait",
10 | "newArchEnabled": true,
11 | "icon": "./src/assets/images/icon.png",
12 | "web": {
13 | "bundler": "metro"
14 | },
15 | "plugins": [
16 | "expo-router",
17 | "expo-font",
18 | "expo-localization",
19 | [
20 | "@sentry/react-native/expo",
21 | {
22 | "organization": "ansari-project-llc",
23 | "project": "ansari-frontend",
24 | "url": "https://sentry.io/"
25 | }
26 | ],
27 | [
28 | "expo-splash-screen",
29 | {
30 | "backgroundColor": "#FFFFFF",
31 | "image": "./src/assets/images/splash-icon.png",
32 | "dark": {
33 | "backgroundColor": "#0F0F0F",
34 | "image": "./src/assets/images/splash-icon-dark.png"
35 | },
36 | "imageWidth": 200,
37 | "resizeMode": "contain"
38 | }
39 | ],
40 | [
41 | "expo-screen-orientation",
42 | {
43 | "initialOrientation": "PORTRAIT"
44 | }
45 | ]
46 | ],
47 | "extra": {
48 | "supportsRTL": true,
49 | "router": {
50 | "origin": false
51 | },
52 | "eas": {
53 | "projectId": "e2f465a7-8007-4e83-91ba-4a9df4c5209a"
54 | }
55 | },
56 | "android": {
57 | "package": "chat.ansari.app"
58 | },
59 | "ios": {
60 | "bundleIdentifier": "chat.ansari.app",
61 | "supportsTablet": false,
62 | "infoPlist": {
63 | "ITSAppUsesNonExemptEncryption": false
64 | }
65 | },
66 | "runtimeVersion": {
67 | "policy": "fingerprint"
68 | },
69 | "updates": {
70 | "url": "https://u.expo.dev/e2f465a7-8007-4e83-91ba-4a9df4c5209a"
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true)
3 | return {
4 | presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
5 |
6 | plugins: ['@babel/plugin-proposal-export-namespace-from', 'react-native-reanimated/plugin'],
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/buildprod.sh:
--------------------------------------------------------------------------------
1 | cp .env.prod .env
2 | source .env
3 | rm -rf yarn.lock node_modules build package-lock.json
4 | yarn install
5 | yarn add --dev @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier eslint-config-prettier eslint-plugin-prettier react-app-rewired react-app-rewire-alias
6 | yarn build
7 |
--------------------------------------------------------------------------------
/deploy_frontend.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Configurations
5 | BUCKET_NAME=${BUCKET_NAME:-"ansari.chat"}
6 | REGION=${REGION:-"us-west1"}
7 | BUILD_DIR=${BUILD_DIR:-"$PWD/dist"}
8 | MAIN_PAGE=${MAIN_PAGE:-"index.html"}
9 | # Make the error page also index.html to do client-side routing
10 | ERROR_PAGE=${ERROR_PAGE:-"index.html"}
11 |
12 | # Validations
13 | [[ -z "$BUCKET_NAME" || ! "$BUCKET_NAME" =~ ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ ]] && { echo "Invalid BUCKET_NAME."; exit 1; }
14 | command -v gcloud &>/dev/null || { echo "gcloud not found."; exit 1; }
15 |
16 | # Build React App (if applicable)
17 | [[ "$REBUILD_APP" == true ]] && { command -v yarn &>/dev/null && yarn build || { echo "Yarn not found."; exit 1; }; }
18 | [[ ! -d "$BUILD_DIR" ]] && { echo "BUILD_DIR not found."; exit 1; }
19 |
20 | # Create Bucket if Not Exists
21 | gcloud storage buckets list --filter="name:$BUCKET_NAME" --format="value(name)" | grep -q "$BUCKET_NAME" || \
22 | gcloud storage buckets create "gs://$BUCKET_NAME" --location="$REGION"
23 |
24 | # Upload Files
25 | gcloud storage rsync -r --delete-unmatched-destination-objects "$BUILD_DIR" "gs://$BUCKET_NAME"
26 |
27 | # Set Website Configuration
28 | gcloud storage buckets update "gs://$BUCKET_NAME" --web-main-page-suffix="$MAIN_PAGE" --web-error-page="$ERROR_PAGE"
29 |
30 | # Make Bucket Public (if applicable)
31 | [[ "$MAKE_BUCKET_PUBLIC" == true ]] && \
32 | gcloud storage buckets add-iam-policy-binding "gs://$BUCKET_NAME" --member="allUsers" --role="roles/storage.objectViewer"
33 |
34 | # Output Final URL
35 | echo "Deployment complete. Your site: https://$BUCKET_NAME"
36 |
37 |
--------------------------------------------------------------------------------
/docs/docs/faq.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions
2 |
3 | ## What is Ansari?
4 |
5 | Ansari is an AI assistant to help increase understanding of the Islamic faith and help Muslims practise their faith more effectively.
6 |
7 | ## Who wrote Ansari?
8 |
9 | Waleed Kadous ([site](http://walee.dk/home), [LinkedIn](https://www.linkedin.com/in/waleedkadous/)) is the primary author of Ansari. EndeavorPal has been helping with the web frontend and mobile applications.
10 |
11 | ## Where do I send my thoughts, errors, feedback about Ansari?
12 |
13 | Please send to [feedback@ansari.chat]("mailto:feedback@ansari.chat"). It is checked daily.
14 |
15 | ## Is there a particular Large Language Model Ansari uses?
16 |
17 | Yes. It uses GPT-4-Turbo from [OpenAI](https://openai.com). We have also tried GPT-3.5-Turbo, but the accuracy really suffers. In one test, GPT-4-Turbo running Ansari got 97% accuracy; GPT-3.5-Turbo with identical prompts, tools etc, only got 77% accuracy.
18 |
19 | ## How is it different to GPT-4-Turbo, then?
20 |
21 | It differs in the following ways:
22 |
23 | - Carefully crafted system prompts to ensure information is presented from an Islamic perspective.
24 | - Tools specifically to retrieve Qur'an and Sunnah for retrieval augmented generation. This helps significantly reduce hallucination.
25 | - Has been validated on multiple Islamic data sets.
26 | - Is constrained to focus and answer questions primarily on topics related to Islam.
27 |
28 | ## Is Ansari Open Source?
29 |
30 | Yes it is. Here is the source code for the [backend](https://github.com/waleedkadous/ansari-backend) and the [frontend](https://github.com/waleedkadous/ansari-frontend).
31 |
32 | ## Does Ansari cost money?
33 |
34 | Users do not have to pay anything to use Ansari. However, LLMs are very expensive to use (especially high end onesl ike GPT-4-Turbo). So each request and response costs about 5c or so.
35 |
36 |
--------------------------------------------------------------------------------
/docs/docs/index.md:
--------------------------------------------------------------------------------
1 | ## Ansari
2 |
3 | Ansari is an AI assistant to increase understanding of the Islamic faith and to help Muslims improve their practice of Islam.
4 |
5 | Since AI assistants are such a new thing and Islamic AI assistants even moreso, this documentation is intended to help explain how to use Ansari and share some of the findings and work behind it.
6 |
7 | In the rest of this documentation, we will cover:
8 |
9 | - [What Ansari can do](capabilities.md)
10 | - [The work that has gone in to validating Ansari](validation.md).
11 | - [Frequently asked questions about Ansari](faq.md).
12 |
13 | There's also a section on [additional material](material.md) about Ansari.
14 |
15 | If you have any suggestions for the documentation, please send e-mail to [feedback@ansari.chat](mailto:feedback@ansari.chat).
16 |
17 |
--------------------------------------------------------------------------------
/docs/docs/material.md:
--------------------------------------------------------------------------------
1 | # Additional Material
2 |
3 | ## Presentation about Ansari at International Conference on Islamic Applications in Computer Science and Technologies 2023
4 |
5 | ### Video
6 |
7 |
8 |
9 | ### Slides
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Ansari
2 | site_url: https://beta.ansari.chat/docs
3 | nav:
4 | - Home: index.md
5 | - What Ansari can do: capabilities.md
6 | - Validation: validation.md
7 | - FAQ: faq.md
8 | - Additional material: material.md
9 |
10 | theme: readthedocs
11 |
12 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 15.0.12",
4 | "appVersionSource": "remote"
5 | },
6 | "build": {
7 | "development": {
8 | "environment": "development",
9 | "channel": "development",
10 | "developmentClient": true,
11 | "distribution": "internal"
12 | },
13 | "preview": {
14 | "environment": "preview",
15 | "channel": "preview",
16 | "distribution": "internal"
17 | },
18 | "production-internal": {
19 | "environment": "production",
20 | "channel": "production",
21 | "distribution": "internal"
22 | },
23 | "production": {
24 | "environment": "production",
25 | "channel": "production",
26 | "autoIncrement": true,
27 | "android": {
28 | "credentialsSource": "remote"
29 | }
30 | }
31 | },
32 | "submit": {
33 | "production": {
34 | "android": {
35 | "serviceAccountKeyPath": "./google-service-account/service-account.json",
36 | "track": "internal"
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getSentryExpoConfig } = require('@sentry/react-native/metro')
2 | const { withNativeWind } = require('nativewind/metro')
3 | const { wrapWithReanimatedMetroConfig } = require('react-native-reanimated/metro-config')
4 |
5 | const config = getSentryExpoConfig(__dirname)
6 |
7 | const nativeWindConfig = withNativeWind(config, { input: './src/global.css' })
8 |
9 | module.exports = wrapWithReanimatedMetroConfig(nativeWindConfig)
10 |
--------------------------------------------------------------------------------
/nativewind-env.d.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
--------------------------------------------------------------------------------
/nginx.conf.sample:
--------------------------------------------------------------------------------
1 | # HTTP Server Configuration
2 |
3 | server {
4 | listen 80;
5 | listen [::]:80;
6 | server_name {your-domain}; # Replace with your domain name
7 |
8 | if ($host = {your-domain}) {
9 | return 301 https://$host$request_uri;
10 | }
11 |
12 | return 404;
13 | }
14 |
15 | server {
16 | server_name {your-domain}; # Replace with your domain name
17 | root {project-path}/build; # Replace with the path to your project build directory
18 |
19 | # HTTPS Server Configuration (Uncomment and configure if SSL is enabled)
20 | listen 443 ssl;
21 | listen [::]:443 ssl;
22 | server_name {your-domain}; # Replace with your domain name
23 |
24 | ssl_certificate {path-to-ssl-keys}/fullchain.pem; # SSL certificate path
25 | ssl_certificate_key {path-to-ssl-keys}/privkey.pem; # SSL key path
26 |
27 | # Default configurations
28 | index index.html;
29 | charset utf-8;
30 |
31 | # Main location block
32 | location / {
33 | try_files $uri $uri/ /index.html?$query_string;
34 | }
35 |
36 | # Proxy Configuration (Uncomment and configure if needed)
37 | # location /api {
38 | # proxy_buffering off;
39 | # proxy_pass {backend-path}/api; # Replace with your backend path
40 | # }
41 |
42 | # Static file caching
43 | location ~* \.(ico|css|js|gif|jpe?g|png|webp)$ {
44 | expires 30d;
45 | add_header Vary Accept-Encoding;
46 | access_log off;
47 | }
48 |
49 | # Security configurations
50 | location ~ /\.ht {
51 | deny all;
52 | }
53 |
54 | location ~ (/\.ht|web.config|/\.git|/\.env) {
55 | deny all;
56 | }
57 |
58 | # Logging configurations
59 | access_log off;
60 | error_log /var/log/nginx/error.log error;
61 | }
62 |
--------------------------------------------------------------------------------
/public/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/public/icon-192x192.png
--------------------------------------------------------------------------------
/public/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/public/icon-256x256.png
--------------------------------------------------------------------------------
/public/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/public/icon-512x512.png
--------------------------------------------------------------------------------
/public/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/public/images/background.png
--------------------------------------------------------------------------------
/public/logo-192_x_192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/public/logo-192_x_192.png
--------------------------------------------------------------------------------
/public/logo-256_x_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/public/logo-256_x_256.png
--------------------------------------------------------------------------------
/public/logo-512_x_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/public/logo-512_x_512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Ansari Chat",
3 | "name": "Ansari Chat App",
4 | "icons": [
5 | {
6 | "src": "logo-192_x_192.png",
7 | "type": "image/png",
8 | "sizes": "192x192"
9 | },
10 | {
11 | "src": "logo-256_x_256.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo-512_x_512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "scope": "/",
23 | "display": "standalone",
24 | "theme_color": "#08786B",
25 | "background_color": "#ffffff",
26 | "orientation": "any"
27 | }
28 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/app/(app)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth } from '@/hooks'
2 | import React from 'react'
3 | import { View } from 'react-native'
4 | import Footer from '@/components/Footer'
5 | import Header from '@/components/Header'
6 | import { MenuDrawer } from '@/components/menu'
7 | import { Redirect, Slot } from 'expo-router'
8 | import RootImageBackground from '@/components/RootImageBackground'
9 | import { SafeAreaView } from 'react-native-safe-area-context'
10 | import KeyboardHandler from '@/components/KeyboardHandler'
11 |
12 | /**
13 | * AppLayout Component.
14 | * This component serves as the main layout for the application.
15 | * It includes the header, side menu, and main content area.
16 | */
17 | export const AppLayout = () => {
18 | // Hook to check authentication status
19 | const { isAuthenticated, accessToken } = useAuth()
20 |
21 | const showFooter = true
22 |
23 | if (!isAuthenticated || !accessToken) {
24 | return
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {showFooter && }
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default AppLayout
50 |
--------------------------------------------------------------------------------
/src/app/(app)/index.tsx:
--------------------------------------------------------------------------------
1 | import { ChatContainer, Toast } from '@/components'
2 | import { useAuth, useScreenInfo } from '@/hooks'
3 | import { AppDispatch, setActiveThread, toggleSideMenu } from '@/store'
4 | import React, { useEffect, useState } from 'react'
5 | import { View } from 'react-native'
6 | import { useDispatch } from 'react-redux'
7 | import { useLocalSearchParams } from 'expo-router'
8 |
9 | const HomeScreen: React.FC = () => {
10 | const dispatch = useDispatch()
11 | const { isAuthenticated, isGuest } = useAuth()
12 | const [toastVisible, setToastVisible] = useState(false)
13 | const params = useLocalSearchParams()
14 | const errorMessage = params.errorMsg || null
15 | const { isMobile } = useScreenInfo()
16 |
17 | // Initialize activeThread to null when the component mounts
18 | useEffect(() => {
19 | // If the user is authenticated and not a guest, set the active thread to null.
20 | // Guest users will have the active thread set to the last thread they were in.
21 | if (isAuthenticated && !isGuest) {
22 | dispatch(setActiveThread(null))
23 | }
24 | if (!isMobile && !isGuest) {
25 | dispatch(toggleSideMenu(true))
26 | }
27 | }, [])
28 |
29 | useEffect(() => {
30 | if (errorMessage !== null && errorMessage.length > 0) {
31 | setToastVisible(true)
32 | }
33 | return () => setToastVisible(false)
34 | }, [errorMessage])
35 |
36 | return (
37 |
38 |
39 | {toastVisible && setToastVisible(false)} />}
40 |
41 | )
42 | }
43 |
44 | export default HomeScreen
45 |
--------------------------------------------------------------------------------
/src/app/(app)/logout.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { ActivityIndicator, useColorScheme, View } from 'react-native'
3 | import { getThemeStyle } from '@/utils'
4 | import { useLogout, useScreenInfo } from '@/hooks'
5 |
6 | const LogoutScreen: React.FC = () => {
7 | const { width, height } = useScreenInfo()
8 | const colorScheme = useColorScheme()
9 | const doLogout = useLogout()
10 |
11 | useEffect(() => {
12 | doLogout()
13 | }, [])
14 |
15 | return (
16 |
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default LogoutScreen
28 |
--------------------------------------------------------------------------------
/src/app/(public)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth, useScreenInfo } from '@/hooks'
2 | import React from 'react'
3 | import { View } from 'react-native'
4 | import ActionButtons from '@/components/ActionButtons'
5 | import Footer from '@/components/Footer'
6 | import { Redirect, Slot } from 'expo-router'
7 | import RootImageBackground from '@/components/RootImageBackground'
8 |
9 | /**
10 | * PublicLayout Component.
11 | * This component defines the layout for public pages.
12 | * It includes a header, main content area, and a footer.
13 | * @returns JSX element representing the PublicLayout component.
14 | */
15 | export const PublicLayout = () => {
16 | const { isAuthenticated, accessToken } = useAuth()
17 | // Hook to get screen information
18 | const { isSmallScreen, isMobile } = useScreenInfo()
19 |
20 | if (isAuthenticated && accessToken) {
21 | return
22 | }
23 |
24 | return (
25 |
26 |
27 | {isMobile && (
28 |
29 |
30 |
31 | )}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | export default PublicLayout
43 |
--------------------------------------------------------------------------------
/src/app/+html.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollViewStyleReset } from 'expo-router/html'
2 |
3 | // This file is web-only and used to configure the root HTML for every
4 | // web page during static rendering.
5 | // The contents of this function only run in Node.js environments and
6 | // do not have access to the DOM or browser APIs.
7 | export default function Root({ children }: { children: React.ReactNode }) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | {/*
16 | This viewport disables scaling which makes the mobile website act more like a native app.
17 | However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
18 |
19 | */}
20 |
24 |
25 |
26 |
27 |
28 | Ansari Chat
29 |
30 | {/*
31 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
32 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
33 | */}
34 |
35 |
36 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
37 |
38 | {/* Add any additional elements that you want globally available on web... */}
39 |
40 | {children}
41 |
42 | )
43 | }
44 |
45 | const responsiveBackground = `
46 | body {
47 | background-color: #fff;
48 | }
49 | @media (prefers-color-scheme: dark) {
50 | body {
51 | background-color: #000;
52 | }
53 | }`
54 |
--------------------------------------------------------------------------------
/src/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect, useLocalSearchParams } from 'expo-router'
3 |
4 | // Define the NotFoundScreen component
5 | const NotFoundScreen: React.FC = () => {
6 | const params = useLocalSearchParams()
7 |
8 | return
9 | }
10 |
11 | export default NotFoundScreen
12 |
--------------------------------------------------------------------------------
/src/app/share/[shareThreadId].tsx:
--------------------------------------------------------------------------------
1 | import { ShareContainer } from '@/components'
2 | import { useChat } from '@/hooks'
3 | import { AppDispatch } from '@/store'
4 | import { fetchSharedThread } from '@/store/actions'
5 | import { Helpers } from '@/utils'
6 | import React, { useEffect } from 'react'
7 | import { View } from 'react-native'
8 | import { useDispatch } from 'react-redux'
9 | import { useRouter, useLocalSearchParams } from 'expo-router'
10 |
11 | /**
12 | * Displays a chat interface allowing users to send and view messages within a thread.
13 | * Handles thread creation and message sending with real-time updates.
14 | */
15 | const ShareScreen: React.FC = () => {
16 | const { shareThreadId } = useLocalSearchParams<{ shareThreadId?: string }>()
17 | const dispatch = useDispatch()
18 | const { abortRequest } = useChat()
19 | const router = useRouter()
20 |
21 | // Fetches thread details on threadId change
22 | useEffect(() => {
23 | if (shareThreadId) {
24 | dispatch(fetchSharedThread(shareThreadId))
25 | .unwrap()
26 | .catch((error) => {
27 | router.push('/404')
28 | })
29 | } else {
30 | router.push('/404')
31 | }
32 | }, [shareThreadId, dispatch, router])
33 |
34 | // Clean up the abort controller on unmount or when the component is no longer active
35 | useEffect(() => {
36 | return () => {
37 | abortRequest()
38 | }
39 | }, [])
40 |
41 | return (
42 |
43 |
44 |
45 | )
46 | }
47 |
48 | export default ShareScreen
49 |
--------------------------------------------------------------------------------
/src/app/share/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { useScreenInfo } from '@/hooks'
2 | import React from 'react'
3 | import { ScrollView, View } from 'react-native'
4 | import ActionButtons from '@/components/ActionButtons'
5 | import Footer from '@/components/Footer'
6 | import { Slot } from 'expo-router'
7 | import RootImageBackground from '@/components/RootImageBackground'
8 |
9 | /**
10 | * ShareLayout Component.
11 | * This component defines the layout for public pages.
12 | * It includes a header, main content area, and a footer.
13 | * @returns JSX element representing the ShareLayout component.
14 | */
15 | export const ShareLayout = () => {
16 | const { isSmallScreen, isMobile } = useScreenInfo()
17 |
18 | return (
19 |
20 |
21 | {isMobile && (
22 |
23 |
24 |
25 | )}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | export default ShareLayout
37 |
--------------------------------------------------------------------------------
/src/app/welcome/_layout.tsx:
--------------------------------------------------------------------------------
1 | import RootImageBackground from '@/components/RootImageBackground'
2 | import { useAuth } from '@/hooks'
3 | import { Redirect, Slot } from 'expo-router'
4 | import React from 'react'
5 | import { View } from 'react-native'
6 |
7 | /**
8 | * WelcomeLayout Component.
9 | * This component defines the layout for public pages.
10 | * It includes a header, main content area, and a footer.
11 | * @returns JSX element representing the WelcomeLayout component.
12 | */
13 | export const WelcomeLayout = () => {
14 | const { isAuthenticated, accessToken } = useAuth()
15 |
16 | if (isAuthenticated && accessToken) {
17 | return
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default WelcomeLayout
30 |
--------------------------------------------------------------------------------
/src/assets/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/src/assets/images/background.png
--------------------------------------------------------------------------------
/src/assets/images/icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/src/assets/images/icon-dark.png
--------------------------------------------------------------------------------
/src/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/src/assets/images/icon.png
--------------------------------------------------------------------------------
/src/assets/images/splash-icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/src/assets/images/splash-icon-dark.png
--------------------------------------------------------------------------------
/src/assets/images/splash-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansari-project/ansari-frontend/e23a4ef43ae9a8f2c0943d40a94ed83c93c7eb45/src/assets/images/splash-icon.png
--------------------------------------------------------------------------------
/src/components/ActionButtons.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth, useDirection } from '@/hooks'
2 | import React from 'react'
3 | import { View } from 'react-native'
4 | import InfoPopup from './InfoPopup'
5 | import LanguageSelector from './LanguageSelector'
6 |
7 | export type Props = {
8 | isTop: boolean
9 | margin?: number
10 | }
11 |
12 | const ActionButtons: React.FC = ({ isTop, margin }: Props) => {
13 | const { isRTL } = useDirection()
14 | const { isAuthenticated } = useAuth()
15 |
16 | const marginStyle = {
17 | marginLeft: isRTL && !isTop ? undefined : margin || 0,
18 | marginRight: isRTL && !isTop ? margin || 0 : undefined,
19 | }
20 |
21 | return (
22 |
23 |
24 | {isAuthenticated && }
25 |
26 | )
27 | }
28 |
29 | export default ActionButtons
30 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth, useScreenInfo } from '@/hooks'
2 | import { RootState } from '@/store'
3 | import React from 'react'
4 | import { useTranslation } from 'react-i18next'
5 | import { Keyboard, Pressable, View } from 'react-native'
6 | import { useSelector } from 'react-redux'
7 | import ActionButtons from './ActionButtons'
8 | import Subscription from './Subscription'
9 | import TermsAndPrivacy from './TermsAndPrivacy'
10 | import { NameContainer } from './menu'
11 | import StyledText from './StyledText'
12 |
13 | const Footer: React.FC = () => {
14 | const { t } = useTranslation()
15 | const { isMobile, isSmallScreen } = useScreenInfo()
16 | const { isAuthenticated, isGuest } = useAuth()
17 | const displayName = !(isAuthenticated && isGuest && (isMobile || isSmallScreen))
18 | const theme = useSelector((state: RootState) => state.theme.theme)
19 | const isInputFullMode = useSelector((state: RootState) => state.input.fullMode)
20 |
21 | if (isInputFullMode) {
22 | return null
23 | }
24 |
25 | const containerClass = 'w-full flex-row justify-start items-center py-2 px-2'
26 |
27 | if (!isAuthenticated) {
28 | return (
29 |
30 | {!isMobile && }
31 |
32 |
33 | )
34 | }
35 |
36 | if (isGuest) {
37 | return (
38 |
39 |
45 | {!isMobile && }
46 |
47 | {t('authorizedFooterText')}
48 |
49 |
50 |
51 | )
52 | }
53 |
54 | return (
55 |
56 |
57 |
58 | {t('authorizedFooterText')}
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
66 | export default Footer
67 |
--------------------------------------------------------------------------------
/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth, useDirection, useScreenInfo } from '@/hooks'
2 | import { AppDispatch, RootState, toggleSideMenu } from '@/store'
3 | import React from 'react'
4 | import { Pressable, View } from 'react-native'
5 | import { useDispatch, useSelector } from 'react-redux'
6 | import ActionButtons from './ActionButtons'
7 | import { MenuIcon } from './svg'
8 |
9 | const Header: React.FC = () => {
10 | const { isAuthenticated, isGuest } = useAuth()
11 | const { isRTL } = useDirection()
12 | const { isSmallScreen, isMobile } = useScreenInfo()
13 | const isSideMenuOpened = useSelector((state: RootState) => state.sideMenu.isOpen)
14 | const displayMenuDrawer = isAuthenticated && !isGuest && (!isSideMenuOpened || isSmallScreen)
15 | const isInputFullMode = useSelector((state: RootState) => state.input.fullMode)
16 | const theme = useSelector((state: RootState) => state.theme.theme)
17 | const dispatch = useDispatch()
18 |
19 | if (isInputFullMode || (!isSmallScreen && !isAuthenticated)) {
20 | return null
21 | }
22 |
23 | const togglePopup = () => {
24 | dispatch(toggleSideMenu(!isSideMenuOpened))
25 | }
26 |
27 | return (
28 |
32 |
33 |
34 | {displayMenuDrawer && (
35 | togglePopup()} className='p-2 rounded'>
36 |
37 |
38 | )}
39 |
40 |
41 | {isMobile && }
42 |
43 |
44 |
45 | )
46 | }
47 |
48 | export default Header
49 |
--------------------------------------------------------------------------------
/src/components/KeyboardHandler.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useKeyboardHandler } from 'react-native-keyboard-controller'
3 | import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
4 |
5 | const PADDING_BOTTOM = 0
6 |
7 | const useGradualAnimation = () => {
8 | const height = useSharedValue(PADDING_BOTTOM)
9 |
10 | useKeyboardHandler(
11 | {
12 | onMove: (event) => {
13 | 'worklet'
14 | height.value = Math.max(event.height, PADDING_BOTTOM)
15 | },
16 | },
17 | [],
18 | )
19 |
20 | return { height }
21 | }
22 |
23 | const KeyboardHandler: React.FC = () => {
24 | const { height } = useGradualAnimation()
25 |
26 | const animatedStyle = useAnimatedStyle(() => {
27 | return {
28 | height: Math.abs(height.value),
29 | marginBottom: height.value > 0 ? 0 : PADDING_BOTTOM,
30 | width: 'auto',
31 | }
32 | })
33 |
34 | return
35 | }
36 |
37 | export default KeyboardHandler
38 |
--------------------------------------------------------------------------------
/src/components/LoadingScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ActivityIndicator, useColorScheme, View } from 'react-native'
3 | import { useScreenInfo } from '../hooks'
4 | import { getThemeStyle } from '@/utils'
5 |
6 | const LoadingScreen: React.FC = () => {
7 | const { width, height } = useScreenInfo()
8 | const colorScheme = useColorScheme()
9 |
10 | return (
11 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default LoadingScreen
23 |
--------------------------------------------------------------------------------
/src/components/MaintenanceScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Text, View } from 'react-native'
3 | import RootContainer from './RootContainer'
4 | import RootImageBackground from './RootImageBackground'
5 | import { LogoIcon } from './svg'
6 |
7 | const MaintenanceScreen: React.FC = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | Ansari Chat is currently under maintenance.
16 |
17 |
18 | We are working hard to bring you the best experience possible.
19 |
20 |
21 | Thank you for your patience!
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default MaintenanceScreen
31 |
--------------------------------------------------------------------------------
/src/components/RootContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { GestureHandlerRootView } from 'react-native-gesture-handler'
3 |
4 | export type Props = {
5 | children: React.ReactNode
6 | }
7 |
8 | const RootContainer: React.FC = ({ children }) => {
9 | return {children}
10 | }
11 |
12 | export default RootContainer
13 |
--------------------------------------------------------------------------------
/src/components/RootContainer.web.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { i18n } from '@/i18n'
3 | import { View } from 'react-native'
4 | import { inject } from '@vercel/analytics'
5 |
6 | inject()
7 |
8 | export type Props = {
9 | children: React.ReactNode
10 | }
11 |
12 | const RootContainer: React.FC = ({ children }) => {
13 | useEffect(() => {
14 | document.dir = i18n.dir(i18n.language)
15 | }, [])
16 |
17 | return {children}
18 | }
19 |
20 | export default RootContainer
21 |
--------------------------------------------------------------------------------
/src/components/RootImageBackground.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ImageBackground } from 'react-native'
3 |
4 | interface RootImageBackgroundProps {
5 | children: React.ReactNode
6 | className?: string
7 | }
8 |
9 | const RootImageBackground: React.FC = ({ children, className }) => {
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | }
16 |
17 | export default RootImageBackground
18 |
--------------------------------------------------------------------------------
/src/components/RootImageBackground.web.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View } from 'react-native'
3 |
4 | interface RootImageBackgroundProps {
5 | children: React.ReactNode
6 | className?: string
7 | }
8 |
9 | const RootImageBackground: React.FC = ({ children, className }) => {
10 | return (
11 |
15 | {children}
16 |
17 | )
18 | }
19 |
20 | export default RootImageBackground
21 |
--------------------------------------------------------------------------------
/src/components/StyledText.tsx:
--------------------------------------------------------------------------------
1 | import { useDirection, useScreenInfo } from '@/hooks'
2 | import { RootState } from '@/store'
3 | import React from 'react'
4 | import { Text, TextProps } from 'react-native'
5 | import { useSelector } from 'react-redux'
6 |
7 | interface StyledTextProps extends TextProps {
8 | variant?: 'h1' | 'h2' | 'h3' | 'body' | 'button'
9 | color?: string
10 | textAlign?: 'left' | 'center' | 'right'
11 | }
12 |
13 | export const StyledText: React.FC = ({
14 | children,
15 | variant = 'body',
16 | color = 'text',
17 | textAlign,
18 | style,
19 | className = '',
20 | ...props
21 | }) => {
22 | const { isSmallScreen } = useScreenInfo()
23 | const { isRTL } = useDirection()
24 | const theme = useSelector((state: RootState) => state.theme.theme)
25 |
26 | const getTextAlign = () => {
27 | if (textAlign) return textAlign
28 |
29 | return isSmallScreen ? 'center' : isRTL ? 'right' : 'left'
30 | }
31 |
32 | const getColor = () => {
33 | switch (color) {
34 | case 'primary':
35 | return theme.primaryColor
36 | case 'link':
37 | return theme.linkColor
38 | case 'primaryButton':
39 | return theme.buttonPrimaryColor
40 | case 'secondaryButton':
41 | return theme.buttonSecondaryColor
42 | case 'text':
43 | return theme.textColor
44 | case 'yellow':
45 | return theme.yellowColor
46 | default:
47 | return theme.textColor
48 | }
49 | }
50 |
51 | const getVariantClasses = () => {
52 | switch (variant) {
53 | case 'h1':
54 | return `${isSmallScreen ? 'text-[28px] leading-[32px]' : 'text-[42px] leading-[50px]'}`
55 | case 'h2':
56 | return 'font-semibold text-[24px] leading-[29px]'
57 | case 'h3':
58 | return 'font-semibold text-[20px] leading-[22px]'
59 | default:
60 | return ''
61 | }
62 | }
63 |
64 | return (
65 |
77 | {children}
78 |
79 | )
80 | }
81 |
82 | export default StyledText
83 |
--------------------------------------------------------------------------------
/src/components/Subscription.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth } from '@/hooks'
2 | import { RootState } from '@/store'
3 | import React from 'react'
4 | import { Trans, useTranslation } from 'react-i18next'
5 | import { Linking, Pressable, Text, View } from 'react-native'
6 | import { useSelector } from 'react-redux'
7 |
8 | /**
9 | * Subscription Component
10 | *
11 | * This component is used to render a subscription link with internationalized text.
12 | * It uses the `Trans` component from `react-i18next` for language translation, ensuring
13 | * the component text changes according to the currently selected language.
14 | */
15 | const Subscription: React.FC = () => {
16 | // Hook from react-i18next to access i18n instance
17 | // The i18n instance provides the current language information and translation functionality.
18 | // When the language changes, the useTranslation hook in the component will force this component to re-render itself.
19 | const { i18n } = useTranslation()
20 | const theme = useSelector((state: RootState) => state.theme.theme)
21 | const { isAuthenticated, isGuest } = useAuth()
22 |
23 | // Allow the component to render only when the user is not authenticated or is a guest
24 | if (isAuthenticated && !isGuest) {
25 | return null
26 | }
27 |
28 | return (
29 |
30 | {
33 | if (typeof window !== 'undefined' && 'open' in window) {
34 | window.open(process.env.EXPO_PUBLIC_SUBSCRIBE_URL, '_blank')
35 | } else {
36 | Linking.openURL(process.env.EXPO_PUBLIC_SUBSCRIBE_URL)
37 | }
38 | }}
39 | >
40 |
41 | {/* The `Trans` component from `react-i18next` is used to render translated text.
42 | - `components` prop allows embedding React components within the translated text.
43 | - In this case, a span element with green color and underline is used for part of the text.
44 | - `i18nKey` is the key in the translation files that holds the text to be translated. */}
45 | ,
48 | }}
49 | i18nKey='subscribe'
50 | i18n={i18n}
51 | />
52 |
53 |
54 |
55 | )
56 | }
57 |
58 | export default Subscription
59 |
--------------------------------------------------------------------------------
/src/components/TermsAndPrivacy.tsx:
--------------------------------------------------------------------------------
1 | import { useDirection, useScreenInfo } from '@/hooks'
2 | import { RootState } from '@/store'
3 | import getEnv from '@/utils/getEnv'
4 | import React from 'react'
5 | import { useTranslation } from 'react-i18next'
6 | import { Linking, Pressable, Text, View } from 'react-native'
7 | import { useSelector } from 'react-redux'
8 |
9 | type Props = {
10 | marginLeft?: number | string
11 | }
12 |
13 | const TermsAndPrivacy: React.FC = ({ marginLeft = -40 }) => {
14 | const { t } = useTranslation()
15 | const { isRTL } = useDirection()
16 | const { isMobile } = useScreenInfo()
17 | const theme = useSelector((state: RootState) => state.theme.theme)
18 |
19 | const marginLeftStyle = isMobile ? (isRTL ? marginLeft : 0) : isRTL ? 0 : marginLeft
20 |
21 | return (
22 |
23 | {
26 | // Open the subscription URL in a new tab on web, and in the system browser on native
27 | if (typeof window !== 'undefined' && 'open' in window) {
28 | window.open(getEnv('TERMS_URL'), '_blank')
29 | } else {
30 | Linking.openURL(getEnv('TERMS_URL'))
31 | }
32 | }}
33 | >
34 |
35 | {t('termOfUse')}
36 |
37 |
38 |
39 | |
40 |
41 | {
44 | // Open the subscription URL in a new tab on web, and in the system browser on native
45 | if (typeof window !== 'undefined' && 'open' in window) {
46 | window.open(getEnv('PRIVACY_URL'), '_blank')
47 | } else {
48 | Linking.openURL(getEnv('PRIVACY_URL'))
49 | }
50 | }}
51 | >
52 |
53 | {t('privacyPolicy')}
54 |
55 |
56 |
57 | )
58 | }
59 |
60 | export default TermsAndPrivacy
61 |
--------------------------------------------------------------------------------
/src/components/Toast.tsx:
--------------------------------------------------------------------------------
1 | import { useScreenInfo } from '@/hooks'
2 | import React, { useEffect, useState } from 'react'
3 | import { Animated, Platform } from 'react-native'
4 | import StyledText from './StyledText'
5 |
6 | interface ToastProps {
7 | message: string | React.ReactNode
8 | duration: number
9 | backgroundColor?: string
10 | onDismiss: () => void
11 | }
12 |
13 | const Toast: React.FC = ({ message, duration, onDismiss, backgroundColor }) => {
14 | const [fadeAnim] = useState(new Animated.Value(0)) // Initial value for opacity: 0
15 | const { isSmallScreen } = useScreenInfo()
16 |
17 | const color = backgroundColor ?? 'red'
18 |
19 | useEffect(() => {
20 | // Fade in
21 | Animated.timing(fadeAnim, {
22 | toValue: 1,
23 | duration: 400,
24 | useNativeDriver: Platform.OS !== 'web',
25 | }).start()
26 |
27 | // After the duration, fade out and call onDismiss
28 | const timer = setTimeout(() => {
29 | Animated.timing(fadeAnim, {
30 | toValue: 0,
31 | duration: 400,
32 | useNativeDriver: Platform.OS !== 'web',
33 | }).start(onDismiss)
34 | }, duration)
35 |
36 | return () => clearTimeout(timer)
37 | }, [fadeAnim, duration, onDismiss])
38 |
39 | return (
40 |
44 | {typeof message === 'string' ? {message} : message}
45 |
46 | )
47 | }
48 |
49 | export default Toast
50 |
--------------------------------------------------------------------------------
/src/components/buttons/ENButton.tsx:
--------------------------------------------------------------------------------
1 | import { useDirection, useScreenInfo } from '@/hooks'
2 | import { RootState } from '@/store'
3 | import { createGeneralThemedStyles } from '@/utils'
4 | import React, { useState } from 'react'
5 | import { Pressable, Text, ViewStyle } from 'react-native'
6 | import { useSelector } from 'react-redux'
7 |
8 | interface Props {
9 | text: string
10 | onClick: () => void
11 | submittingText?: string
12 | isSubmitting?: boolean
13 | buttonStyle?: ViewStyle
14 | buttonHoverStyle?: ViewStyle
15 | buttonTextStyle?: ViewStyle
16 | buttonHoverTextStyle?: ViewStyle
17 | }
18 |
19 | const ENButton: React.FC = ({
20 | text,
21 | submittingText,
22 | buttonStyle,
23 | buttonHoverStyle,
24 | buttonTextStyle,
25 | buttonHoverTextStyle,
26 | isSubmitting,
27 | onClick,
28 | }) => {
29 | const { isSmallScreen } = useScreenInfo()
30 | const theme = useSelector((state: RootState) => state.theme.theme)
31 | const [isHover, setIsHover] = useState(false)
32 | const { isRTL } = useDirection()
33 |
34 | const generalStyle = createGeneralThemedStyles(theme, isRTL, isSmallScreen)
35 |
36 | return (
37 | setIsHover(true)}
46 | onMouseLeave={() => setIsHover(false)}
47 | >
48 |
55 | {isSubmitting ? submittingText || text : text}
56 |
57 |
58 | )
59 | }
60 |
61 | export default ENButton
62 |
--------------------------------------------------------------------------------
/src/components/chat/ShareContainer.tsx:
--------------------------------------------------------------------------------
1 | import { useChat, useScreenInfo } from '@/hooks'
2 | import { RootState } from '@/store'
3 | import React, { useEffect } from 'react'
4 | import { useTranslation } from 'react-i18next'
5 | import { View } from 'react-native'
6 | import { useSelector } from 'react-redux'
7 | import { useRouter } from 'expo-router'
8 | import ENButton from '../buttons/ENButton'
9 | import MessageList, { MessageListRef } from './MessageList'
10 | import StyledText from '../StyledText'
11 |
12 | const ShareContainer: React.FC = () => {
13 | const { activeThread } = useChat()
14 | const sideMenuWidth = useSelector((state: RootState) => state.sideMenu.width)
15 | const theme = useSelector((state: RootState) => state.theme.theme)
16 | const { t } = useTranslation()
17 | const router = useRouter()
18 |
19 | // State to track the last known content height
20 | const messageListRef = React.useRef(null)
21 | const { contentWidth } = useScreenInfo(sideMenuWidth)
22 |
23 | const startChat = () => {
24 | router.push('/')
25 | }
26 |
27 | // Clean up the abort controller on unmount or when the component is no longer active
28 | useEffect(() => {
29 | if (messageListRef.current) {
30 | messageListRef.current.scrollToBottom()
31 | }
32 | }, [])
33 |
34 | return (
35 |
36 |
43 |
44 | {activeThread?.name || t('newChat')}
45 |
46 |
47 |
48 |
55 |
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
63 | export default ShareContainer
64 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ConfirmationDialog } from './ConfirmationDialog'
2 | export { default as Footer } from './Footer'
3 | export { default as Header } from './Header'
4 | export { default as LoadingScreen } from './LoadingScreen'
5 | export { default as MaintenanceScreen } from './MaintenanceScreen'
6 | export { default as Subscription } from './Subscription'
7 | export { default as Toast } from './Toast'
8 | export { default as AppUpdatePopup } from './AppUpdatePopup'
9 | export { default as ChatContainer } from './chat/ChatContainer'
10 | export { default as ShareContainer } from './chat/ShareContainer'
11 | export { default as ChatInput } from './chat/ChatInput'
12 | export { default as MessageBubble } from './chat/MessageBubble'
13 | export { default as PromptCard } from './prompts/PromptCard'
14 | export { default as PromptList } from './prompts/PromptList'
15 | export { default as ENButton } from './buttons/ENButton'
16 |
--------------------------------------------------------------------------------
/src/components/menu/MenuDrawer.tsx:
--------------------------------------------------------------------------------
1 | import { Drawer } from 'react-native-drawer-layout'
2 | import { AppDispatch, RootState, toggleSideMenu } from '@/store'
3 | import React from 'react'
4 | import { View } from 'react-native'
5 | import { useDispatch, useSelector } from 'react-redux'
6 | import SideMenuBody from './SideMenuBody'
7 | import { useAuth, useDirection, useScreenInfo } from '@/hooks'
8 |
9 | export type Props = {
10 | children: React.ReactNode
11 | }
12 |
13 | const MenuDrawer: React.FC = ({ children }) => {
14 | const isSideMenuOpened = useSelector((state: RootState) => state.sideMenu.isOpen)
15 | const { isRTL } = useDirection()
16 | const { isMobile } = useScreenInfo()
17 | const { isGuest } = useAuth()
18 | const dispatch = useDispatch()
19 | const theme = useSelector((state: RootState) => state.theme.theme)
20 |
21 | const togglePopup = (open: boolean) => {
22 | dispatch(toggleSideMenu(open))
23 | }
24 |
25 | return (
26 | togglePopup(true)}
38 | onClose={() => togglePopup(false)}
39 | renderDrawerContent={() => (
40 |
41 |
42 |
43 | )}
44 | >
45 | {children}
46 |
47 | )
48 | }
49 |
50 | export default MenuDrawer
51 |
--------------------------------------------------------------------------------
/src/components/menu/index.ts:
--------------------------------------------------------------------------------
1 | export { default as MenuDrawer } from './MenuDrawer'
2 | export { default as NameContainer } from './NameContainer'
3 | export { default as SideMenuBody } from './SideMenuBody'
4 |
--------------------------------------------------------------------------------
/src/components/prompts/PromptCard.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNativeSvg } from '@/components/svg'
2 | import { useDirection, useScreenInfo } from '@/hooks'
3 | import { RootState } from '@/store'
4 | import React from 'react'
5 | import { Pressable, View } from 'react-native'
6 | import { useSelector } from 'react-redux'
7 | import StyledText from '@/components/StyledText'
8 |
9 | interface Props {
10 | title: string
11 | subtitle: string
12 | Icon: typeof ReactNativeSvg
13 | onPress: () => void
14 | isMiddle: boolean
15 | }
16 |
17 | const PromptCard: React.FC = ({ title, subtitle, Icon, onPress, isMiddle }: Props) => {
18 | const [isPressed, setIsPressed] = React.useState(false)
19 | const [isHovered, setIsHovered] = React.useState(false)
20 | const { isSmallScreen } = useScreenInfo()
21 | const { isRTL } = useDirection()
22 | const theme = useSelector((state: RootState) => state.theme.theme)
23 |
24 | const isActive = isPressed || isHovered
25 |
26 | return (
27 | {
37 | onPress()
38 | setIsPressed(true)
39 | setTimeout(() => setIsPressed(false), 300) // Reset the color after 300ms
40 | }}
41 | onMouseEnter={() => setIsHovered(true)}
42 | onMouseLeave={() => setIsHovered(false)}
43 | >
44 |
45 |
50 | {title}
51 |
52 |
57 | {subtitle}
58 |
59 |
60 |
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | export default PromptCard
68 |
--------------------------------------------------------------------------------
/src/components/svg/AddIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 | import { G, Path, ClipPath, Rect } from 'react-native-svg'
4 |
5 | const AddIcon: React.FC = (props: Props) => {
6 | return (
7 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default AddIcon
31 |
--------------------------------------------------------------------------------
/src/components/svg/ChatDeleteLineIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 | import { Path } from 'react-native-svg'
4 |
5 | const ChatDeleteLineIcon: React.FC = (props: Props) => {
6 | return (
7 |
14 |
20 |
21 | )
22 | }
23 |
24 | export default ChatDeleteLineIcon
25 |
--------------------------------------------------------------------------------
/src/components/svg/ChatIcon.tsx:
--------------------------------------------------------------------------------
1 | import { useDirection } from '@/hooks'
2 | import React from 'react'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 | import { Path } from 'react-native-svg'
5 |
6 | const ChatIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 | const svgProps = {
9 | width: props.width || '32',
10 | height: props.height || '32',
11 | viewBox: props.viewBox || '0 0 32 32',
12 | fill: props.fill || '#FFFFFF',
13 | transform: props.transform || isRTL ? 'scale(-1, 1)' : 'scale(1, 1)',
14 | }
15 | return (
16 |
24 |
29 |
34 |
35 | )
36 | }
37 |
38 | export default ChatIcon
39 |
--------------------------------------------------------------------------------
/src/components/svg/CheckIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const CheckIcon: React.FC = (props: Props) => {
6 | return (
7 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default CheckIcon
23 |
--------------------------------------------------------------------------------
/src/components/svg/CloseIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const CloseIcon: React.FC = (props: Props) => {
6 | return (
7 |
14 |
19 |
20 | )
21 | }
22 |
23 | export default CloseIcon
24 |
--------------------------------------------------------------------------------
/src/components/svg/CollapseIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 | import { useDirection } from '@/hooks'
5 |
6 | const CollapseIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 | return (
9 |
17 |
22 |
23 | )
24 | }
25 |
26 | export default CollapseIcon
27 |
--------------------------------------------------------------------------------
/src/components/svg/CopyIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { G, Path, ClipPath, Rect } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const CopyIcon: React.FC = (props: Props) => {
6 | return (
7 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default CopyIcon
30 |
--------------------------------------------------------------------------------
/src/components/svg/DarkIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const DarkIcon: React.FC = (props: Props) => {
6 | return (
7 |
15 |
19 |
20 | )
21 | }
22 |
23 | export default DarkIcon
24 |
--------------------------------------------------------------------------------
/src/components/svg/DeleteIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const DeleteIcon: React.FC = (props: Props) => {
6 | return (
7 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default DeleteIcon
24 |
--------------------------------------------------------------------------------
/src/components/svg/DoubleCheckIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const DoubleCheckIcon: React.FC = (props: Props) => {
6 | return (
7 |
16 |
23 |
24 | )
25 | }
26 |
27 | export default DoubleCheckIcon
28 |
--------------------------------------------------------------------------------
/src/components/svg/EditIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 | import { useDirection } from '@/hooks'
5 |
6 | const EditIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 | return (
9 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default EditIcon
24 |
--------------------------------------------------------------------------------
/src/components/svg/ExpandIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 | import { useDirection } from '@/hooks'
5 |
6 | const ExpandIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 | return (
9 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default ExpandIcon
24 |
--------------------------------------------------------------------------------
/src/components/svg/EyeIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const EyeIcon: React.FC = (props: Props) => {
6 | return (
7 |
17 | {props.name === 'eye' && (
18 | <>
19 |
20 |
21 | >
22 | )}
23 | {props.name === 'eye-slash' && (
24 | <>
25 |
26 |
27 |
28 | >
29 | )}
30 |
31 | )
32 | }
33 |
34 | export default EyeIcon
35 |
--------------------------------------------------------------------------------
/src/components/svg/FlagIcon.tsx:
--------------------------------------------------------------------------------
1 | import { useDirection } from '@/hooks'
2 | import { G, Path } from 'react-native-svg'
3 | import React from 'react'
4 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
5 |
6 | const FlagIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 | const transform = isRTL ? [{ scale: -1 }] : [{ scale: 1 }]
9 |
10 | return (
11 |
19 |
20 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default FlagIcon
30 |
--------------------------------------------------------------------------------
/src/components/svg/FullScreenExitIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 |
4 | const FullscreenexitIcon: React.FC = (props: Props) => {
5 | return (
6 |
13 | )
14 | }
15 |
16 | export default FullscreenexitIcon
17 |
--------------------------------------------------------------------------------
/src/components/svg/FullScreenIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 |
4 | const FullscreenIcon: React.FC = (props: Props) => {
5 | return (
6 |
13 | )
14 | }
15 |
16 | export default FullscreenIcon
17 |
--------------------------------------------------------------------------------
/src/components/svg/InfoIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const InfoIcon: React.FC = (props: Props) => {
6 | return (
7 |
16 |
20 |
21 | )
22 | }
23 |
24 | export default InfoIcon
25 |
--------------------------------------------------------------------------------
/src/components/svg/InformationGreenIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 | import { Path } from 'react-native-svg'
4 |
5 | const InformationgreenIcon: React.FC = (props: Props) => {
6 | return (
7 |
14 |
20 |
21 | )
22 | }
23 |
24 | export default InformationgreenIcon
25 |
--------------------------------------------------------------------------------
/src/components/svg/InformationIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 | import { Path } from 'react-native-svg'
4 |
5 | const InformationIcon: React.FC = (props: Props) => {
6 | return (
7 |
14 |
20 |
21 | )
22 | }
23 |
24 | export default InformationIcon
25 |
--------------------------------------------------------------------------------
/src/components/svg/LanguageIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const LanguageIcon: React.FC = (props: Props) => {
6 | return (
7 |
15 |
20 |
21 | )
22 | }
23 |
24 | export default LanguageIcon
25 |
--------------------------------------------------------------------------------
/src/components/svg/LightIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const LightIcon: React.FC = (props: Props) => {
6 | return (
7 |
13 |
17 |
18 | )
19 | }
20 |
21 | export default LightIcon
22 |
--------------------------------------------------------------------------------
/src/components/svg/LineIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const LineIcon: React.FC = (props: Props) => {
6 | return (
7 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default LineIcon
20 |
--------------------------------------------------------------------------------
/src/components/svg/LoadingIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 |
4 | const LoadingIcon: React.FC = (props: Props) => {
5 | return (
6 |
13 | )
14 | }
15 |
16 | export default LoadingIcon
17 |
--------------------------------------------------------------------------------
/src/components/svg/LogoutIcon.tsx:
--------------------------------------------------------------------------------
1 | import { useDirection } from '@/hooks'
2 | import { Path } from 'react-native-svg'
3 | import React from 'react'
4 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
5 |
6 | const LogoutIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 | const transform = isRTL ? [{ scale: -1 }] : [{ scale: 1 }]
9 |
10 | return (
11 |
22 |
28 |
35 |
36 | )
37 | }
38 |
39 | export default LogoutIcon
40 |
--------------------------------------------------------------------------------
/src/components/svg/MenuIcon.tsx:
--------------------------------------------------------------------------------
1 | import { useDirection } from '@/hooks'
2 | import { G, Path } from 'react-native-svg'
3 | import React from 'react'
4 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
5 |
6 | const MenuIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 | const transform = isRTL ? [{ scale: -1 }] : [{ scale: 1 }]
9 |
10 | return (
11 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default MenuIcon
30 |
--------------------------------------------------------------------------------
/src/components/svg/MenuKebabIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { G, Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const MenuKebabIcon: React.FC = (props: Props) => {
6 | return (
7 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default MenuKebabIcon
26 |
--------------------------------------------------------------------------------
/src/components/svg/MessageLoaderIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 |
4 | const MessageloaderIcon: React.FC = (props: Props) => {
5 | return (
6 |
13 | )
14 | }
15 |
16 | export default MessageloaderIcon
17 |
--------------------------------------------------------------------------------
/src/components/svg/PaperPlaneLeftIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 | import { Path } from 'react-native-svg'
4 |
5 | const PaperplaneleftIcon: React.FC = (props: Props) => {
6 | return (
7 |
14 |
20 |
21 | )
22 | }
23 |
24 | export default PaperplaneleftIcon
25 |
--------------------------------------------------------------------------------
/src/components/svg/PaperPlaneRightIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 | import { Path } from 'react-native-svg'
4 |
5 | const PaperplanerightIcon: React.FC = (props: Props) => {
6 | return (
7 |
14 |
20 |
21 | )
22 | }
23 |
24 | export default PaperplanerightIcon
25 |
--------------------------------------------------------------------------------
/src/components/svg/ReactNativeSvg.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { ColorValue, View, StyleSheet } from 'react-native'
3 | import Svg, { SvgProps } from 'react-native-svg'
4 |
5 | export type Props = {
6 | hoverFill?: ColorValue
7 | hoverStroke?: ColorValue
8 | style?: object
9 | } & SvgProps
10 |
11 | const ReactNativeSvg = (props: Props) => {
12 | const [isHover, setIsHover] = useState(0)
13 |
14 | if (typeof props.width === 'string' && props.width.includes('%')) {
15 | delete props.width
16 | }
17 |
18 | if (typeof props.height === 'string' && props.height.includes('%')) {
19 | delete props.height
20 | }
21 |
22 | let { hoverFill, hoverStroke, ...svgProps } = props
23 |
24 | hoverFill = hoverFill || props.fill
25 | hoverStroke = hoverStroke || props.stroke
26 |
27 | svgProps = {
28 | width: props.width || '100%',
29 | height: props.height || '100%',
30 | viewBox: props.viewBox || '0 0 24 24',
31 | ...svgProps,
32 | fill: isHover === 1 ? hoverFill : props.fill,
33 | stroke: isHover === 1 ? hoverStroke : props.stroke,
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | svgContainer: {
38 | overflow: 'visible',
39 | width: svgProps.width,
40 | height: svgProps.height,
41 | },
42 | })
43 |
44 | return (
45 | setIsHover(1)}
48 | onMouseLeave={() => setIsHover(0)}
49 | >
50 |
51 |
52 | )
53 | }
54 |
55 | export default ReactNativeSvg
56 |
--------------------------------------------------------------------------------
/src/components/svg/RenameIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const RenameIcon: React.FC = (props: Props) => {
6 | return (
7 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default RenameIcon
24 |
--------------------------------------------------------------------------------
/src/components/svg/RightArrowIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 | import { useDirection } from '@/hooks'
5 |
6 | const RightArrowIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 |
9 | return (
10 |
17 |
21 |
22 | )
23 | }
24 |
25 | export default RightArrowIcon
26 |
--------------------------------------------------------------------------------
/src/components/svg/ScrollToBottomIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const ScrollToBottomIcon: React.FC = (props: Props) => {
6 | return (
7 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default ScrollToBottomIcon
24 |
--------------------------------------------------------------------------------
/src/components/svg/SelectTextIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 |
4 | const SelecttextIcon: React.FC = (props: Props) => {
5 | return (
6 |
13 | )
14 | }
15 |
16 | export default SelecttextIcon
17 |
--------------------------------------------------------------------------------
/src/components/svg/SendIcon.tsx:
--------------------------------------------------------------------------------
1 | import { useDirection } from '@/hooks'
2 | import { Path } from 'react-native-svg'
3 | import React from 'react'
4 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
5 |
6 | const SendIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 | const svgProps = {
9 | width: props.width || '20',
10 | height: props.height || '20',
11 | viewBox: props.viewBox || '0 0 20 20',
12 | fill: props.fill || '#fff',
13 | fillOpacity: props.fillOpacity || '1',
14 | }
15 | return (
16 |
23 |
27 |
28 | )
29 | }
30 |
31 | export default SendIcon
32 |
--------------------------------------------------------------------------------
/src/components/svg/SettingIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Path, Circle } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const SettingIcon: React.FC = (props: Props) => {
6 | return (
7 |
17 |
23 |
24 |
25 | )
26 | }
27 |
28 | export default SettingIcon
29 |
--------------------------------------------------------------------------------
/src/components/svg/ShareIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { G, Path, ClipPath, Rect } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 | import { useDirection } from '@/hooks'
5 |
6 | const EditIcon: React.FC = (props: Props) => {
7 | const { isRTL } = useDirection()
8 |
9 | return (
10 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default EditIcon
31 |
--------------------------------------------------------------------------------
/src/components/svg/StopIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { G, Path } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const StopIcon: React.FC = (props: Props) => {
6 | const svgProps = {
7 | width: props.width || '16',
8 | height: props.height || '16',
9 | viewBox: props.viewBox || '0 0 16 16',
10 | fill: props.fill || 'rgb(0%,23.529412%,20.784314%)',
11 | version: props.version || '1.1',
12 | }
13 | return (
14 |
15 |
16 |
25 |
34 |
35 |
36 | )
37 | }
38 |
39 | export default StopIcon
40 |
--------------------------------------------------------------------------------
/src/components/svg/StopResponseIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 |
4 | const StopresponseIcon: React.FC = (props: Props) => {
5 | return (
6 |
13 | )
14 | }
15 |
16 | export default StopresponseIcon
17 |
--------------------------------------------------------------------------------
/src/components/svg/UserIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { G, Path, ClipPath, Rect } from 'react-native-svg'
3 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
4 |
5 | const UserIcon: React.FC = (props: Props) => {
6 | const svgProps = {
7 | width: props.width || '19',
8 | height: props.height || '19',
9 | viewBox: props.viewBox || '0 0 19 19',
10 | fill: props.fill || '#ffffff',
11 | }
12 | return (
13 |
14 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default UserIcon
33 |
--------------------------------------------------------------------------------
/src/components/svg/WarningCircleIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactNativeSvg, { Props } from './ReactNativeSvg'
3 | import { Path } from 'react-native-svg'
4 |
5 | const WarningcircleIcon: React.FC = (props: Props) => {
6 | return (
7 |
14 |
20 |
21 | )
22 | }
23 |
24 | export default WarningcircleIcon
25 |
--------------------------------------------------------------------------------
/src/components/threads/IconContainer.tsx:
--------------------------------------------------------------------------------
1 | import { ChatIcon, DeleteIcon, RenameIcon, ShareIcon } from '@/components/svg'
2 | import { RootState, Thread } from '@/store'
3 | import React from 'react'
4 | import { Platform, Pressable, View } from 'react-native'
5 | import { useSelector } from 'react-redux'
6 |
7 | type IconContainerProps = {
8 | thread: Thread
9 | isRTL: boolean
10 | /* eslint-disable no-unused-vars */
11 | onThreadSelect?: (aboutToShareThread: Thread) => void
12 | onThreadDelete?: (aboutToDeleteThread: Thread) => void
13 | onThreadRename?: (aboutToRenameThread: Thread) => void
14 | onThreadShare?: (aboutToShareThread: Thread) => void
15 | /* eslint-disable no-unused-vars */
16 | }
17 |
18 | const IconContainer: React.FC = ({
19 | thread,
20 | isRTL,
21 | onThreadSelect,
22 | onThreadDelete,
23 | onThreadRename,
24 | onThreadShare,
25 | }) => {
26 | const theme = useSelector((state: RootState) => state.theme.theme)
27 |
28 | return (
29 |
30 | {onThreadSelect && (
31 | onThreadSelect(thread)} className='py-2 px-4'>
32 |
33 |
34 | )}
35 | {Platform.OS === 'web' && onThreadRename && (
36 | onThreadRename(thread)} className='py-2 px-4'>
37 |
38 |
39 | )}
40 | {onThreadShare && (
41 | onThreadShare(thread)} className='py-2 px-4'>
42 |
43 |
44 | )}
45 | {onThreadDelete && (
46 | onThreadDelete(thread)}
48 | className={`py-2 px-4 ${isRTL && onThreadRename ? 'ml-2' : 'mr-2'}`}
49 | >
50 |
51 |
52 | )}
53 | {/* Additional icons can be added here */}
54 |
55 | )
56 | }
57 |
58 | export default IconContainer
59 |
--------------------------------------------------------------------------------
/src/components/threads/index.ts:
--------------------------------------------------------------------------------
1 | export { default as IconContainer } from './IconContainer'
2 | export { default as ThreadCard } from './ThreadCard'
3 | export { default as ThreadsList } from './ThreadsList'
4 |
--------------------------------------------------------------------------------
/src/constant/index.ts:
--------------------------------------------------------------------------------
1 | export enum Role {
2 | ASSISTANT = 'assistant',
3 | USER = 'user',
4 | }
5 |
--------------------------------------------------------------------------------
/src/env/index.ts:
--------------------------------------------------------------------------------
1 | const envConfig = {
2 | API_URL: () => process.env.EXPO_PUBLIC_API_URL!,
3 | LOADING_MESSAGE_RESPONSE_TIMEOUT: () =>
4 | process.env.EXPO_PUBLIC_API_TIMEOUT ? Number.parseInt(process.env.EXPO_PUBLIC_API_TIMEOUT) : 60000,
5 | }
6 |
7 | export const getEnv = (key: T) => {
8 | return envConfig[key]() as ReturnType<(typeof envConfig)[T]>
9 | }
10 |
--------------------------------------------------------------------------------
/src/errors/ApplicationError.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ApplicationError extends the built-in Error class to provide custom error handling.
3 | */
4 | class ApplicationError extends Error {
5 | statusCode: number
6 |
7 | constructor(message: string, statusCode: number = 500) {
8 | super(message)
9 | this.name = this.constructor.name
10 | this.statusCode = statusCode
11 |
12 | // Maintains proper stack trace for where our error was thrown (only available on V8)
13 | if (Error.captureStackTrace) {
14 | Error.captureStackTrace(this, this.constructor)
15 | }
16 | }
17 | }
18 |
19 | export default ApplicationError
20 |
--------------------------------------------------------------------------------
/src/errors/ForbiddenError.ts:
--------------------------------------------------------------------------------
1 | import ApplicationError from './ApplicationError'
2 |
3 | /**
4 | * ForbiddenError represents an HTTP 403 error, indicating that the request was valid
5 | * but the server is refusing action. The user might not have the necessary permissions
6 | * for a resource, or may need an account of some sort.
7 | */
8 | class ForbiddenError extends ApplicationError {
9 | constructor(message: string = 'Forbidden') {
10 | super(message, 403)
11 | }
12 | }
13 |
14 | export default ForbiddenError
15 |
--------------------------------------------------------------------------------
/src/errors/NotFoundError.ts:
--------------------------------------------------------------------------------
1 | import ApplicationError from './ApplicationError'
2 |
3 | /**
4 | * NotFoundError represents an HTTP 404 error, indicating that the requested resource
5 | * was not found on the server. This can be used when a resource does not exist or
6 | * if there was an attempt to fetch data that is not available.
7 | */
8 | class NotFoundError extends ApplicationError {
9 | constructor(message: string = 'Not Found') {
10 | super(message, 404)
11 | }
12 | }
13 |
14 | export default NotFoundError
15 |
--------------------------------------------------------------------------------
/src/errors/TokenRefreshError.ts:
--------------------------------------------------------------------------------
1 | import ApplicationError from './ApplicationError'
2 |
3 | /**
4 | * Represents an error related to failing token refresh operations.
5 | *
6 | * This custom error class extends the generic ApplicationError to provide
7 | * a specific error type for token refresh failures. It's used when a refresh
8 | * token is invalid, expired, or when the server encounters an issue that
9 | * prevents issuing a new access token.
10 | *
11 | * @extends ApplicationError
12 | */
13 | class TokenRefreshError extends ApplicationError {
14 | /**
15 | * Constructs a new TokenRefreshError instance.
16 | *
17 | * @param {string} message - The error message. Defaults to "Token refresh failed"
18 | * to indicate a general token refresh failure.
19 | * @param {number} statusCode - The HTTP status code associated with the error.
20 | * Set to 401 Unauthorized to indicate that the
21 | * request has not been applied because it lacks
22 | * valid authentication credentials for the target
23 | * resource. This status code is used here to
24 | * suggest that the client needs to re-authenticate
25 | * (e.g., by logging in or obtaining a new token).
26 | */
27 | constructor(message: string = 'Token refresh failed') {
28 | super(message, 401) // Use 401 Unauthorized to indicate authentication issues.
29 | }
30 | }
31 |
32 | // Export the TokenRefreshError class for use throughout the application.
33 | export default TokenRefreshError
34 |
--------------------------------------------------------------------------------
/src/errors/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ApplicationError } from './ApplicationError'
2 | export { default as ForbiddenError } from './ForbiddenError'
3 | export { default as NotFoundError } from './NotFoundError'
4 | export { default as TokenRefreshError } from './TokenRefreshError'
5 |
--------------------------------------------------------------------------------
/src/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/hooks/chat/index.ts:
--------------------------------------------------------------------------------
1 | export { useChat } from './useChat'
2 | export { useFeedbackHandler } from './useFeedbackHandler'
3 | export { useFeedbackService } from './useFeedbackService'
4 | export { useScrollManagement } from './useScrollManagement'
5 |
--------------------------------------------------------------------------------
/src/hooks/chat/useFeedbackService.ts:
--------------------------------------------------------------------------------
1 | import fetchFeedbacksForLanguage, { FeedbacksByCategory } from '@/services/FeedbackService'
2 | import { useEffect, useState } from 'react'
3 |
4 | export const useFeedbackService = (language: string): FeedbacksByCategory => {
5 | const [feedbacks, setFeedbacks] = useState({} as FeedbacksByCategory)
6 |
7 | useEffect(() => {
8 | const fetchData = async () => {
9 | const fetchedFeedbacks = await fetchFeedbacksForLanguage(language)
10 | setFeedbacks(fetchedFeedbacks)
11 | }
12 |
13 | fetchData().catch(console.error)
14 | }, [language])
15 |
16 | return feedbacks
17 | }
18 |
--------------------------------------------------------------------------------
/src/hooks/chat/useScrollManagement.ts:
--------------------------------------------------------------------------------
1 | import debounce from 'lodash.debounce'
2 | import { useCallback, useEffect, useState } from 'react'
3 | import { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'
4 |
5 | type UseScrollManagementType = {
6 | isScrolledUp: boolean
7 | // eslint-disable-next-line no-unused-vars
8 | handleScroll: (event: NativeSyntheticEvent) => void
9 | scrollToBottom: () => void
10 | }
11 |
12 | export const useScrollManagement = (scrollThreshold: number = 50): UseScrollManagementType => {
13 | const [isScrolledUp, setIsScrolledUp] = useState(false)
14 |
15 | const handleScrollDebounced = useCallback(
16 | debounce((isScrolledUp: boolean) => {
17 | setIsScrolledUp(isScrolledUp)
18 | }, 100),
19 | [],
20 | )
21 |
22 | const handleScroll = useCallback(
23 | (event: NativeSyntheticEvent) => {
24 | const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent
25 |
26 | // Use a threshold to determine if the user is "close enough" to the bottom to auto-scroll.
27 | // This can help account for minor discrepancies in scroll position calculations.
28 | const closeToBottom = contentSize.height - contentOffset.y - layoutMeasurement.height < scrollThreshold
29 | handleScrollDebounced(!closeToBottom)
30 | },
31 | [handleScrollDebounced],
32 | )
33 |
34 | const scrollToBottom = useCallback(() => {
35 | setIsScrolledUp(false)
36 | }, [])
37 |
38 | // Clean up the handleScrollDebounced on unmount or when the component is no longer active
39 | useEffect(() => {
40 | return () => {
41 | handleScrollDebounced.cancel()
42 | }
43 | }, [])
44 |
45 | return { isScrolledUp, handleScroll, scrollToBottom }
46 | }
47 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chat'
2 | export { useAuth } from './useAuth'
3 | export { useDirection } from './useDirection'
4 | export { useGuest } from './useGuest'
5 | export { useLogout } from './useLogout'
6 | export { useScreenInfo } from './useScreenInfo'
7 | export { useToggleInfoPopup } from './useToggleInfoPopup'
8 | export { useTokenFromUrl } from './useTokenFromUrl'
9 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.ts:
--------------------------------------------------------------------------------
1 | // useAuth.ts
2 | // This custom hook provides a convenient way to access authentication-related data
3 | // (such as the current user, authentication status, and token) from the Redux store.
4 |
5 | import { RootState } from '@/store'
6 | import { useSelector } from 'react-redux'
7 |
8 | /**
9 | * A hook that abstracts the Redux state access for authentication.
10 | * It uses the useSelector hook from React-Redux to extract auth-related data.
11 | *
12 | * @returns An object containing isAuthenticated, token, and user information.
13 | */
14 | export const useAuth = () => {
15 | // Access the relevant part of the Redux state
16 | const authState = useSelector((state: RootState) => state.auth)
17 |
18 | // Return the necessary parts of the auth state
19 | return {
20 | isAuthenticated: authState.isAuthenticated, // Indicates if the user is currently authenticated
21 | isGuest: authState.isGuest, // Indicates if the user is currently guest login
22 | accessToken: authState.accessToken, // The JWT token for authenticated requests
23 | refreshToken: authState.refreshToken, // The JWT Refresh token for authenticated requests
24 | user: authState.user, // The user's details
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/hooks/useDeleteAccount.ts:
--------------------------------------------------------------------------------
1 | import { useAuth } from '@/hooks/useAuth'
2 | import { AppDispatch, deleteAccount } from '@/store'
3 | import { useDispatch } from 'react-redux'
4 | import { resetChatState, resetReactionButtons } from '../store/slices'
5 |
6 | export const useDeleteAccount = () => {
7 | const dispatch = useDispatch()
8 | const { accessToken } = useAuth()
9 |
10 | const doDeleteAccount = async () => {
11 | try {
12 | await dispatch(deleteAccount(String(accessToken)))
13 | } catch (error) {
14 | console.error('Error deleting account:', error)
15 | throw error
16 | } finally {
17 | dispatch(resetChatState())
18 | dispatch(resetReactionButtons())
19 | }
20 | }
21 |
22 | return doDeleteAccount
23 | }
24 |
--------------------------------------------------------------------------------
/src/hooks/useDirection.ts:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 |
3 | /**
4 | * This hook returns whether the current language direction is Right-To-Left (RTL) or not.
5 | * It uses the `i18n.dir()` method from `react-i18next` to determine the direction.
6 | *
7 | * @returns An object containing the `isRTL` property, which is a boolean indicating whether the direction is RTL or not.
8 | */
9 | export const useDirection = () => {
10 | // Get the `i18n` instance from the `useTranslation` hook
11 | const { i18n } = useTranslation()
12 |
13 | // Determine if the direction is RTL by comparing `i18n.dir()` to 'rtl'
14 | const isRTL = i18n.dir() === 'rtl'
15 |
16 | // Return an object containing the `isRTL` property
17 | return {
18 | isRTL,
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/hooks/useGuest.ts:
--------------------------------------------------------------------------------
1 | // useGuest.ts
2 | // Hook for handling guest user login within a React / React Native application.
3 |
4 | import { AppDispatch, guestLogin } from '@/store'
5 | import { useState } from 'react'
6 | import { useDispatch } from 'react-redux'
7 | import { useRouter } from 'expo-router'
8 |
9 | /**
10 | * A custom hook for handling guest login functionality.
11 | *
12 | * It provides state and functions necessary to perform a guest login, track the loading state,
13 | * and handle any potential errors during the login process.
14 | */
15 | export const useGuest = () => {
16 | // Redux dispatch function for dispatching actions.
17 | const dispatch = useDispatch()
18 | // Navigation hook to programmatically navigate between routes.
19 | const router = useRouter()
20 |
21 | // State to track whether the guest login process is loading.
22 | const [guestLoading, setGuestLoading] = useState(false)
23 | // State to track any errors that may occur during the guest login process.
24 | const [guestLoginError, setGuestLoginError] = useState(null)
25 |
26 | /**
27 | * Handles the guest login process.
28 | *
29 | * It dispatches the guestLogin action, handles successful login by navigating to the homepage,
30 | * and handles any errors by setting the guestLoginError state.
31 | */
32 | const handleGuestLogin = async () => {
33 | try {
34 | setGuestLoading(true)
35 | setGuestLoginError(null)
36 | // Dispatching the guestLogin action and waiting for it to complete.
37 | await dispatch(guestLogin()).unwrap()
38 | // On success, navigate to the homepage and reset loading state.
39 | router.push('/')
40 | } catch (error) {
41 | console.error('Error logging in as guest:', error)
42 | // On failure, set error state and log the error.
43 | setGuestLoginError(error as Error)
44 | } finally {
45 | // Reset loading state regardless of success or failure.
46 | setGuestLoading(false)
47 | }
48 | }
49 |
50 | // Exposing the guest login state and control functions.
51 | return { guestLoading, handleGuestLogin, guestLoginError }
52 | }
53 |
--------------------------------------------------------------------------------
/src/hooks/useLogout.ts:
--------------------------------------------------------------------------------
1 | import { useAuth } from '@/hooks/useAuth'
2 | import { AppDispatch, logout } from '@/store'
3 | import { useDispatch } from 'react-redux'
4 | import { resetChatState, resetReactionButtons } from '../store/slices'
5 |
6 | export const useLogout = () => {
7 | const dispatch = useDispatch()
8 | const { accessToken } = useAuth()
9 |
10 | const doLogout = async () => {
11 | try {
12 | await dispatch(logout(String(accessToken)))
13 | } catch (error) {
14 | console.error('Error logging out:', error)
15 | } finally {
16 | dispatch(resetChatState())
17 | dispatch(resetReactionButtons())
18 | }
19 | }
20 |
21 | return doLogout
22 | }
23 |
--------------------------------------------------------------------------------
/src/hooks/useToggleInfoPopup.ts:
--------------------------------------------------------------------------------
1 | import { toggleInformationPopup } from '@/store'
2 | import { useDispatch } from 'react-redux'
3 |
4 | export const useToggleInfoPopup = () => {
5 | const dispatch = useDispatch()
6 | const toggleInfoPopup = (openInfoPopup: boolean) => {
7 | dispatch(toggleInformationPopup(openInfoPopup))
8 | }
9 |
10 | return toggleInfoPopup
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/useTokenFromUrl.ts:
--------------------------------------------------------------------------------
1 | import { useRouter, useLocalSearchParams } from 'expo-router'
2 |
3 | export const useTokenFromUrl = () => {
4 | const router = useRouter()
5 | const { token } = useLocalSearchParams()
6 | if (!token) {
7 | router.push('/login')
8 | }
9 | return token
10 | }
11 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | export { default as i18n } from './i18n'
2 |
--------------------------------------------------------------------------------
/src/i18n/locales/ar.json:
--------------------------------------------------------------------------------
1 | {
2 | "gettingWrongSometimes": "ماالسلام عليكم. !سمي إنصاري. ما زلت أخطئ في بعض الأحيان. ومن الأفضل دائمًا استشارة عالم إسلامي حقيقي. ",
3 | "flaggingInstructions": {
4 | "desktop": "إذا قلت أي شيء خاطئ أو مربك أو رائع أو مضحك أو مثير للاهتمام، يرجى الإبلاغ عنه. سيتم مراجعة أي شيء تم وضع علامة عليه من قبل البشر. للإبلاغ عن محادثة، ما عليك سوى قول'أريد الإبلاغ عن هذه المحادثة.'",
5 | "mobile": "إذا قلت أي شيء خاطئ أو مربك أو رائع أو مضحك أو مثير للاهتمام، يرجى الإبلاغ عنه. <1> المزيد >"
6 | },
7 | "multilingualMessage": {
8 | "desktop": "أنا متعدد اللغات. أستطيع أن أفهم اللغة العربية (بما في ذلك الترجمة الصوتية)، والتركية، والأردية، والبهاسا، والبوسنية والعديد من اللغات الأخرى.",
9 | "mobile": "أنا متعدد اللغات. <1> المزيد1>"
10 | },
11 | "promptPlaceholder": "اكتب رسالة",
12 | "duaToMake": "ماذا ادعو",
13 | "inParticularSituation": "في حالات معينة",
14 | "spiritualRemedies": "العلاجات الروحانية ",
15 | "challengesFacing": "للتحديات التي اواجهها",
16 | "islamicPerspectives": "وجهات نظر إسلامية",
17 | "onTopics": "عن المواضيع",
18 | "selectLanguage": "اختر لغة",
19 | "messageSent": "رسالة ",
20 | "information": "معلومات",
21 | "comprehensiveGuide": "انقر <1>هنا1> للحصول على دليل أكثر شمولاً حول ما يمكن أن يفعله أنصاري.",
22 | "startConversation": "ابدأ محادثة جديدة",
23 | "newConversationConfirmation": "هل أنت متأكد أنك تريد بدء محادثة جديدة؟",
24 | "conversationLostWarning": "يرجى ملاحظة أنه سيتم فقدان المحادثة السابقة.",
25 | "cancel": "إلغاء",
26 | "yes": "نعم",
27 | "backendErrorMessage": "هناك خطأ ما. إذا استمرت هذه المشكلة، فيرجى الاتصال بنا من خلال مركز المساعدة الخاص بنا على <1>feedback@ansari.chat1>",
28 | "subscribe": "مشغل بواسطة Ansari"
29 | }
--------------------------------------------------------------------------------
/src/i18n/locales/ar/feedback.json:
--------------------------------------------------------------------------------
1 | {
2 | "good": [
3 | {
4 | "id": "good1",
5 | "label": "صحيح",
6 | "value": "correct"
7 | },
8 | {
9 | "id": "good2",
10 | "label": "سهل الفهم",
11 | "value": "understandable"
12 | },
13 | {
14 | "id": "good3",
15 | "label": "مكتمل",
16 | "value": "complete"
17 | }
18 | ],
19 | "bad": [
20 | {
21 | "id": "bad2",
22 | "label": "لا يكون صحيحاً في الواقع",
23 | "value": "incorrect"
24 | },
25 | {
26 | "id": "bad3",
27 | "label": "أخرى",
28 | "value": "other"
29 | }
30 | ],
31 | "redflag": [
32 | {
33 | "id": "redflag1",
34 | "label": "مضارب / غير آمن",
35 | "value": "offensive"
36 | },
37 | {
38 | "id": "redflag2",
39 | "label": "أخرى",
40 | "value": "other"
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/src/i18n/locales/ar/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "تسجيل الدخول",
3 | "email": "البريد الإلكتروني",
4 | "password": "كلمة المرور",
5 | "submit": "إرسال",
6 | "submitting": "جار التحميل",
7 | "register": "التسجيل",
8 | "dontHaveAccount": "ليس لديك حساب؟",
9 | "successMessage": "تم تسجيل الدخول بنجاح",
10 | "errorMessage": "فشل تسجيل الدخول. يرجى المحاولة مرة أخرى.",
11 | "invalidCredentials": "بيانات الاعتماد غير صالحة",
12 | "emailRequiredField": "البريد الإلكتروني مطلوب.",
13 | "emailInvalid": "أدخل بريدًا إلكترونيًا صالحًا.",
14 | "passwordRequiredField": "كلمة المرور مطلوبة.",
15 | "passwordLength": "يجب أن تكون كلمة المرور على الأقل 8 أحرف.",
16 | "unexpectedError": "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى في وقت لاحق.",
17 | "forgetPassword": "هل نسيت كلمة المرور؟",
18 | "guestLogin": "تسجيل الدخول كضيف",
19 | "forgotYourPassword": "نسيت كلمة المرور",
20 | "yourEmail": "عنوان بريدك الإلكتروني",
21 | "forgotMessage": "فقط أخبرنا بعنوان بريدك الإلكتروني وسنرسل لك رابط إعادة تعيين كلمة المرور الذي سيتيح لك اختيار واحدة جديدة.",
22 | "forgotSuccessMessage": "سيتم إرسال بريد إلكتروني إلى عنوان البريد الإلكتروني المقدم لإعادة تعيين الحساب إذا كان موجودًا في النظام.",
23 | "continue": "تابع",
24 | "back": "رجوع",
25 | "emailValidationMessage": "الرجاء إدخال عنوان بريد إلكتروني صحيح",
26 | "emailRequired": "البريد الإلكتروني مطلوب"
27 | }
28 |
--------------------------------------------------------------------------------
/src/i18n/locales/ar/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "التسجيل",
3 | "email": "البريد الإلكتروني",
4 | "password": "كلمة المرور",
5 | "firstName": "الاسم الأول",
6 | "lastName": "الاسم الأخير",
7 | "confirmPassword": "تأكيد كلمة المرور",
8 | "register": "التسجيل",
9 | "registering": "جار التحميل",
10 | "alreadyHaveAccount": "هل لديك حساب بالفعل؟",
11 | "goToLogin": "الانتقال إلى تسجيل الدخول",
12 | "emailRequired": "البريد الإلكتروني مطلوب.",
13 | "emailInvalid": "البريد الإلكتروني غير صالح.",
14 | "passwordRequired": "كلمة المرور مطلوبة.",
15 | "passwordLength": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل.",
16 | "confirmPasswordRequired": "تأكيد كلمة المرور مطلوب.",
17 | "confirmPasswordMatch": "يجب أن تتطابق كلمات المرور.",
18 | "firstNameRequired": "الاسم الأول مطلوب.",
19 | "lastNameRequired": "الاسم الأخير مطلوب.",
20 | "registerSuccess": "تم التسجيل بنجاح.",
21 | "registerFailure": "فشل التسجيل.",
22 | "loginHere": "تسجيل الدخول",
23 | "passwordReset": "إعادة تعيين كلمة المرور",
24 | "passwordResetSuccess": "تمت إعادة تعيين كلمة المرور بنجاح",
25 | "passwordResetSuccessMessage": "تمت إعادة تعيين كلمة مرورك.",
26 | "continue": "تابع",
27 | "back": "رجوع",
28 | "passwordSchema": "يجب أن تحتوي كلمة المرور",
29 | "minLengthMessage": "على الأقل {{min}} أحرف طولاً",
30 | "minUppercaseMessage": "على الأقل {{min}} حرفًا كبيرًا",
31 | "minNumberMessage": "على الأقل {{min}} رقمًا",
32 | "minSymbolsMessage": "على الأقل {{min}} رمزًا خاصًا",
33 | "mailListText": "اشترك في قائمتنا البريدية للحصول على التحديثات!"
34 | }
35 |
--------------------------------------------------------------------------------
/src/i18n/locales/bs.json:
--------------------------------------------------------------------------------
1 | {
2 | "gettingWrongSometimes": "I dalje ponekad griješim. Uvijek je najbolje konsultovati pravog islamskog učenjaka.",
3 | "flaggingInstructions": {
4 | "desktop": "Ako kažem nešto pogrešno, zbunjujuće, sjajno, smiješno ili zanimljivo, molimo vas da to označite. Sve što je označeno biće pregledano od strane ljudi. Da označite razgovor, samo recite 'Želim označiti ovaj razgovor.'",
5 | "mobile": "Ako kažem nešto pogrešno, zbunjujuće, sjajno, smiješno ili zanimljivo, molimo vas da to označite.<1>više1>"
6 | },
7 | "multilingualMessage": {
8 | "desktop": "Ja sam višejezičan. Razumijem arapski (uključujući transliteraciju), turski, urdu, bahasa, bosanski i mnoge druge jezike.",
9 | "mobile": "Ja sam višejezičan.<1>više1>"
10 | },
11 | "promptPlaceholder": "Unesite poruku",
12 | "duaToMake": "Dua za napraviti",
13 | "inParticularSituation": "u konkretnoj situaciji",
14 | "spiritualRemedies": "Duhovni lijekovi",
15 | "challengesFacing": "za izazove sa kojima se suočavate",
16 | "islamicPerspectives": "islamske perspektive",
17 | "onTopics": "na teme",
18 | "selectLanguage": "Odaberite jezik",
19 | "messageSent": "poruka",
20 | "information": "Informacije",
21 | "comprehensiveGuide": "Kliknite <1>ovdje1> za opsežniji vodič o tome šta Ansari može učiniti.",
22 | "startConversation": "Započnite novi razgovor",
23 | "newConversationConfirmation": "Jeste li sigurni da želite započeti novi razgovor?",
24 | "conversationLostWarning": "Imajte na umu da će prethodni razgovor biti izgubljen.",
25 | "cancel": "Otkaži",
26 | "yes": "Da",
27 | "backendErrorMessage": "Nešto je pošlo po zlu. Ako se ovaj problem nastavi, kontaktirajte nas putem našeg centra za pomoć na adresi <1>feedback@ansari.chat1>",
28 | "subscribe": "Powered by Ansari"
29 | }
--------------------------------------------------------------------------------
/src/i18n/locales/bs/feedback.json:
--------------------------------------------------------------------------------
1 | {
2 | "good": [
3 | {
4 | "id": "good1",
5 | "label": "Ispravno",
6 | "value": "correct"
7 | },
8 | {
9 | "id": "good2",
10 | "label": "Jednostavno razumljivo",
11 | "value": "understandable"
12 | },
13 | {
14 | "id": "good3",
15 | "label": "Potpuno",
16 | "value": "complete"
17 | }
18 | ],
19 | "bad": [
20 | {
21 | "id": "bad2",
22 | "label": "Nepravilno faktično",
23 | "value": "incorrect"
24 | },
25 | {
26 | "id": "bad3",
27 | "label": "Drugo",
28 | "value": "other"
29 | }
30 | ],
31 | "redflag": [
32 | {
33 | "id": "redflag1",
34 | "label": "Neprikladno / Neposredno",
35 | "value": "offensive"
36 | },
37 | {
38 | "id": "redflag2",
39 | "label": "Drugo",
40 | "value": "other"
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/src/i18n/locales/bs/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Prijava",
3 | "email": "E-pošta",
4 | "password": "Lozinka",
5 | "submit": "Pošalji",
6 | "submitting": "Učitavanje",
7 | "register": "Registracija",
8 | "dontHaveAccount": "Nemate račun?",
9 | "successMessage": "Uspješna prijava",
10 | "errorMessage": "Prijava nije uspjela. Pokušajte ponovo.",
11 | "invalidCredentials": "Nevažeći pristupni podaci",
12 | "emailRequiredField": "E-pošta je obavezna.",
13 | "emailInvalid": "Unesite ispravnu e-poštu.",
14 | "passwordRequiredField": "Lozinka je obavezna.",
15 | "passwordLength": "Lozinka mora imati barem 8 znakova.",
16 | "unexpectedError": "Došlo je do neočekivane greške. Molimo vas da pokušate ponovo kasnije.",
17 | "forgetPassword": "Zaboravili ste lozinku?",
18 | "guestLogin": "Prijavite se kao gost",
19 | "forgotYourPassword": "Zaboravili ste lozinku",
20 | "yourEmail": "Vaša email adresa",
21 | "forgotMessage": "Samo nam javite vašu email adresu i poslat ćemo vam link za resetiranje lozinke koji će vam omogućiti odabir nove.",
22 | "forgotSuccessMessage": "Email će biti poslan na navedenu email adresu kako bi se resetovao račun ako postoji u sistemu.",
23 | "continue": "Nastavi",
24 | "back": "Nazad",
25 | "emailValidationMessage": "Unesite ispravnu email adresu",
26 | "emailRequired": "Email je obavezan"
27 | }
28 |
--------------------------------------------------------------------------------
/src/i18n/locales/bs/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Registracija",
3 | "email": "Email",
4 | "password": "Lozinka",
5 | "firstName": "Ime",
6 | "lastName": "Prezime",
7 | "confirmPassword": "Potvrdi lozinku",
8 | "register": "Registriraj se",
9 | "registering": "Učitavanje",
10 | "alreadyHaveAccount": "Već imate račun?",
11 | "goToLogin": "Idi na prijavu",
12 | "emailRequired": "Email je obavezan.",
13 | "emailInvalid": "Email je nevažeći.",
14 | "passwordRequired": "Lozinka je obavezna.",
15 | "passwordLength": "Lozinka mora biti dugačka najmanje 8 znakova.",
16 | "confirmPasswordRequired": "Potvrda lozinke je obavezna.",
17 | "confirmPasswordMatch": "Lozinke se moraju podudarati.",
18 | "firstNameRequired": "Ime je obavezno.",
19 | "lastNameRequired": "Prezime je obavezno.",
20 | "registerSuccess": "Registracija uspješna.",
21 | "registerFailure": "Registracija neuspješna.",
22 | "loginHere": "Prijavi se ovdje",
23 | "passwordReset": "Resetiranje lozinke",
24 | "passwordResetSuccess": "Resetiranje lozinke uspješno",
25 | "passwordResetSuccessMessage": "Vaša lozinka je resetirana.",
26 | "continue": "Nastavi",
27 | "back": "Nazad",
28 | "passwordSchema": "Lozinka mora sadržavati",
29 | "minLengthMessage": "najmanje {{min}} karaktera",
30 | "minUppercaseMessage": "najmanje {{min}} velikih slova",
31 | "minNumberMessage": "najmanje {{min}} brojeva",
32 | "minSymbolsMessage": "najmanje {{min}} posebnih znakova",
33 | "mailListText": "Pretplatite se na našu mailing listu za ažuriranja!"
34 | }
35 |
--------------------------------------------------------------------------------
/src/i18n/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "gettingWrongSometimes": "Assalamu alaikum! My name is Ansari. I can help you with your questions about Islam.\nI still get things wrong sometimes. It is always best to consult a real Islamic Scholar.",
3 | "flaggingInstructions": {
4 | "desktop": "If I say anything wrong, confusing, great, funny or interesting, please e-mail <1>feedback@ansari.chat1>.",
5 | "mobile": "Feedback? Please email us!<1>more1>"
6 | },
7 | "multilingualMessage": {
8 | "desktop": "I am multilingual. I can understand Arabic (including transliteration), Bahasa, Bosnian, French Turkish, Urdu and many other languages.",
9 | "mobile": "I am multilingual. <1>more1>"
10 | },
11 | "promptPlaceholder": "Type message",
12 | "duaToMake": "Dua to make",
13 | "inParticularSituation": "in particular situation",
14 | "spiritualRemedies": "Spiritual remedies",
15 | "challengesFacing": "for challenges you are facing",
16 | "islamicPerspectives": "Islamic perspectives",
17 | "onTopics": "on topics",
18 | "selectLanguage": "Select a Language",
19 | "messageSent": "message",
20 | "information": "Information",
21 | "comprehensiveGuide": "Click <1>here1> for a more comprehensive guide to what Ansari can do.",
22 | "startConversation": "Start New Conversation",
23 | "newConversationConfirmation": "Are you certain that you want to begin a new conversation?",
24 | "conversationLostWarning": "Please note that the previous conversation will be lost.",
25 | "cancel": "Cancel",
26 | "yes": "Yes",
27 | "backendErrorMessage": "Something went wrong. If this issue persists please contact us through our help center at <1>feedback@ansari.chat1>",
28 | "subscribe": "<1>Subscribe1> to our mailing list for updates!"
29 | }
--------------------------------------------------------------------------------
/src/i18n/locales/en/feedback.json:
--------------------------------------------------------------------------------
1 | {
2 | "good": [
3 | {
4 | "id": "good1",
5 | "label": "Correct",
6 | "value": "correct"
7 | },
8 | {
9 | "id": "good2",
10 | "label": "Easy to understand",
11 | "value": "understandable"
12 | },
13 | {
14 | "id": "good3",
15 | "label": "Complete",
16 | "value": "complete"
17 | }
18 | ],
19 | "bad": [
20 | {
21 | "id": "bad2",
22 | "label": "Not factually correct",
23 | "value": "incorrect"
24 | },
25 | {
26 | "id": "bad3",
27 | "label": "Other",
28 | "value": "other"
29 | }
30 | ],
31 | "redflag": [
32 | {
33 | "id": "redflag1",
34 | "label": "Offensive / Unsafe",
35 | "value": "offensive"
36 | },
37 | {
38 | "id": "redflag2",
39 | "label": "Other",
40 | "value": "other"
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/src/i18n/locales/en/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Welcome Back",
3 | "email": "Email",
4 | "password": "Password",
5 | "submit": "Submit",
6 | "submitting": "Loading",
7 | "register": "Register",
8 | "dontHaveAccount": "Don't have an account?",
9 | "successMessage": "Login successful",
10 | "errorMessage": "Login failed. Please try again.",
11 | "invalidCredentials": "Invalid credentials",
12 | "emailRequiredField": "Email is required.",
13 | "emailInvalid": "Enter a valid email.",
14 | "passwordRequiredField": "Password is required.",
15 | "passwordLength": "Password must be at least 8 characters long.",
16 | "unexpectedError": "An unexpected error occurred. Please try again later.",
17 | "forgetPassword": "Forgot your password?",
18 | "guestLogin": "Login as Guest",
19 | "forgotYourPassword": "Forgot Your Password",
20 | "yourEmail": "Your email address",
21 | "forgotMessage": "Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.",
22 | "forgotSuccessMessage": "An email will be sent to the provided email address to reset the account if it exists within the system.",
23 | "continue": "Continue",
24 | "back": "Back",
25 | "emailValidationMessage": "Please enter a valid email address",
26 | "emailRequired": "Email is required"
27 | }
28 |
--------------------------------------------------------------------------------
/src/i18n/locales/en/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Register",
3 | "email": "Email",
4 | "password": "Password",
5 | "firstName": "First name",
6 | "lastName": "Last name",
7 | "confirmPassword": "Confirm password",
8 | "register": "Register",
9 | "registering": "Loading",
10 | "alreadyHaveAccount": "Already have an account?",
11 | "goToLogin": "Go to login",
12 | "emailRequired": "Email is required.",
13 | "emailInvalid": "Email is invalid.",
14 | "passwordRequired": "Password is required.",
15 | "passwordLength": "Password must be at least 8 characters long.",
16 | "confirmPasswordRequired": "Confirm password is required.",
17 | "confirmPasswordMatch": "Passwords must match.",
18 | "firstNameRequired": "First name is required.",
19 | "lastNameRequired": "Last name is required.",
20 | "registerSuccess": "Registration successful.",
21 | "registerFailure": "Registration failed.",
22 | "loginHere": "Login",
23 | "passwordReset": "Reset password",
24 | "passwordResetSuccess": "Password reset successful",
25 | "passwordResetSuccessMessage": "Your password has been reset.",
26 | "continue": "Continue",
27 | "back": "Back",
28 | "passwordSchema": "Password must contain",
29 | "minLengthMessage": "at least {{min}} characters long",
30 | "minUppercaseMessage": "at least {{min}} uppercase letter(s)",
31 | "minNumberMessage": "at least {{min}} number(s)",
32 | "minSymbolsMessage": "at least {{min}} special character(s)",
33 | "mailListText": "Subscribe to our mailing list for updates!"
34 | }
35 |
--------------------------------------------------------------------------------
/src/i18n/locales/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "gettingWrongSometimes": "Il m'arrive encore de me tromper parfois. Il est toujours préférable de consulter un véritable érudit islamique.",
3 | "flaggingInstructions": {
4 | "desktop": "Si je dis quelque chose de faux, de déroutant, de génial, de drôle ou d'intéressant, veuillez le signaler. Tout ce qui est signalé sera examiné par des humains. Pour signaler une conversation, dites simplement 'Je veux signaler cette conversation.'",
5 | "mobile": "Si je dis quelque chose de faux, de déroutant, de génial, de drôle ou d'intéressant, veuillez le signaler. <1>plus1>"
6 | },
7 | "multilingualMessage": {
8 | "desktop": "Je suis multilingue. Je peux comprendre l'arabe (y compris la translittération), le turc, l'ourdou, le bahasa, le bosniaque et bien d'autres langues.",
9 | "mobile": "Je suis multilingue. <1>plus1>"
10 | },
11 | "promptPlaceholder": "Tapez le message",
12 | "duaToMake": "Dua à faire",
13 | "inParticularSituation": "dans une situation particulière",
14 | "spiritualRemedies": "Remèdes spirituels",
15 | "challengesFacing": "pour les défis auxquels vous êtes confronté",
16 | "islamicPerspectives": "Perspectives islamiques",
17 | "onTopics": "sur les sujets",
18 | "selectLanguage": "Sélectionnez une langue",
19 | "messageSent": "message",
20 | "informations": "Informations",
21 | "comprehensiveGuide": "Cliquez <1>ici1> pour un guide plus complet de ce qu'Ansari peut faire.",
22 | "startConversation": "Démarrer une nouvelle conversation",
23 | "newConversationConfirmation": "Êtes-vous certain de vouloir démarrer une nouvelle conversation ?",
24 | "conversationLostWarning": "Veuillez noter que la conversation précédente sera perdue.",
25 | "cancel": "Annuler",
26 | "yes": "Oui",
27 | "backendErrorMessage": "Quelque chose s'est mal passé. Si ce problème persiste, veuillez nous contacter via notre centre d'aide à <1>feedback@ansari.chat1>",
28 | "subscribe": "Propulsé par Ansari"
29 | }
--------------------------------------------------------------------------------
/src/i18n/locales/fr/feedback.json:
--------------------------------------------------------------------------------
1 | {
2 | "good": [
3 | {
4 | "id": "good1",
5 | "label": "Correct",
6 | "value": "correct"
7 | },
8 | {
9 | "id": "good2",
10 | "label": "Facile à comprendre",
11 | "value": "understandable"
12 | },
13 | {
14 | "id": "good3",
15 | "label": "Complète",
16 | "value": "complete"
17 | }
18 | ],
19 | "bad": [
20 | {
21 | "id": "bad2",
22 | "label": "Pas exactement correct",
23 | "value": "incorrect"
24 | },
25 | {
26 | "id": "bad3",
27 | "label": "Autre",
28 | "value": "other"
29 | }
30 | ],
31 | "redflag": [
32 | {
33 | "id": "redflag1",
34 | "label": "Offensif / Dangereux",
35 | "value": "offensive"
36 | },
37 | {
38 | "id": "redflag2",
39 | "label": "Autre",
40 | "value": "other"
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/src/i18n/locales/fr/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Connexion",
3 | "email": "E-mail",
4 | "password": "Mot de passe",
5 | "submit": "Soumettre",
6 | "submitting": "Chargement",
7 | "register": "S'inscrire",
8 | "dontHaveAccount": "Vous n'avez pas de compte ?",
9 | "successMessage": "Connexion réussie",
10 | "errorMessage": "Échec de la connexion. Veuillez réessayer.",
11 | "invalidCredentials": "Identifiants invalides",
12 | "emailRequiredField": "L'e-mail est requis.",
13 | "emailInvalid": "Entrez une adresse e-mail valide.",
14 | "passwordRequiredField": "Le mot de passe est requis.",
15 | "passwordLength": "Le mot de passe doit comporter au moins 8 caractères.",
16 | "unexpectedError": "Une erreur inattendue s'est produite. Veuillez réessayer ultérieurement.",
17 | "forgetPassword": "Mot de passe oublié ?",
18 | "guestLogin": "Connexion en tant qu'invité",
19 | "forgotYourPassword": "Mot de passe oublié",
20 | "yourEmail": "Votre adresse e-mail",
21 | "forgotMessage": "Indiquez simplement votre adresse e-mail et nous vous enverrons un lien de réinitialisation de mot de passe qui vous permettra d'en choisir un nouveau.",
22 | "forgotSuccessMessage": "Un email sera envoyé à l'adresse e-mail fournie pour réinitialiser le compte s'il existe dans le système.",
23 | "continue": "Continuer",
24 | "back": "Retour",
25 | "emailValidationMessage": "Veuillez saisir une adresse e-mail valide",
26 | "emailRequired": "L'email est requis"
27 | }
28 |
--------------------------------------------------------------------------------
/src/i18n/locales/fr/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Inscription",
3 | "email": "E-mail",
4 | "password": "Mot de passe",
5 | "firstName": "Prénom",
6 | "lastName": "Nom de famille",
7 | "confirmPassword": "Confirmer le mot de passe",
8 | "register": "S'inscrire",
9 | "registering": "Chargement",
10 | "alreadyHaveAccount": "Vous avez déjà un compte ?",
11 | "goToLogin": "Aller à la connexion",
12 | "emailRequired": "L'e-mail est requis.",
13 | "emailInvalid": "Entrez une adresse e-mail valide.",
14 | "passwordRequired": "Le mot de passe est requis.",
15 | "passwordLength": "Le mot de passe doit comporter au moins 8 caractères.",
16 | "confirmPasswordRequired": "La confirmation du mot de passe est requise.",
17 | "confirmPasswordMatch": "Les mots de passe doivent correspondre.",
18 | "firstNameRequired": "Le prénom est requis.",
19 | "lastNameRequired": "Le nom de famille est requis.",
20 | "registerSuccess": "Inscription réussie.",
21 | "registerFailure": "Échec de l'inscription.",
22 | "loginHere": "Connexion",
23 | "passwordReset": "Réinitialiser le mot de passe",
24 | "passwordResetSuccess": "Réinitialisation du mot de passe réussie",
25 | "passwordResetSuccessMessage": "Votre mot de passe a été réinitialisé.",
26 | "continue": "Continuer",
27 | "back": "Retour",
28 | "passwordSchema": "Le mot de passe doit contenir",
29 | "minLengthMessage": "au moins {{min}} caractères de longueur",
30 | "minUppercaseMessage": "au moins {{min}} lettre(s) majuscule(s)",
31 | "minNumberMessage": "au moins {{min}} chiffre(s)",
32 | "minSymbolsMessage": "au moins {{min}} caractère(s) spécial(aux)",
33 | "mailListText": "Abonnez-vous à notre liste de diffusion pour les mises à jour !"
34 | }
35 |
--------------------------------------------------------------------------------
/src/i18n/locales/id.json:
--------------------------------------------------------------------------------
1 | {
2 | "gettingWrongSometimes": "Terkadang saya masih melakukan kesalahan. Yang terbaik adalah selalu berkonsultasi dengan Cendekiawan Islam sejati.",
3 | "flaggingInstructions": {
4 | "desktop": "Jika saya mengatakan sesuatu yang salah, membingungkan, bagus, lucu atau menarik, harap tandai. Apa pun yang ditandai akan ditinjau oleh manusia. Untuk menandai percakapan, cukup ucapkan 'Saya ingin menandai percakapan ini'.",
5 | "mobile": "Jika saya mengatakan sesuatu yang salah, membingungkan, bagus, lucu atau menarik, harap tandai. <1>lagi1>"
6 | },
7 | "multilingualMessage": {
8 | "desktop": "Saya multibahasa. Saya bisa memahami bahasa Arab (termasuk transliterasi), Turki, Urdu, Indonesia, Bosnia dan banyak bahasa lainnya.",
9 | "mobile": "Saya multibahasa. <1>lagi1>"
10 | },
11 | "promptPlaceholder": "Ketik pesan",
12 | "duaToMake": "Dua yang harus dilakukan",
13 | "inParticularSituation": "dalam situasi tertentu",
14 | "spiritualRemedies": "Pengobatan rohani",
15 | "challengesFacing": "untuk tantangan yang Anda hadapi",
16 | "islamicPerspectives": "perspektif Islam",
17 | "onTopics": "pada topik",
18 | "selectLanguage": "Pilih Bahasa",
19 | "messageSent": "pesan",
20 | "information": "Informasi",
21 | "comprehensiveGuide": "Klik <1>di sini1> untuk panduan lebih komprehensif tentang apa yang dapat dilakukan Ansari.",
22 | "startConversation": "Mulai Percakapan Baru",
23 | "newConversationConfirmation": "Apakah Anda yakin ingin memulai percakapan baru?",
24 | "conversationLostWarning": "Harap dicatat bahwa percakapan sebelumnya akan hilang.",
25 | "cancel": "Membatalkan",
26 | "yes": "Ya",
27 | "subscribe": "didukung oleh Ansari",
28 | "backendErrorMessage": "Ada yang salah. Jika masalah ini terus berlanjut, harap hubungi kami melalui pusat bantuan kami di <1>feedback@ansari.chat1>"
29 | }
--------------------------------------------------------------------------------
/src/i18n/locales/id/feedback.json:
--------------------------------------------------------------------------------
1 | {
2 | "good": [
3 | {
4 | "id": "good1",
5 | "label": "Benar",
6 | "value": "correct"
7 | },
8 | {
9 | "id": "good2",
10 | "label": "Mudah dipahami",
11 | "value": "understandable"
12 | },
13 | {
14 | "id": "good3",
15 | "label": "Lengkap",
16 | "value": "complete"
17 | }
18 | ],
19 | "bad": [
20 | {
21 | "id": "bad2",
22 | "label": "Tidak benar fakta",
23 | "value": "incorrect"
24 | },
25 | {
26 | "id": "bad3",
27 | "label": "Lainnya",
28 | "value": "other"
29 | }
30 | ],
31 | "redflag": [
32 | {
33 | "id": "redflag1",
34 | "label": "Kasar / Tidak aman",
35 | "value": "offensive"
36 | },
37 | {
38 | "id": "redflag2",
39 | "label": "Lainnya",
40 | "value": "other"
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/src/i18n/locales/id/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Masuk",
3 | "email": "Surel",
4 | "password": "Kata Sandi",
5 | "submit": "Kirim",
6 | "submitting": "Memuat",
7 | "register": "Daftar",
8 | "dontHaveAccount": "Belum punya akun?",
9 | "successMessage": "Masuk berhasil",
10 | "errorMessage": "Masuk gagal. Silakan coba lagi.",
11 | "invalidCredentials": "Kredensial tidak valid",
12 | "emailRequiredField": "Surel wajib diisi.",
13 | "emailInvalid": "Masukkan surel yang valid.",
14 | "passwordRequiredField": "Kata sandi wajib diisi.",
15 | "passwordLength": "Kata sandi harus minimal 8 karakter.",
16 | "unexpectedError": "Terjadi kesalahan yang tidak terduga. Silakan coba lagi nanti.",
17 | "forgetPassword": "Lupa kata sandi?",
18 | "guestLogin": "Masuk sebagai Tamu",
19 | "forgotYourPassword": "Lupa Kata Sandi Anda",
20 | "yourEmail": "Alamat email Anda",
21 | "forgotMessage": "Beritahu kami alamat email Anda dan kami akan mengirimkan tautan reset kata sandi yang akan memungkinkan Anda memilih yang baru.",
22 | "forgotSuccessMessage": "Sebuah email akan dikirimkan ke alamat email yang disediakan untuk mereset akun jika ada dalam sistem.",
23 | "continue": "Lanjutkan",
24 | "back": "Kembali"
25 | }
26 |
--------------------------------------------------------------------------------
/src/i18n/locales/id/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Daftar",
3 | "email": "Email",
4 | "password": "Kata Sandi",
5 | "firstName": "Nama Depan",
6 | "lastName": "Nama Belakang",
7 | "confirmPassword": "Konfirmasi Kata Sandi",
8 | "register": "Daftar",
9 | "registering": "Memuat",
10 | "alreadyHaveAccount": "Sudah punya akun?",
11 | "goToLogin": "Menuju login",
12 | "emailRequired": "Email wajib diisi.",
13 | "emailInvalid": "Masukkan email yang valid.",
14 | "passwordRequired": "Kata sandi wajib diisi.",
15 | "passwordLength": "Kata sandi harus terdiri dari setidaknya 8 karakter.",
16 | "confirmPasswordRequired": "Konfirmasi kata sandi wajib diisi.",
17 | "confirmPasswordMatch": "Kata sandi harus cocok.",
18 | "firstNameRequired": "Nama depan wajib diisi.",
19 | "lastNameRequired": "Nama belakang wajib diisi.",
20 | "registerSuccess": "Pendaftaran berhasil.",
21 | "registerFailure": "Pendaftaran gagal.",
22 | "loginHere": "Masuk",
23 | "passwordReset": "Atur ulang kata sandi",
24 | "passwordResetSuccess": "Atur ulang kata sandi berhasil",
25 | "passwordResetSuccessMessage": "Kata sandi Anda telah diatur ulang.",
26 | "continue": "Lanjutkan",
27 | "back": "Kembali",
28 | "passwordSchema": "Password harus mengandung",
29 | "minLengthMessage": "setidaknya {{min}} karakter panjangnya",
30 | "minUppercaseMessage": "setidaknya {{min}} huruf kapital",
31 | "minNumberMessage": "setidaknya {{min}} angka",
32 | "minSymbolsMessage": "setidaknya {{min}} karakter khusus",
33 | "mailListText": "Berlangganan ke daftar surat kami untuk pembaruan!"
34 | }
35 |
--------------------------------------------------------------------------------
/src/i18n/locales/tml/feedback.json:
--------------------------------------------------------------------------------
1 | {
2 | "good": [
3 | {
4 | "id": "good1",
5 | "label": "இது மிகச்சரியான/உண்மையான தகவல்",
6 | "value": "correct"
7 | },
8 | {
9 | "id": "good2",
10 | "label": "எளிதில் விளங்கக்கூடியது",
11 | "value": "understandable"
12 | },
13 | {
14 | "id": "good3",
15 | "label": "முழுமை",
16 | "value": "complete"
17 | }
18 | ],
19 | "bad": [
20 | {
21 | "id": "bad2",
22 | "label": "உண்மையற்றது/தவறான தகவல்",
23 | "value": "incorrect"
24 | },
25 | {
26 | "id": "bad3",
27 | "label": "தவறான தகவல்",
28 | "value": "other"
29 | }
30 | ],
31 | "redflag": [
32 | {
33 | "id": "redflag1",
34 | "label": "அவமதிப்பான/மனத்தை புண்படுத்துகிற/பாதுகாப்பற்ற தகவல் ",
35 | "value": "offensive"
36 | },
37 | {
38 | "id": "redflag2",
39 | "label": "தவறான தகவல்",
40 | "value": "other"
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/src/i18n/locales/tml/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "மீண்டும் வரவேற்கிறோம்",
3 | "email": "மின்னஞ்சல்",
4 | "password": "கடவுச்சொல்",
5 | "submit": "சமர்ப்பி",
6 | "submitting": "சமர்ப்பிக்கிறது",
7 | "register": "பதிவு செய்",
8 | "dontHaveAccount": "அக்கவுண்ட் இல்லையா?",
9 | "successMessage": "வெற்றிகரமான உள்நுழைவு",
10 | "errorMessage": "உள்நுழைவு தோல்வியடைந்தது, மீண்டும் முயற்சிக்கவும்n.",
11 | "invalidCredentials": "உங்களின் மின்னஞ்சலோ, கடவுச்சொல்லோ தவறானது",
12 | "emailRequiredField": "மின்னஞ்சல் தேவை.",
13 | "emailInvalid": "சரியான மின்னஞ்சலை உள்ளிடவும்.",
14 | "passwordRequiredField": "கடவுச்சொல் தேவை.",
15 | "passwordLength": "கடவுச்சொல் குறைந்தது 8 எழுத்துக்கள் நீளமாக இருக்க வேண்டும்.",
16 | "unexpectedError": "எதிர்பாராத பிழை ஏற்பட்டது. பிறகு முயற்சிக்கவும்.",
17 | "forgetPassword": "உங்கள் கடவுச்சொல்லை மறந்துவிட்டீர்களா?",
18 | "guestLogin": "விருந்தினராக உள்நுழைக",
19 | "forgotYourPassword": "உங்கள் கடவுச்சொல்லை மறந்துவிட்டீர்களா",
20 | "yourEmail": "உங்கள் மின்னஞ்சல் முகவரி",
21 | "forgotMessage": "உங்கள் மின்னஞ்சல் முகவரியை எங்களுக்குத் தெரியப்படுத்துங்கள், புதியதாக வேறு ஒரு கடவுச்சொல்லை அமைக்க கடவுச்சொல் மீட்டமைப்பு இணைப்பை நாங்கள் உங்களுக்கு மின்னஞ்சல் செய்வோம்.",
22 | "forgotSuccessMessage": "உங்களுக்கு எங்களிடம் அக்கவுண்ட் இருந்தால் உங்களின் அக்கவுண்ட்டை மீட்டமைக்க , நீங்கள் வழங்கிய மின்னஞ்சல் முகவரிக்கு மின்னஞ்சல் அனுப்பப்படும்.",
23 | "continue": "தொடரவும்",
24 | "back": "பின் செல்",
25 | "emailValidationMessage": "சரியான மின்னஞ்சல் முகவரியை உள்ளிடவும்",
26 | "emailRequired": "மின்னஞ்சல் தேவை"
27 | }
--------------------------------------------------------------------------------
/src/i18n/locales/tml/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "பதிவு செய்",
3 | "email": "மின்னஞ்சல்",
4 | "password": "கடவுச்சொல்",
5 | "firstName": "முதல் பெயர்",
6 | "lastName": "கடைசி பெயர்",
7 | "confirmPassword": "கடவுச்சொல்லை உறுதிப்படுத்தவும்",
8 | "register": "பதிவு செய்",
9 | "registering": "பதிவு செய்யப்படுகிறது",
10 | "alreadyHaveAccount": "ஏற்கனவே கணக்கு உள்ளதா?",
11 | "goToLogin": "உள்நுழைய செல்லவும்",
12 | "emailRequired": "மின்னஞ்சல் தேவை.",
13 | "emailInvalid": "மின்னஞ்சல் தவறானது.",
14 | "passwordRequired": "கடவுச்சொல் தேவை.",
15 | "passwordLength": "கடவுச்சொல் குறைந்தது 8 எழுத்துக்கள் நீளமாக இருக்க வேண்டும்.",
16 | "confirmPasswordRequired": "கடவுச்சொல்லை உறுதிப்படுத்த வேண்டும்.",
17 | "confirmPasswordMatch": "கடவுச்சொற்கள் பொருந்த வேண்டும்.",
18 | "firstNameRequired": "முதல் பெயர் தேவை.",
19 | "lastNameRequired": "கடைசி பெயர் தேவை.",
20 | "registerSuccess": "பதிவு வெற்றிகரமாக முடிந்தது.",
21 | "registerFailure": "பதிவு தோல்வியடைந்தது.",
22 | "loginHere": "உள்நுழை",
23 | "passwordReset": "கடவுச்சொல்லை மாற்றியமைக்க",
24 | "passwordResetSuccess": "கடவுச்சொல் வெற்றிகரமாக மாற்றியமைக்கப்பட்டது.",
25 | "passwordResetSuccessMessage": "உங்களின் கடவுச்சொல் வெற்றிகரமாக மாற்றியமைக்கப்பட்டது.",
26 | "continue": "தொடரவும்",
27 | "back": "பின் செல்",
28 | "passwordSchema": "உங்கள் கடவுச்சொல்லில் இருக்கவேண்டியவை",
29 | "minLengthMessage": "குறைந்தபட்சம் {{min}} எழுத்துக்கள் நீளமாக இருக்க வேண்டடும்.",
30 | "minUppercaseMessage": "குறைந்தபட்சம் {{min}} பெரிய ஆங்கில எழுத்து(கள்)",
31 | "minNumberMessage": "குறைந்தபட்சம் {{min}} நம்பர்(கள்)",
32 | "minSymbolsMessage": "குறைந்தபட்சம் {{min}} சிறப்பு எழுத்து(கள்)",
33 | "mailListText": "எங்களிடம் இருந்து அப்டேட்டுகளை பெற எங்களிடம் (மின்னஞ்சல் பட்டியலில்) சாப்ஸ்க்ரைப் பண்ணிக்கொள்ளவும்!"
34 | }
--------------------------------------------------------------------------------
/src/i18n/locales/tur.json:
--------------------------------------------------------------------------------
1 | {
2 | "gettingWrongSometimes": "Hala bazen bazı şeyleri yanlış anlıyorum. Gerçek bir İslam alimi ile görüşmek her zaman en iyisidir.",
3 | "flaggingInstructions": {
4 | "desktop": "Yanlış, kafa karıştırıcı, harika, komik veya ilginç bir şey söylersem lütfen işaretleyin. İşaretlenen her şey insanlar tarafından incelenecektir. Bir sohbeti işaretlemek için 'Bu sohbeti işaretlemek istiyorum' demeniz yeterli.",
5 | "mobile": "Yanlış, kafa karıştırıcı, harika, komik veya ilginç bir şey söylersem lütfen işaretleyin. <1>Daha1>"
6 | },
7 | "multilingualMessage": {
8 | "desktop": "Ben çok dilliyim. Arapça (harf çevirisi dahil), Türkçe, Urduca, Bahasa, Boşnakça ve diğer birçok dili anlayabiliyorum.",
9 | "mobile": "Ben çok dilliyim. <1>Daha1>"
10 | },
11 | "promptPlaceholder": "Mesaj yazın",
12 | "duaToMake": "Yapmak için dua",
13 | "inParticularSituation": "özel durumda",
14 | "spiritualRemedies": "Manevi çareler",
15 | "challengesFacing": "karşılaştığınız zorluklar için",
16 | "islamicPerspectives": "İslami perspektifler",
17 | "onTopics": "konularda",
18 | "selectLanguage": "Bir dil seç",
19 | "messageSent": "İleti",
20 | "information": "Bilgi",
21 | "comprehensiveGuide": "Ansari'nin neler yapabileceğine ilişkin daha kapsamlı bir kılavuz için <1>burayı1> tıklayın.",
22 | "startConversation": "Yeni Konuşma Başlat",
23 | "newConversationConfirmation": "Yeni bir sohbet başlatmak istediğinizden emin misiniz?",
24 | "conversationLostWarning": "Lütfen önceki görüşmenin kaybolacağını unutmayın.",
25 | "cancel": "İptal etmek",
26 | "yes": "Evet",
27 | "subscribe": "Ansari tarafından desteklenmektedir",
28 | "backendErrorMessage": "Bir şeyler yanlış gitti. Bu sorun devam ederse lütfen şu adresteki yardım merkezimiz aracılığıyla bizimle iletişime geçin: <1>feedback@ansari.chat1>"
29 | }
--------------------------------------------------------------------------------
/src/i18n/locales/tur/feedback.json:
--------------------------------------------------------------------------------
1 | {
2 | "good": [
3 | {
4 | "id": "good1",
5 | "label": "Doğru",
6 | "value": "correct"
7 | },
8 | {
9 | "id": "good2",
10 | "label": "Kolay Anlaşılır",
11 | "value": "understandable"
12 | },
13 | {
14 | "id": "good3",
15 | "label": "Tam",
16 | "value": "complete"
17 | }
18 | ],
19 | "bad": [
20 | {
21 | "id": "bad2",
22 | "label": "Tamamen Doğru Değil",
23 | "value": "incorrect"
24 | },
25 | {
26 | "id": "bad3",
27 | "label": "Diğer",
28 | "value": "other"
29 | }
30 | ],
31 | "redflag": [
32 | {
33 | "id": "redflag1",
34 | "label": "Saldırgan / Tehlikeli",
35 | "value": "offensive"
36 | },
37 | {
38 | "id": "redflag2",
39 | "label": "Diğer",
40 | "value": "other"
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/src/i18n/locales/tur/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Giriş Yap",
3 | "email": "E-posta",
4 | "password": "Şifre",
5 | "submit": "Gönder",
6 | "submitting": "Yükleniyor",
7 | "register": "Kayıt Ol",
8 | "dontHaveAccount": "Hesabınız yok mu?",
9 | "successMessage": "Giriş başarılı",
10 | "errorMessage": "Giriş başarısız. Lütfen tekrar deneyin.",
11 | "invalidCredentials": "Geçersiz kimlik bilgileri",
12 | "emailRequiredField": "E-posta zorunlu.",
13 | "emailInvalid": "Geçerli bir e-posta adresi girin.",
14 | "passwordRequiredField": "Şifre zorunlu.",
15 | "passwordLength": "Şifre en az 8 karakter uzunluğunda olmalıdır.",
16 | "unexpectedError": "Beklenmeyen bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
17 | "forgetPassword": "Şifrenizi mi unuttunuz?",
18 | "guestLogin": "Misafir olarak Giriş Yap",
19 | "forgotYourPassword": "Şifrenizi mi unuttunuz",
20 | "yourEmail": "E-posta adresiniz",
21 | "forgotMessage": "Sadece bize e-posta adresinizi bildirin ve size yeni bir tane seçme imkanı sunacak olan şifre sıfırlama bağlantısını e-posta ile göndereceğiz.",
22 | "forgotSuccessMessage": "Eğer sistemde varsa, hesabınızı sıfırlamak için sağlanan e-posta adresine bir e-posta gönderilecektir.",
23 | "continue": "Devam Et",
24 | "back": "Geri",
25 | "emailValidationMessage": "Lütfen geçerli bir e-posta adresi girin",
26 | "emailRequired": "E-posta gerekli"
27 | }
28 |
--------------------------------------------------------------------------------
/src/i18n/locales/tur/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Kayıt Ol",
3 | "email": "E-posta",
4 | "password": "Şifre",
5 | "firstName": "Ad",
6 | "lastName": "Soyad",
7 | "confirmPassword": "Şifreyi Onayla",
8 | "register": "Kayıt Ol",
9 | "registering": "Yükleniyor",
10 | "alreadyHaveAccount": "Zaten bir hesabınız mı var?",
11 | "goToLogin": "Giriş yapmak için git",
12 | "emailRequired": "E-posta gerekli.",
13 | "emailInvalid": "Geçerli bir e-posta adresi girin.",
14 | "passwordRequired": "Şifre gerekli.",
15 | "passwordLength": "Şifre en az 8 karakter uzunluğunda olmalıdır.",
16 | "confirmPasswordRequired": "Şifre onayı gerekli.",
17 | "confirmPasswordMatch": "Şifreler eşleşmiyor.",
18 | "firstNameRequired": "Ad gerekli.",
19 | "lastNameRequired": "Soyad gerekli.",
20 | "registerSuccess": "Kayıt başarılı.",
21 | "registerFailure": "Kayıt başarısız.",
22 | "loginHere": "Giriş",
23 | "passwordReset": "Şifreyi Sıfırla",
24 | "passwordResetSuccess": "Şifre sıfırlama başarılı",
25 | "passwordResetSuccessMessage": "Şifreniz sıfırlandı.",
26 | "continue": "Devam Et",
27 | "back": "Geri",
28 | "passwordSchema": "Parola şunları içermelidir",
29 | "minLengthMessage": "en az {{min}} karakter uzunluğunda olmalıdır",
30 | "minUppercaseMessage": "en az {{min}} büyük harf içermelidir",
31 | "minNumberMessage": "en az {{min}} rakam içermelidir",
32 | "minSymbolsMessage": "en az {{min}} özel karakter içermelidir",
33 | "mailListText": "Güncellemeler için posta listemize abone olun!"
34 | }
35 |
--------------------------------------------------------------------------------
/src/i18n/locales/ur.json:
--------------------------------------------------------------------------------
1 | {
2 | "gettingWrongSometimes": "مجھے اب بھی کبھی کبھی چیزیں غلط ہوجاتی ہیں۔ کسی حقیقی اسلامی اسکالر سے مشورہ کرنا ہمیشہ بہتر ہے۔",
3 | "flaggingInstructions": {
4 | "desktop": "اگر میں کچھ غلط، مبہم، زبردست، مضحکہ خیز یا دلچسپ کہتا ہوں، تو براہ کرم اسے نشان زد کریں۔ کسی بھی چیز پر جھنڈا لگایا گیا ہے اس کا انسانوں کے ذریعہ جائزہ لیا جائے گا۔ کسی گفتگو کو جھنڈا لگانے کے لیے، صرف اتنا بولیں کہ 'میں اس گفتگو کو پرچم لگانا چاہتا ہوں۔'",
5 | "mobile": "اگر میں کچھ غلط، مبہم، زبردست، مضحکہ خیز یا دلچسپ کہوں، تو براہ کرم اسے جھنڈا لگائیں۔ <1>مزید1>"
6 | },
7 | "multilingualMessage": {
8 | "desktop": "میں کثیر لسانی ہوں۔ میں عربی (بشمول نقل حرفی)، ترکی، اردو، بہاسہ، بوسنیائی اور بہت سی دوسری زبانیں سمجھ سکتا ہوں۔",
9 | "mobile": "میں کثیر لسانی ہوں۔<1>مزید1>"
10 | },
11 | "promptPlaceholder": "پیغام ٹائپ کریں۔",
12 | "duaToMake": "بنانے کی دعا",
13 | "inParticularSituation": "خاص صورت حال میں",
14 | "spiritualRemedies": "روحانی علاج",
15 | "challengesFacing": "ان چیلنجوں کے لیے جن کا آپ سامنا کر رہے ہیں۔",
16 | "islamicPerspectives": "اسلامی نقطہ نظر",
17 | "onTopics": "موضوعات پر",
18 | "selectLanguage": "ایک زبان منتخب کریں۔",
19 | "messageSent": "پیغام",
20 | "message received": "یہ انصاری بوٹ کا ورچوئل جواب ہے۔",
21 | "information": "معلومات",
22 | "comprehensiveGuide": "انصاری کیا کر سکتے ہیں اس بارے میں مزید جامع گائیڈ کے لیے <1>یہاں1> کلک کریں۔",
23 | "startConversation": "نئی گفتگو شروع کریں۔",
24 | "newConversationConfirmation": "کیا آپ کو یقین ہے کہ آپ ایک نئی بات چیت شروع کرنا چاہتے ہیں؟",
25 | "conversationLostWarning": "براہ کرم نوٹ کریں کہ پچھلی گفتگو ختم ہو جائے گی۔",
26 | "cancel": "منسوخ کریں۔",
27 | "yes": "جی ہاں",
28 | "subscribe": "کی طرف سے طاقت Ansari",
29 | "backendErrorMessage": "کچھ غلط ہو گیا. اگر یہ مسئلہ برقرار رہتا ہے تو براہ کرم ہمارے ہیلپ سنٹر کے ذریعے ہم سے رابطہ کریں۔ <1>feedback@ansari.chat1>"
30 | }
--------------------------------------------------------------------------------
/src/i18n/locales/ur/feedback.json:
--------------------------------------------------------------------------------
1 | {
2 | "good": [
3 | {
4 | "id": "good1",
5 | "label": "صحیح",
6 | "value": "correct"
7 | },
8 | {
9 | "id": "good2",
10 | "label": "سمجھنے میں آسان",
11 | "value": "understandable"
12 | },
13 | {
14 | "id": "good3",
15 | "label": "مکمل",
16 | "value": "complete"
17 | }
18 | ],
19 | "bad": [
20 | {
21 | "id": "bad2",
22 | "label": "حقائقی طور پر غلط",
23 | "value": "incorrect"
24 | },
25 | {
26 | "id": "bad3",
27 | "label": "دیگر",
28 | "value": "other"
29 | }
30 | ],
31 | "redflag": [
32 | {
33 | "id": "redflag1",
34 | "label": "توہین آمیز / نا محفوظ",
35 | "value": "offensive"
36 | },
37 | {
38 | "id": "redflag2",
39 | "label": "دیگر",
40 | "value": "other"
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/src/i18n/locales/ur/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "لاگ ان",
3 | "email": "ای-میل",
4 | "password": "پاسورڈ",
5 | "submit": "جمع کریں",
6 | "submitting": "لوڈ ہو رہا ہے",
7 | "register": "رجسٹر",
8 | "dontHaveAccount": "اکاؤنٹ نہیں ہے؟",
9 | "successMessage": "لاگ ان کامیاب ہوگیا",
10 | "errorMessage": "لاگ ان ناکام ہوگیا۔ براہ کرم دوبارہ کوشش کریں۔",
11 | "invalidCredentials": "غلط اعتبارات",
12 | "emailRequiredField": "ای-میل ضروری ہے۔",
13 | "emailInvalid": "معتبر ای-میل درج کریں۔",
14 | "passwordRequiredField": "پاسورڈ ضروری ہے۔",
15 | "passwordLength": "پاسورڈ کم سے کم 8 حروفوں کا ہونا چاہئے۔",
16 | "unexpectedError": "ایک غیر متوقع خرابی واقع ہوگئی ہے۔ براہ کرم بعد میں دوبارہ کوشش کریں۔",
17 | "forgetPassword": "اپنا پاسورڈ بھول گئے ہیں؟",
18 | "guestLogin": "مہمان کے طور پر لاگ ان",
19 | "continue": "جاری رکھیں",
20 | "back": "واپس"
21 | }
22 |
--------------------------------------------------------------------------------
/src/i18n/locales/ur/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "رجسٹر",
3 | "email": "ای-میل",
4 | "password": "پاسورڈ",
5 | "firstName": "پہلا نام",
6 | "lastName": "آخری نام",
7 | "confirmPassword": "پاسورڈ کی تصدیق کریں",
8 | "register": "رجسٹر کریں",
9 | "registering": "لوڈ ہو رہا ہے",
10 | "alreadyHaveAccount": "پہلے ہی اکاؤنٹ ہے؟",
11 | "goToLogin": "لاگ ان کریں",
12 | "emailRequired": "ای-میل ضروری ہے۔",
13 | "emailInvalid": "ای-میل غلط ہے۔",
14 | "passwordRequired": "پاسورڈ ضروری ہے۔",
15 | "passwordLength": "پاسورڈ کم سے کم 8 حروفوں کا ہونا چاہئے۔",
16 | "confirmPasswordRequired": "پاسورڈ کی تصدیق ضروری ہے۔",
17 | "confirmPasswordMatch": "پاسورڈ میچ ہونا چاہئے۔",
18 | "firstNameRequired": "پہلا نام ضروری ہے۔",
19 | "lastNameRequired": "آخری نام ضروری ہے۔",
20 | "registerSuccess": "رجسٹریشن کامیاب ہوگئی ہے۔",
21 | "registerFailure": "رجسٹریشن ناکام ہوگئی ہے۔",
22 | "loginHere": "لاگ ان ہیں",
23 | "passwordReset": "پاسورڈ ری سیٹ",
24 | "passwordResetSuccess": "پاسورڈ ری سیٹ کامیاب ہوگئی ہے",
25 | "passwordResetSuccessMessage": "آپ کا پاسورڈ ری سیٹ ہوگیا ہے۔",
26 | "continue": "جاری رکھیں",
27 | "back": "واپس",
28 | "passwordSchema": "پاس ورڈ میں شامل ہونا ضروری ہے",
29 | "minLengthMessage": "کم از کم {{min}} حروف لمبائی ہونی چاہئے",
30 | "minUppercaseMessage": "کم از کم {{min}} بڑے حرف ہونے چاہئے",
31 | "minNumberMessage": "کم از کم {{min}} نمبرز ہونے چاہئے",
32 | "minSymbolsMessage": "کم از کم {{min}} خاص حروف ہونے چاہئے",
33 | "mailListText": "اپ ڈیٹس کے لئے ہماری میلنگ لسٹ میں شامل ہوں!"
34 | }
35 |
--------------------------------------------------------------------------------
/src/i18n/resources.ts:
--------------------------------------------------------------------------------
1 | // TODO: Not compatible with the Metro Bundler
2 | // Function to load the locale files
3 | // const loadLocaleFiles = (locale: string) => ({
4 | // common: require(`./locales/${locale}/common.json`),
5 | // login: require(`./locales/${locale}/login.json`),
6 | // register: require(`./locales/${locale}/register.json`),
7 | // })
8 |
9 | const arFiles = () => ({
10 | common: require('./locales/ar/common.json'),
11 | login: require('./locales/ar/login.json'),
12 | register: require('./locales/ar/register.json'),
13 | })
14 |
15 | const bsFiles = () => ({
16 | common: require('./locales/bs/common.json'),
17 | login: require('./locales/bs/login.json'),
18 | register: require('./locales/bs/register.json'),
19 | })
20 |
21 | const enFiles = () => ({
22 | common: require('./locales/en/common.json'),
23 | login: require('./locales/en/login.json'),
24 | register: require('./locales/en/register.json'),
25 | })
26 |
27 | const frFiles = () => ({
28 | common: require('./locales/fr/common.json'),
29 | login: require('./locales/fr/login.json'),
30 | register: require('./locales/fr/register.json'),
31 | })
32 |
33 | const idFiles = () => ({
34 | common: require('./locales/id/common.json'),
35 | login: require('./locales/id/login.json'),
36 | register: require('./locales/id/register.json'),
37 | })
38 |
39 | const tmlFiles = () => ({
40 | common: require('./locales/tml/common.json'),
41 | login: require('./locales/tml/login.json'),
42 | register: require('./locales/tml/register.json'),
43 | })
44 |
45 | const turFiles = () => ({
46 | common: require('./locales/tur/common.json'),
47 | login: require('./locales/tur/login.json'),
48 | register: require('./locales/tur/register.json'),
49 | })
50 |
51 | const urFiles = () => ({
52 | common: require('./locales/ur/common.json'),
53 | login: require('./locales/ur/login.json'),
54 | register: require('./locales/ur/register.json'),
55 | })
56 |
57 | const resources = {
58 | ar: arFiles(),
59 | bs: bsFiles(),
60 | en: enFiles(),
61 | fr: frFiles(),
62 | id: idFiles(),
63 | tml: tmlFiles(),
64 | tur: turFiles(),
65 | ur: urFiles(),
66 | }
67 |
68 | export default resources
69 |
--------------------------------------------------------------------------------
/src/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './message'
2 |
--------------------------------------------------------------------------------
/src/interfaces/message.ts:
--------------------------------------------------------------------------------
1 | import { Role } from '@/constant'
2 |
3 | export interface MessageModel {
4 | role: Role
5 | content: string
6 | error?: boolean
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/FeedbackService.ts:
--------------------------------------------------------------------------------
1 | import arFeedback from '@/i18n/locales/ar/feedback.json'
2 | import bsFeedback from '@/i18n/locales/bs/feedback.json'
3 | import enFeedback from '@/i18n/locales/en/feedback.json'
4 | import frFeedback from '@/i18n/locales/fr/feedback.json'
5 | import idFeedback from '@/i18n/locales/id/feedback.json'
6 | import tmlFeedback from '@/i18n/locales/tml/feedback.json'
7 | import turFeedback from '@/i18n/locales/tur/feedback.json'
8 | import urFeedback from '@/i18n/locales/ur/feedback.json'
9 |
10 | export type Feedback = {
11 | id: string
12 | label: string
13 | value: string
14 | }
15 |
16 | // Define a type for the collection of feedbacks, categorized by type
17 | export type FeedbacksByCategory = {
18 | good: Feedback[]
19 | bad: Feedback[]
20 | redflag: Feedback[]
21 | [key: string]: Feedback[]
22 | }
23 |
24 | // Define a type for the feedbacks organized by language
25 | export type FeedbacksByLanguage = {
26 | [key: string]: FeedbacksByCategory // e.g., 'en': {good: [...], bad: [...]}, 'ar': {...}, etc.
27 | }
28 |
29 | // This function simulates fetching feedbacks based on the current language
30 | const fetchFeedbacksForLanguage = async (language: string): Promise => {
31 | // Mock data structure for demonstration. Replace with your actual data source.
32 | const data: FeedbacksByLanguage = {
33 | ar: arFeedback,
34 | bs: bsFeedback,
35 | en: enFeedback,
36 | fr: frFeedback,
37 | id: idFeedback,
38 | tml: tmlFeedback,
39 | tur: turFeedback,
40 | ur: urFeedback,
41 | }
42 |
43 | return data[language] || data.en // Default to English if language not found
44 | }
45 |
46 | export default fetchFeedbacksForLanguage
47 |
--------------------------------------------------------------------------------
/src/services/PromptsService.ts:
--------------------------------------------------------------------------------
1 | import arPrompts from '@/i18n/locales/ar/prompts.json'
2 | import bsPrompts from '@/i18n/locales/bs/prompts.json'
3 | import enPrompts from '@/i18n/locales/en/prompts.json'
4 | import frPrompts from '@/i18n/locales/fr/prompts.json'
5 | import idPrompts from '@/i18n/locales/id/prompts.json'
6 | import tmlPrompts from '@/i18n/locales/tml/prompts.json'
7 | import turPrompts from '@/i18n/locales/tur/prompts.json'
8 | import urPrompts from '@/i18n/locales/ur/prompts.json'
9 |
10 | // Define a type for individual prompt entries
11 | export type Prompt = {
12 | id: string
13 | title: string
14 | description: string
15 | }
16 |
17 | // Define a type for the collection of prompts, categorized by type
18 | export type PromptsByCategory = {
19 | dua: Prompt[]
20 | perspectives: Prompt[]
21 | remedies: Prompt[]
22 | [key: string]: Prompt[]
23 | }
24 |
25 | // Define a type for the prompts organized by language
26 | export type PromptsByLanguage = {
27 | [key: string]: PromptsByCategory // e.g., 'en': {dua: [...], perspectives: [...], remedies: [...]}, 'ar': {...}, etc.
28 | }
29 |
30 | // This function simulates fetching prompts based on the current language
31 | const fetchPromptsForLanguage = async (language: string): Promise => {
32 | // Mock data structure for demonstration. Replace with your actual data source.
33 | const data: PromptsByLanguage = {
34 | ar: arPrompts,
35 | bs: bsPrompts,
36 | en: enPrompts,
37 | fr: frPrompts,
38 | id: idPrompts,
39 | tml: tmlPrompts,
40 | tur: turPrompts,
41 | ur: urPrompts,
42 | }
43 |
44 | const promptsByLanguage = data[language] || data.en // Default to English if language not found
45 |
46 | // Initialize an empty object to store one random prompt per category
47 | const randomPrompts: PromptsByCategory = { dua: [], perspectives: [], remedies: [] }
48 |
49 | // Iterate over each category to select one random prompt
50 | Object.keys(promptsByLanguage).forEach((category) => {
51 | const prompts: Prompt[] = promptsByLanguage[category]
52 | const randomIndex = Math.floor(Math.random() * prompts.length)
53 | // Assign the randomly selected prompt to the corresponding category in the result object
54 | randomPrompts[category] = [prompts[randomIndex]] // Wrap in an array to match PromptsByCategory type
55 | })
56 |
57 | return randomPrompts
58 | }
59 |
60 | // Export the Prompt interface and the fetchPromptsForLanguage function as separate values
61 | export default fetchPromptsForLanguage
62 |
--------------------------------------------------------------------------------
/src/services/StorageService.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage'
2 |
3 | const ACCESS_TOKEN_KEY = 'ac-at'
4 | const REFRESH_TOKEN_KEY = 'ac-rt'
5 |
6 | class StorageService {
7 | async saveTokens(accessToken?: string, refreshToken?: string): Promise {
8 | try {
9 | if (accessToken) await AsyncStorage.setItem(ACCESS_TOKEN_KEY, accessToken)
10 |
11 | if (refreshToken) await AsyncStorage.setItem(REFRESH_TOKEN_KEY, refreshToken)
12 | } catch (error) {
13 | console.error('Error storing items:', error)
14 | }
15 | }
16 |
17 | async getTokens(): Promise<{ accessToken: string | null; refreshToken: string | null }> {
18 | try {
19 | const accessToken = await AsyncStorage.getItem(ACCESS_TOKEN_KEY)
20 | const refreshToken = await AsyncStorage.getItem(REFRESH_TOKEN_KEY)
21 | return { accessToken, refreshToken }
22 | } catch (error) {
23 | console.error('Error retrieving item:', error)
24 | return { accessToken: null, refreshToken: null }
25 | }
26 | }
27 |
28 | async getAccessToken(): Promise {
29 | try {
30 | return await AsyncStorage.getItem(ACCESS_TOKEN_KEY)
31 | } catch (error) {
32 | console.error('Error retrieving item:', error)
33 | return null
34 | }
35 | }
36 |
37 | async getRefreshToken(): Promise {
38 | try {
39 | return await AsyncStorage.getItem(REFRESH_TOKEN_KEY)
40 | } catch (error) {
41 | console.error('Error retrieving item:', error)
42 | return null
43 | }
44 | }
45 |
46 | async removeTokens(): Promise {
47 | try {
48 | await AsyncStorage.removeItem(ACCESS_TOKEN_KEY)
49 | await AsyncStorage.removeItem(REFRESH_TOKEN_KEY)
50 | // Legacy items
51 | await AsyncStorage.removeItem('au')
52 | await AsyncStorage.removeItem('ek')
53 | } catch (error) {
54 | console.error('Error removing items:', error)
55 | }
56 | }
57 | }
58 |
59 | export default StorageService
60 |
--------------------------------------------------------------------------------
/src/services/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ApiService } from './ApiService'
2 | export { default as ChatService } from './ChatService'
3 | export { default as FeedbackService } from './FeedbackService'
4 | export { default as PromptsService } from './PromptsService'
5 | export { userService as UserService } from './UserService'
6 |
--------------------------------------------------------------------------------
/src/store/actions/index.ts:
--------------------------------------------------------------------------------
1 | export { login, logout, register, guestLogin, deleteAccount } from './authActions'
2 | export {
3 | addMessage,
4 | createThread,
5 | deleteThread,
6 | fetchThread,
7 | fetchThreads,
8 | sendFeedback,
9 | setThreadName,
10 | fetchSharedThread,
11 | getShareThreadId,
12 | } from './chatActions'
13 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './slices'
3 | export * from './types'
4 | export { default as initStore } from './store'
5 | export type { AppDispatch, RootState, AppThunk } from './store'
6 |
--------------------------------------------------------------------------------
/src/store/slices/index.ts:
--------------------------------------------------------------------------------
1 | export { default as authReducer, loadAuthState, refreshTokens } from './authSlice'
2 | export type { AuthState } from './authSlice'
3 | export {
4 | addMessageToActiveThread,
5 | addStreamMessageToActiveThread,
6 | default as chatReducer,
7 | resetChatState,
8 | setActiveThread,
9 | setError,
10 | setLoading,
11 | setThreads,
12 | } from './chatSlice'
13 | export { default as informationPopupReducer, toggleInformationPopup } from './informationPopupSlice'
14 | export { default as inputFullModeReducer, tootleInputFullMode } from './inputFullModeSlice'
15 | export { default as sideMenuReducer, toggleSideMenu } from './sideMenuSlice'
16 | export { default as reactionButtonsReducer, resetReactionButtons, setReactionButton } from './reactionButtonsSlice'
17 | export { default as themeReducer, setTheme } from './themeSlice'
18 | export { default as shareReducer, toggleSharePopup } from './shareSlice'
19 | export type { ReactionButtonsState } from './reactionButtonsSlice'
20 |
--------------------------------------------------------------------------------
/src/store/slices/informationPopupSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2 |
3 | interface InformationPopupState {
4 | isOpen: boolean | undefined
5 | }
6 |
7 | const initialState: InformationPopupState = {
8 | isOpen: false,
9 | }
10 |
11 | const informationPopupSlice = createSlice({
12 | name: 'informationPopup',
13 | initialState,
14 | reducers: {
15 | toggleInformationPopup: (state, action: PayloadAction) => {
16 | state.isOpen = action.payload
17 | },
18 | },
19 | })
20 |
21 | export const { toggleInformationPopup } = informationPopupSlice.actions
22 |
23 | export default informationPopupSlice.reducer
24 |
--------------------------------------------------------------------------------
/src/store/slices/inputFullModeSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2 |
3 | const inputFullModeSlice = createSlice({
4 | name: 'input',
5 | initialState: {
6 | fullMode: false,
7 | },
8 | reducers: {
9 | tootleInputFullMode: (state, action: PayloadAction) => {
10 | state.fullMode = action.payload
11 | },
12 | },
13 | })
14 |
15 | export const { tootleInputFullMode } = inputFullModeSlice.actions
16 |
17 | export default inputFullModeSlice.reducer
18 |
--------------------------------------------------------------------------------
/src/store/slices/reactionButtonsSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2 | import { FeedbackClass } from '../types'
3 |
4 | // Define the state structure for a single reaction button.
5 | export interface ReactionButtonsState {
6 | threadId: string | null
7 | messageId: string | null
8 | selectedIcon: FeedbackClass | null
9 | }
10 |
11 | // Initialize the state with an empty array.
12 | const initialState: ReactionButtonsState[] = []
13 |
14 | // Create the slice.
15 | const reactionButtonsSlice = createSlice({
16 | name: 'reactionButtons',
17 | initialState,
18 | reducers: {
19 | // Action to set the state of a specific reaction button.
20 | setReactionButton: (state, action: PayloadAction) => {
21 | const index = state.findIndex(
22 | (reactionButtonState) =>
23 | reactionButtonState.threadId === action.payload.threadId &&
24 | reactionButtonState.messageId === action.payload.messageId,
25 | )
26 | if (index === -1) {
27 | // If the reaction button does not exist in the state, add it.
28 | state.push(action.payload)
29 | } else {
30 | // If it exists, update its state.
31 | state[index] = action.payload
32 | }
33 | },
34 | // Action to reset the states of all reaction buttons.
35 | resetReactionButtons: () => {
36 | return initialState
37 | },
38 | },
39 | })
40 |
41 | // Export the action creators.
42 | export const { setReactionButton, resetReactionButtons } = reactionButtonsSlice.actions
43 |
44 | // Export the reducer as default.
45 | export default reactionButtonsSlice.reducer
46 |
--------------------------------------------------------------------------------
/src/store/slices/shareSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2 |
3 | interface ShareState {
4 | isOpen: boolean
5 | }
6 |
7 | const initialState: ShareState = {
8 | isOpen: false,
9 | }
10 |
11 | const shareSlice = createSlice({
12 | name: 'share',
13 | initialState,
14 | reducers: {
15 | toggleSharePopup(state, action: PayloadAction) {
16 | state.isOpen = action.payload
17 | },
18 | },
19 | })
20 |
21 | export const { toggleSharePopup } = shareSlice.actions
22 | export default shareSlice.reducer
23 |
--------------------------------------------------------------------------------
/src/store/slices/sideMenuSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2 |
3 | const sideMenuSlice = createSlice({
4 | name: 'sideMenu',
5 | initialState: {
6 | isOpen: false,
7 | width: 0,
8 | },
9 | reducers: {
10 | toggleSideMenu: (state, action: PayloadAction) => {
11 | state.isOpen = action.payload
12 | state.width = action.payload ? 300 : 0
13 | },
14 | },
15 | })
16 |
17 | export const { toggleSideMenu } = sideMenuSlice.actions
18 |
19 | export default sideMenuSlice.reducer
20 |
--------------------------------------------------------------------------------
/src/store/slices/themeSlice.ts:
--------------------------------------------------------------------------------
1 | import { darkTheme, Theme } from '@/utils/theme'
2 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
3 |
4 | interface ThemeState {
5 | theme: Theme // Assuming your theme is represented as a string
6 | }
7 |
8 | const initialState: ThemeState = {
9 | theme: darkTheme, // Initial theme
10 | }
11 |
12 | const themeSlice = createSlice({
13 | name: 'theme',
14 | initialState,
15 | reducers: {
16 | setTheme(state, action: PayloadAction) {
17 | state.theme = action.payload
18 | },
19 | },
20 | })
21 |
22 | export const { setTheme } = themeSlice.actions
23 | export default themeSlice.reducer
24 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import { Action, combineReducers, configureStore, ThunkAction, ThunkDispatch } from '@reduxjs/toolkit'
2 |
3 | import {
4 | authReducer,
5 | chatReducer,
6 | informationPopupReducer,
7 | inputFullModeReducer,
8 | loadAuthState,
9 | reactionButtonsReducer,
10 | shareReducer,
11 | sideMenuReducer,
12 | themeReducer,
13 | } from './slices'
14 |
15 | const rootReducer = combineReducers({
16 | auth: authReducer,
17 | chat: chatReducer,
18 | informationPopup: informationPopupReducer,
19 | sideMenu: sideMenuReducer,
20 | reactionButtons: reactionButtonsReducer,
21 | theme: themeReducer,
22 | share: shareReducer,
23 | input: inputFullModeReducer,
24 | // Add other reducers here...
25 | })
26 |
27 | const initStore = async () => {
28 | const preloadedState = await loadAuthState()
29 |
30 | const store = configureStore({
31 | reducer: rootReducer,
32 | preloadedState,
33 | devTools: __DEV__,
34 | })
35 |
36 | return store
37 | }
38 |
39 | // export type AppDispatch = typeof initStore.dispatch
40 | export type RootState = ReturnType
41 | export type AppDispatch = ThunkDispatch
42 | export type AppThunk = ThunkAction>
43 |
44 | export default initStore
45 |
--------------------------------------------------------------------------------
/src/store/types/chatTypes.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /**
3 | * Represents the role of a user in the chat.
4 | */
5 | export enum UserRole {
6 | User = 'user',
7 | Assistant = 'assistant',
8 | }
9 |
10 | /**
11 | * Enum representing different feedback classes.
12 | */
13 | export enum FeedbackClass {
14 | ThumbsUp = 'thumbsup',
15 | ThumbsDown = 'thumbsdown',
16 | RedFlag = 'redflag',
17 | }
18 |
19 | /* eslint-disable no-unused-vars */
20 |
21 | /**
22 | * Represents a request to add a message.
23 | */
24 | export interface AddMessageRequest {
25 | /**
26 | * The role of the sender, restricted to 'user'.
27 | */
28 | role: UserRole.User
29 |
30 | /**
31 | * The content of the message.
32 | */
33 | content: string
34 | }
35 |
36 | /**
37 | * Represents a thread in the chat.
38 | */
39 | export interface Thread {
40 | id: string // Unique identifier for the thread
41 | name?: string | null // Thread name
42 | messages: Message[] // Array of messages in the thread
43 | date?: Date | number | null // Creation/Update timestamp
44 | }
45 |
46 | /**
47 | * Represents a chat message.
48 | */
49 | export interface Message {
50 | /**
51 | * Unique identifier for the message.
52 | */
53 | id: string
54 | /**
55 | * The role of the sender, using UserRole enum.
56 | */
57 | role: UserRole
58 | /**
59 | * Content of the message.
60 | */
61 | content: string
62 | /**
63 | * Timestamp of when the message was sent.
64 | */
65 | timestamp?: string
66 | }
67 |
68 | /**
69 | * Represents a request to change the name of a thread.
70 | */
71 | export interface ThreadNameRequest {
72 | name: string // New name for the thread
73 | }
74 |
75 | /**
76 | * Represents a feedback request.
77 | */
78 | export interface FeedbackRequest {
79 | /**
80 | * The ID of the thread.
81 | */
82 | threadId: string
83 | /**
84 | * The ID of the message.
85 | */
86 | messageId: string
87 | /**
88 | * The class of feedback.
89 | */
90 | feedbackClass: FeedbackClass
91 | /**
92 | * Additional comments for the feedback.
93 | */
94 | comment: string
95 | }
96 |
--------------------------------------------------------------------------------
/src/store/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chatTypes'
2 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 |
--------------------------------------------------------------------------------
/src/types/types.ts:
--------------------------------------------------------------------------------
1 | // Type for Register request data
2 | export type RegisterRequest = {
3 | email: string
4 | password: string
5 | first_name: string
6 | last_name: string
7 | register_to_mail_list: boolean
8 | }
9 |
10 | // Type for Register response data
11 | export type RegisterResponse = {
12 | status: string
13 | message?: string
14 | error?: string
15 | detail?: string
16 | }
17 |
18 | // Type for Login request data
19 | export type LoginRequest = {
20 | email: string
21 | password: string
22 | guest?: boolean
23 | }
24 |
25 | // Type for Login response data
26 | export type LoginResponse = {
27 | status: string
28 | access_token?: string
29 | refresh_token?: string
30 | message?: string
31 | error?: string
32 | first_name?: string
33 | last_name?: string
34 | }
35 |
36 | // Type for RefreshToken Response data
37 | export type RefreshTokenResponse = {
38 | status: string
39 | access_token: string
40 | refresh_token: string
41 | }
42 |
43 | // Type for Application User
44 | export type User = {
45 | firstName: string
46 | lastName: string
47 | email: string
48 | }
49 |
50 | // Type for Application User
51 | export type Error = {
52 | message: string
53 | }
54 |
55 | // Type for ShareThred response data
56 | export type ShareThreadResponse = {
57 | status: string
58 | share_id: string
59 | }
60 |
61 | export type ResetPasswordResponse = { status: string }
62 |
63 | // Type for app version check request
64 | export type AppVersionCheckRequest = {
65 | native_application_version: string
66 | native_build_version: string
67 | platform: 'web' | 'ios' | 'android'
68 | }
69 |
70 | // Type for app version check response
71 | export type AppVersionCheckResponse = {
72 | maintenance_mode: boolean
73 | update_available: boolean
74 | force_update_required: boolean
75 | }
76 |
--------------------------------------------------------------------------------
/src/utils/getEnv.ts:
--------------------------------------------------------------------------------
1 | const envConfig = {
2 | API_URL: () => process.env.EXPO_PUBLIC_API_V2_URL!,
3 | SHARE_URL: () => process.env.EXPO_PUBLIC_SHARE_URL!,
4 | LOADING_MESSAGE_RESPONSE_TIMEOUT: () =>
5 | process.env.EXPO_PUBLIC_API_TIMEOUT ? Number.parseInt(process.env.EXPO_PUBLIC_API_TIMEOUT) : 60000,
6 | FEEDBACK_EMAIL: () => process.env.EXPO_PUBLIC_FEEDBACK_EMAIL,
7 | FEEDBACK_MAIL_TO: () => `mailto:${process.env.EXPO_PUBLIC_FEEDBACK_EMAIL}`,
8 | COMPREHENSIVE_GUIDE_URL: () => process.env.EXPO_PUBLIC_COMPREHENSIVE_GUIDE_URL!,
9 | PRIVACY_URL: () => process.env.EXPO_PUBLIC_PRIVACY_URL!,
10 | TERMS_URL: () => process.env.EXPO_PUBLIC_TERMS_URL!,
11 | SUBSCRIBE_URL: () => process.env.EXPO_PUBLIC_SUBSCRIBE_URL!,
12 | ENABLE_SHARE: () => process.env.EXPO_PUBLIC_ENABLE_SHARE === 'true',
13 | }
14 |
15 | const getEnv = (key: T) => {
16 | return envConfig[key]() as ReturnType<(typeof envConfig)[T]>
17 | }
18 |
19 | export default getEnv
20 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as GetEnv } from './getEnv'
2 | export { default as Helpers } from './helpers'
3 | export { default as createGeneralThemedStyles } from './styles'
4 | export * from './theme'
5 |
--------------------------------------------------------------------------------
/src/validation/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useLoginSchema } from './loginSchema'
2 | export { default as useRegisterSchema } from './registerSchema'
3 |
--------------------------------------------------------------------------------
/src/validation/loginSchema.ts:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import * as Yup from 'yup'
3 |
4 | // Function component to use the useTranslation hook and export validation constants
5 | const useLoginSchema = () => {
6 | const { t } = useTranslation('login')
7 |
8 | return {
9 | email: Yup.string().email(t('emailInvalid')).required(t('emailRequiredField')),
10 | password: Yup.string().min(8, t('passwordLength')).required(t('passwordRequiredField')),
11 | }
12 | }
13 |
14 | export default useLoginSchema
15 |
--------------------------------------------------------------------------------
/src/validation/registerSchema.ts:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import * as Yup from 'yup'
3 |
4 | // Function component to use the useTranslation hook and export validation constants
5 | const useRegisterSchema = () => {
6 | const { t } = useTranslation('register')
7 |
8 | const passwordStrength = (minLength: number, minUppercase: number, minNumbers: number, minSymbols: number) =>
9 | Yup.string()
10 | .required(t('passwordRequired'))
11 | .test('password-strength', 'Password does not meet requirements', (value = '') => {
12 | const errors = []
13 | // Check for minimum length
14 | if (value.length < minLength) {
15 | errors.push(t('minLengthMessage', { min: minLength }))
16 | }
17 |
18 | // Check for uppercase letters
19 | const uppercaseMatches = value.match(/[A-Z]/g) || []
20 | if (uppercaseMatches.length < minUppercase) {
21 | errors.push(t('minUppercaseMessage', { min: minUppercase }))
22 | }
23 |
24 | // Check for numbers
25 | const numberMatches = value.match(/[0-9]/g) || []
26 | if (numberMatches.length < minNumbers) {
27 | errors.push(t('minNumberMessage', { min: minNumbers }))
28 | }
29 |
30 | // Check for symbols
31 | const symbolMatches = value.match(/[!@#$%^&*(),.?":{}|<>]/g) || []
32 | if (symbolMatches.length < minSymbols) {
33 | errors.push(t('minSymbolsMessage', { min: minSymbols }))
34 | }
35 |
36 | if (errors.length > 0) {
37 | return new Yup.ValidationError(t('passwordSchema') + ` \n${errors.join('\n')}.`, null, 'password')
38 | }
39 |
40 | // If no errors, return true
41 | return true
42 | })
43 |
44 | return {
45 | email: Yup.string().email(t('emailInvalid')).required(t('emailRequired')),
46 | // password: Yup.string().min(8, t('passwordLength')).required(t('passwordRequired')),
47 | password: passwordStrength(8, 1, 1, 1),
48 | confirmPassword: Yup.string()
49 | .oneOf([Yup.ref('password'), undefined], t('confirmPasswordMatch'))
50 | .required(t('confirmPasswordRequired')),
51 | firstName: Yup.string().required(t('firstNameRequired')),
52 | lastName: Yup.string().required(t('lastNameRequired')),
53 | registerToMailList: Yup.boolean().oneOf([true, false], 'mainListRequired'),
54 | }
55 | }
56 |
57 | export default useRegisterSchema
58 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
4 | presets: [require('nativewind/preset')],
5 | theme: {
6 | container: {
7 | center: true,
8 | },
9 | extend: {
10 | colors: {
11 | transparent: 'transparent',
12 | current: 'currentColor',
13 | background: '#F2F2F2',
14 | 'user-message-color': '#F2F9FF',
15 | green: '#08786B',
16 | 'green-bold': '#003C35',
17 | orange: '#F29B00',
18 | black: '#020202',
19 | },
20 | fontFamily: {
21 | roboto: ['Roboto'],
22 | },
23 | width: {
24 | 116: '460px',
25 | },
26 | height: {
27 | 116: '460px',
28 | },
29 | },
30 | },
31 | plugins: [],
32 | }
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "forceConsistentCasingInFileNames": true,
6 | "noFallthroughCasesInSwitch": true,
7 | "baseUrl": ".",
8 | "paths": {
9 | "@/*": ["src/*"]
10 | }
11 | },
12 | "include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildCommand": "expo export -p web",
3 | "outputDirectory": "dist",
4 | "devCommand": "expo",
5 | "cleanUrls": true,
6 | "framework": null,
7 | "rewrites": [
8 | {
9 | "source": "/:path*",
10 | "destination": "/"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------