inital commit
1
.gitignore
vendored
@@ -5,6 +5,7 @@ node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
.idea/
|
||||
dist/
|
||||
web-build/
|
||||
|
||||
|
||||
20
App.tsx
@@ -1,20 +0,0 @@
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Open up App.tsx to start working on your app!</Text>
|
||||
<StatusBar style="auto" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
4
AppEntry.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import registerRootComponent from 'expo/build/launch/registerRootComponent';
|
||||
import App from "App";
|
||||
|
||||
registerRootComponent(App);
|
||||
10
app.json
@@ -4,27 +4,27 @@
|
||||
"slug": "Assemblr",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"icon": "./src/assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"image": "./src/assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
"src/**/*"
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"foregroundImage": "./src/assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
"favicon": "./src/assets/favicon.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
metro.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
resolver: {
|
||||
extraNodeModules: new Proxy({}, {
|
||||
get: (target, name) => path.join(process.cwd(), `src/${name}`)
|
||||
}),
|
||||
},
|
||||
watchFolders: [
|
||||
path.resolve(__dirname, 'src')
|
||||
],
|
||||
};
|
||||
3928
package-lock.json
generated
26
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "assemblr",
|
||||
"version": "1.0.0",
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"main": "AppEntry.ts",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
@@ -9,14 +9,34 @@
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/webpack-config": "^19.0.0",
|
||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||
"@react-navigation/native": "^6.1.7",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@rneui/base": "^4.0.0-rc.8",
|
||||
"@rneui/themed": "^4.0.0-rc.8",
|
||||
"axios": "^1.5.0",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"expo": "~49.0.8",
|
||||
"expo-secure-store": "~12.3.1",
|
||||
"expo-status-bar": "~1.6.0",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.4"
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.72.4",
|
||||
"react-native-modal": "^13.0.1",
|
||||
"react-native-responsive-dimensions": "^3.1.1",
|
||||
"react-native-responsive-fontsize": "^0.5.1",
|
||||
"react-native-safe-area-context": "^4.7.2",
|
||||
"react-native-screens": "~3.22.0",
|
||||
"react-native-vector-icons": "^10.0.0",
|
||||
"react-native-web": "~0.19.6",
|
||||
"react-native-webview": "^13.6.0",
|
||||
"react-redux": "^8.1.2",
|
||||
"redux": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@types/react": "~18.0.14",
|
||||
"@types/react": "~18.2.14",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"private": true
|
||||
|
||||
32
src/App.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import {Provider} from "react-redux";
|
||||
|
||||
import {useFonts} from 'expo-font';
|
||||
import {store} from "./redux/store";
|
||||
import React from "react";
|
||||
import CommonPage from "./screens/CommonPage/CommonPage";
|
||||
|
||||
|
||||
export default function App() {
|
||||
let [fontsLoading] = useFonts({
|
||||
'SF Pro Text': require('./assets/fonts/SF-Pro.ttf')
|
||||
})
|
||||
if (!fontsLoading)
|
||||
return <View><Text>Loading...</Text></View>
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<CommonPage/>
|
||||
</Provider>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
25
src/api/apiClient.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios, {AxiosHeaders, AxiosRequestConfig, AxiosRequestHeaders, InternalAxiosRequestConfig} from 'axios';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: 'http://192.168.1.101:5000',
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(async (config) => {
|
||||
const accessToken = await SecureStore.getItemAsync('access_token');
|
||||
if (!config.headers) {
|
||||
config.headers = new AxiosHeaders();
|
||||
}
|
||||
if (accessToken) {
|
||||
config.headers.set('Authorization', `Bearer ${accessToken}`, true);
|
||||
}
|
||||
config.validateStatus = (status) => {
|
||||
return true
|
||||
};
|
||||
|
||||
return config;
|
||||
}, function (error) {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
export default apiClient;
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/fonts/SF-Pro-Display-Black.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-BlackItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-Bold.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-BoldItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-Heavy.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-HeavyItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-Light.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-LightItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-Medium.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-MediumItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-Regular.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-RegularItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-Semibold.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-SemiboldItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-Thin.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-ThinItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-Ultralight.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Display-UltralightItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Italic.ttf
Normal file
BIN
src/assets/fonts/SF-Pro-Rounded-Black.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Rounded-Bold.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Rounded-Heavy.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Rounded-Light.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Rounded-Medium.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Rounded-Regular.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Rounded-Semibold.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Rounded-Thin.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Rounded-Ultralight.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-Black.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-BlackItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-Bold.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-BoldItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-Heavy.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-HeavyItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-Light.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-LightItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-Medium.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-MediumItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-Regular.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-RegularItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-Semibold.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-SemiboldItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-Thin.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-ThinItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-Ultralight.otf
Normal file
BIN
src/assets/fonts/SF-Pro-Text-UltralightItalic.otf
Normal file
BIN
src/assets/fonts/SF-Pro.ttf
Normal file
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
src/assets/icons/barcode.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/icons/box.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src/assets/icons/home.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/icons/money.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/assets/icons/profile.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/icons/scan.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/assets/img/login/backgroud.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/img/login/telegram.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
43
src/components/BasicButton/BasicButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import {FC} from "react";
|
||||
import {StyleSheet, TouchableOpacity, Text, View, StyleProp, ViewStyle, GestureResponderEvent} from "react-native";
|
||||
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import DText from "../DText/DText";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
isUnset?: boolean;
|
||||
onPress?: (event: GestureResponderEvent) => void
|
||||
};
|
||||
|
||||
const BasicButton: FC<Props> = ({label, onPress, style, isUnset = false}) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<View style={[styles.container, style]}>
|
||||
<DText style={styles.text}>{label}</DText>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
backgroundColor: '#2478F8',
|
||||
borderRadius: responsiveWidth(1),
|
||||
padding: responsiveWidth(2),
|
||||
flex: 1,
|
||||
|
||||
},
|
||||
text: {
|
||||
color: "white",
|
||||
fontSize: RFPercentage(2),
|
||||
textAlignVertical:"center",
|
||||
}
|
||||
});
|
||||
|
||||
export default BasicButton;
|
||||
19
src/components/DText/DText.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, {FC, ReactElement} from "react";
|
||||
import {StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
|
||||
type Props = {
|
||||
children: string;
|
||||
style?: StyleProp<TextStyle>;
|
||||
}
|
||||
const DText: FC<Props> = ({children, style}) => {
|
||||
return (
|
||||
<Text style={[styles.text, style]}>{children}</Text>
|
||||
)
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
fontFamily: 'SF Pro Text',
|
||||
}
|
||||
})
|
||||
export default DText;
|
||||
60
src/components/SearchBar/ScanModal.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {FC, useEffect, useRef} from "react";
|
||||
import {GestureResponderEvent, StyleSheet, Text, TextInput, View} from "react-native";
|
||||
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import Modal from "react-native-modal";
|
||||
import BasicButton from "../BasicButton/BasicButton";
|
||||
import DText from "../DText/DText";
|
||||
|
||||
type Props = {
|
||||
visible: boolean
|
||||
onCancelButtonPress?: (event: GestureResponderEvent) => void,
|
||||
onChanged: (text: string) => void
|
||||
}
|
||||
const ScanModal: FC<Props> = ({visible, onCancelButtonPress, onChanged}) => {
|
||||
const inputRef = useRef<TextInput | null>(null);
|
||||
useEffect(() => {
|
||||
if (visible) inputRef.current?.focus();
|
||||
}, [visible]);
|
||||
return (
|
||||
<Modal isVisible={visible}>
|
||||
<View style={styles.container}>
|
||||
<DText style={styles.text}>Наведите сканер на штрихкод товара или заказа</DText>
|
||||
<BasicButton onPress={onCancelButtonPress} style={styles.cancelButton} label={"Отмена"}/>
|
||||
<TextInput onEndEditing={(e) => {
|
||||
onChanged(e.nativeEvent.text);
|
||||
}} style={styles.pseudoInput} ref={inputRef}/>
|
||||
</View>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: responsiveWidth(1),
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: responsiveWidth(18),
|
||||
paddingVertical: responsiveHeight(10),
|
||||
gap: responsiveHeight(3)
|
||||
},
|
||||
text: {
|
||||
color: "#2B2D3A",
|
||||
fontSize: RFPercentage(3),
|
||||
textAlign: "center"
|
||||
},
|
||||
cancelButton: {
|
||||
flex: 0,
|
||||
width: responsiveWidth(30)
|
||||
},
|
||||
pseudoInput: {
|
||||
backgroundColor: "red",
|
||||
opacity: 0,
|
||||
position: "absolute",
|
||||
zIndex: -1
|
||||
}
|
||||
})
|
||||
export default ScanModal;
|
||||
84
src/components/SearchBar/SearchBar.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, {FC, useRef, useState} from "react";
|
||||
import {Image, StyleSheet, TextInput, TouchableOpacity, View} from "react-native";
|
||||
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import BasicButton from "../BasicButton/BasicButton";
|
||||
import ScanModal from "./ScanModal";
|
||||
import telegramAuthButton from "../TelegramAuthButton/TelegramAuthButton";
|
||||
|
||||
type Props = {
|
||||
onSearch: (text: string) => void;
|
||||
}
|
||||
const SearchBar: FC<Props> = ({onSearch}) => {
|
||||
const [isScanModalVisible, setIsScanModalVisible] = useState<boolean>(false);
|
||||
const [searchInput, setSearchInput] = useState<string>("");
|
||||
const textInputRef = useRef<TextInput>(null);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScanModal
|
||||
onChanged={(text) => {
|
||||
setIsScanModalVisible(false);
|
||||
onSearch(text);
|
||||
}}
|
||||
onCancelButtonPress={() => setIsScanModalVisible(false)}
|
||||
visible={isScanModalVisible}/>
|
||||
<BasicButton onPress={() => {
|
||||
onSearch(searchInput);
|
||||
if (textInputRef.current) {
|
||||
textInputRef.current.clear();
|
||||
}
|
||||
}} style={styles.scanButton} label={"Поиск"}/>
|
||||
<View style={styles.scanImageWrapper}>
|
||||
<TouchableOpacity onPress={() => setIsScanModalVisible(true)}>
|
||||
<Image style={styles.scanImage} source={require('assets/icons/scan.png')}/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TextInput ref={textInputRef} onChangeText={(text) => {
|
||||
setSearchInput(text);
|
||||
}} style={styles.textInput} placeholder={"Введите запрос"}/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const height = 6;
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
display: "flex",
|
||||
marginHorizontal: responsiveWidth(5),
|
||||
flexDirection: "row-reverse",
|
||||
height: responsiveHeight(height),
|
||||
alignItems: "flex-end"
|
||||
},
|
||||
scanImage: {
|
||||
height: responsiveHeight(5),
|
||||
width: responsiveHeight(5),
|
||||
|
||||
},
|
||||
scanButton: {
|
||||
borderRadius: 0,
|
||||
borderTopRightRadius: responsiveWidth(1),
|
||||
borderBottomRightRadius: responsiveWidth(1),
|
||||
width: responsiveWidth(25)
|
||||
},
|
||||
scanImageWrapper: {
|
||||
paddingHorizontal: responsiveWidth(1),
|
||||
height: responsiveHeight(height),
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
borderWidth: 1,
|
||||
borderColor: "#A5A5A5",
|
||||
},
|
||||
textInput: {
|
||||
height: responsiveHeight(height),
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: "#A5A5A5",
|
||||
borderTopLeftRadius: responsiveWidth(1),
|
||||
borderBottomLeftRadius: responsiveWidth(1),
|
||||
paddingLeft: responsiveHeight(2),
|
||||
fontSize: RFPercentage(2),
|
||||
fontFamily: 'SF Pro Text'
|
||||
}
|
||||
})
|
||||
|
||||
export default SearchBar
|
||||
49
src/components/TelegramAuthButton/TelegramAuthButton.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import {FC, useCallback} from "react";
|
||||
import {
|
||||
GestureResponderEvent,
|
||||
Linking,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableNativeFeedback,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
|
||||
type Props = {
|
||||
onPress?: (event: GestureResponderEvent) => void
|
||||
}
|
||||
|
||||
const TelegramAuthButton: FC<Props> = ({onPress}) => {
|
||||
return (
|
||||
<TouchableNativeFeedback onPress={onPress}>
|
||||
<View style={styles.buttonContainer}>
|
||||
<View style={styles.buttonContent}>
|
||||
<Text style={styles.buttonText}>Войти</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonContent: {
|
||||
backgroundColor: '#0090c9',
|
||||
borderRadius: RFPercentage(1),
|
||||
paddingHorizontal: 50,
|
||||
flexDirection: 'row', // Сохранено, так как возможно добавление иконки или другого элемента в будущем
|
||||
},
|
||||
buttonText: {
|
||||
flex: 1,
|
||||
fontSize: RFPercentage(3),
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export default TelegramAuthButton;
|
||||
129
src/features/auth/authSlice.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import {createSlice, createAsyncThunk, PayloadAction, ThunkDispatch} from '@reduxjs/toolkit';
|
||||
import apiClient from 'api/apiClient';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import {AppDispatch} from "../../redux/store";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {AxiosError} from "axios";
|
||||
import {RejectedAction} from "@reduxjs/toolkit/dist/query/core/buildThunks";
|
||||
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
|
||||
import {EndpointBuilder} from "@reduxjs/toolkit/dist/query/endpointDefinitions";
|
||||
|
||||
interface AuthState {
|
||||
accessToken: string | null;
|
||||
status: 'idle' | 'loading' | 'succeeded' | 'failed';
|
||||
errorMessage: string | null;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
accessToken: null,
|
||||
status: 'idle',
|
||||
errorMessage: null,
|
||||
isAuthenticated: false
|
||||
};
|
||||
|
||||
export const pokemonApi = createApi({
|
||||
reducerPath: 'pokemonApi',
|
||||
baseQuery: fetchBaseQuery({baseUrl: 'https://pokeapi.co/api/v2/'}),
|
||||
endpoints: (builder) => ({
|
||||
getPokemonByName: builder.query<any, string>({
|
||||
query: (name) => `pokemon/${name}`,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
export const {useGetPokemonByNameQuery} = pokemonApi;
|
||||
|
||||
export const loginUser = createAsyncThunk(
|
||||
'auth/login',
|
||||
async (credentials: { login: string; password: string }, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await apiClient.post<{ access_token: string }>('/auth/login', credentials);
|
||||
if (response.status == 401) {
|
||||
throw Error("Invalid credentials");
|
||||
}
|
||||
return response.data.access_token;
|
||||
} catch (error: any) {
|
||||
if (error instanceof AxiosError)
|
||||
return rejectWithValue('Server response error');
|
||||
else if (error instanceof Error)
|
||||
return rejectWithValue('Invalid credentials');
|
||||
return rejectWithValue('Unexpected error')
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const loadToken = createAsyncThunk<
|
||||
void,
|
||||
undefined,
|
||||
{
|
||||
dispatch: AppDispatch // Тип для dispatch
|
||||
}
|
||||
>('auth/loadToken', async (_, {dispatch}) => {
|
||||
try {
|
||||
const token = await SecureStore.getItemAsync('access_token');
|
||||
if (token) {
|
||||
dispatch(authSlice.actions.setToken(token));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Couldn't read token", error);
|
||||
}
|
||||
});
|
||||
|
||||
export const logoutUser = createAsyncThunk<
|
||||
void,
|
||||
undefined,
|
||||
{
|
||||
dispatch: AppDispatch // Тип для dispatch
|
||||
}
|
||||
>('auth/loadToken', async (_, {dispatch}) => {
|
||||
try {
|
||||
await SecureStore.deleteItemAsync('access_token');
|
||||
dispatch(authSlice.actions.logout())
|
||||
} catch (error) {
|
||||
console.error("Couldn't read token", error);
|
||||
}
|
||||
});
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
setToken: (state, action: PayloadAction<string>) => {
|
||||
state.accessToken = action.payload;
|
||||
state.isAuthenticated = true;
|
||||
state.status = "succeeded";
|
||||
},
|
||||
logout: (state) => {
|
||||
state.accessToken = null;
|
||||
state.status = "idle";
|
||||
state.isAuthenticated = false
|
||||
state.errorMessage = null;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(loginUser.pending, (state) => {
|
||||
state.accessToken = "";
|
||||
state.status = 'loading';
|
||||
state.errorMessage = null;
|
||||
state.isAuthenticated = false;
|
||||
}).addCase(loginUser.fulfilled, (state, action: PayloadAction<string>) => {
|
||||
let accessToken = action.payload;
|
||||
|
||||
state.accessToken = accessToken;
|
||||
state.status = 'succeeded';
|
||||
state.errorMessage = null
|
||||
state.isAuthenticated = true;
|
||||
|
||||
SecureStore.setItemAsync('access_token', accessToken);
|
||||
}).addCase(loginUser.rejected, (state, action) => {
|
||||
if (action.payload)
|
||||
state.errorMessage = action.payload as string;
|
||||
state.status = "failed";
|
||||
state.isAuthenticated = false;
|
||||
state.accessToken = "";
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export default authSlice.reducer;
|
||||
16
src/redux/store.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {configureStore} from '@reduxjs/toolkit';
|
||||
|
||||
import authReducer, {pokemonApi} from 'features/auth/authSlice';
|
||||
import {useDispatch} from "react-redux";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
[pokemonApi.reducerPath]: pokemonApi.reducer
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(pokemonApi.middleware)
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
16
src/screens/BarcodeScreen/BarcodeScreen.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import {Button, Text, View} from "react-native";
|
||||
import {useAppDispatch} from "../../redux/store";
|
||||
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice";
|
||||
import * as process from "process";
|
||||
|
||||
function BarcodeScreen() {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text style={{fontSize: 36}}>Barcode</Text>
|
||||
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default BarcodeScreen;
|
||||
14
src/screens/BoxScreen/BoxScreen.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import {Button, Text, View} from "react-native";
|
||||
import {useAppDispatch} from "../../redux/store";
|
||||
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice";
|
||||
import * as process from "process";
|
||||
|
||||
function BoxScreen() {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{fontSize: 36}}>Box</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default BoxScreen;
|
||||
27
src/screens/CommonPage/CommonPage.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import {SafeAreaView, StyleSheet, View} from "react-native";
|
||||
import LoginScreen from "../LoginScreen/LoginScreen";
|
||||
import MainScreen from "../MainScreen/MainScreen";
|
||||
import SearchBar from "../../components/SearchBar/SearchBar";
|
||||
import React from "react";
|
||||
|
||||
function CommonPage() {
|
||||
|
||||
return (
|
||||
|
||||
<View style={styles.main}>
|
||||
<MainScreen/>
|
||||
</View>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
main: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
flex: 1
|
||||
}
|
||||
});
|
||||
|
||||
export default CommonPage;
|
||||
23
src/screens/HomeScreen/HomeScreen.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import {Button, Modal, SafeAreaView, Text, View} from "react-native";
|
||||
import {useAppDispatch} from "../../redux/store";
|
||||
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice";
|
||||
import * as process from "process";
|
||||
import React, {useState} from "react";
|
||||
import BasicButton from "../../components/BasicButton/BasicButton";
|
||||
import SearchBar from "components/SearchBar/SearchBar";
|
||||
import ScanModal from "components/SearchBar/ScanModal";
|
||||
import DText from "../../components/DText/DText";
|
||||
|
||||
function HomeScreen() {
|
||||
return (
|
||||
<View style={{backgroundColor: "white"}}>
|
||||
<SearchBar onSearch={(text) => {
|
||||
console.log(`From scanner: ${text}`)
|
||||
}}/>
|
||||
<DText>Хуй</DText>
|
||||
</View>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeScreen;
|
||||
90
src/screens/LoginScreen/LoginScreen.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import {FC, useCallback, useEffect, useState} from "react";
|
||||
import {StyleSheet, Text, View, ImageBackground, Linking} from 'react-native';
|
||||
import TelegramAuthButton from "components/TelegramAuthButton/TelegramAuthButton";
|
||||
import WebView from "react-native-webview";
|
||||
import InputField from "./components/InputField";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {loadToken, loginUser} from "features/auth/authSlice";
|
||||
import {AppDispatch, RootState, useAppDispatch} from "redux/store";
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import {initializeUseSelector} from "react-redux/es/hooks/useSelector";
|
||||
import HomeScreen from "../HomeScreen/HomeScreen";
|
||||
import {RFPercentage, RFValue} from "react-native-responsive-fontsize";
|
||||
import {responsiveWidth} from "react-native-responsive-dimensions";
|
||||
|
||||
function LoginScreen() {
|
||||
const dispatch = useAppDispatch();
|
||||
const [login, setLogin] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const {status, errorMessage, isAuthenticated} = useSelector((state: RootState) => state.auth);
|
||||
const handleLogin = async () => {
|
||||
dispatch(loginUser({login: login, password: password}));
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(loadToken());
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{<View style={styles.container}>
|
||||
<ImageBackground source={require('assets/img/login/backgroud.png')} resizeMode="cover"
|
||||
style={styles.image}>
|
||||
<View style={styles.block}>
|
||||
<Text style={styles.authText}>Авторизация</Text>
|
||||
<InputField onChange={setLogin} placeholder={"Логин"}/>
|
||||
<InputField onChange={setPassword} secureTextEntry={false} placeholder={"Пароль"}/>
|
||||
<TelegramAuthButton onPress={handleLogin}/>
|
||||
<Text style={{fontSize: 36}}>{errorMessage}</Text>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
</View>}
|
||||
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
image: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
text: {
|
||||
color: 'white',
|
||||
fontSize: 42,
|
||||
lineHeight: 84,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#000000c0',
|
||||
},
|
||||
authText: {
|
||||
color: '#2478F8',
|
||||
fontFamily: 'SF Pro Text',
|
||||
fontSize: RFPercentage(3),
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '500',
|
||||
},
|
||||
block: {
|
||||
marginHorizontal: responsiveWidth(10),
|
||||
paddingVertical: "5%",
|
||||
paddingHorizontal: "5%",
|
||||
borderRadius: RFPercentage(5),
|
||||
borderColor: "#2478F8",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
backgroundColor: "#C6E0EA",
|
||||
alignSelf: "center",
|
||||
opacity: 0.8,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 30
|
||||
}
|
||||
})
|
||||
export default LoginScreen;
|
||||
53
src/screens/LoginScreen/components/InputField.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import {FC, useCallback} from "react";
|
||||
import {StyleSheet, Text, TextInput, TouchableOpacity, View} from "react-native";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import {responsiveWidth} from "react-native-responsive-dimensions";
|
||||
|
||||
type Props = {
|
||||
placeholder?: string;
|
||||
onChange?: (text: string) => void,
|
||||
secureTextEntry?: boolean
|
||||
}
|
||||
|
||||
const InputField: FC<Props> = ({placeholder, onChange, secureTextEntry = false}) => {
|
||||
return (
|
||||
<View style={styles.fieldContainer}>
|
||||
<View style={styles.textInputWrapper}>
|
||||
<TextInput
|
||||
placeholder={placeholder}
|
||||
autoCorrect={false}
|
||||
autoCapitalize={"none"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
style={styles.textInput}
|
||||
onChangeText={onChange}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
textInputWrapper: {
|
||||
backgroundColor: '#0090c9',
|
||||
borderRadius: RFPercentage(1),
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: responsiveWidth(3),
|
||||
},
|
||||
fieldLabel: {
|
||||
marginLeft: 40,
|
||||
alignSelf: 'flex-start',
|
||||
fontSize: RFPercentage(3),
|
||||
marginBottom: 10,
|
||||
},
|
||||
fieldContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
fontSize: RFPercentage(3),
|
||||
color: 'white',
|
||||
},
|
||||
});
|
||||
|
||||
export default InputField;
|
||||
108
src/screens/MainScreen/MainScreen.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import {StyleSheet, Text, View, ImageBackground, Linking, Image} from 'react-native';
|
||||
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
||||
import HomeScreen from "../HomeScreen/HomeScreen";
|
||||
import BoxScreen from "../BoxScreen/BoxScreen";
|
||||
import BarcodeScreen from "../BarcodeScreen/BarcodeScreen";
|
||||
import MoneyScreen from "../MoneyScreen/MoneyScreen";
|
||||
import ProfileScreen from "../ProfileScreen/ProfileScreen";
|
||||
import {NavigationContainer} from "@react-navigation/native";
|
||||
import moneyScreen from "../MoneyScreen/MoneyScreen";
|
||||
import profileScreen from "../ProfileScreen/ProfileScreen";
|
||||
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
|
||||
import {RFPercentage} from "react-native-responsive-fontsize";
|
||||
import LoginScreen from "../LoginScreen/LoginScreen";
|
||||
|
||||
|
||||
interface CustomTabProps {
|
||||
name: string;
|
||||
component: React.ComponentType<any>;
|
||||
icon: any;
|
||||
}
|
||||
|
||||
const CustomTab = ({name, component, icon}: CustomTabProps) => ({
|
||||
name,
|
||||
component,
|
||||
options: {
|
||||
tabBarIcon: ({focused, color, size}: { focused: boolean; color: string; size: number }) => (
|
||||
<Image
|
||||
source={icon}
|
||||
style={{
|
||||
width: RFPercentage(4),
|
||||
height: RFPercentage(4),
|
||||
tintColor: color
|
||||
}}
|
||||
/>
|
||||
),
|
||||
tabBarLabel: () => null,
|
||||
}
|
||||
});
|
||||
|
||||
function MainScreen() {
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
const tabScreens = [
|
||||
{
|
||||
name: "Home",
|
||||
component: HomeScreen,
|
||||
icon: require('assets/icons/home.png')
|
||||
},
|
||||
{
|
||||
name: "Box",
|
||||
component: BoxScreen,
|
||||
icon: require('assets/icons/box.png')
|
||||
},
|
||||
{
|
||||
name: "Barcode",
|
||||
component: BarcodeScreen,
|
||||
icon: require('assets/icons/barcode.png')
|
||||
},
|
||||
{
|
||||
name: "Money",
|
||||
component: moneyScreen,
|
||||
icon: require('assets/icons/money.png')
|
||||
},
|
||||
{
|
||||
name: "Profile",
|
||||
component: profileScreen,
|
||||
icon: require('assets/icons/profile.png')
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<NavigationContainer>
|
||||
|
||||
<Tab.Navigator screenOptions={{
|
||||
tabBarStyle: styles.tabBarStyle,
|
||||
headerTitle: ""
|
||||
}}>
|
||||
{tabScreens.map(tabScreen =>
|
||||
<Tab.Screen key={tabScreen.name} {...CustomTab({
|
||||
name: tabScreen.name,
|
||||
component: tabScreen.component,
|
||||
icon: tabScreen.icon,
|
||||
})}/>
|
||||
)}
|
||||
</Tab.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
tabBarStyle: {
|
||||
position: 'absolute',
|
||||
left: responsiveWidth(10),
|
||||
right: responsiveWidth(10),
|
||||
bottom: 0,
|
||||
borderTopLeftRadius: 18,
|
||||
borderTopRightRadius: 18,
|
||||
elevation: 10,
|
||||
height: responsiveHeight(8),
|
||||
paddingHorizontal: responsiveWidth(5)
|
||||
}
|
||||
|
||||
})
|
||||
export default MainScreen;
|
||||
15
src/screens/MoneyScreen/MoneyScreen.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import {Button, Text, View} from "react-native";
|
||||
import {useAppDispatch} from "../../redux/store";
|
||||
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice";
|
||||
import * as process from "process";
|
||||
|
||||
function MoneyScreen() {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{fontSize: 36}}>Money</Text>
|
||||
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MoneyScreen;
|
||||
15
src/screens/ProfileScreen/ProfileScreen.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import {Button, Text, View} from "react-native";
|
||||
import {useAppDispatch} from "../../redux/store";
|
||||
import {logoutUser, useGetPokemonByNameQuery} from "../../features/auth/authSlice";
|
||||
import * as process from "process";
|
||||
|
||||
function ProfileScreen() {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text style={{fontSize: 36}}>Profile</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileScreen;
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"baseUrl": "./src"
|
||||
}
|
||||
}
|
||||
|
||||