This commit is contained in:
2024-10-31 23:43:20 +01:00
commit fe9dd37c82
70 changed files with 3746 additions and 0 deletions

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
/.expo
node_modules

7
.eslintrc.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
root: true,
extends: ['expo', 'eslint:recommended', 'universe/native'],
rules: {
'react-hooks/exhaustive-deps': 'warn',
},
}

89
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,89 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'
on:
push:
branches: ['main']
pull_request:
branches: ['main']
schedule:
- cron: '23 19 * * 0'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: '/language:${{matrix.language}}'

27
.github/workflows/eas-build.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: EAS Build
on:
workflow_dispatch:
# push:
# branches:
# - main
jobs:
build:
name: Install and build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18.x
cache: npm
- name: Setup Expo and EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: npm ci
- name: Build on EAS
run: eas build --platform all --non-interactive --no-wait

36
.github/workflows/eas-reviews.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: EAS Review
on:
workflow_dispatch:
# pull_request:
# branches:
# - main
jobs:
preview:
runs-on: ubuntu-latest
steps:
- name: 🏗 Setup repo
uses: actions/checkout@v4
- name: 🏗 Setup Node
uses: actions/setup-node@v4
with:
node-version: 18.x
cache: yarn
- name: 🏗 Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: 📦 Install dependencies
run: yarn install
- name: 🚀 Create preview
uses: expo/expo-github-action/preview@v8
with:
# `github.event.pull_request.head.ref` is only available on `pull_request` triggers.
# Use your own, or keep the automatically infered branch name from `--auto`, when using different triggers.
command: eas update --auto --branch ${{ github.event.pull_request.head.ref }}

32
.github/workflows/eas-update.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: EAS Update
on:
workflow_dispatch:
# push:
# branches:
# - main
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: 🏗 Setup repo
uses: actions/checkout@v4
- name: 🏗 Setup Node
uses: actions/setup-node@v4
with:
node-version: 18.x
cache: yarn
- name: 🏗 Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: 📦 Install dependencies
run: yarn install
- name: 🚀 Create update
run: eas update --auto --non-interactive

44
.github/workflows/eslint.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# ESLint is a tool for identifying and reporting on patterns
# found in ECMAScript/JavaScript code.
# More details at https://github.com/eslint/eslint
# and https://eslint.org
name: ESLint
on:
push:
branches: ['main']
pull_request:
# The branches below must be a subset of the branches above
branches: ['main']
jobs:
eslint:
name: Run eslint scanning
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: |
npm ci
npm install eslint
- name: Run ESLint
run: npx eslint .
--config .eslintrc.js
--ext .js,.jsx,.ts,.tsx

39
.github/workflows/prettier.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# Prettier is an opinionated code formatter.
# It enforces a consistent style by parsing your code and re-printing it with its own rules
# that take the maximum line length into account, wrapping code when necessary.
# More details at https://github.com/prettier/prettier
# and https://prttier.io
name: Prettier
on:
push:
branches: ['main']
pull_request:
# The branches below must be a subset of the branches above
branches: ['main']
jobs:
eslint:
name: Run prettier checking
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run Prettier check
run: npx prettier --check .

76
.gitignore vendored Normal file
View File

@@ -0,0 +1,76 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli
expo-env.d.ts
# @end expo-cli

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"semi": false,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "all",
"singleQuote": true,
"endOfLine": "auto"
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Yousef Abu Shanab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

105
README.md Normal file
View File

@@ -0,0 +1,105 @@
# expo-react-native-paper
[![EAS Build](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/eas-build.yml/badge.svg)](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/eas-build.yml)
[![EAS Review](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/eas-reviews.yml/badge.svg)](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/eas-reviews.yml)
[![EAS Update](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/eas-update.yml/badge.svg)](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/eas-update.yml)
[![CodeQL](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/codeql.yml/badge.svg)](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/codeql.yml)
[![ESLint](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/eslint.yml/badge.svg)](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/eslint.yml)
[![Prettier](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/prettier.yml/badge.svg)](https://github.com/youzarsiph/expo-react-native-paper/actions/workflows/prettier.yml)
This repository is a template for React Native Expo applications. It integrates Expo Router and React Native Paper. It also demonstrates how to use Github Actions for linting and formatting.
## Features
- Expo
- Expo Router
- Material Design V3
- Light & Dark modes with custom themes
- Cross Platform
- LTR and RTL support
- Multi lingual (`ar`, `en` and `tr`)
- CI/CD. For more info checkout this [page](https://github.com/expo/expo-github-action/tree/main)
## Platforms
- Web
- IOS
- Android
## Screenshots
![Home Screen (Tabs)](./screenshots/home-default-light.png)
![Profile Screen (Tabs)](./screenshots/profile-teal-dark.png)
![Settings Screen (Tabs)](./screenshots/settings-lime-light.png)
![Modal Screen (Stack)](./screenshots/modal-light-red.png)
![Search Screen (Stack)](./screenshots/search-orange-dark.png)
![Login Screen (Stack)](./screenshots/login-violet-light.png)
![Signup Screen (Stack)](./screenshots/signup-green-dark.png)
![Home Screen (Drawer)](./screenshots/home-blue-dark.png)
![Profile Screen (Drawer)](./screenshots/profile-olive-light.png)
![Settings Screen (Drawer)](./screenshots/settings-violet-light.png)
## Getting Started
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
### Prerequisites
- Node.js
- npm
### Installation
Clone the repo:
```bash
git clone https://github.com/youzarsiph/expo-react-native-paper.git
```
Open `package.json` and update the `name` field to match your app's name:
```jsonc
{
// Change the following line
"name": "expo-react-native-paper",
"main": "expo-router/entry",
"version": "1.0.0",
...
}
```
Install dependencies:
```bash
npm install
```
Run the app:
```bash
npm start
```
## Built With
- TypeScript
- React
- React Native
- Expo
- Expo Router
- React Native Paper
## Contributing
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
1. Fork the Project
2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
3. Commit your Changes (git commit -m 'Add some AmazingFeature')
4. Push to the Branch (git push origin feature/AmazingFeature)
5. Open a Pull Request
## License
Distributed under the MIT License. See LICENSE for more information.

43
app.json Normal file
View File

@@ -0,0 +1,43 @@
{
"expo": {
"name": "expo-react-native-paper",
"slug": "expo-react-native-paper",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
"expo-secure-store",
"expo-font",
"expo-localization"
],
"experiments": {
"typedRoutes": true
},
"extra": {
"supportsRTL": true
}
}
}

18
app/(auth)/_layout.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { Stack } from 'expo-router'
import Locales from '@/lib/locales'
import { StackHeader } from '@/lib/ui'
const Layout = () => (
<Stack
screenOptions={{
animation: 'slide_from_bottom',
header: (props) => <StackHeader navProps={props} children={undefined} />,
}}
>
<Stack.Screen name="login" options={{ title: Locales.t('login') }} />
<Stack.Screen name="signup" options={{ title: Locales.t('signup') }} />
</Stack>
)
export default Layout

107
app/(auth)/login.tsx Normal file
View File

@@ -0,0 +1,107 @@
import { Image } from 'expo-image'
import { router } from 'expo-router'
import { Formik } from 'formik'
import React from 'react'
import {
Button,
Surface,
TextInput,
HelperText,
Text,
} from 'react-native-paper'
import * as Yup from 'yup'
import { styles } from '@/lib/ui'
const Login = () => (
<Surface style={{ ...styles.screen, alignItems: undefined }}>
<Image
alt="Logo"
source={require('@/assets/images/icon.png')}
style={{
height: 150,
width: 150,
borderRadius: 16,
marginBottom: 32,
marginHorizontal: 'auto',
}}
/>
<Text variant="headlineLarge" style={{ textAlign: 'center' }}>
Welcome to ERNP
</Text>
<Text variant="bodyLarge" style={{ textAlign: 'center' }}>
We're excited to have you back. Please log in to continue.
</Text>
<Formik
initialValues={{ username: '', password: '' }}
onSubmit={(values) => console.log(values)}
validationSchema={Yup.object().shape({
username: Yup.string()
.min(3, 'Too Short!')
.max(64, 'Too Long!')
.required('Please enter a username.'),
password: Yup.string()
.min(8, 'Too Short! must be at least 8 characters.')
.max(64, 'Too Long!')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])/,
'Must 1 uppercase, 1 lowercase, 1 number and 1 special case character',
)
.required('Please enter a password'),
})}
>
{({ handleChange, handleBlur, handleSubmit, values, errors }) => (
<>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Username"
value={values.username}
error={!!errors.username}
onBlur={handleBlur('username')}
right={64 - values.username.length}
placeholder="Enter your username..."
onChangeText={handleChange('username')}
/>
<HelperText type="error" visible={!!errors.username}>
{errors.username}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Password"
value={values.password}
error={!!errors.password}
onBlur={handleBlur('password')}
right={64 - values.password.length}
placeholder="Enter your password..."
onChangeText={handleChange('password')}
/>
<HelperText type="error" visible={!!errors.password}>
{errors.password}
</HelperText>
</Surface>
<Button mode="contained" onPress={() => handleSubmit()}>
Login
</Button>
</>
)}
</Formik>
<Button
mode="contained-tonal"
onPress={() => router.push('/(auth)/signup')}
>
New here?
</Button>
</Surface>
)
export default Login

176
app/(auth)/signup.tsx Normal file
View File

@@ -0,0 +1,176 @@
import { Image } from 'expo-image'
import { router } from 'expo-router'
import { Formik } from 'formik'
import React from 'react'
import { ScrollView } from 'react-native'
import {
Button,
Surface,
TextInput,
HelperText,
Text,
} from 'react-native-paper'
import * as Yup from 'yup'
import { styles } from '@/lib/ui'
const SignUp = () => (
<ScrollView style={{ flex: 1 }}>
<Surface style={{ ...styles.screen, alignItems: undefined }}>
<Image
alt="Logo"
source={require('@/assets/images/icon.png')}
style={{
height: 150,
width: 150,
borderRadius: 16,
marginBottom: 32,
marginHorizontal: 'auto',
}}
/>
<Text variant="headlineLarge" style={{ textAlign: 'center' }}>
Join ERNP Today!
</Text>
<Text variant="bodyLarge" style={{ textAlign: 'center' }}>
We're thrilled to have you on board. Let's get you set up.
</Text>
<Formik
initialValues={{
username: '',
password: '',
email: '',
firstName: '',
lastName: '',
}}
onSubmit={(values) => console.log(values)}
validationSchema={Yup.object().shape({
username: Yup.string()
.min(3, 'Too Short!')
.max(64, 'Too Long!')
.required('Please enter a username.'),
password: Yup.string()
.min(8, 'Too Short! must be at least 8 characters.')
.max(64, 'Too Long!')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])/,
'Must 1 uppercase, 1 lowercase, 1 number and 1 special case character',
)
.required('Please enter a password'),
email: Yup.string().email().required('Please enter an email.'),
firstName: Yup.string()
.min(3, 'Too Short!')
.max(64, 'Too Long!')
.required('Please enter a first name.'),
lastName: Yup.string()
.min(3, 'Too Short!')
.max(64, 'Too Long!')
.required('Please enter a last name.'),
})}
>
{({ handleChange, handleBlur, handleSubmit, values, errors }) => (
<>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Username"
value={values.username}
error={!!errors.username}
onBlur={handleBlur('username')}
right={64 - values.username.length}
placeholder="Enter your username..."
onChangeText={handleChange('username')}
/>
<HelperText type="error" visible={!!errors.username}>
{errors.username}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Password"
value={values.password}
error={!!errors.password}
onBlur={handleBlur('password')}
right={64 - values.password.length}
placeholder="Enter your password..."
onChangeText={handleChange('password')}
/>
<HelperText type="error" visible={!!errors.password}>
{errors.password}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Email"
value={values.email}
error={!!errors.email}
onBlur={handleBlur('email')}
right={64 - values.email.length}
placeholder="Enter your email..."
onChangeText={handleChange('email')}
/>
<HelperText type="error" visible={!!errors.email}>
{errors.email}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="First name"
value={values.firstName}
error={!!errors.firstName}
onBlur={handleBlur('firstName')}
right={64 - values.firstName.length}
placeholder="Enter your first name..."
onChangeText={handleChange('firstName')}
/>
<HelperText type="error" visible={!!errors.firstName}>
{errors.firstName}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Last name"
value={values.lastName}
error={!!errors.lastName}
onBlur={handleBlur('lastName')}
right={64 - values.lastName.length}
placeholder="Enter your first name..."
onChangeText={handleChange('lastName')}
/>
<HelperText type="error" visible={!!errors.lastName}>
{errors.lastName}
</HelperText>
</Surface>
<Button mode="contained" onPress={() => handleSubmit()}>
Sign up
</Button>
</>
)}
</Formik>
<Button
mode="contained-tonal"
onPress={() => router.push('/(auth)/login')}
>
Already have an account?
</Button>
</Surface>
</ScrollView>
)
export default SignUp

126
app/(tabs)/_layout.tsx Normal file
View File

@@ -0,0 +1,126 @@
import { MaterialCommunityIcons } from '@expo/vector-icons'
import { Tabs, router } from 'expo-router'
import React from 'react'
import { Appbar, Menu, Tooltip } from 'react-native-paper'
import Locales from '@/lib/locales'
import { TabBar, TabsHeader } from '@/lib/ui'
const TabLayout = () => {
const [visible, setVisible] = React.useState(false)
return (
<Tabs
tabBar={(props) => <TabBar {...props} />}
screenOptions={{
tabBarHideOnKeyboard: true,
header: (props) => <TabsHeader navProps={props} children={undefined} />,
}}
>
<Tabs.Screen
name="index"
options={{
title: Locales.t('titleHome'),
headerRight: () => (
<>
<Tooltip title={Locales.t('search')}>
<Appbar.Action
icon="magnify"
onPress={() => router.push('/search')}
/>
</Tooltip>
<Menu
statusBarHeight={48}
visible={visible}
onDismiss={() => setVisible(false)}
anchor={
<Tooltip title={Locales.t('options')}>
<Appbar.Action
icon="dots-vertical"
onPress={() => setVisible(true)}
/>
</Tooltip>
}
>
<Menu.Item
title={Locales.t('titleSettings')}
leadingIcon="cog"
onPress={() => router.push('/(tabs)/settings')}
/>
<Menu.Item
title={Locales.t('stackNav')}
leadingIcon="card-multiple-outline"
onPress={() => router.push('/modal')}
/>
<Menu.Item
title={Locales.t('drawerNav')}
leadingIcon="gesture-swipe"
onPress={() => router.push('/drawer')}
/>
</Menu>
</>
),
tabBarIcon: (props) => (
<MaterialCommunityIcons
{...props}
size={24}
name={props.focused ? 'home' : 'home-outline'}
/>
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: Locales.t('profile'),
headerRight: () => (
<>
<Tooltip title={Locales.t('search')}>
<Appbar.Action
icon="magnify"
onPress={() => router.push('/search')}
/>
</Tooltip>
<Tooltip title={Locales.t('titleSettings')}>
<Appbar.Action
icon="cog"
onPress={() => router.push('/(tabs)/settings')}
/>
</Tooltip>
</>
),
tabBarIcon: (props) => (
<MaterialCommunityIcons
{...props}
size={24}
name={props.focused ? 'account' : 'account-outline'}
/>
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: Locales.t('titleSettings'),
headerRight: () => (
<Tooltip title={Locales.t('drawerNav')}>
<Appbar.Action
icon="gesture-swipe"
onPress={() => router.push('/drawer')}
/>
</Tooltip>
),
tabBarIcon: (props) => (
<MaterialCommunityIcons
{...props}
size={24}
name={props.focused ? 'cog' : 'cog-outline'}
/>
),
}}
/>
</Tabs>
)
}
export default TabLayout

13
app/(tabs)/index.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react'
import { Surface } from 'react-native-paper'
import Locales from '@/lib/locales'
import { ScreenInfo, styles } from '@/lib/ui'
const TabsHome = () => (
<Surface style={styles.screen}>
<ScreenInfo title={Locales.t('titleHome')} path="app/(tabs)/index.tsx" />
</Surface>
)
export default TabsHome

34
app/(tabs)/profile.tsx Normal file
View File

@@ -0,0 +1,34 @@
import { router } from 'expo-router'
import React from 'react'
import { Button, Surface } from 'react-native-paper'
import Locales from '@/lib/locales'
import { ScreenInfo, styles } from '@/lib/ui'
const Profile = () => (
<Surface style={styles.screen}>
<ScreenInfo title={Locales.t('profile')} path="app/(tabs)/profile.tsx" />
<Surface
elevation={0}
style={{
padding: 16,
gap: 16,
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
}}
>
<Button mode="contained" onPress={() => router.push('/(auth)/login')}>
Login
</Button>
<Button mode="contained" onPress={() => router.push('/(auth)/signup')}>
Sign Up
</Button>
</Surface>
</Surface>
)
export default Profile

310
app/(tabs)/settings.tsx Normal file
View File

@@ -0,0 +1,310 @@
import * as SecureStore from 'expo-secure-store'
import React from 'react'
import { Platform, useColorScheme } from 'react-native'
import {
Surface,
List,
Menu,
Button,
IconButton,
Snackbar,
Icon,
} from 'react-native-paper'
import Locales from '@/lib/locales'
import { Color, Language, Setting } from '@/lib/types'
import { Colors, LoadingIndicator, ScreenInfo, styles } from '@/lib/ui'
import { Languages } from '@/lib/utils'
const Settings = () => {
const colorScheme = useColorScheme()
const [loading, setLoading] = React.useState<boolean>(false)
const [message, setMessage] = React.useState({ visible: false, content: '' })
const [settings, setSettings] = React.useState<Setting>({
color: 'default',
language: 'auto',
theme: 'auto',
})
const [display, setDisplay] = React.useState({
color: false,
language: false,
theme: false,
})
React.useEffect(() => {
setLoading(true)
if (Platform.OS !== 'web') {
SecureStore.getItemAsync('settings')
.then((result) =>
setSettings(JSON.parse(result ?? JSON.stringify(settings))),
)
.catch((res) =>
setMessage({
visible: true,
content: res.message,
}),
)
}
setLoading(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const themeColors =
Colors[
settings.theme === 'auto' ? (colorScheme ?? 'light') : settings.theme
]
return (
<Surface style={{ flex: 1 }}>
{loading ? (
<LoadingIndicator />
) : (
<Surface elevation={0}>
<List.AccordionGroup>
<List.Accordion
id="1"
title={Locales.t('appearance')}
left={(props) => <List.Icon {...props} icon="palette" />}
>
<List.Item
title={Locales.t('language')}
description={Locales.t('changeLanguage')}
left={(props) => <List.Icon {...props} icon="translate" />}
right={(props) => (
<Menu
visible={display.language}
onDismiss={() =>
setDisplay({ ...display, language: false })
}
anchor={
<IconButton
{...props}
icon="pencil"
onPress={() =>
setDisplay({ ...display, language: true })
}
/>
}
>
<Menu.Item
title="System"
trailingIcon={
settings.language === 'auto' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, language: 'auto' })
setDisplay({ ...display, language: false })
}}
/>
{Object.entries(Languages).map((lang) => (
<Menu.Item
key={lang[0]}
title={`${lang[1].name} / ${lang[1].nativeName}`}
trailingIcon={
settings.language === lang[0] ? 'check' : undefined
}
onPress={() => {
setSettings({
...settings,
language: lang[0] as Language,
})
setDisplay({ ...display, language: false })
}}
/>
))}
</Menu>
)}
/>
<List.Item
title={Locales.t('mode')}
description={Locales.t('changeMode')}
left={(props) => (
<List.Icon
{...props}
icon={
settings.theme === 'auto'
? 'theme-light-dark'
: settings.theme === 'light'
? 'weather-sunny'
: 'weather-night'
}
/>
)}
right={(props) => (
<Menu
visible={display.theme}
onDismiss={() => setDisplay({ ...display, theme: false })}
anchor={
<IconButton
{...props}
icon="pencil"
onPress={() => setDisplay({ ...display, theme: true })}
/>
}
>
<Menu.Item
title={Locales.t('system')}
leadingIcon="theme-light-dark"
trailingIcon={
settings.theme === 'auto' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, theme: 'auto' })
setDisplay({ ...display, theme: false })
}}
/>
<Menu.Item
title={Locales.t('lightMode')}
leadingIcon="weather-sunny"
trailingIcon={
settings.theme === 'light' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, theme: 'light' })
setDisplay({ ...display, theme: false })
}}
/>
<Menu.Item
title={Locales.t('darkMode')}
leadingIcon="weather-night"
trailingIcon={
settings.theme === 'dark' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, theme: 'dark' })
setDisplay({ ...display, theme: false })
}}
/>
</Menu>
)}
/>
<List.Item
title={Locales.t('color')}
description={Locales.t('changeColor')}
left={(props) => (
<List.Icon
{...props}
icon="palette-swatch-variant"
color={
Colors[
settings.theme === 'auto'
? (colorScheme ?? 'light')
: settings.theme
][settings.color]?.primary
}
/>
)}
right={(props) => (
<Menu
visible={display.color}
onDismiss={() => setDisplay({ ...display, color: false })}
anchor={
<IconButton
{...props}
icon="pencil"
onPress={() => setDisplay({ ...display, color: true })}
/>
}
>
{Object.keys(Colors.light).map((color) => (
<Surface
key={color}
elevation={0}
style={{
width: '100%',
flexDirection: 'row',
alignItems: 'center',
}}
>
<Surface
elevation={0}
style={{
padding: 4,
marginLeft: 8,
borderRadius: 16,
backgroundColor:
color !== settings.color
? undefined
: themeColors[color]?.primary,
}}
>
<Icon
size={24}
source="palette"
color={
color !== settings.color
? themeColors[color as Color]?.primary
: themeColors[color].onPrimary
}
/>
</Surface>
<Menu.Item
key={color}
title={Locales.t(color)}
onPress={() => {
setSettings({
...settings,
color: color as Color,
})
setDisplay({ ...display, color: false })
}}
/>
</Surface>
))}
</Menu>
)}
/>
</List.Accordion>
</List.AccordionGroup>
</Surface>
)}
<Surface elevation={0} style={styles.screen}>
<ScreenInfo
title={Locales.t('titleSettings')}
path="app/(tabs)/settings.tsx"
/>
</Surface>
<Button
mode="contained"
style={{ margin: 16 }}
onPress={() =>
Platform.OS !== 'web'
? SecureStore.setItemAsync('settings', JSON.stringify(settings))
.then(() =>
setMessage({
visible: true,
content: Locales.t('restartApp'),
}),
)
.catch((res) =>
setMessage({
visible: true,
content: res.message,
}),
)
: setMessage({
visible: true,
content: Locales.t('notAvailable'),
})
}
>
{Locales.t('save')}
</Button>
<Snackbar
visible={message.visible}
onDismiss={() => setMessage({ ...message, visible: false })}
onIconPress={() => setMessage({ ...message, visible: false })}
>
{message.content}
</Snackbar>
</Surface>
)
}
export default Settings

42
app/+html.tsx Normal file
View File

@@ -0,0 +1,42 @@
import { ScrollViewStyleReset } from 'expo-router/html'
import React from 'react'
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
)
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`

22
app/+not-found.tsx Normal file
View File

@@ -0,0 +1,22 @@
import { Link, Stack } from 'expo-router'
import React from 'react'
import { Surface, Text } from 'react-native-paper'
import Locales from '@/lib/locales'
import { styles } from '@/lib/ui'
const NotFound = () => (
<Surface style={styles.screen}>
<Stack.Screen options={{ title: Locales.t('titleNotFound') }} />
<Text variant="displayLarge">{Locales.t('titleNotFound')}</Text>
<Text variant="bodyLarge">{Locales.t('screen404')}</Text>
<Link href="/">
<Text variant="bodyLarge">{Locales.t('goHome')}</Text>
</Link>
</Surface>
)
export default NotFound

122
app/_layout.tsx Normal file
View File

@@ -0,0 +1,122 @@
import { MaterialCommunityIcons } from '@expo/vector-icons'
import {
useFonts,
JetBrainsMono_400Regular,
} from '@expo-google-fonts/jetbrains-mono'
import { NotoSans_400Regular } from '@expo-google-fonts/noto-sans'
import * as Localization from 'expo-localization'
import { SplashScreen, Stack } from 'expo-router'
import * as SecureStore from 'expo-secure-store'
import React from 'react'
import { Platform, useColorScheme } from 'react-native'
import { PaperProvider } from 'react-native-paper'
import Locales from '@/lib/locales'
import { Setting } from '@/lib/types'
import { StackHeader, Themes } from '@/lib/ui'
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router'
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(tabs)',
}
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync()
const RootLayout = () => {
const [loaded, error] = useFonts({
NotoSans_400Regular,
JetBrainsMono_400Regular,
...MaterialCommunityIcons.font,
})
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
React.useEffect(() => {
if (error) throw error
}, [error])
React.useEffect(() => {
if (loaded) {
SplashScreen.hideAsync()
}
}, [loaded])
if (!loaded) {
return null
}
return <RootLayoutNav />
}
const RootLayoutNav = () => {
const colorScheme = useColorScheme()
const [settings, setSettings] = React.useState<Setting>({
theme: 'auto',
color: 'default',
language: 'auto',
})
// Load settings from the device
React.useEffect(() => {
if (Platform.OS !== 'web') {
SecureStore.getItemAsync('settings').then((result) => {
if (result === null) {
SecureStore.setItemAsync('settings', JSON.stringify(settings)).then(
(res) => console.log(res),
)
}
setSettings(JSON.parse(result ?? JSON.stringify(settings)))
})
} else {
setSettings({ ...settings, theme: colorScheme ?? 'light' })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
React.useEffect(() => {
if (settings.language === 'auto') {
Locales.locale = Localization.getLocales()[0].languageCode ?? 'en'
} else {
Locales.locale = settings.language
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<PaperProvider
theme={
Themes[
settings.theme === 'auto' ? (colorScheme ?? 'dark') : settings.theme
][settings.color]
}
>
<Stack
screenOptions={{
animation: 'slide_from_bottom',
header: (props) => (
<StackHeader navProps={props} children={undefined} />
),
}}
>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="drawer" options={{ headerShown: false }} />
<Stack.Screen name="search" options={{ title: Locales.t('search') }} />
<Stack.Screen
name="modal"
options={{ title: Locales.t('titleModal'), presentation: 'modal' }}
/>
</Stack>
</PaperProvider>
)
}
export default RootLayout

124
app/drawer/_layout.tsx Normal file
View File

@@ -0,0 +1,124 @@
import { router } from 'expo-router'
import { Drawer } from 'expo-router/drawer'
import React from 'react'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { Appbar, Menu, Tooltip, useTheme } from 'react-native-paper'
import Locales from '@/lib/locales'
import { DrawerContent, DrawerHeader } from '@/lib/ui'
const DrawerLayout = () => {
const theme = useTheme()
const [visible, setVisible] = React.useState(false)
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Drawer
drawerContent={(props) => (
<DrawerContent
navProps={props}
showDivider={false}
children={undefined}
title="Drawer Navigation"
/>
)}
screenOptions={{
drawerStyle: {
backgroundColor: theme.colors.background,
paddingTop: 32,
},
header: (props) => (
<DrawerHeader navProps={props} children={undefined} />
),
}}
>
<Drawer.Screen
name="index"
options={{
drawerLabel: Locales.t('titleHome'),
title: Locales.t('titleHome'),
headerRight: () => (
<>
<Tooltip title={Locales.t('search')}>
<Appbar.Action
icon="magnify"
onPress={() => router.push('/search')}
/>
</Tooltip>
<Menu
statusBarHeight={48}
visible={visible}
onDismiss={() => setVisible(false)}
anchor={
<Tooltip title={Locales.t('options')}>
<Appbar.Action
icon="dots-vertical"
onPress={() => setVisible(true)}
/>
</Tooltip>
}
>
<Menu.Item
title={Locales.t('titleSettings')}
leadingIcon="cog"
onPress={() => router.push('/drawer/settings')}
/>
<Menu.Item
title={Locales.t('stackNav')}
leadingIcon="card-multiple-outline"
onPress={() => router.push('/modal')}
/>
<Menu.Item
title={Locales.t('drawerNav')}
leadingIcon="gesture-swipe"
onPress={() => router.push('/drawer')}
/>
</Menu>
</>
),
}}
/>
<Drawer.Screen
name="profile"
options={{
drawerLabel: Locales.t('profile'),
title: Locales.t('profile'),
headerRight: () => (
<>
<Tooltip title={Locales.t('search')}>
<Appbar.Action
icon="magnify"
onPress={() => router.push('/search')}
/>
</Tooltip>
<Tooltip title={Locales.t('titleSettings')}>
<Appbar.Action
icon="cog"
onPress={() => router.push('/(tabs)/settings')}
/>
</Tooltip>
</>
),
}}
/>
<Drawer.Screen
name="settings"
options={{
drawerLabel: Locales.t('titleSettings'),
title: Locales.t('titleSettings'),
headerRight: () => (
<Tooltip title={Locales.t('stackNav')}>
<Appbar.Action
icon="card-multiple-outline"
onPress={() => router.push('/modal')}
/>
</Tooltip>
),
}}
/>
</Drawer>
</GestureHandlerRootView>
)
}
export default DrawerLayout

13
app/drawer/index.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react'
import { Surface } from 'react-native-paper'
import Locales from '@/lib/locales'
import { ScreenInfo, styles } from '@/lib/ui'
const DrawerHome = () => (
<Surface style={styles.screen}>
<ScreenInfo title={Locales.t('titleHome')} path="app/drawer/index.tsx" />
</Surface>
)
export default DrawerHome

34
app/drawer/profile.tsx Normal file
View File

@@ -0,0 +1,34 @@
import { router } from 'expo-router'
import React from 'react'
import { Button, Surface } from 'react-native-paper'
import Locales from '@/lib/locales'
import { ScreenInfo, styles } from '@/lib/ui'
const Profile = () => (
<Surface style={styles.screen}>
<ScreenInfo title={Locales.t('profile')} path="app/(tabs)/profile.tsx" />
<Surface
elevation={0}
style={{
padding: 16,
gap: 16,
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
}}
>
<Button mode="contained" onPress={() => router.push('/(auth)/login')}>
Login
</Button>
<Button mode="contained" onPress={() => router.push('/(auth)/signup')}>
Sign Up
</Button>
</Surface>
</Surface>
)
export default Profile

310
app/drawer/settings.tsx Normal file
View File

@@ -0,0 +1,310 @@
import * as SecureStore from 'expo-secure-store'
import React from 'react'
import { Platform, useColorScheme } from 'react-native'
import {
Surface,
List,
Menu,
Button,
IconButton,
Snackbar,
Icon,
} from 'react-native-paper'
import Locales from '@/lib/locales'
import { Color, Language, Setting } from '@/lib/types'
import { Colors, LoadingIndicator, ScreenInfo, styles } from '@/lib/ui'
import { Languages } from '@/lib/utils'
const Settings = () => {
const colorScheme = useColorScheme()
const [loading, setLoading] = React.useState<boolean>(false)
const [message, setMessage] = React.useState({ visible: false, content: '' })
const [settings, setSettings] = React.useState<Setting>({
color: 'default',
language: 'auto',
theme: 'auto',
})
const [display, setDisplay] = React.useState({
color: false,
language: false,
theme: false,
})
React.useEffect(() => {
setLoading(true)
if (Platform.OS !== 'web') {
SecureStore.getItemAsync('settings')
.then((result) =>
setSettings(JSON.parse(result ?? JSON.stringify(settings))),
)
.catch((res) =>
setMessage({
visible: true,
content: res.message,
}),
)
}
setLoading(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const themeColors =
Colors[
settings.theme === 'auto' ? (colorScheme ?? 'light') : settings.theme
]
return (
<Surface style={{ flex: 1 }}>
{loading ? (
<LoadingIndicator />
) : (
<Surface elevation={0}>
<List.AccordionGroup>
<List.Accordion
id="1"
title={Locales.t('appearance')}
left={(props) => <List.Icon {...props} icon="palette" />}
>
<List.Item
title={Locales.t('language')}
description={Locales.t('changeLanguage')}
left={(props) => <List.Icon {...props} icon="translate" />}
right={(props) => (
<Menu
visible={display.language}
onDismiss={() =>
setDisplay({ ...display, language: false })
}
anchor={
<IconButton
{...props}
icon="pencil"
onPress={() =>
setDisplay({ ...display, language: true })
}
/>
}
>
<Menu.Item
title="System"
trailingIcon={
settings.language === 'auto' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, language: 'auto' })
setDisplay({ ...display, language: false })
}}
/>
{Object.entries(Languages).map((lang) => (
<Menu.Item
key={lang[0]}
title={`${lang[1].name} / ${lang[1].nativeName}`}
trailingIcon={
settings.language === lang[0] ? 'check' : undefined
}
onPress={() => {
setSettings({
...settings,
language: lang[0] as Language,
})
setDisplay({ ...display, language: false })
}}
/>
))}
</Menu>
)}
/>
<List.Item
title={Locales.t('mode')}
description={Locales.t('changeMode')}
left={(props) => (
<List.Icon
{...props}
icon={
settings.theme === 'auto'
? 'theme-light-dark'
: settings.theme === 'light'
? 'weather-sunny'
: 'weather-night'
}
/>
)}
right={(props) => (
<Menu
visible={display.theme}
onDismiss={() => setDisplay({ ...display, theme: false })}
anchor={
<IconButton
{...props}
icon="pencil"
onPress={() => setDisplay({ ...display, theme: true })}
/>
}
>
<Menu.Item
title={Locales.t('system')}
leadingIcon="theme-light-dark"
trailingIcon={
settings.theme === 'auto' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, theme: 'auto' })
setDisplay({ ...display, theme: false })
}}
/>
<Menu.Item
title={Locales.t('lightMode')}
leadingIcon="weather-sunny"
trailingIcon={
settings.theme === 'light' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, theme: 'light' })
setDisplay({ ...display, theme: false })
}}
/>
<Menu.Item
title={Locales.t('darkMode')}
leadingIcon="weather-night"
trailingIcon={
settings.theme === 'dark' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, theme: 'dark' })
setDisplay({ ...display, theme: false })
}}
/>
</Menu>
)}
/>
<List.Item
title={Locales.t('color')}
description={Locales.t('changeColor')}
left={(props) => (
<List.Icon
{...props}
icon="palette-swatch-variant"
color={
Colors[
settings.theme === 'auto'
? (colorScheme ?? 'light')
: settings.theme
][settings.color]?.primary
}
/>
)}
right={(props) => (
<Menu
visible={display.color}
onDismiss={() => setDisplay({ ...display, color: false })}
anchor={
<IconButton
{...props}
icon="pencil"
onPress={() => setDisplay({ ...display, color: true })}
/>
}
>
{Object.keys(Colors.light).map((color) => (
<Surface
key={color}
elevation={0}
style={{
width: '100%',
flexDirection: 'row',
alignItems: 'center',
}}
>
<Surface
elevation={0}
style={{
padding: 4,
marginLeft: 8,
borderRadius: 16,
backgroundColor:
color !== settings.color
? undefined
: themeColors[color]?.primary,
}}
>
<Icon
size={24}
source="palette"
color={
color !== settings.color
? themeColors[color as Color]?.primary
: themeColors[color].onPrimary
}
/>
</Surface>
<Menu.Item
key={color}
title={Locales.t(color)}
onPress={() => {
setSettings({
...settings,
color: color as Color,
})
setDisplay({ ...display, color: false })
}}
/>
</Surface>
))}
</Menu>
)}
/>
</List.Accordion>
</List.AccordionGroup>
</Surface>
)}
<Surface elevation={0} style={styles.screen}>
<ScreenInfo
title={Locales.t('titleSettings')}
path="app/(tabs)/settings.tsx"
/>
</Surface>
<Button
mode="contained"
style={{ margin: 16 }}
onPress={() =>
Platform.OS !== 'web'
? SecureStore.setItemAsync('settings', JSON.stringify(settings))
.then(() =>
setMessage({
visible: true,
content: Locales.t('restartApp'),
}),
)
.catch((res) =>
setMessage({
visible: true,
content: res.message,
}),
)
: setMessage({
visible: true,
content: Locales.t('notAvailable'),
})
}
>
{Locales.t('save')}
</Button>
<Snackbar
visible={message.visible}
onDismiss={() => setMessage({ ...message, visible: false })}
onIconPress={() => setMessage({ ...message, visible: false })}
>
{message.content}
</Snackbar>
</Surface>
)
}
export default Settings

18
app/modal.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { StatusBar } from 'expo-status-bar'
import React from 'react'
import { Platform } from 'react-native'
import { Surface } from 'react-native-paper'
import Locales from '@/lib/locales'
import { ScreenInfo, styles } from '@/lib/ui'
const Modal = () => (
<Surface style={styles.screen}>
<ScreenInfo title={Locales.t('titleModal')} path="app/modal.tsx" />
{/* Use a light status bar on iOS to account for the black space above the modal */}
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
</Surface>
)
export default Modal

39
app/search.tsx Normal file
View File

@@ -0,0 +1,39 @@
import React from 'react'
import { Searchbar, Surface } from 'react-native-paper'
import Locales from '@/lib/locales'
import { ScreenInfo, styles } from '@/lib/ui'
const Search = () => {
const [query, setQuery] = React.useState('')
const [loading, setLoading] = React.useState(false)
// Search logic
React.useEffect(() => {
if (query !== '') {
setLoading(true)
}
setTimeout(() => {
setLoading(false)
}, 1000)
}, [query])
return (
<Surface style={{ flex: 1, gap: 16 }}>
<Searchbar
value={query}
loading={loading}
onChangeText={(v) => setQuery(v)}
placeholder="Type here to search..."
style={{ marginTop: 16, marginHorizontal: 16 }}
/>
<Surface style={styles.screen}>
<ScreenInfo title={Locales.t('search')} path="app/search.tsx" />
</Surface>
</Surface>
)
}
export default Search

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

6
babel.config.ts Normal file
View File

@@ -0,0 +1,6 @@
module.exports = function (api: { cache: (arg0: boolean) => void }) {
api.cache(true)
return {
presets: ['babel-preset-expo'],
}
}

BIN
bun.lockb Executable file

Binary file not shown.

50
lib/locales/ar.ts Normal file
View File

@@ -0,0 +1,50 @@
/**
* Arabic Translations
*/
const Arabic = {
login: 'تسجيل الدخول',
signup: 'إنشاء حساب',
profile: 'الملف الشخصي',
options: 'الخيارات',
search: 'البحث',
stackNav: 'التنقل المكدس',
drawerNav: 'التنقل الدرج',
appearance: 'المظهر',
language: 'اللغة',
changeLanguage: 'تغيير لغة التطبيق',
system: 'النظام',
mode: 'الوضع',
changeMode: 'التبديل بين الوضع الفاتح والوضع الداكن',
lightMode: 'فاتح',
darkMode: 'داكن',
color: 'اللون',
changeColor: 'تغيير لون السمة',
changeScreenCode:
'قم بتغيير أي نص، احفظ الملف، وسيتم تحديث التطبيق الخاص بك تلقائيًا',
goHome: 'الذهاب إلى الشاشة الرئيسية',
openScreenCode: 'افتح الكود لهذه الشاشة',
save: 'حفظ',
screen404: 'هذه الشاشة غير موجودة',
titleHome: 'الرئيسية',
titleModal: 'مشروط',
titleNotFound: 'لم يتم العثور',
titleSettings: 'الإعدادات',
restartApp: 'أعد تشغيل التطبيق لتطبيق التغييرات',
notAvailable: 'Expo SecureStore غير متوفر للويب',
adaptive: 'تلقائي',
default: 'افتراضي',
orange: 'برتقالي',
red: 'أحمر',
violet: 'بنفسجي',
indigo: 'أزرق داكن',
blue: 'أزرق',
teal: 'أزرق فاتح',
cyan: 'سماوي',
green: 'أخضر',
lime: 'ليموني',
olive: 'زيتوني',
brown: 'بني',
}
export default Arabic

50
lib/locales/en.ts Normal file
View File

@@ -0,0 +1,50 @@
/**
* English Translations
*/
const English = {
login: 'Log in',
signup: 'Sign up',
profile: 'Profile',
options: 'Options',
search: 'Search',
stackNav: 'Stack Navigation',
drawerNav: 'Drawer Navigation',
appearance: 'Appearance',
language: 'Language',
changeLanguage: "Change app's language",
system: 'System',
mode: 'Mode',
changeMode: 'Switch between light and dark mode',
lightMode: 'Light',
darkMode: 'Dark',
color: 'Color',
changeColor: 'Change theme color',
changeScreenCode:
'Change any of the text, save the file, and your app will automatically update',
goHome: 'Go to home screen',
openScreenCode: 'Open up the code for this screen',
save: 'Save',
screen404: "This screen doesn't exist",
titleHome: 'Home',
titleModal: 'Modal',
titleNotFound: 'Not Found',
titleSettings: 'Settings',
restartApp: 'Restart the app to apply changes',
notAvailable: 'Expo SecureStore is not available for web',
adaptive: 'adaptive',
default: 'default',
orange: 'orange',
red: 'red',
violet: 'violet',
indigo: 'indigo',
blue: 'blue',
teal: 'teal',
cyan: 'cyan',
green: 'green',
lime: 'lime',
olive: 'olive',
brown: 'brown',
}
export default English

19
lib/locales/index.ts Normal file
View File

@@ -0,0 +1,19 @@
/**
* Locales
*/
import { I18n } from 'i18n-js'
import Arabic from '@/lib/locales/ar'
import English from '@/lib/locales/en'
import Turkish from '@/lib/locales/tr'
const Locales = new I18n({
ar: Arabic,
en: English,
tr: Turkish,
})
Locales.enableFallback = true
export default Locales

48
lib/locales/tr.ts Normal file
View File

@@ -0,0 +1,48 @@
/**
* Turkish Translations
*/
const Turkish = {
login: 'Giriş',
signup: 'Hesap aç',
profile: 'Profil',
search: 'Ara',
drawerNav: 'Çekmece Navigasyon',
appearance: 'Görünüm',
language: 'Dil',
changeLanguage: 'Uygulamanın dilini değiştir',
system: 'Sistem',
mode: 'Mod',
changeMode: 'Aydınlık ve karanlık mod arasında geçiş yap',
lightMode: 'Aydınlık',
darkMode: 'Karanlık',
color: 'Renk',
changeColor: 'Tema rengini değiştir',
changeScreenCode:
'Herhangi bir metni değiştirin, dosyayı kaydedin ve uygulamanız otomatik olarak güncellenecektir',
goHome: 'Ana ekrana git',
openScreenCode: 'Bu ekranın kodunu aç',
save: 'Kaydet',
screen404: 'Bu ekran mevcut değil',
titleHome: 'Ana Sayfa',
titleModal: 'Modal',
titleNotFound: 'Bulunamadı',
titleSettings: 'Ayarlar',
restartApp: 'Değişiklikleri uygulamak için uygulamayı yeniden başlatın',
notAvailable: 'Expo SecureStore web için kullanılabilir değil',
adaptive: 'adaptive',
default: 'varsayılan',
orange: 'turuncu',
red: 'kırmızı',
violet: 'mor',
indigo: 'lacivert',
blue: 'mavi',
teal: 'turkuaz',
cyan: 'gökyüzü mavi',
green: 'yeşil',
lime: 'limon yeşili',
olive: 'zeytin yeşili',
brown: 'kahverengi',
}
export default Turkish

5
lib/types/Color.ts Normal file
View File

@@ -0,0 +1,5 @@
import { Colors } from '@/lib/ui'
type Color = keyof typeof Colors.light
export default Color

5
lib/types/Language.ts Normal file
View File

@@ -0,0 +1,5 @@
import { Languages } from '@/lib/utils'
type Language = keyof typeof Languages
export default Language

9
lib/types/Setting.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Color, Language } from '@/lib/types'
type Setting = {
color: Color
theme: 'light' | 'dark' | 'auto'
language: Language | 'auto'
}
export default Setting

9
lib/types/index.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* Types
*/
import Color from '@/lib/types/Color'
import Language from '@/lib/types/Language'
import Setting from '@/lib/types/Setting'
export type { Color, Language, Setting }

View File

@@ -0,0 +1,40 @@
import { DrawerContentComponentProps } from '@react-navigation/drawer'
import { router } from 'expo-router'
import React from 'react'
import { Drawer, DrawerSectionProps } from 'react-native-paper'
import Locales from '@/lib/locales'
interface DrawerContentProps extends DrawerSectionProps {
navProps: DrawerContentComponentProps
}
const DrawerContent = (props: DrawerContentProps) => (
<Drawer.Section {...props}>
<Drawer.Item
label={Locales.t('goHome')}
icon="arrow-left"
onPress={() => router.replace('/')}
/>
<Drawer.Item
label={Locales.t('titleHome')}
icon="home"
active={props.navProps.state.index === 0}
onPress={() => router.push('/drawer')}
/>
<Drawer.Item
label={Locales.t('profile')}
icon="account"
active={props.navProps.state.index === 1}
onPress={() => router.push('/drawer/profile')}
/>
<Drawer.Item
label={Locales.t('titleSettings')}
icon="cog"
active={props.navProps.state.index === 2}
onPress={() => router.push('/drawer/settings')}
/>
</Drawer.Section>
)
export default DrawerContent

View File

@@ -0,0 +1,27 @@
import { DrawerHeaderProps as BaseProps } from '@react-navigation/drawer'
import { getHeaderTitle } from '@react-navigation/elements'
import React from 'react'
import { Appbar, AppbarProps } from 'react-native-paper'
interface DrawerHeaderProps extends AppbarProps {
navProps: BaseProps
}
const DrawerHeader = (props: DrawerHeaderProps) => (
<Appbar.Header {...props}>
<Appbar.Action
icon="menu"
onPress={() => props.navProps.navigation.openDrawer()}
/>
<Appbar.Content
title={getHeaderTitle(props.navProps.options, props.navProps.route.name)}
/>
{props.navProps.options.headerRight
? props.navProps.options.headerRight({})
: undefined}
</Appbar.Header>
)
export default DrawerHeader

View File

@@ -0,0 +1,30 @@
import { Canvas, LinearGradient, Rect, vec } from '@shopify/react-native-skia'
import { Platform, useWindowDimensions } from 'react-native'
import { useTheme } from 'react-native-paper'
const GradientBackground = (props: { height?: 'full' }) => {
const theme = useTheme()
const { height, width } = useWindowDimensions()
return Platform.OS !== 'web' ? (
<Canvas
style={{
left: 0,
right: 0,
position: 'absolute',
height: props.height ? height : 300,
width,
}}
>
<Rect x={0} y={0} width={width} height={props.height ? height : 300}>
<LinearGradient
start={vec(0, 0)}
end={vec(width, width)}
colors={[theme.colors.primary, theme.colors.inversePrimary]}
/>
</Rect>
</Canvas>
) : undefined
}
export default GradientBackground

View File

@@ -0,0 +1,13 @@
import React from 'react'
import { ActivityIndicator, Surface } from 'react-native-paper'
const LoadingIndicator = () => (
<Surface
elevation={0}
style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}
>
<ActivityIndicator />
</Surface>
)
export default LoadingIndicator

View File

@@ -0,0 +1,24 @@
import { Chip, Text } from 'react-native-paper'
import Locales from '@/lib/locales'
import GradientBackground from '@/lib/ui/components/GradientBackground'
const ScreenInfo = (props: { title: string; path: string }) => (
<>
<GradientBackground />
<Text variant="displaySmall">{props.title}</Text>
<Text variant="bodyLarge">{Locales.t('openScreenCode')}</Text>
<Chip textStyle={{ fontFamily: 'JetBrainsMono_400Regular' }}>
{props.path}
</Chip>
<Text variant="bodyLarge" style={{ textAlign: 'center' }}>
{Locales.t('changeScreenCode')}
</Text>
</>
)
export default ScreenInfo

View File

@@ -0,0 +1,34 @@
import { getHeaderTitle } from '@react-navigation/elements'
import { NativeStackHeaderProps } from '@react-navigation/native-stack'
import React from 'react'
import { Appbar, AppbarProps } from 'react-native-paper'
interface StackHeaderProps extends AppbarProps {
navProps: NativeStackHeaderProps
}
const StackHeader = (props: StackHeaderProps) => (
<Appbar.Header {...props}>
{props.navProps.options.headerLeft
? props.navProps.options.headerLeft({
canGoBack: props.navProps.navigation.canGoBack(),
})
: undefined}
{props.navProps.back ? (
<Appbar.BackAction onPress={props.navProps.navigation.goBack} />
) : null}
<Appbar.Content
title={getHeaderTitle(props.navProps.options, props.navProps.route.name)}
/>
{props.navProps.options.headerRight
? props.navProps.options.headerRight({
canGoBack: props.navProps.navigation.canGoBack(),
})
: undefined}
</Appbar.Header>
)
export default StackHeader

View File

@@ -0,0 +1,49 @@
import { BottomTabBarProps } from '@react-navigation/bottom-tabs'
import { CommonActions } from '@react-navigation/native'
import React from 'react'
import { BottomNavigation } from 'react-native-paper'
const TabBar = (props: BottomTabBarProps) => (
<BottomNavigation.Bar
shifting
navigationState={props.state}
safeAreaInsets={props.insets}
onTabPress={({ route, preventDefault }) => {
const event = props.navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
})
if (event.defaultPrevented) {
preventDefault()
} else {
props.navigation.dispatch({
...CommonActions.navigate(route.name, route.params),
target: props.state.key,
})
}
}}
renderIcon={({ route, focused, color }) => {
const { options } = props.descriptors[route.key]
if (options.tabBarIcon) {
return options.tabBarIcon({ focused, color, size: 24 })
}
return null
}}
getLabelText={({ route }) => {
const { options } = props.descriptors[route.key]
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.title
return label
}}
/>
)
export default TabBar

View File

@@ -0,0 +1,26 @@
import { BottomTabHeaderProps } from '@react-navigation/bottom-tabs'
import { getHeaderTitle } from '@react-navigation/elements'
import React from 'react'
import { Appbar, AppbarProps } from 'react-native-paper'
interface TabsHeaderProps extends AppbarProps {
navProps: BottomTabHeaderProps
}
const TabsHeader = (props: TabsHeaderProps) => (
<Appbar.Header {...props}>
{props.navProps.options.headerLeft
? props.navProps.options.headerLeft({})
: undefined}
<Appbar.Content
title={getHeaderTitle(props.navProps.options, props.navProps.route.name)}
/>
{props.navProps.options.headerRight
? props.navProps.options.headerRight({})
: undefined}
</Appbar.Header>
)
export default TabsHeader

View File

@@ -0,0 +1,23 @@
/**
* Components
*/
import DrawerContent from '@/lib/ui/components/DrawerContent'
import DrawerHeader from '@/lib/ui/components/DrawerHeader'
import GradientBackground from '@/lib/ui/components/GradientBackground'
import LoadingIndicator from '@/lib/ui/components/LoadingIndicator'
import ScreenInfo from '@/lib/ui/components/ScreenInfo'
import StackHeader from '@/lib/ui/components/StackHeader'
import TabBar from '@/lib/ui/components/TabBar'
import TabsHeader from '@/lib/ui/components/TabsHeader'
export {
DrawerContent,
DrawerHeader,
GradientBackground,
LoadingIndicator,
ScreenInfo,
StackHeader,
TabBar,
TabsHeader,
}

6
lib/ui/index.tsx Normal file
View File

@@ -0,0 +1,6 @@
/**
* UI
*/
export * from '@/lib/ui/components'
export * from '@/lib/ui/styles'

902
lib/ui/styles/colors.ts Normal file
View File

@@ -0,0 +1,902 @@
/**
* Custom colors for using with themes
*/
import { MD3DarkTheme, MD3LightTheme } from 'react-native-paper'
const Colors = {
light: {
default: {
primary: MD3LightTheme.colors.primary,
onPrimary: MD3LightTheme.colors.onPrimary,
},
orange: {
primary: 'rgb(176, 46, 0)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(255, 219, 209)',
onPrimaryContainer: 'rgb(59, 9, 0)',
secondary: 'rgb(119, 87, 78)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(255, 219, 209)',
onSecondaryContainer: 'rgb(44, 21, 15)',
tertiary: 'rgb(108, 93, 47)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(246, 225, 166)',
onTertiaryContainer: 'rgb(35, 27, 0)',
background: 'rgb(255, 251, 255)',
onBackground: 'rgb(32, 26, 24)',
surface: 'rgb(255, 251, 255)',
onSurface: 'rgb(32, 26, 24)',
surfaceVariant: 'rgb(245, 222, 216)',
onSurfaceVariant: 'rgb(83, 67, 63)',
outline: 'rgb(133, 115, 110)',
outlineVariant: 'rgb(216, 194, 188)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(54, 47, 45)',
inverseOnSurface: 'rgb(251, 238, 235)',
inversePrimary: 'rgb(255, 181, 160)',
elevation: {
level0: 'transparent',
level1: 'rgb(251, 241, 242)',
level2: 'rgb(249, 235, 235)',
level3: 'rgb(246, 229, 227)',
level4: 'rgb(246, 226, 224)',
level5: 'rgb(244, 222, 219)',
},
surfaceDisabled: 'rgba(32, 26, 24, 0.12)',
onSurfaceDisabled: 'rgba(32, 26, 24, 0.38)',
backdrop: 'rgba(59, 45, 41, 0.4)',
},
red: {
primary: 'rgb(185, 12, 85)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(255, 217, 223)',
onPrimaryContainer: 'rgb(63, 0, 24)',
secondary: 'rgb(117, 86, 92)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(255, 217, 223)',
onSecondaryContainer: 'rgb(43, 21, 26)',
tertiary: 'rgb(122, 87, 51)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(255, 220, 189)',
onTertiaryContainer: 'rgb(44, 22, 0)',
background: 'rgb(255, 251, 255)',
onBackground: 'rgb(32, 26, 27)',
surface: 'rgb(255, 251, 255)',
onSurface: 'rgb(32, 26, 27)',
surfaceVariant: 'rgb(243, 221, 224)',
onSurfaceVariant: 'rgb(82, 67, 70)',
outline: 'rgb(132, 115, 117)',
outlineVariant: 'rgb(214, 194, 196)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(53, 47, 48)',
inverseOnSurface: 'rgb(250, 238, 239)',
inversePrimary: 'rgb(255, 177, 194)',
elevation: {
level0: 'transparent',
level1: 'rgb(252, 239, 247)',
level2: 'rgb(249, 232, 241)',
level3: 'rgb(247, 225, 236)',
level4: 'rgb(247, 222, 235)',
level5: 'rgb(245, 218, 231)',
},
surfaceDisabled: 'rgba(32, 26, 27, 0.12)',
onSurfaceDisabled: 'rgba(32, 26, 27, 0.38)',
backdrop: 'rgba(58, 45, 47, 0.4)',
},
violet: {
primary: 'rgb(140, 51, 179)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(248, 216, 255)',
onPrimaryContainer: 'rgb(50, 0, 71)',
secondary: 'rgb(105, 89, 109)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(241, 220, 244)',
onSecondaryContainer: 'rgb(35, 23, 40)',
tertiary: 'rgb(129, 82, 80)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(255, 218, 216)',
onTertiaryContainer: 'rgb(51, 17, 17)',
background: 'rgb(255, 251, 255)',
onBackground: 'rgb(30, 27, 30)',
surface: 'rgb(255, 251, 255)',
onSurface: 'rgb(30, 27, 30)',
surfaceVariant: 'rgb(235, 223, 233)',
onSurfaceVariant: 'rgb(76, 68, 77)',
outline: 'rgb(125, 116, 125)',
outlineVariant: 'rgb(206, 195, 205)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(51, 47, 51)',
inverseOnSurface: 'rgb(246, 239, 243)',
inversePrimary: 'rgb(235, 178, 255)',
elevation: {
level0: 'transparent',
level1: 'rgb(249, 241, 251)',
level2: 'rgb(246, 235, 249)',
level3: 'rgb(242, 229, 247)',
level4: 'rgb(241, 227, 246)',
level5: 'rgb(239, 223, 244)',
},
surfaceDisabled: 'rgba(30, 27, 30, 0.12)',
onSurfaceDisabled: 'rgba(30, 27, 30, 0.38)',
backdrop: 'rgba(53, 46, 54, 0.4)',
},
indigo: {
primary: 'rgb(104, 71, 192)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(232, 221, 255)',
onPrimaryContainer: 'rgb(33, 0, 93)',
secondary: 'rgb(97, 91, 113)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(232, 222, 248)',
onSecondaryContainer: 'rgb(29, 25, 43)',
tertiary: 'rgb(125, 82, 96)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(255, 217, 227)',
onTertiaryContainer: 'rgb(49, 16, 29)',
background: 'rgb(255, 251, 255)',
onBackground: 'rgb(28, 27, 30)',
surface: 'rgb(255, 251, 255)',
onSurface: 'rgb(28, 27, 30)',
surfaceVariant: 'rgb(230, 224, 236)',
onSurfaceVariant: 'rgb(72, 69, 78)',
outline: 'rgb(121, 117, 127)',
outlineVariant: 'rgb(202, 196, 207)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(49, 48, 51)',
inverseOnSurface: 'rgb(244, 239, 244)',
inversePrimary: 'rgb(206, 189, 255)',
elevation: {
level0: 'transparent',
level1: 'rgb(247, 242, 252)',
level2: 'rgb(243, 237, 250)',
level3: 'rgb(238, 231, 248)',
level4: 'rgb(237, 229, 247)',
level5: 'rgb(234, 226, 246)',
},
surfaceDisabled: 'rgba(28, 27, 30, 0.12)',
onSurfaceDisabled: 'rgba(28, 27, 30, 0.38)',
backdrop: 'rgba(50, 47, 56, 0.4)',
},
blue: {
primary: 'rgb(0, 99, 154)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(206, 229, 255)',
onPrimaryContainer: 'rgb(0, 29, 50)',
secondary: 'rgb(81, 96, 111)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(213, 228, 247)',
onSecondaryContainer: 'rgb(14, 29, 42)',
tertiary: 'rgb(104, 88, 122)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(238, 219, 255)',
onTertiaryContainer: 'rgb(35, 21, 51)',
background: 'rgb(252, 252, 255)',
onBackground: 'rgb(26, 28, 30)',
surface: 'rgb(252, 252, 255)',
onSurface: 'rgb(26, 28, 30)',
surfaceVariant: 'rgb(222, 227, 235)',
onSurfaceVariant: 'rgb(66, 71, 78)',
outline: 'rgb(114, 119, 127)',
outlineVariant: 'rgb(194, 199, 207)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(47, 48, 51)',
inverseOnSurface: 'rgb(240, 240, 244)',
inversePrimary: 'rgb(150, 204, 255)',
elevation: {
level0: 'transparent',
level1: 'rgb(239, 244, 250)',
level2: 'rgb(232, 240, 247)',
level3: 'rgb(224, 235, 244)',
level4: 'rgb(222, 234, 243)',
level5: 'rgb(217, 231, 241)',
},
surfaceDisabled: 'rgba(26, 28, 30, 0.12)',
onSurfaceDisabled: 'rgba(26, 28, 30, 0.38)',
backdrop: 'rgba(44, 49, 55, 0.4)',
},
teal: {
primary: 'rgb(0, 104, 116)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(151, 240, 255)',
onPrimaryContainer: 'rgb(0, 31, 36)',
secondary: 'rgb(74, 98, 103)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(205, 231, 236)',
onSecondaryContainer: 'rgb(5, 31, 35)',
tertiary: 'rgb(82, 94, 125)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(218, 226, 255)',
onTertiaryContainer: 'rgb(14, 27, 55)',
background: 'rgb(250, 253, 253)',
onBackground: 'rgb(25, 28, 29)',
surface: 'rgb(250, 253, 253)',
onSurface: 'rgb(25, 28, 29)',
surfaceVariant: 'rgb(219, 228, 230)',
onSurfaceVariant: 'rgb(63, 72, 74)',
outline: 'rgb(111, 121, 122)',
outlineVariant: 'rgb(191, 200, 202)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(46, 49, 50)',
inverseOnSurface: 'rgb(239, 241, 241)',
inversePrimary: 'rgb(79, 216, 235)',
elevation: {
level0: 'transparent',
level1: 'rgb(238, 246, 246)',
level2: 'rgb(230, 241, 242)',
level3: 'rgb(223, 237, 238)',
level4: 'rgb(220, 235, 237)',
level5: 'rgb(215, 232, 234)',
},
surfaceDisabled: 'rgba(25, 28, 29, 0.12)',
onSurfaceDisabled: 'rgba(25, 28, 29, 0.38)',
backdrop: 'rgba(41, 50, 52, 0.4)',
},
cyan: {
primary: 'rgb(0, 107, 94)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(118, 248, 226)',
onPrimaryContainer: 'rgb(0, 32, 27)',
secondary: 'rgb(74, 99, 94)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(205, 232, 225)',
onSecondaryContainer: 'rgb(6, 32, 27)',
tertiary: 'rgb(68, 97, 121)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(202, 230, 255)',
onTertiaryContainer: 'rgb(0, 30, 48)',
background: 'rgb(250, 253, 251)',
onBackground: 'rgb(25, 28, 27)',
surface: 'rgb(250, 253, 251)',
onSurface: 'rgb(25, 28, 27)',
surfaceVariant: 'rgb(218, 229, 225)',
onSurfaceVariant: 'rgb(63, 73, 70)',
outline: 'rgb(111, 121, 118)',
outlineVariant: 'rgb(190, 201, 197)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(45, 49, 48)',
inverseOnSurface: 'rgb(239, 241, 239)',
inversePrimary: 'rgb(85, 219, 198)',
elevation: {
level0: 'transparent',
level1: 'rgb(238, 246, 243)',
level2: 'rgb(230, 241, 238)',
level3: 'rgb(223, 237, 234)',
level4: 'rgb(220, 236, 232)',
level5: 'rgb(215, 233, 229)',
},
surfaceDisabled: 'rgba(25, 28, 27, 0.12)',
onSurfaceDisabled: 'rgba(25, 28, 27, 0.38)',
backdrop: 'rgba(41, 50, 48, 0.4)',
},
green: {
primary: 'rgb(16, 109, 32)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(157, 248, 152)',
onPrimaryContainer: 'rgb(0, 34, 4)',
secondary: 'rgb(82, 99, 79)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(213, 232, 206)',
onSecondaryContainer: 'rgb(17, 31, 15)',
tertiary: 'rgb(56, 101, 106)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(188, 235, 240)',
onTertiaryContainer: 'rgb(0, 32, 35)',
background: 'rgb(252, 253, 246)',
onBackground: 'rgb(26, 28, 25)',
surface: 'rgb(252, 253, 246)',
onSurface: 'rgb(26, 28, 25)',
surfaceVariant: 'rgb(222, 229, 216)',
onSurfaceVariant: 'rgb(66, 73, 64)',
outline: 'rgb(114, 121, 111)',
outlineVariant: 'rgb(194, 201, 189)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(47, 49, 45)',
inverseOnSurface: 'rgb(240, 241, 235)',
inversePrimary: 'rgb(130, 219, 126)',
elevation: {
level0: 'transparent',
level1: 'rgb(240, 246, 235)',
level2: 'rgb(233, 242, 229)',
level3: 'rgb(226, 237, 223)',
level4: 'rgb(224, 236, 220)',
level5: 'rgb(219, 233, 216)',
},
surfaceDisabled: 'rgba(26, 28, 25, 0.12)',
onSurfaceDisabled: 'rgba(26, 28, 25, 0.38)',
backdrop: 'rgba(44, 50, 42, 0.4)',
},
lime: {
primary: 'rgb(56, 107, 1)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(183, 244, 129)',
onPrimaryContainer: 'rgb(13, 32, 0)',
secondary: 'rgb(87, 98, 74)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(218, 231, 201)',
onSecondaryContainer: 'rgb(21, 30, 12)',
tertiary: 'rgb(56, 102, 100)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(187, 236, 233)',
onTertiaryContainer: 'rgb(0, 32, 31)',
background: 'rgb(253, 253, 245)',
onBackground: 'rgb(26, 28, 24)',
surface: 'rgb(253, 253, 245)',
onSurface: 'rgb(26, 28, 24)',
surfaceVariant: 'rgb(224, 228, 214)',
onSurfaceVariant: 'rgb(68, 72, 62)',
outline: 'rgb(116, 121, 109)',
outlineVariant: 'rgb(196, 200, 186)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(47, 49, 44)',
inverseOnSurface: 'rgb(241, 241, 234)',
inversePrimary: 'rgb(156, 215, 105)',
elevation: {
level0: 'transparent',
level1: 'rgb(243, 246, 233)',
level2: 'rgb(237, 241, 226)',
level3: 'rgb(231, 237, 218)',
level4: 'rgb(229, 236, 216)',
level5: 'rgb(225, 233, 211)',
},
surfaceDisabled: 'rgba(26, 28, 24, 0.12)',
onSurfaceDisabled: 'rgba(26, 28, 24, 0.38)',
backdrop: 'rgba(45, 50, 40, 0.4)',
},
olive: {
primary: 'rgb(95, 98, 0)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(229, 234, 93)',
onPrimaryContainer: 'rgb(28, 29, 0)',
secondary: 'rgb(96, 96, 67)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(229, 229, 192)',
onSecondaryContainer: 'rgb(28, 29, 6)',
tertiary: 'rgb(61, 102, 88)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(191, 236, 218)',
onTertiaryContainer: 'rgb(0, 33, 24)',
background: 'rgb(255, 251, 255)',
onBackground: 'rgb(28, 28, 23)',
surface: 'rgb(255, 251, 255)',
onSurface: 'rgb(28, 28, 23)',
surfaceVariant: 'rgb(229, 227, 209)',
onSurfaceVariant: 'rgb(72, 71, 59)',
outline: 'rgb(121, 120, 105)',
outlineVariant: 'rgb(201, 199, 182)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(49, 49, 43)',
inverseOnSurface: 'rgb(244, 240, 232)',
inversePrimary: 'rgb(200, 206, 68)',
elevation: {
level0: 'transparent',
level1: 'rgb(247, 243, 242)',
level2: 'rgb(242, 239, 235)',
level3: 'rgb(237, 234, 227)',
level4: 'rgb(236, 233, 224)',
level5: 'rgb(233, 230, 219)',
},
surfaceDisabled: 'rgba(28, 28, 23, 0.12)',
onSurfaceDisabled: 'rgba(28, 28, 23, 0.38)',
backdrop: 'rgba(49, 49, 37, 0.4)',
},
brown: {
primary: 'rgb(155, 68, 39)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(255, 219, 208)',
onPrimaryContainer: 'rgb(58, 11, 0)',
secondary: 'rgb(119, 87, 77)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(255, 219, 208)',
onSecondaryContainer: 'rgb(44, 22, 14)',
tertiary: 'rgb(107, 94, 47)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(244, 226, 167)',
onTertiaryContainer: 'rgb(34, 27, 0)',
background: 'rgb(255, 251, 255)',
onBackground: 'rgb(32, 26, 24)',
surface: 'rgb(255, 251, 255)',
onSurface: 'rgb(32, 26, 24)',
surfaceVariant: 'rgb(245, 222, 215)',
onSurfaceVariant: 'rgb(83, 67, 63)',
outline: 'rgb(133, 115, 110)',
outlineVariant: 'rgb(216, 194, 188)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(54, 47, 45)',
inverseOnSurface: 'rgb(251, 238, 234)',
inversePrimary: 'rgb(255, 181, 158)',
elevation: {
level0: 'transparent',
level1: 'rgb(250, 242, 244)',
level2: 'rgb(247, 236, 238)',
level3: 'rgb(244, 231, 231)',
level4: 'rgb(243, 229, 229)',
level5: 'rgb(241, 225, 225)',
},
surfaceDisabled: 'rgba(32, 26, 24, 0.12)',
onSurfaceDisabled: 'rgba(32, 26, 24, 0.38)',
backdrop: 'rgba(59, 45, 41, 0.4)',
},
},
dark: {
default: {
primary: MD3DarkTheme.colors.primary,
onPrimary: MD3DarkTheme.colors.onPrimary,
},
orange: {
primary: 'rgb(255, 183, 134)',
onPrimary: 'rgb(80, 36, 0)',
primaryContainer: 'rgb(114, 54, 0)',
onPrimaryContainer: 'rgb(255, 220, 198)',
secondary: 'rgb(229, 191, 168)',
onSecondary: 'rgb(66, 43, 27)',
secondaryContainer: 'rgb(91, 65, 48)',
onSecondaryContainer: 'rgb(255, 220, 198)',
tertiary: 'rgb(201, 202, 147)',
onTertiary: 'rgb(49, 50, 10)',
tertiaryContainer: 'rgb(72, 73, 31)',
onTertiaryContainer: 'rgb(229, 230, 173)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(32, 26, 23)',
onBackground: 'rgb(236, 224, 218)',
surface: 'rgb(32, 26, 23)',
onSurface: 'rgb(236, 224, 218)',
surfaceVariant: 'rgb(82, 68, 60)',
onSurfaceVariant: 'rgb(215, 195, 183)',
outline: 'rgb(159, 141, 131)',
outlineVariant: 'rgb(82, 68, 60)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(236, 224, 218)',
inverseOnSurface: 'rgb(54, 47, 43)',
inversePrimary: 'rgb(150, 73, 0)',
elevation: {
level0: 'transparent',
level1: 'rgb(43, 34, 29)',
level2: 'rgb(50, 39, 32)',
level3: 'rgb(57, 43, 35)',
level4: 'rgb(59, 45, 36)',
level5: 'rgb(63, 48, 39)',
},
surfaceDisabled: 'rgba(236, 224, 218, 0.12)',
onSurfaceDisabled: 'rgba(236, 224, 218, 0.38)',
backdrop: 'rgba(58, 46, 38, 0.4)',
},
red: {
primary: 'rgb(255, 177, 194)',
onPrimary: 'rgb(102, 0, 43)',
primaryContainer: 'rgb(143, 0, 63)',
onPrimaryContainer: 'rgb(255, 217, 223)',
secondary: 'rgb(228, 189, 195)',
onSecondary: 'rgb(67, 41, 47)',
secondaryContainer: 'rgb(91, 63, 69)',
onSecondaryContainer: 'rgb(255, 217, 223)',
tertiary: 'rgb(236, 190, 145)',
onTertiary: 'rgb(70, 42, 9)',
tertiaryContainer: 'rgb(96, 64, 29)',
onTertiaryContainer: 'rgb(255, 220, 189)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(32, 26, 27)',
onBackground: 'rgb(236, 224, 224)',
surface: 'rgb(32, 26, 27)',
onSurface: 'rgb(236, 224, 224)',
surfaceVariant: 'rgb(82, 67, 70)',
onSurfaceVariant: 'rgb(214, 194, 196)',
outline: 'rgb(158, 140, 143)',
outlineVariant: 'rgb(82, 67, 70)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(236, 224, 224)',
inverseOnSurface: 'rgb(53, 47, 48)',
inversePrimary: 'rgb(185, 12, 85)',
elevation: {
level0: 'transparent',
level1: 'rgb(43, 34, 35)',
level2: 'rgb(50, 38, 40)',
level3: 'rgb(57, 43, 45)',
level4: 'rgb(59, 44, 47)',
level5: 'rgb(63, 47, 50)',
},
surfaceDisabled: 'rgba(236, 224, 224, 0.12)',
onSurfaceDisabled: 'rgba(236, 224, 224, 0.38)',
backdrop: 'rgba(58, 45, 47, 0.4)',
},
violet: {
primary: 'rgb(235, 178, 255)',
onPrimary: 'rgb(82, 0, 113)',
primaryContainer: 'rgb(114, 17, 153)',
onPrimaryContainer: 'rgb(248, 216, 255)',
secondary: 'rgb(212, 192, 215)',
onSecondary: 'rgb(57, 44, 61)',
secondaryContainer: 'rgb(80, 66, 85)',
onSecondaryContainer: 'rgb(241, 220, 244)',
tertiary: 'rgb(245, 183, 181)',
onTertiary: 'rgb(76, 37, 36)',
tertiaryContainer: 'rgb(102, 59, 57)',
onTertiaryContainer: 'rgb(255, 218, 216)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(30, 27, 30)',
onBackground: 'rgb(232, 224, 229)',
surface: 'rgb(30, 27, 30)',
onSurface: 'rgb(232, 224, 229)',
surfaceVariant: 'rgb(76, 68, 77)',
onSurfaceVariant: 'rgb(206, 195, 205)',
outline: 'rgb(151, 142, 151)',
outlineVariant: 'rgb(76, 68, 77)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(232, 224, 229)',
inverseOnSurface: 'rgb(51, 47, 51)',
inversePrimary: 'rgb(140, 51, 179)',
elevation: {
level0: 'transparent',
level1: 'rgb(40, 35, 41)',
level2: 'rgb(46, 39, 48)',
level3: 'rgb(53, 44, 55)',
level4: 'rgb(55, 45, 57)',
level5: 'rgb(59, 48, 62)',
},
surfaceDisabled: 'rgba(232, 224, 229, 0.12)',
onSurfaceDisabled: 'rgba(232, 224, 229, 0.38)',
backdrop: 'rgba(53, 46, 54, 0.4)',
},
indigo: {
primary: 'rgb(206, 189, 255)',
onPrimary: 'rgb(57, 5, 144)',
primaryContainer: 'rgb(80, 43, 167)',
onPrimaryContainer: 'rgb(232, 221, 255)',
secondary: 'rgb(203, 195, 220)',
onSecondary: 'rgb(51, 45, 65)',
secondaryContainer: 'rgb(73, 68, 88)',
onSecondaryContainer: 'rgb(232, 222, 248)',
tertiary: 'rgb(239, 184, 201)',
onTertiary: 'rgb(73, 37, 50)',
tertiaryContainer: 'rgb(99, 59, 73)',
onTertiaryContainer: 'rgb(255, 217, 227)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(28, 27, 30)',
onBackground: 'rgb(230, 225, 230)',
surface: 'rgb(28, 27, 30)',
onSurface: 'rgb(230, 225, 230)',
surfaceVariant: 'rgb(72, 69, 78)',
onSurfaceVariant: 'rgb(202, 196, 207)',
outline: 'rgb(148, 143, 153)',
outlineVariant: 'rgb(72, 69, 78)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(230, 225, 230)',
inverseOnSurface: 'rgb(49, 48, 51)',
inversePrimary: 'rgb(104, 71, 192)',
elevation: {
level0: 'transparent',
level1: 'rgb(37, 35, 41)',
level2: 'rgb(42, 40, 48)',
level3: 'rgb(48, 45, 55)',
level4: 'rgb(49, 46, 57)',
level5: 'rgb(53, 50, 62)',
},
surfaceDisabled: 'rgba(230, 225, 230, 0.12)',
onSurfaceDisabled: 'rgba(230, 225, 230, 0.38)',
backdrop: 'rgba(50, 47, 56, 0.4)',
},
blue: {
primary: 'rgb(150, 204, 255)',
onPrimary: 'rgb(0, 51, 83)',
primaryContainer: 'rgb(0, 74, 117)',
onPrimaryContainer: 'rgb(206, 229, 255)',
secondary: 'rgb(185, 200, 218)',
onSecondary: 'rgb(35, 50, 64)',
secondaryContainer: 'rgb(58, 72, 87)',
onSecondaryContainer: 'rgb(213, 228, 247)',
tertiary: 'rgb(211, 191, 230)',
onTertiary: 'rgb(56, 42, 73)',
tertiaryContainer: 'rgb(79, 64, 97)',
onTertiaryContainer: 'rgb(238, 219, 255)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(26, 28, 30)',
onBackground: 'rgb(226, 226, 229)',
surface: 'rgb(26, 28, 30)',
onSurface: 'rgb(226, 226, 229)',
surfaceVariant: 'rgb(66, 71, 78)',
onSurfaceVariant: 'rgb(194, 199, 207)',
outline: 'rgb(140, 145, 152)',
outlineVariant: 'rgb(66, 71, 78)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(226, 226, 229)',
inverseOnSurface: 'rgb(47, 48, 51)',
inversePrimary: 'rgb(0, 99, 154)',
elevation: {
level0: 'transparent',
level1: 'rgb(32, 37, 41)',
level2: 'rgb(36, 42, 48)',
level3: 'rgb(40, 47, 55)',
level4: 'rgb(41, 49, 57)',
level5: 'rgb(43, 53, 62)',
},
surfaceDisabled: 'rgba(226, 226, 229, 0.12)',
onSurfaceDisabled: 'rgba(226, 226, 229, 0.38)',
backdrop: 'rgba(44, 49, 55, 0.4)',
},
teal: {
primary: 'rgb(79, 216, 235)',
onPrimary: 'rgb(0, 54, 61)',
primaryContainer: 'rgb(0, 79, 88)',
onPrimaryContainer: 'rgb(151, 240, 255)',
secondary: 'rgb(177, 203, 208)',
onSecondary: 'rgb(28, 52, 56)',
secondaryContainer: 'rgb(51, 75, 79)',
onSecondaryContainer: 'rgb(205, 231, 236)',
tertiary: 'rgb(186, 198, 234)',
onTertiary: 'rgb(36, 48, 77)',
tertiaryContainer: 'rgb(59, 70, 100)',
onTertiaryContainer: 'rgb(218, 226, 255)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(25, 28, 29)',
onBackground: 'rgb(225, 227, 227)',
surface: 'rgb(25, 28, 29)',
onSurface: 'rgb(225, 227, 227)',
surfaceVariant: 'rgb(63, 72, 74)',
onSurfaceVariant: 'rgb(191, 200, 202)',
outline: 'rgb(137, 146, 148)',
outlineVariant: 'rgb(63, 72, 74)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(225, 227, 227)',
inverseOnSurface: 'rgb(46, 49, 50)',
inversePrimary: 'rgb(0, 104, 116)',
elevation: {
level0: 'transparent',
level1: 'rgb(28, 37, 39)',
level2: 'rgb(29, 43, 46)',
level3: 'rgb(31, 49, 52)',
level4: 'rgb(32, 51, 54)',
level5: 'rgb(33, 54, 58)',
},
surfaceDisabled: 'rgba(225, 227, 227, 0.12)',
onSurfaceDisabled: 'rgba(225, 227, 227, 0.38)',
backdrop: 'rgba(41, 50, 52, 0.4)',
},
cyan: {
primary: 'rgb(85, 219, 198)',
onPrimary: 'rgb(0, 55, 48)',
primaryContainer: 'rgb(0, 80, 71)',
onPrimaryContainer: 'rgb(118, 248, 226)',
secondary: 'rgb(177, 204, 197)',
onSecondary: 'rgb(28, 53, 48)',
secondaryContainer: 'rgb(51, 75, 70)',
onSecondaryContainer: 'rgb(205, 232, 225)',
tertiary: 'rgb(172, 202, 229)',
onTertiary: 'rgb(19, 51, 72)',
tertiaryContainer: 'rgb(44, 74, 96)',
onTertiaryContainer: 'rgb(202, 230, 255)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(25, 28, 27)',
onBackground: 'rgb(224, 227, 225)',
surface: 'rgb(25, 28, 27)',
onSurface: 'rgb(224, 227, 225)',
surfaceVariant: 'rgb(63, 73, 70)',
onSurfaceVariant: 'rgb(190, 201, 197)',
outline: 'rgb(137, 147, 144)',
outlineVariant: 'rgb(63, 73, 70)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(224, 227, 225)',
inverseOnSurface: 'rgb(45, 49, 48)',
inversePrimary: 'rgb(0, 107, 94)',
elevation: {
level0: 'transparent',
level1: 'rgb(28, 38, 36)',
level2: 'rgb(30, 43, 41)',
level3: 'rgb(32, 49, 46)',
level4: 'rgb(32, 51, 48)',
level5: 'rgb(33, 55, 51)',
},
surfaceDisabled: 'rgba(224, 227, 225, 0.12)',
onSurfaceDisabled: 'rgba(224, 227, 225, 0.38)',
backdrop: 'rgba(41, 50, 48, 0.4)',
},
green: {
primary: 'rgb(130, 219, 126)',
onPrimary: 'rgb(0, 57, 10)',
primaryContainer: 'rgb(0, 83, 18)',
onPrimaryContainer: 'rgb(157, 248, 152)',
secondary: 'rgb(186, 204, 179)',
onSecondary: 'rgb(37, 52, 35)',
secondaryContainer: 'rgb(59, 75, 56)',
onSecondaryContainer: 'rgb(213, 232, 206)',
tertiary: 'rgb(160, 207, 212)',
onTertiary: 'rgb(0, 54, 59)',
tertiaryContainer: 'rgb(31, 77, 82)',
onTertiaryContainer: 'rgb(188, 235, 240)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(26, 28, 25)',
onBackground: 'rgb(226, 227, 221)',
surface: 'rgb(26, 28, 25)',
onSurface: 'rgb(226, 227, 221)',
surfaceVariant: 'rgb(66, 73, 64)',
onSurfaceVariant: 'rgb(194, 201, 189)',
outline: 'rgb(140, 147, 136)',
outlineVariant: 'rgb(66, 73, 64)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(226, 227, 221)',
inverseOnSurface: 'rgb(47, 49, 45)',
inversePrimary: 'rgb(16, 109, 32)',
elevation: {
level0: 'transparent',
level1: 'rgb(31, 38, 30)',
level2: 'rgb(34, 43, 33)',
level3: 'rgb(37, 49, 36)',
level4: 'rgb(39, 51, 37)',
level5: 'rgb(41, 55, 39)',
},
surfaceDisabled: 'rgba(226, 227, 221, 0.12)',
onSurfaceDisabled: 'rgba(226, 227, 221, 0.38)',
backdrop: 'rgba(44, 50, 42, 0.4)',
},
lime: {
primary: 'rgb(156, 215, 105)',
onPrimary: 'rgb(26, 55, 0)',
primaryContainer: 'rgb(40, 80, 0)',
onPrimaryContainer: 'rgb(183, 244, 129)',
secondary: 'rgb(190, 203, 174)',
onSecondary: 'rgb(41, 52, 31)',
secondaryContainer: 'rgb(63, 74, 52)',
onSecondaryContainer: 'rgb(218, 231, 201)',
tertiary: 'rgb(160, 207, 205)',
onTertiary: 'rgb(0, 55, 54)',
tertiaryContainer: 'rgb(30, 78, 77)',
onTertiaryContainer: 'rgb(187, 236, 233)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(26, 28, 24)',
onBackground: 'rgb(227, 227, 220)',
surface: 'rgb(26, 28, 24)',
onSurface: 'rgb(227, 227, 220)',
surfaceVariant: 'rgb(68, 72, 62)',
onSurfaceVariant: 'rgb(196, 200, 186)',
outline: 'rgb(142, 146, 134)',
outlineVariant: 'rgb(68, 72, 62)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(227, 227, 220)',
inverseOnSurface: 'rgb(47, 49, 44)',
inversePrimary: 'rgb(56, 107, 1)',
elevation: {
level0: 'transparent',
level1: 'rgb(33, 37, 28)',
level2: 'rgb(36, 43, 31)',
level3: 'rgb(40, 49, 33)',
level4: 'rgb(42, 50, 34)',
level5: 'rgb(44, 54, 35)',
},
surfaceDisabled: 'rgba(227, 227, 220, 0.12)',
onSurfaceDisabled: 'rgba(227, 227, 220, 0.38)',
backdrop: 'rgba(45, 50, 40, 0.4)',
},
olive: {
primary: 'rgb(200, 206, 68)',
onPrimary: 'rgb(49, 51, 0)',
primaryContainer: 'rgb(71, 74, 0)',
onPrimaryContainer: 'rgb(229, 234, 93)',
secondary: 'rgb(201, 201, 165)',
onSecondary: 'rgb(49, 50, 25)',
secondaryContainer: 'rgb(72, 72, 45)',
onSecondaryContainer: 'rgb(229, 229, 192)',
tertiary: 'rgb(163, 208, 190)',
onTertiary: 'rgb(9, 55, 43)',
tertiaryContainer: 'rgb(36, 78, 65)',
onTertiaryContainer: 'rgb(191, 236, 218)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(28, 28, 23)',
onBackground: 'rgb(229, 226, 218)',
surface: 'rgb(28, 28, 23)',
onSurface: 'rgb(229, 226, 218)',
surfaceVariant: 'rgb(72, 71, 59)',
onSurfaceVariant: 'rgb(201, 199, 182)',
outline: 'rgb(147, 145, 130)',
outlineVariant: 'rgb(72, 71, 59)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(229, 226, 218)',
inverseOnSurface: 'rgb(49, 49, 43)',
inversePrimary: 'rgb(95, 98, 0)',
elevation: {
level0: 'transparent',
level1: 'rgb(37, 37, 25)',
level2: 'rgb(42, 42, 27)',
level3: 'rgb(47, 48, 28)',
level4: 'rgb(49, 49, 28)',
level5: 'rgb(52, 53, 29)',
},
surfaceDisabled: 'rgba(229, 226, 218, 0.12)',
onSurfaceDisabled: 'rgba(229, 226, 218, 0.38)',
backdrop: 'rgba(49, 49, 37, 0.4)',
},
brown: {
primary: 'rgb(255, 181, 158)',
onPrimary: 'rgb(93, 24, 0)',
primaryContainer: 'rgb(124, 45, 18)',
onPrimaryContainer: 'rgb(255, 219, 208)',
secondary: 'rgb(231, 189, 177)',
onSecondary: 'rgb(68, 42, 34)',
secondaryContainer: 'rgb(93, 64, 55)',
onSecondaryContainer: 'rgb(255, 219, 208)',
tertiary: 'rgb(215, 198, 141)',
onTertiary: 'rgb(58, 48, 5)',
tertiaryContainer: 'rgb(82, 70, 26)',
onTertiaryContainer: 'rgb(244, 226, 167)',
error: 'rgb(255, 180, 171)',
onError: 'rgb(105, 0, 5)',
errorContainer: 'rgb(147, 0, 10)',
onErrorContainer: 'rgb(255, 180, 171)',
background: 'rgb(32, 26, 24)',
onBackground: 'rgb(237, 224, 220)',
surface: 'rgb(32, 26, 24)',
onSurface: 'rgb(237, 224, 220)',
surfaceVariant: 'rgb(83, 67, 63)',
onSurfaceVariant: 'rgb(216, 194, 188)',
outline: 'rgb(160, 141, 135)',
outlineVariant: 'rgb(83, 67, 63)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(237, 224, 220)',
inverseOnSurface: 'rgb(54, 47, 45)',
inversePrimary: 'rgb(155, 68, 39)',
elevation: {
level0: 'transparent',
level1: 'rgb(43, 34, 31)',
level2: 'rgb(50, 38, 35)',
level3: 'rgb(57, 43, 39)',
level4: 'rgb(59, 45, 40)',
level5: 'rgb(63, 48, 43)',
},
surfaceDisabled: 'rgba(237, 224, 220, 0.12)',
onSurfaceDisabled: 'rgba(237, 224, 220, 0.38)',
backdrop: 'rgba(59, 45, 41, 0.4)',
},
},
}
export default Colors

20
lib/ui/styles/index.ts Normal file
View File

@@ -0,0 +1,20 @@
/**
* Styles
*/
import { StyleSheet } from 'react-native'
import Colors from '@/lib/ui/styles/colors'
import Themes from '@/lib/ui/styles/themes'
const styles = StyleSheet.create({
screen: {
flex: 1,
gap: 16,
padding: 32,
alignItems: 'center',
justifyContent: 'center',
},
})
export { Colors, Themes, styles }

212
lib/ui/styles/themes.ts Normal file
View File

@@ -0,0 +1,212 @@
/**
* Themes
*/
import {
DarkTheme as NavigationDarkTheme,
DefaultTheme as NavigationDefaultTheme,
} from '@react-navigation/native'
import {
adaptNavigationTheme,
MD3LightTheme,
MD3DarkTheme,
configureFonts,
} from 'react-native-paper'
import Colors from '@/lib/ui/styles/colors'
const { LightTheme, DarkTheme } = adaptNavigationTheme({
reactNavigationLight: NavigationDefaultTheme,
reactNavigationDark: NavigationDarkTheme,
})
const fonts = configureFonts({ config: { fontFamily: 'NotoSans_400Regular' } })
const BaseLightTheme = {
...LightTheme,
...MD3LightTheme,
fonts,
}
const BaseDarkTheme = {
...DarkTheme,
...MD3DarkTheme,
fonts,
}
const Themes = {
light: {
default: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.default,
},
},
orange: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.orange,
},
},
red: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.red,
},
},
violet: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.violet,
},
},
indigo: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.indigo,
},
},
blue: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.blue,
},
},
teal: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.teal,
},
},
cyan: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.cyan,
},
},
green: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.green,
},
},
lime: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.lime,
},
},
olive: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.olive,
},
},
brown: {
...BaseLightTheme,
colors: {
...BaseLightTheme.colors,
...Colors.light.brown,
},
},
},
dark: {
default: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.default,
},
},
red: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.red,
},
},
orange: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.orange,
},
},
violet: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.violet,
},
},
indigo: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.indigo,
},
},
blue: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.blue,
},
},
teal: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.teal,
},
},
cyan: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.cyan,
},
},
green: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.green,
},
},
lime: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.lime,
},
},
olive: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.olive,
},
},
brown: {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
...Colors.dark.brown,
},
},
},
}
export default Themes

7
lib/utils/index.ts Normal file
View File

@@ -0,0 +1,7 @@
/**
* Utilities
*/
import Languages from '@/lib/utils/languages'
export { Languages }

20
lib/utils/languages.ts Normal file
View File

@@ -0,0 +1,20 @@
/**
* Available languages
*/
const Languages = {
ar: {
name: 'Arabic',
nativeName: 'العربية',
},
en: {
name: 'English',
nativeName: 'English',
},
tr: {
name: 'Turkish',
nativeName: 'Türkçe',
},
}
export default Languages

65
package.json Normal file
View File

@@ -0,0 +1,65 @@
{
"name": "expo-react-native-paper",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest --watchAll",
"lint": "eslint . --fix",
"expo:fix": "expo install --fix",
"expo:lint": "expo lint",
"format": "prettier -w ."
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"@expo-google-fonts/jetbrains-mono": "^0.2.3",
"@expo-google-fonts/noto-sans": "^0.2.3",
"@expo/vector-icons": "^14.0.0",
"@react-navigation/drawer": "^6.6.15",
"@react-navigation/native": "^6.0.2",
"@shopify/flash-list": "1.6.4",
"@shopify/react-native-skia": "1.2.3",
"expo": "~51.0.31",
"expo-font": "~12.0.5",
"expo-linking": "~6.3.1",
"expo-localization": "~15.0.3",
"expo-router": "~3.5.23",
"expo-secure-store": "~13.0.1",
"expo-splash-screen": "~0.27.4",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.4",
"expo-web-browser": "~13.0.3",
"formik": "^2.4.6",
"i18n-js": "^4.4.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.5",
"react-native-gesture-handler": "~2.16.1",
"react-native-paper": "^5.12.3",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
"react-native-web": "~0.19.6",
"yup": "^1.4.0",
"expo-image": "~1.12.15"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.2.45",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"eslint": "^8.57.0",
"eslint-config-expo": "^7.1.2",
"eslint-config-universe": "^12.0.0",
"jest": "^29.2.1",
"jest-expo": "~51.0.4",
"prettier": "^3.2.5",
"react-test-renderer": "18.2.0",
"typescript": "~5.3.3"
},
"private": true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

10
tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}