Compare commits

...

10 commits

49 changed files with 20717 additions and 140 deletions

View file

@ -1,50 +1,9 @@
# Welcome to your Expo app 👋
# Donut Shop Application with React Native
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
This project is a donut shop mobile application, where users can search for the list of donuts of your choice, and add to you favorite list. Users can also place the order and get a receipt through this mobile application.
## Get started
1. Install dependencies
```bash
npm install
```
2. Start the app
```bash
npx expo start
```
In the output, you'll find options to open the app in a
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
## Learn more
To learn more about developing your project with Expo, look at the following resources:
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Join the community
Join our community of developers creating universal apps.
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
## To start the project
* Download [Expo Go](https://expo.dev/go) on your device to launch the project.
* To start the project, run this command in your terminal `npx expo start --tunnel`
* Scan the QR code that shows in your terminal.
* For more information about Tunneling, please read [Expo](https://docs.expo.dev/more/expo-cli/#tunneling).

View file

@ -0,0 +1,59 @@
import { StyleSheet, Text, View, Pressable, Image} from 'react-native'
import React from 'react'
import { useNavigation } from '@react-navigation/native';
const HomeCardItem = () => {
const navigation = useNavigation();
return (
<View style={{padding: 5}}>
<View style={{shadowColor: '#B4B4B8', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.5, shadowRadius: 1,}}>
<Pressable>
<View style={{borderColor: '#EEEEEE', borderWidth: 1, borderRadius: 5}}>
<Image source={require('../../assets/images/menu/coming_soon.png')} style={{width: '100%', height: 200, borderTopLeftRadius: 5, borderTopRightRadius: 5}}/>
<View style={{padding: 10}}>
<Text style={{fontSize: 16, fontWeight: 600, letterSpacing: 0.5, marginBottom: 10}}>Pokémon Donuts<Text style={{fontSize: 12, color: '#DA7297'}}>(Coming soon)</Text></Text>
<Text>The limited-edition Pokémon Donuts is coming soon!</Text>
</View>
</View>
</Pressable>
</View>
<View style={{marginTop: 10, shadowColor: '#B4B4B8', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.5, shadowRadius: 1,}}>
<Pressable onPress={() => navigation.navigate('menu')}>
<View style={{borderColor: '#EEEEEE', borderWidth: 1, borderRadius: 5}}>
<Image source={require('../../assets/images/menu/new_special.jpg')} style={{width: '100%', height: 200, borderTopLeftRadius: 5, borderTopRightRadius: 5}}/>
<View style={{padding: 10}}>
<Text style={{fontSize: 16, fontWeight: 600, letterSpacing: 0.5, marginBottom: 10}}>Mothers Day Box for your mama</Text>
<Text>Lets show our appreciation for our moms by treating them with the special Mothers Day Box!</Text>
<Pressable onPress={() => navigation.navigate('menu')} style={{marginTop: 10, padding: 10, backgroundColor: '#DA7297', width: 100, borderRadius: 5, alignItems: 'center'}}>
<Text style={{color: 'white', fontWeight: 500}}>Order Now</Text>
</Pressable>
</View>
</View>
</Pressable>
</View>
<View style={{marginTop: 10, shadowColor: '#B4B4B8', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.5, shadowRadius: 1,}}>
<Pressable onPress={() => navigation.navigate('menu')}>
<View style={{borderColor: '#EEEEEE', borderWidth: 1, borderRadius: 5}}>
<Image source={require('../../assets/images/menu/new_drink.jpg')} style={{width: '100%', height: 200, borderTopLeftRadius: 5, borderTopRightRadius: 5}}/>
<View style={{padding: 10}}>
<Text style={{fontSize: 16, fontWeight: 600, letterSpacing: 0.5, marginBottom: 10}}>Colorful Smoothies are here</Text>
<Text>The colors are not due to artificial colorings, but rather from natural fruits and vegetables</Text>
<Pressable onPress={() => navigation.navigate('menu')} style={{marginTop: 10, padding: 10, backgroundColor: '#DA7297', width: 100, borderRadius: 5, alignItems: 'center'}}>
<Text style={{color: 'white', fontWeight: 500}}>Order Now</Text>
</Pressable>
</View>
</View>
</Pressable>
</View>
</View>
)
}
export default HomeCardItem
const styles = StyleSheet.create({})

View file

@ -0,0 +1,58 @@
import { Pressable, StyleSheet, Text, View } from 'react-native';
import React from 'react';
import OrderReceiptItem from '../components/OrderReceiptItem';
interface OrderHistoryCardProps {
navigationHandler: any;
CartList: any;
CartListPrice: string;
OrderDate: string;
}
const OrderReceiptCard: React.FC<OrderHistoryCardProps> = ({
navigationHandler,
CartList,
CartListPrice,
OrderDate,
}) => {
return (
<View style={{borderColor: 'gray', borderWidth:1, marginBottom: 10}}>
<View style={{justifyContent: 'flex-end', padding: 10, backgroundColor: '#EEEEEE'}}>
<Text style={{fontWeight: 600}}>{OrderDate}</Text>
</View>
<View style={{gap: 10}}>
<View style={{borderColor: 'black', borderWidth: 0.5, borderStyle: 'dashed'}}/>
{CartList.map((data: any, index: any) => (
<Pressable key={index.toString() + data.id} onPress={() => {
navigationHandler({
index: data.index,
id: data.id,
type: data.type,
});
}}>
<OrderReceiptItem
type={data.type}
name={data.name}
donutname={data.donutname}
image_item={data.image_item}
prices={data.prices}
ItemPrice={data.ItemPrice}
/>
</Pressable>
))}
</View>
<View style={{borderColor: 'black', borderWidth: 0.5, borderStyle: 'dashed'}}/>
<View style={{flexDirection: 'row', justifyContent: 'space-between', gap: 20, alignItems: 'center', padding: 10}}>
<Text style={{fontWeight: 600}}>Total Amount</Text>
<Text style={{fontWeight: 600}}>$ {CartListPrice}</Text>
</View>
</View>
)
}
export default OrderReceiptCard
const styles = StyleSheet.create({})

View file

@ -0,0 +1,53 @@
import { Image, ImageProps, StyleSheet, Text, View } from 'react-native'
import React from 'react'
interface OrderReceiptItemProps{
type: string;
name: string;
donutname: string;
image_item: ImageProps;
prices: any;
ItemPrice: string;
}
const OrderReceiptItem: React.FC<OrderReceiptItemProps> = ({
type,
name,
donutname,
image_item,
prices,
ItemPrice,
}) => {
return (
<View style={{paddingHorizontal: 10, marginBottom: 10}}>
<View style={{flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center'}}>
<View style={{flexDirection: 'row', gap: 10, alignItems: 'center'}}>
<Image source={image_item} style={{width: 50, height: 50}} />
{prices.map((item: any, index: any) => (
<View key={index.toString()}>
<Text style={{width: 200}}>{name || donutname} <Text style={{fontWeight: 600}}>{item.size}</Text></Text>
</View>
))}
</View>
<View>
<Text>${ItemPrice}</Text>
</View>
</View>
{prices.map((data: any, index: any) => (
<View key={index.toString()} style={{flex: 1, alignItems: 'center', flexDirection: 'row', justifyContent: 'space-between', marginTop: 10}}>
<View style={{flex:1, flexDirection: 'row', gap: 10}}>
<Text>${data.price}</Text>
<Text>X {data.quantity}</Text>
</View>
<Text>${(data.quantity * data.price).toFixed(2).toString()}</Text>
</View>
))}
</View>
)
}
export default OrderReceiptItem
const styles = StyleSheet.create({})

View file

@ -10,7 +10,7 @@ import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
const Detail = ({navigation, route}: any) => {
const ItemOfIndex = useStore((state: any) =>
route.params.type == 'Donut' ? state.AllDonutList2 : state.AllDrinkList2,
route.params.type == 'Donut' ? state.AllDonutMenu : state.AllDrinkMenu,
)[route.params.index];
const addToFavoriteList = useStore((state: any) => state.addToFavoriteList);

View file

@ -9,7 +9,7 @@ import PaymentFooter from "../components/PaymentFooter";
const DetailDrink = ({navigation, route}: any) => {
const ItemOfIndex = useStore((state: any) =>
route.params.type == 'Donut' ? state.AllDonutList2 : state.AllDrinkList2,
route.params.type == 'Donut' ? state.AllDonutMenu : state.AllDrinkMenu,
)[route.params.index];
const addToFavoriteList = useStore((state: any) => state.addToFavoriteList);

View file

@ -1,6 +1,7 @@
import { View, Text, ScrollView, StyleSheet } from 'react-native'
import { View, Text, ScrollView, StyleSheet, Image, Pressable } from 'react-native'
import React from 'react'
import HomeHeader from '../components/HomeHeader'
import HomeCardItem from '../components/HomeCardItem'
import { StatusBar } from 'expo-status-bar';
const Home = () => {
@ -10,6 +11,7 @@ const Home = () => {
<StatusBar style="dark"/>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.ScrollViewFlex}>
<HomeHeader />
<HomeCardItem />
</ScrollView>
</View>
)

View file

@ -1,4 +1,4 @@
import { View, Text, ScrollView, StyleSheet, Pressable, Dimensions, ToastAndroid} from 'react-native'
import { View, Text, ScrollView, StyleSheet, Pressable, Dimensions} from 'react-native'
import React, { useState } from 'react'
import { useStore } from '../store/store';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
@ -32,8 +32,8 @@ const getDonutList = (category: string, data: any) => {
const Menu = ({navigation}: any) => {
const DonutList = useStore((state: any) => state.AllDonutList2);
const DrinkList = useStore((state: any) => state.AllDrinkList2);
const DonutList = useStore((state: any) => state.AllDonutMenu);
const DrinkList = useStore((state: any) => state.AllDrinkMenu);
const [categories, setCategories] = useState(getCategoriesFromData(DonutList));
const [searchMenu, setSearchMenu] = useState('');

View file

@ -1,10 +1,46 @@
import { View, Text } from 'react-native'
import React from 'react'
import { View, Text, ScrollView, Image } from 'react-native';
import React from 'react';
import { useStore } from '../store/store';
import OrderReceiptCard from '../components/OrderReceiptCard';
const OrderReceipt = ({navigation}: any) => {
const ReceiptList = useStore((state: any) => state.ReceiptList);
const navigationHandler = ({index, id, type}: any) => {
navigation.push('detail', {
index,
id,
type,
});
};
const OrderReceipt = () => {
return (
<View style={{backgroundColor: 'white', flex: 1}}>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{flexGrow: 1, marginTop: 20, paddingHorizontal: 20, paddingVertical: 10}}>
<View style={{flex: 1, justifyContent: 'space-between'}}>
<Text style={{fontSize: 30, fontWeight: 500}}>Receipt</Text>
{ReceiptList.length == 0 ? (
<View>
<Text>OrderHistory</Text>
<Image source={require('../../assets/images/misc/receipt.png')} style={{ width: 200, height: 200, resizeMode: "contain"}} />
</View>
):(
<View style={{marginTop: 20}}>
{ReceiptList.map((data: any, index: any) => (
<OrderReceiptCard
key={index.toString()}
navigationHandler={navigationHandler}
CartList={data.CartList}
CartListPrice={data.CartListPrice}
OrderDate={data.OrderDate}
/>
))}
</View>
)}
</View>
</ScrollView>
</View>
)
}

View file

@ -4,6 +4,7 @@ import { Ionicons } from '@expo/vector-icons';
import PaymentOption from "../components/PaymentOption";
import PaymentFooter from "../components/PaymentFooter";
import { LinearGradient } from 'expo-linear-gradient';
import { useStore } from '../store/store';
const PaymentOptionList = [
@ -29,10 +30,34 @@ const PaymentOptionList = [
const Payment = ({navigation, route}: any) => {
const calculateCartPrice = useStore((state: any) => state.calculateCartPrice);
const addToReceiptList = useStore(((state: any) => state.addToReceiptList));
const [paymentMethod, setPaymentMenthod] = useState('Credit card');
const [showApprove, setShowApprove] = useState(false);
const buttonPressHandler = () => {
setShowApprove(true);
addToReceiptList();
calculateCartPrice();
setTimeout(() => {
setShowApprove(false);
navigation.navigate('receipt')
}, 3000)
};
return (
<View style={{flex:1, backgroundColor: 'white'}}>
{showApprove ? (
<View style={{flex: 1, position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, zIndex: 1000, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center'}}>
<Image source={require('../../assets/images/misc/check.png')} style={{width: 100, height: 100}} />
<Text style={{marginTop: 20, marginBottom: 10, fontSize: 26, color: '#88D66C'}}>Approved!</Text>
<Text style={{fontSize: 20, color: '#88D66C'}}>Thank you for your order!</Text>
</View>
) : (
<></>
)}
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{flexGrow: 1}}>
<View style={{marginTop: 20, paddingHorizontal: 20, paddingVertical: 10, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between'}}>
<Pressable onPress={() => {navigation.pop()}}>
@ -71,7 +96,7 @@ const Payment = ({navigation, route}: any) => {
<PaymentFooter
buttonTitle={`Pay with ${paymentMethod}`}
price={{price: route.params.amount, currency: '$'}}
buttonPressHandler={() => {}}
buttonPressHandler={buttonPressHandler}
/>
</View>
)

View file

@ -8,8 +8,8 @@ import DrinkData from "../data/DrinkData";
export const useStore = create(
persist(
(set, get) => ({
AllDonutList1: DonutData,
AllDrinkList: DrinkData,
AllDonutMenu: DonutData,
AllDrinkMenu: DrinkData,
CartPrice: 0,
FavoriteList: [],
CartList: [],
@ -68,25 +68,25 @@ export const useStore = create(
set(
produce(state => {
if(type == 'Donut'){
for(let i = 0; i < state.AllDonutList1.length; i++){
if(state.AllDonutList1[i].id == id){
if(state.AllDonutList1[i].favourite == false){
state.AllDonutList1[i].favourite = true;
state.FavoriteList.unshift(state.AllDonutList1[i]);
for(let i = 0; i < state.AllDonutMenu.length; i++){
if(state.AllDonutMenu[i].id == id){
if(state.AllDonutMenu[i].favourite == false){
state.AllDonutMenu[i].favourite = true;
state.FavoriteList.unshift(state.AllDonutMenu[i]);
} else{
state.AllDonutList1[i].favourite = false;
state.AllDonutMenu[i].favourite = false;
}
break;
}
}
} else if(type == 'Drink'){
for(let i = 0; i< state.AllDrinkList.length; i++){
if(state.AllDrinkList[i].id == id){
if(state.AllDrinkList[i].favourite == false){
state.AllDrinkList[i].favourite = true;
state.FavoriteList.unshift(state.AllDrinkList[i]);
for(let i = 0; i < state.AllDrinkMenu.length; i++){
if(state.AllDrinkMenu[i].id == id){
if(state.AllDrinkMenu[i].favourite == false){
state.AllDrinkMenu[i].favourite = true;
state.FavoriteList.unshift(state.AllDrinkMenu[i]);
} else{
state.AllDrinkList[i].favourite = false;
state.AllDrinkMenu[i].favourite = false;
}
break;
}
@ -98,23 +98,23 @@ export const useStore = create(
set(
produce(state => {
if(type = 'Donut'){
for(let i = 0; i < state.AllDonutList1.length; i++){
if(state.AllDonutList1[i].id == id){
if(state.AllDonutList1[i].favourite == true){
state.AllDonutList1[i].favourite = false;
for(let i = 0; i < state.AllDonutMenu.length; i++){
if(state.AllDonutMenu[i].id == id){
if(state.AllDonutMenu[i].favourite == true){
state.AllDonutMenu[i].favourite = false;
} else{
state.AllDonutList1[i].favourite = true;
state.AllDonutMenu[i].favourite = true;
}
break;
}
}
} else if(type = 'Drink'){
for(let i = 0; i < state.AllDrinkList.length; i++){
if(state.AllDrinkList[i].id == id){
if(state.AllDrinkList[i].favourite == true){
state.AllDrinkList[i].favourite = false;
for(let i = 0; i < state.AllDrinkMenu.length; i++){
if(state.AllDrinkMenu[i].id == id){
if(state.AllDrinkMenu[i].favourite == true){
state.AllDrinkMenu[i].favourite = false;
} else{
state.AllDrinkList[i].favourite = true;
state.AllDrinkMenu[i].favourite = true;
}
break;
}
@ -130,6 +130,72 @@ export const useStore = create(
state.FavoriteList.splice(spliceIndex, 1);
}),
),
addCartItemQuantity:(id: string, size: string) =>
set(
produce(state => {
for(let i = 0; i < state.CartList.length; i++){
if(state.CartList[i].id == id){
for (let j = 0; j < state.CartList[i].prices.length; j++) {
if (state.CartList[i].prices[j].size == size) {
state.CartList[i].prices[j].quantity++;
break;
}
}
}
}
}),
),
removeCartItemQuantity:(id: string, size: string) =>
set(
produce(state => {
for (let i = 0; i < state.CartList.length; i++) {
if(state.CartList[i].id == id){
for (let j = 0; j < state.CartList[i].prices.length; j++) {
if (state.CartList[i].prices[j].size == size) {
if(state.CartList[i].prices.length > 1){
if (state.CartList[i].prices[j].quantity > 1) {
state.CartList[i].prices[j].quantity--;
} else{
state.CartList[i].prices.splice(j, 1);
}
} else{
if (state.CartList[i].prices[j].quantity > 1) {
state.CartList[i].prices[j].quantity--;
} else {
state.CartList.splice(i, 1);
}
}
break
}
}
}
}
}),
),
addToReceiptList:() =>
set(
produce(state => {
let temp = state.CartList.reduce(
(accumulator: number, currentValue: any) =>
accumulator + parseFloat(currentValue.ItemPrice),
0,
);
if(state.ReceiptList.length > 0){
state.ReceiptList.unshift({
OrderDate: new Date().toDateString() + ' ' + new Date().toLocaleTimeString(),
CartList: state.CartList,
CartListPrice: temp.toFixed(2).toString(),
})
} else{
state.ReceiptList.push({
OrderDate: new Date().toDateString() + ' ' + new Date().toLocaleTimeString(),
CartList: state.CartList,
CartListPrice: temp.toFixed(2).toString(),
});
}
state.CartList = [];
}),
),
}),
{
name: 'donutshop',

23
ev_station/.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# macOS
.DS_Store
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli
expo-env.d.ts
# @end expo-cli
.env
app/misc/FirebaseConfig.js

9
ev_station/README.md Normal file
View file

@ -0,0 +1,9 @@
# EV Charging Station Finder with React Native
EV Charging Station Finder is mobile application where users can search for the nearest EV charging station, save their list, and start their route to the picked EV station.
## To start the project
* Download [Expo Go](https://expo.dev/go) on your device to launch the project.
* To start the project, run this command in your terminal `npx expo start --tunnel`
* Scan the QR code that shows in your terminal.
* For more information about Tunneling, please read [Expo](https://docs.expo.dev/more/expo-cli/#tunneling).

42
ev_station/app.json Normal file
View file

@ -0,0 +1,42 @@
{
"expo": {
"name": "ev_station",
"slug": "ev_station",
"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"
},
"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-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow ev_station to use your location."
}
],
"expo-router"
],
"experiments": {
"typedRoutes": true
}
}
}

View file

@ -0,0 +1,46 @@
import { View, Text, StyleSheet } from 'react-native'
import React from 'react'
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
import HomeScreen from "../screen/HomeScreen"
import SaveScreen from "../screen/SaveScreen"
import {Entypo, MaterialIcons, AntDesign} from '@expo/vector-icons';
const Tab = createBottomTabNavigator();
const HomeNavigator = () => {
return (
<Tab.Navigator screenOptions={{headerShown: false, tabBarHideOnKeyboard: true}}>
<Tab.Screen name='home' component={HomeScreen}
options={{
tabBarLabel: "Home",
tabBarActiveTintColor: '#379777',
tabBarInactiveTintColor: 'gray',
tabBarIcon:({focused}) =>
focused? (
<Entypo name="home" size={26} color="#379777" style={{marginTop: 5, marginBottom: 5}}/>
) : (
<Entypo name="home" size={26} color="gray" style={{marginTop: 5, marginBottom: 5}}/>
)
}}
/>
<Tab.Screen name='save' component={SaveScreen}
options={{
tabBarLabel: "Save",
tabBarActiveTintColor: '#379777',
tabBarInactiveTintColor: 'gray',
tabBarIcon:({focused}) =>
focused? (
<MaterialIcons name="data-saver-on" size={26} color="#379777" style={{marginTop: 5, marginBottom: 5}}/>
) : (
<MaterialIcons name="data-saver-on" size={26} color="gray" style={{marginTop: 5, marginBottom: 5}}/>
)
}}
/>
</Tab.Navigator>
)
}
export default HomeNavigator
const styles = StyleSheet.create({})

View file

@ -0,0 +1,3 @@
import { createContext } from "react";
export const MarkerContext = createContext(null);

View file

@ -0,0 +1,3 @@
import { createContext } from "react";
export const UserLocationContext = createContext(null);

76
ev_station/app/index.js Normal file
View file

@ -0,0 +1,76 @@
import { StatusBar } from "expo-status-bar";
import { View, Text } from "react-native";
import Login from "./screen/loginScreen/Login";
import { ClerkProvider, SignedIn, SignedOut } from '@clerk/clerk-expo';
import *as SecureStore from "expo-secure-store";
import * as Location from 'expo-location';
import { NavigationContainer } from "@react-navigation/native";
import HomeNavigator from "./Navigation/HomeNavigator";
import { useEffect, useState } from "react";
import { UserLocationContext } from "./context/UserLocationContext";
const tokenCache = {
async getToken(key){
try {
return SecureStore.getItemAsync(key);
} catch (error) {
return null;
}
},
async saveToken(key, value){
try {
return SecureStore.setItemAsync(key, value);
} catch (error) {
return;
}
},
}
export default function Index() {
const PUBLIC_KEY = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
//use current location
const [location, setLocation] = useState(null);
const [errorMsg, setErrorMsg] = useState(null);
useEffect(() => {
(async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('Permission to access location was denied');
return;
}
let location = await Location.getCurrentPositionAsync({});
setLocation(location.coords);
})();
}, []);
let text = 'Waiting..';
if (errorMsg) {
text = errorMsg;
} else if (location) {
text = JSON.stringify(location);
}
return (
<ClerkProvider tokenCache={tokenCache} publishableKey={PUBLIC_KEY}>
<UserLocationContext.Provider value={{location, setLocation}}>
<View style={{flex: 1, backgroundColor: 'white'}}>
<StatusBar style="auto"/>
<SignedIn>
<NavigationContainer independent={true}>
<HomeNavigator />
</NavigationContainer>
</SignedIn>
<SignedOut>
<Login/>
</SignedOut>
</View>
</UserLocationContext.Provider>
</ClerkProvider>
);
}

View file

@ -0,0 +1,26 @@
import axios from 'axios'
const BASE_URL = 'https://places.googleapis.com/v1/places:searchNearby';
const API_KEY = process.env.EXPO_PUBLIC_GOOGLE_PLACE_API_KEY;
const config = {
headers:{
'Content-Type': 'application/json',
'X-Goog-Api-Key': API_KEY,
'X-Goog-FieldMask': [
'places.displayName',
'places.formattedAddress',
'places.location',
'places.evChargeOptions',
'places.photos',
'places.id'
]
}
}
const NewNearByPlace = (data) => axios.post(BASE_URL, data, config).catch(function (error) {
console.log(error.toJSON());
});
;
export default{NewNearByPlace, API_KEY}

View file

@ -0,0 +1,19 @@
import { Image, StyleSheet, Text, View } from 'react-native'
import React from 'react'
import { useUser } from '@clerk/clerk-expo';
const HomeHeader = () => {
const {user} = useUser();
return (
<View style={{backgroundColor: 'white', paddingHorizontal: 20, paddingVertical: 10}}>
<View style={{flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center'}}>
<Image source={require('../../assets/images/ev_images/EV.png')} style={{width: 120, height: 60, objectFit: 'contain'}}/>
<Image source={{uri: user?.imageUrl}} style={{width: 40, height: 40, borderRadius: '50%'}}/>
</View>
</View>
)
}
export default HomeHeader
const styles = StyleSheet.create({})

View file

@ -0,0 +1,62 @@
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
import React, { useContext, useEffect, useState } from 'react';
import MapViewScreen from "../screen/mapView/MapViewScreen";
import HomeHeader from './HomeHeader';
import HomeSearch from './HomeSearch';
import { UserLocationContext } from '../context/UserLocationContext';
import GlobalApi from '../misc/GlobalApi';
import EVListView from '../screen/googleList/EVListView';
import { MarkerContext } from '../context/MakerContext';
const HomeScreen = () => {
const {location, setLocation} = useContext(UserLocationContext);
const [placeList, setPlaceList] = useState([]);
const [selectedMarker, setSelectedMarker] = useState('');
useEffect(() => {
location && GetNearByPlace();
},[location])
const GetNearByPlace = () => {
const data = {
"includedTypes": ["electric_vehicle_charging_station"],
"maxResultCount": 10,
"locationRestriction": {
"circle": {
"center": {
"latitude": location?.latitude,
"longitude": location?.longitude
},
"radius": 5000.0
}
}
}
GlobalApi.NewNearByPlace(data).then(res => {
console.log(JSON.stringify(res.data));
setPlaceList(res.data?.places);
})
}
return (
<MarkerContext.Provider value={{selectedMarker, setSelectedMarker}}>
<View style={{flex: 1, backgroundColor: 'white'}}>
<View style={{position: 'absolute', zIndex: 10, width: '100%'}}>
<HomeHeader />
<HomeSearch searchedLocation={(location) => setLocation({latitude: location.lat, longitude: location.lng})}/>
</View>
{placeList && <MapViewScreen placeList={placeList}/>}
<View style={{position: 'absolute', bottom: 0, zIndex: 20, width: '100%'}}>
{placeList && <EVListView placeList={placeList}/>}
</View>
</View>
</MarkerContext.Provider>
)
}
export default HomeScreen
const styles = StyleSheet.create({})

View file

@ -0,0 +1,34 @@
import { StyleSheet, Text, View } from 'react-native'
import React from 'react'
import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
const mapApiKey = process.env.EXPO_PUBLIC_GOOGLE_PLACE_API_KEY;
const HomeSearch = ({searchedLocation}) => {
return (
<View style={{marginVertical: 10, marginHorizontal: 10, borderRadius: 5}}>
<GooglePlacesAutocomplete
placeholder='Search EV charging station'
textInputProps={{
placeholderTextColor: '#45474B'
}}
enablePoweredByContainer={false}
fetchDetails={true}
onPress={(data, details = null) => {
searchedLocation(details?.geometry?.location)
}}
query={{
key: mapApiKey,
language: 'en',
}}
onFail={error => console.error(error)}
/>
</View>
)
}
export default HomeSearch
const styles = StyleSheet.create({})

View file

@ -0,0 +1,58 @@
import { ActivityIndicator, FlatList, StyleSheet, Text, View } from 'react-native'
import { collection, query, where, getDocs, getFirestore } from "firebase/firestore";
import React, { useEffect, useState } from 'react'
import { useUser } from '@clerk/clerk-expo';
import { app } from '../misc/FirebaseConfig';
import ItemList from './googleList/ItemList';
const SaveScreen = () => {
const {user} = useUser();
const [saveList, setSaveList] = useState([]);
const [isLoading, setIsLoading] = useState(false);
db = getFirestore(app);
useEffect(() => {
user && getSaveList();
}, [user])
const getSaveList = async() => {
setIsLoading(true);
setSaveList([])
const q = query(collection(db, "ev-save"), where("email", "==", user?.primaryEmailAddress?.emailAddress));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
setSaveList(saveList => [...saveList, doc.data()]);
setIsLoading(false);
});
}
return (
<View style={{flex: 1, backgroundColor: 'white'}}>
{!saveList?
<View style={{flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%'}}>
<ActivityIndicator size={'large'} color={'#379777'}/>
<Text style={{marginTop: 10}}>Loading</Text>
</View> : null
}
<FlatList
onRefresh={() => getSaveList()}
refreshing={isLoading}
data={saveList}
renderItem={({item, index}) => (
<View key={index} style={{marginVertical: 5, marginHorizontal: 5, borderColor: 'gray', borderWidth: 1}}>
<ItemList place={item.place} isSave={true} markedSave={() => getSaveList()}/>
</View>
)}
/>
</View>
)
}
export default SaveScreen
const styles = StyleSheet.create({})

View file

@ -0,0 +1,71 @@
import { Dimensions, FlatList, StyleSheet, Text, View } from 'react-native'
import React, { useContext, useEffect, useRef, useState } from 'react'
import ItemList from './ItemList'
import { MarkerContext } from '@/app/context/MakerContext';
import { collection, query, where, getDocs, getFirestore } from "firebase/firestore";
import { app } from '@/app/misc/FirebaseConfig';
import { useUser } from '@clerk/clerk-expo';
const EVListView = ({placeList}) => {
const {user} = useUser();
const [saveList, setSaveList] = useState([]);
const flatListRef = useRef(null);
const {selectedMarker, setSelectedMarker} = useContext(MarkerContext);
useEffect(() => {
selectedMarker && scrollToIndex(selectedMarker);
}, [selectedMarker])
const scrollToIndex = (index) => {
flatListRef.current?.scrollToIndex({animated: true, index})
}
const getItemLayout = (_,index) => ({
length: Dimensions.get('window').width,
offset: Dimensions.get('window').width*index,
index
});
const db = getFirestore(app);
useEffect(() => {
user && getSaveList();
}, [user])
const getSaveList = async() => {
setSaveList([])
const q = query(collection(db, "ev-save"), where("email", "==", user?.primaryEmailAddress?.emailAddress));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
setSaveList(saveList => [...saveList, doc.data()]);
});
}
const isSave = (place) => {
const result = saveList.find(item => item.place.id == place.id);
return result?true : false;
}
return (
<View>
<FlatList horizontal={true}
getItemLayout={getItemLayout}
data={placeList}
showsHorizontalScrollIndicator={false}
ref={flatListRef}
renderItem={({item, index}) => (
<View key={index}>
<ItemList place={item} isSave={isSave(item)} markedSave={() => getSaveList()}/>
</View>
)}
/>
</View>
)
}
export default EVListView
const styles = StyleSheet.create({})

View file

@ -0,0 +1,66 @@
import { Dimensions, Image, Linking, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
import React from 'react'
import GlobalApi from '@/app/misc/GlobalApi'
import AntDesign from '@expo/vector-icons/AntDesign';
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
import { getFirestore } from "firebase/firestore";
import { app } from "../../misc/FirebaseConfig";
import { doc, setDoc, deleteDoc} from "firebase/firestore";
import { useUser } from '@clerk/clerk-expo';
const ItemList = ({place, isSave, markedSave}) => {
const placePhoto_Base_Url = 'https://places.googleapis.com/v1/'
const {user} = useUser();
const db = getFirestore(app);
const onSave = async(place) => {
await setDoc(doc(db, 'ev-save', (place.id).toString()), {
place: place,
email: user?.primaryEmailAddress?.emailAddress
});
markedSave();
};
const onRemoveSave = async(placeId) => {
await deleteDoc(doc(db, 'ev-save', placeId.toString()));
markedSave();
}
const onStartClick = () => {
const url = Platform.select({
ios: "maps:"+place?.location?.latitude+","+place?.location?.longitude+"?q="+place?.formattedAddress,
});
Linking.openURL(url);
}
return (
<View style={{width: Dimensions.get('screen').width *0.92, height: 280, backgroundColor: 'white', marginLeft: 10, marginVertical: 5 ,borderRadius: 5, flexDirection: 'column', justifyContent: 'space-between'}}>
{!isSave?
<Pressable onPress={() => onSave(place)}
style={{position: 'absolute', zIndex: 20, right: 0, margin: 10, width: 40, height: 40, backgroundColor: '#379777', alignItems: 'center', justifyContent: 'center', borderRadius: '50%'}}>
<AntDesign name="pluscircleo" size={30} color="white" />
</Pressable> :
<Pressable onPress={() => onRemoveSave(place.id)}
style={{position: 'absolute', zIndex: 20, right: 0, margin: 10, width: 40, height: 40, backgroundColor: '#379777', alignItems: 'center', justifyContent: 'center', borderRadius: '50%'}}>
<MaterialCommunityIcons name="map-marker-star" size={30} color="white" />
</Pressable>
}
<Image source={place?.photos?{uri:placePhoto_Base_Url+place.photos[0].name+"/media?key="+GlobalApi.API_KEY+"&maxHeightPx=500&maxWidthPx=500"}
: require('../../../assets/images/ev_images/EV.png')} style={{width: '100%', height: 120, objectFit: 'cover', borderTopLeftRadius: 5, borderTopRightRadius: 5}}/>
<View style={{padding: 10, flexDirection: 'column', justifyContent: 'space-between'}}>
<View>
<Text style={{fontSize: 20, fontWeight: 600, color: '#379777'}}>{place.displayName.text}</Text>
<Text style={{marginTop: 5, color: '#45474B', fontWeight: 600, fontSize: 15}}>Total chargers: {place?.evChargeOptions?.connectorCount}</Text>
<Text style={{marginVertical: 5, color: '#45474B', fontSize: 15}}>{place?.formattedAddress}</Text>
</View>
<Pressable onPress={() => onStartClick()}
style={{backgroundColor: '#379777', alignItems: 'center', justifyContent: 'center', padding: 5, marginTop: 10, borderRadius: 5}}>
<Text style={{color: 'white', fontSize: 18, fontWeight: 500}}>Start</Text>
</Pressable>
</View>
</View>
)
}
export default ItemList
const styles = StyleSheet.create({})

View file

@ -0,0 +1,42 @@
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
import React from 'react';
import *as WebBrowser from "expo-web-browser";
import { useOAuth } from '@clerk/clerk-expo';
import { useWarmUpBrowser } from '../../../hooks/warmUpBrowser'
WebBrowser.maybeCompleteAuthSession();
const Login = () => {
useWarmUpBrowser();
const {startOAuthFlow} = useOAuth({strategy: "oauth_google"})
const loginHandler = async() => {
try {
const {createdSessionId, signIn, signUp, setActive} = await startOAuthFlow();
if(createdSessionId){
setActive({session: createdSessionId});
}
} catch (error) {
console.error("oAuth error");
}
}
return (
<View style={{flex:1, justifyContent: 'center', alignItems: 'center'}}>
<Image source={require('../../../assets/images/ev_images/EV.png')}/>
<View style={{padding: 20, marginVertical: 10}}>
<Text style={{fontSize: 24, textAlign: 'center', paddingHorizontal: 5, fontWeight: 600, color: '#45474B'}}>Find reliable EV charging stations near you just one click</Text>
<Pressable onPress={loginHandler} style={{marginVertical: 50}}>
<View style={{justifyContent: 'center', alignItems: 'center', padding: 10, backgroundColor: '#379777', marginHorizontal: 100, borderRadius: 5}}>
<Text style={{fontSize: 20, fontWeight: 500, color: 'white'}}>Start</Text>
</View>
</Pressable>
</View>
</View>
)
}
export default Login
const styles = StyleSheet.create({})

View file

@ -0,0 +1,38 @@
import { Image, StyleSheet, Text, View } from 'react-native';
import React, { useContext } from 'react';
import MapView, { Marker, PROVIDER_DEFAULT} from 'react-native-maps';
import { UserLocationContext } from '@/app/context/UserLocationContext';
import MarkerList from '../mapView/MarkerList';
const MapViewScreen = ({placeList}) => {
const {location, setLocation} = useContext(UserLocationContext)
return location?.latitude &&(
<View>
<MapView style={{width: '100%', height: '100%'}}
provider={PROVIDER_DEFAULT}
region={{
latitude: location?.latitude,
longitude: location?.longitude,
latitudeDelta: 0.2,
longitudeDelta: 0.2
}}
>
{location? <Marker coordinate={{latitude: location?.latitude, longitude: location?.longitude}}>
<Image source={require('../../../assets/images/ev_images/car.png')} style={{width: 50, height: 50}}/>
</Marker> : null}
{placeList && placeList.map((item, index) => (
<MarkerList key={index} place={item} index={index}/>
))}
</MapView>
</View>
)
}
export default MapViewScreen
const styles = StyleSheet.create({})

View file

@ -0,0 +1,22 @@
import { Image, StyleSheet, Text, View } from 'react-native'
import React, { useContext } from 'react'
import { Marker } from 'react-native-maps'
import { MarkerContext } from '@/app/context/MakerContext'
const MarkerList = ({index, place}) => {
const {selectedMarker, setSelectedMarker} = useContext(MarkerContext);
return place && (
<Marker onPress={() => setSelectedMarker(index)}
coordinate={{
latitude: place.location?.latitude,
longitude: place.location?.longitude
}}>
<Image source={require('../../../assets/images/ev_images/charging.png')} style={{width: 50, height: 50}}/>
</Marker>
)
}
export default MarkerList
const styles = StyleSheet.create({})

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View file

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

View file

@ -0,0 +1,11 @@
import React, { useEffect } from 'react';
import *as WebBrowser from "expo-web-browser";
export const useWarmUpBrowser = () => {
React.useEffect(() => {
void WebBrowser.warmUpAsync();
return() => {
void WebBrowser.coolDownAsync();
}
}, [])
};

19557
ev_station/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

57
ev_station/package.json Normal file
View file

@ -0,0 +1,57 @@
{
"name": "ev_station",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest --watchAll",
"lint": "expo lint"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"@clerk/clerk-expo": "^2.0.0",
"@expo/vector-icons": "^14.0.2",
"@react-navigation/native": "^6.1.18",
"axios": "^1.7.2",
"expo": "~51.0.22",
"expo-constants": "~16.0.2",
"expo-font": "~12.0.9",
"expo-linking": "~6.3.1",
"expo-location": "~17.0.1",
"expo-router": "~3.5.18",
"expo-secure-store": "^13.0.2",
"expo-splash-screen": "~0.27.5",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7",
"expo-web-browser": "~13.0.3",
"firebase": "^10.12.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.3",
"react-native-gesture-handler": "~2.16.1",
"react-native-google-places-autocomplete": "^2.5.6",
"react-native-maps": "1.14.0",
"react-native-places-input": "^1.1.7",
"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.10"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/jest": "^29.5.12",
"@types/react": "~18.2.45",
"@types/react-test-renderer": "^18.0.7",
"jest": "^29.2.1",
"jest-expo": "~51.0.3",
"react-test-renderer": "18.2.0",
"typescript": "~5.3.3"
},
"private": true
}

View file

@ -0,0 +1,73 @@
#!/usr/bin/env node
/**
* This script is used to reset the project to a blank state.
* It moves the /app directory to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file.
* You can remove the `reset-project` script from package.json and safely delete this file after running it.
*/
const fs = require('fs');
const path = require('path');
const root = process.cwd();
const oldDirPath = path.join(root, 'app');
const newDirPath = path.join(root, 'app-example');
const newAppDirPath = path.join(root, 'app');
const indexContent = `import { Text, View } from "react-native";
export default function Index() {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text>Edit app/index.tsx to edit this screen.</Text>
</View>
);
}
`;
const layoutContent = `import { Stack } from "expo-router";
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" />
</Stack>
);
}
`;
fs.rename(oldDirPath, newDirPath, (error) => {
if (error) {
return console.error(`Error renaming directory: ${error}`);
}
console.log('/app moved to /app-example.');
fs.mkdir(newAppDirPath, { recursive: true }, (error) => {
if (error) {
return console.error(`Error creating new app directory: ${error}`);
}
console.log('New /app directory created.');
const indexPath = path.join(newAppDirPath, 'index.tsx');
fs.writeFile(indexPath, indexContent, (error) => {
if (error) {
return console.error(`Error creating index.tsx: ${error}`);
}
console.log('app/index.tsx created.');
const layoutPath = path.join(newAppDirPath, '_layout.tsx');
fs.writeFile(layoutPath, layoutContent, (error) => {
if (error) {
return console.error(`Error creating _layout.tsx: ${error}`);
}
console.log('app/_layout.tsx created.');
});
});
});
});

17
ev_station/tsconfig.json Normal file
View file

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

View file

@ -1,50 +1,9 @@
# Welcome to your Expo app 👋
# News application with React Native
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
This project is a news mobile application that fetches data from [News API](https://newsapi.org/), where users will be able to share and read the latest news from different sources.
## Get started
1. Install dependencies
```bash
npm install
```
2. Start the app
```bash
npx expo start
```
In the output, you'll find options to open the app in a
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
## Learn more
To learn more about developing your project with Expo, look at the following resources:
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Join the community
Join our community of developers creating universal apps.
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
## To start the project
* Download [Expo Go](https://expo.dev/go) on your device to launch the project.
* To start the project, run this command in your terminal `npx expo start --tunnel`
* Scan the QR code that shows in your terminal.
* For more information about Tunneling, please read [Expo](https://docs.expo.dev/more/expo-cli/#tunneling).

View file

@ -6,7 +6,6 @@ import HomeNavigator from './Navigation/HomeNavigator'
export default function Index() {
return (
<NavigationContainer independent={true}>
<KeyboardAvoidingView>
<View style={{height: '100%', width: '100%'}}>