i forgor 💀
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
// https://docs.expo.dev/guides/using-eslint/
|
||||
module.exports = {
|
||||
extends: 'expo',
|
||||
ignorePatterns: ['/dist/*'],
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import {NavigationContainer} from "@react-navigation/native";
|
||||
|
||||
import {AuthProvider} from "@/src/context/AuthProvider";
|
||||
import {ThemeProvider} from "@/src/context/ThemeProvider";
|
||||
import AppNavigator from "@/src/core/AppNavigator";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<NavigationContainer>
|
||||
<AppNavigator />
|
||||
</NavigationContainer>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "matrix-frontend",
|
||||
"slug": "matrix-frontend",
|
||||
"name": "matrix",
|
||||
"slug": "matrix",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
@@ -20,11 +20,10 @@
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"output": "server",
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
@@ -33,7 +32,8 @@
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-secure-store"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import {Tabs} from 'expo-router';
|
||||
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
|
||||
|
||||
export default function TabLayout() {
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: '#ffd33d',
|
||||
headerStyle: {
|
||||
backgroundColor: '#25292e',
|
||||
},
|
||||
headerShadowVisible: false,
|
||||
headerTintColor: '#fff',
|
||||
tabBarStyle: {
|
||||
backgroundColor: '#25292e',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Home',
|
||||
tabBarIcon: ({color, focused}) => (
|
||||
<Ionicons name={focused ? 'home-sharp' : 'home-outline'} color={color} size={24}/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="about"
|
||||
options={{
|
||||
title: 'About',
|
||||
tabBarIcon: ({color, focused}) => (
|
||||
<Ionicons name={focused ? 'information-circle' : 'information-circle-outline'} color={color}
|
||||
size={24}/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import React, {useState} from "react";
|
||||
import SpotifyAuthButton from "@/components/SpotifyAuthButton";
|
||||
import {Token} from "@/services/RestService";
|
||||
|
||||
export default function AboutScreen() {
|
||||
const [token, setToken] = useState<Token | null>(null);
|
||||
|
||||
const handleAuthSuccess = (token: Token) => {
|
||||
setToken(token);
|
||||
console.log('Erhaltener Authentifizierungscode:', token);
|
||||
// Hier kannst du den Code weiterverwenden, um das Access Token zu erhalten
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>About screen</Text>
|
||||
<Text>Willkommen bei der Spotify Authentifizierung</Text>
|
||||
<SpotifyAuthButton onAuthSuccess={handleAuthSuccess}/>
|
||||
{token && <Text>Erhaltener Code: {token.access_token}</Text>}
|
||||
</View>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#25292e',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
import {ActivityIndicator, StyleSheet, Text, View} from 'react-native';
|
||||
import Button from '@/components/Button';
|
||||
import ImageViewer from '@/components/ImageViewer';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import {RestService} from '@/services/RestService';
|
||||
import useService from '@/hooks/useService';
|
||||
|
||||
const PlaceholderImage = require('@/assets/images/GarfieldCharakter.webp');
|
||||
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
WebBrowser.maybeCompleteAuthSession();
|
||||
|
||||
export default function Index() {
|
||||
const {data: usersData, loading: userLoading, error: userError} = useService(RestService.fetchAllUser);
|
||||
|
||||
const pickImageAsync = async () => {
|
||||
let result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ['images'],
|
||||
allowsEditing: true,
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
console.log(result);
|
||||
} else {
|
||||
alert('You did not select any image.');
|
||||
}
|
||||
};
|
||||
|
||||
console.log(usersData);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.imageContainer}>
|
||||
<ImageViewer imgSource={PlaceholderImage}/>
|
||||
</View>
|
||||
<View style={styles.footerContainer}>
|
||||
{userLoading && <ActivityIndicator/>}
|
||||
{userError && <Text>Error: {userError.message}</Text>}
|
||||
{usersData && (
|
||||
<Text>
|
||||
{usersData.users.map((item) => item.name).join('; ')}
|
||||
</Text>
|
||||
)}
|
||||
<Button label="Choose a photo" theme="primary" onPress={pickImageAsync}/>
|
||||
<Button label="Use this photo"/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#25292e',
|
||||
alignItems: 'center',
|
||||
},
|
||||
imageContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
footerContainer: {
|
||||
flex: 1 / 3,
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { Link, Stack } from 'expo-router';
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops! Not Found' }} />
|
||||
<View style={styles.container}>
|
||||
<Link href="/" style={styles.button}>
|
||||
Go back to Home screen!
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#25292e',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
button: {
|
||||
fontSize: 20,
|
||||
textDecorationLine: 'underline',
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
|
||||
import ThemedBackground from "../../src/components/themed/ThemedBackground";
|
||||
import Logo from "../../src/components/Logo";
|
||||
import ThemedHeader from "../../src/components/themed/ThemedHeader";
|
||||
import ThemedParagraph from "../../src/components/themed/ThemedParagraph";
|
||||
import ThemedButton from "../../src/components/themed/ThemedButton";
|
||||
import {useNavigation} from "@react-navigation/core";
|
||||
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
|
||||
export default function HomeScreen() {
|
||||
console.log("HALLO LEUTE");
|
||||
const navigation = useNavigation<any>();
|
||||
const {token, logout} = useAuth();
|
||||
|
||||
return (
|
||||
<ThemedBackground>
|
||||
<Logo/>
|
||||
<ThemedHeader>Welcome 💫</ThemedHeader>
|
||||
<ThemedParagraph>Congratulations you are logged in.</ThemedParagraph>
|
||||
<ThemedParagraph>{token}</ThemedParagraph>
|
||||
<ThemedButton
|
||||
mode="outlined"
|
||||
onPress={() => navigation.navigate("ProtectedScreen")}>
|
||||
OwO what's this?
|
||||
</ThemedButton>
|
||||
|
||||
<ThemedButton
|
||||
mode="outlined"
|
||||
onPress={async () => {
|
||||
await logout();
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{name: "LoginScreen"}],
|
||||
})
|
||||
}
|
||||
}
|
||||
>
|
||||
Sign out
|
||||
</ThemedButton>
|
||||
</ThemedBackground>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
import ThemedBackground from "../../src/components/themed/ThemedBackground";
|
||||
import Logo from "../../src/components/Logo";
|
||||
import ThemedHeader from "../../src/components/themed/ThemedHeader";
|
||||
import ThemedButton from "../../src/components/themed/ThemedButton";
|
||||
import ThemedTextInput from "../../src/components/themed/ThemedTextInput";
|
||||
import BackButton from "../../src/components/BackButton";
|
||||
|
||||
import {useNavigation} from "@react-navigation/core";
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
|
||||
|
||||
export default function LoginScreen() {
|
||||
const {isAuthenticated, login, logout, error} = useAuth();
|
||||
const navigation = useNavigation<any>();
|
||||
const [username, setUsername] = useState({value: ""});
|
||||
const [password, setPassword] = useState({value: ""});
|
||||
const {toggleTheme} = useTheme();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log(isAuthenticated);
|
||||
|
||||
if (isAuthenticated) {
|
||||
console.log("User ist eingeloggt, weiterleiten...");
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{name: "HomeScreen"}],
|
||||
});
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const onLoginPressed = async () => {
|
||||
console.log("Login wird ausgeführt...")
|
||||
await login(username.value, password.value);
|
||||
};
|
||||
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<ThemedBackground>
|
||||
<Logo/>
|
||||
<ThemedHeader>Du bist bereits eingeloggt. Was machst'n hier?</ThemedHeader>
|
||||
<ThemedButton mode="contained" onPress={logout}>
|
||||
Logout
|
||||
</ThemedButton>
|
||||
<ThemedButton mode="outlined" onPress={() => navigation.navigate("HomeScreen")}>
|
||||
Zurück
|
||||
</ThemedButton>
|
||||
</ThemedBackground>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedBackground>
|
||||
<BackButton goBack={navigation.goBack}/>
|
||||
<Logo/>
|
||||
<ThemedHeader>Hello.</ThemedHeader>
|
||||
<ThemedTextInput
|
||||
label="Username"
|
||||
returnKeyType="next"
|
||||
value={username.value}
|
||||
onChangeText={(text: string) => setUsername({value: text})}
|
||||
errorText={error?.id === "username" ? error?.message : ""}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<ThemedTextInput
|
||||
label="Password"
|
||||
returnKeyType="done"
|
||||
value={password.value}
|
||||
onChangeText={(text: string) => setPassword({value: text})}
|
||||
errorText={error?.id === "password" ? error?.message : ""}
|
||||
secureTextEntry
|
||||
/>
|
||||
<ThemedButton mode="outlined" onPress={onLoginPressed}>
|
||||
Log in
|
||||
</ThemedButton>
|
||||
<ThemedButton mode={"outlined"} onPress={toggleTheme}>
|
||||
Toggle Theme
|
||||
</ThemedButton>
|
||||
</ThemedBackground>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
import Logo from "@/src/components/Logo";
|
||||
import ThemedHeader from "@/src/components/themed/ThemedHeader";
|
||||
import ThemedParagraph from "@/src/components/themed/ThemedParagraph";
|
||||
import ThemedButton from "../../src/components/themed/ThemedButton";
|
||||
import {useNavigation} from "@react-navigation/core";
|
||||
import ThemedBackground from "@/src/components/themed/ThemedBackground";
|
||||
|
||||
export default function ProtectedScreen(): JSX.Element {
|
||||
const navigation = useNavigation<any>();
|
||||
const {token, logout} = useAuth();
|
||||
|
||||
return (
|
||||
<ThemedBackground>
|
||||
<Logo/>
|
||||
<ThemedHeader>Welcome 💫</ThemedHeader>
|
||||
<ThemedParagraph>Dies ist geheim. PSST !</ThemedParagraph>
|
||||
<ThemedParagraph>{token}</ThemedParagraph>
|
||||
<ThemedButton
|
||||
mode="outlined"
|
||||
onPress={async () => {
|
||||
await logout();
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{name: "LoginScreen"}],
|
||||
})
|
||||
}
|
||||
}
|
||||
>
|
||||
Sign out
|
||||
</ThemedButton>
|
||||
</ThemedBackground>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Executable → Regular
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 189 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@@ -1,61 +0,0 @@
|
||||
import { StyleSheet, View, Pressable, Text } from 'react-native';
|
||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
theme?: 'primary';
|
||||
onPress?: () => void;
|
||||
};
|
||||
|
||||
export default function Button({ label, theme, onPress }: Props) {
|
||||
if (theme === 'primary') {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.buttonContainer,
|
||||
{ borderWidth: 4, borderColor: '#ffd33d', borderRadius: 18 },
|
||||
]}>
|
||||
<Pressable
|
||||
style={[styles.button, { backgroundColor: '#fff' }]}
|
||||
onPress={onPress}>
|
||||
<FontAwesome name="picture-o" size={18} color="#25292e" style={styles.buttonIcon} />
|
||||
<Text style={[styles.buttonLabel, { color: '#25292e' }]}>{label}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Pressable style={styles.button} onPress={() => alert('You pressed a button.')}>
|
||||
<Text style={styles.buttonLabel}>{label}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
width: 320,
|
||||
height: 68,
|
||||
marginHorizontal: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 3,
|
||||
},
|
||||
button: {
|
||||
borderRadius: 10,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
buttonIcon: {
|
||||
paddingRight: 8,
|
||||
},
|
||||
buttonLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
@@ -8,7 +8,7 @@
|
||||
"developmentClient": true,
|
||||
"distribution": "internal",
|
||||
"env": {
|
||||
"EXPO_PUBLIC_API_URL": "https://led-matrix.onrender.com/api"
|
||||
"EXPO_PUBLIC_API_URL": "https://starappeal.tech/api"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"production": {
|
||||
"autoIncrement": true,
|
||||
"env": {
|
||||
"EXPO_PUBLIC_API_URL": "https://led-matrix.onrender.com/api"
|
||||
"EXPO_PUBLIC_API_URL": "https://staraooeal.tech/api"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
type AsyncCallback<T> = () => Promise<T>;
|
||||
|
||||
interface ServiceResult<T> {
|
||||
data: T | null,
|
||||
error: Error | null,
|
||||
loading: boolean,
|
||||
}
|
||||
|
||||
const useService = <T>(callback: AsyncCallback<T>): ServiceResult<T> => {
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const executeCallback = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await callback();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
executeCallback();
|
||||
}, [callback]);
|
||||
|
||||
return {data, loading, error};
|
||||
}
|
||||
|
||||
export default useService;
|
||||
@@ -1,16 +0,0 @@
|
||||
export default class User {
|
||||
constructor(
|
||||
public name: string,
|
||||
public uuid: string,
|
||||
public id: string,
|
||||
public config : UserConfig
|
||||
) {}
|
||||
}
|
||||
|
||||
export class UserConfig {
|
||||
constructor(
|
||||
public isVisible: boolean ,
|
||||
public canBeModified: boolean,
|
||||
public isAdmin: boolean
|
||||
) {}
|
||||
}
|
||||
Generated
+3921
-677
File diff suppressed because it is too large
Load Diff
+32
-25
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "matrix-frontend",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"test": "jest --watchAll",
|
||||
"lint": "expo lint"
|
||||
},
|
||||
@@ -16,46 +18,51 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/ngrok": "^4.1.3",
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@react-navigation/bottom-tabs": "^7.0.0",
|
||||
"@react-navigation/native": "^7.0.0",
|
||||
"axios": "^1.7.7",
|
||||
"expo": "^52.0.10",
|
||||
"expo-auth-session": "^6.0.0",
|
||||
"@expo/vector-icons": "^14.0.4",
|
||||
"@react-navigation/bottom-tabs": "^7.1.3",
|
||||
"@react-navigation/native": "^7.0.13",
|
||||
"@react-navigation/stack": "^7.0.18",
|
||||
"axios": "^1.7.8",
|
||||
"expo": "^52.0.14",
|
||||
"expo-auth-session": "^6.0.1",
|
||||
"expo-blur": "~14.0.1",
|
||||
"expo-constants": "~17.0.3",
|
||||
"expo-dev-client": "~5.0.4",
|
||||
"expo-font": "~13.0.1",
|
||||
"expo-haptics": "~14.0.0",
|
||||
"expo-image": "~2.0.2",
|
||||
"expo-image": "~2.0.3",
|
||||
"expo-image-picker": "~16.0.3",
|
||||
"expo-linking": "~7.0.3",
|
||||
"expo-router": "~4.0.8",
|
||||
"expo-router": "~4.0.11",
|
||||
"expo-secure-store": "~14.0.0",
|
||||
"expo-splash-screen": "~0.29.13",
|
||||
"expo-status-bar": "~2.0.0",
|
||||
"expo-symbols": "~0.2.0",
|
||||
"expo-system-ui": "~4.0.3",
|
||||
"expo-system-ui": "~4.0.5",
|
||||
"expo-web-browser": "~14.0.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.76.2",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "^4.0.0",
|
||||
"react-native": "0.76.3",
|
||||
"react-native-gesture-handler": "~2.21.2",
|
||||
"react-native-paper": "^5.12.5",
|
||||
"react-native-reanimated": "~3.16.3",
|
||||
"react-native-safe-area-context": "4.14.0",
|
||||
"react-native-screens": "^4.3.0",
|
||||
"react-native-status-bar-height": "^2.6.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-webview": "13.12.2"
|
||||
"react-native-webview": "13.12.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-test-renderer": "^18.3.0",
|
||||
"jest": "^29.2.1",
|
||||
"compression": "^1.7.5",
|
||||
"eslint-config-expo": "~8.0.1",
|
||||
"express": "^4.21.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~52.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path');
|
||||
const { createRequestHandler } = require('@expo/server/adapter/express');
|
||||
|
||||
const express = require('express');
|
||||
const compression = require('compression');
|
||||
const morgan = require('morgan');
|
||||
|
||||
const CLIENT_BUILD_DIR = path.join(process.cwd(), 'dist/client');
|
||||
const SERVER_BUILD_DIR = path.join(process.cwd(), 'dist/server');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(compression());
|
||||
|
||||
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// process.env.NODE_ENV = 'production';
|
||||
|
||||
app.use(
|
||||
express.static(CLIENT_BUILD_DIR, {
|
||||
maxAge: '1h',
|
||||
extensions: ['html'],
|
||||
})
|
||||
);
|
||||
|
||||
app.use(morgan('tiny'));
|
||||
|
||||
app.all(
|
||||
'*',
|
||||
createRequestHandler({
|
||||
build: SERVER_BUILD_DIR,
|
||||
})
|
||||
);
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Express server listening on port ${port}`);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
import NotAuthenticated from "@/src/components/NotAuthenticated";
|
||||
|
||||
const AuthenticatedWrapper: React.FC<{ children: React.ReactNode }> = ({children}) => {
|
||||
const {isAuthenticated} = useAuth();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <NotAuthenticated />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AuthenticatedWrapper;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import {Image, StyleSheet, TouchableOpacity} from "react-native";
|
||||
import {getStatusBarHeight} from "react-native-status-bar-height";
|
||||
|
||||
type Props = {
|
||||
goBack: () => void;
|
||||
}
|
||||
|
||||
export default function BackButton({goBack}: Props) {
|
||||
return (
|
||||
<TouchableOpacity onPress={goBack} style={styles.container}>
|
||||
<Image
|
||||
style={styles.image}
|
||||
source={require("../../assets/items/back.png")}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
top: 10 + getStatusBarHeight(),
|
||||
left: 4,
|
||||
},
|
||||
image: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
});
|
||||
@@ -15,4 +15,4 @@ const styles = StyleSheet.create({
|
||||
height: 440,
|
||||
borderRadius: 18,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { Image, StyleSheet } from "react-native";
|
||||
|
||||
export default function Logo() {
|
||||
return (
|
||||
<Image
|
||||
source={require("../../assets/items/logo.png")}
|
||||
style={styles.image}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
width: 110,
|
||||
height: 110,
|
||||
marginBottom: 8,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import {Image} from "react-native";
|
||||
import React from "react";
|
||||
import ThemedBackground from "@/src/components/themed/ThemedBackground";
|
||||
import ThemedParagraph from "@/src/components/themed/ThemedParagraph";
|
||||
import {useNavigation} from "@react-navigation/core";
|
||||
import ThemedButton from "@/src/components/themed/ThemedButton";
|
||||
|
||||
export default function NotAuthenticated() {
|
||||
const navigation = useNavigation<any>();
|
||||
return (
|
||||
<ThemedBackground>
|
||||
<Image
|
||||
source={require("@/assets/images/GarfieldCharakter.webp")}
|
||||
style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
/>
|
||||
<ThemedParagraph>
|
||||
You are not authenticated. Please log in to view this content.
|
||||
</ThemedParagraph>
|
||||
<ThemedButton mode="outlined" onPress={() => navigation.navigate("LoginScreen")}>
|
||||
Login
|
||||
</ThemedButton>
|
||||
</ThemedBackground>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import {Button} from 'react-native';
|
||||
import {useSpotifyAuth} from '@/hooks/useSpotifyAuth';
|
||||
import {Token} from "@/services/RestService";
|
||||
import {useSpotifyAuth} from '@/src/hooks/useSpotifyAuth';
|
||||
import {Token} from "@/src/services/RestService";
|
||||
import ThemedButton from "@/src/components/themed/ThemedButton";
|
||||
|
||||
interface SpotifyAuthButtonProps {
|
||||
onAuthSuccess: (token: Token) => void;
|
||||
@@ -15,11 +15,11 @@ const SpotifyAuthButton = ({onAuthSuccess}: SpotifyAuthButtonProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={!isReady}
|
||||
title={'Login mit Spotify'}
|
||||
<ThemedButton
|
||||
onPress={promptAuth}
|
||||
/>
|
||||
mode={"outlined"}>
|
||||
{isReady ? 'Sign in with Spotify' : 'Loading...'}
|
||||
</ThemedButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import {ImageBackground, KeyboardAvoidingView, StyleSheet,} from "react-native";
|
||||
import {useTheme} from "../../context/ThemeProvider";
|
||||
import {ThemeType} from "@/src/core/theme";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ThemedBackground({children}: Props) {
|
||||
const {theme} = useTheme();
|
||||
const styles = createStyles(theme);
|
||||
return (
|
||||
<ImageBackground
|
||||
source={require("../../../assets/items/dot.png")}
|
||||
resizeMode="repeat"
|
||||
style={styles.background}
|
||||
>
|
||||
<KeyboardAvoidingView style={styles.container} behavior="padding">
|
||||
{children}
|
||||
</KeyboardAvoidingView>
|
||||
</ImageBackground>
|
||||
);
|
||||
}
|
||||
|
||||
const createStyles = (theme: ThemeType) => {
|
||||
return StyleSheet.create({
|
||||
background: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
width: "100%",
|
||||
maxWidth: 340,
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {Button as PaperButton} from "react-native-paper";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
import {ThemeType} from "@/src/core/theme";
|
||||
|
||||
type Props = {
|
||||
mode: "text" | "outlined" | "contained";
|
||||
style?: any;
|
||||
children: React.ReactNode;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export default function ThemedButton({mode, style, ...props}: Props) {
|
||||
const {theme} = useTheme();
|
||||
const styles = createStyle(theme);
|
||||
return (
|
||||
<PaperButton
|
||||
style={[
|
||||
styles.button,
|
||||
mode === "outlined" && {backgroundColor: theme.colors.background},
|
||||
style,
|
||||
]}
|
||||
labelStyle={styles.text}
|
||||
mode={mode}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const createStyle =(theme:ThemeType) => {
|
||||
return StyleSheet.create({
|
||||
button: {
|
||||
width: "100%",
|
||||
marginVertical: 10,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
text: {
|
||||
fontWeight: "bold",
|
||||
fontSize: 15,
|
||||
lineHeight: 26,
|
||||
color: theme.colors.text,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {Text} from "react-native-paper";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
import {ThemeType} from "@/src/core/theme";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ThemedHeader(props: Props) {
|
||||
const {theme} = useTheme();
|
||||
const styles = createStyles(theme);
|
||||
return <Text style={styles.header} {...props} />;
|
||||
}
|
||||
|
||||
const createStyles = (theme: ThemeType) => {
|
||||
return StyleSheet.create({
|
||||
header: {
|
||||
fontSize: 21,
|
||||
color: theme.colors.primary,
|
||||
fontWeight: "bold",
|
||||
paddingVertical: 12,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {Text} from "react-native-paper";
|
||||
import {ThemeType} from "@/src/core/theme";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function ThemedParagraph(props: Props) {
|
||||
const {theme} = useTheme();
|
||||
const styles = createStyles(theme);
|
||||
return <Text style={styles.text} {...props} />;
|
||||
}
|
||||
|
||||
const createStyles = (theme: ThemeType) => {
|
||||
return StyleSheet.create({
|
||||
text: {
|
||||
color: theme.colors.text,
|
||||
fontSize: 15,
|
||||
lineHeight: 21,
|
||||
textAlign: "center",
|
||||
marginBottom: 12,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {ThemeType} from "@/src/core/theme";
|
||||
import {StyleSheet, Text} from "react-native";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
|
||||
const createStyles = (theme: ThemeType) => {
|
||||
return StyleSheet.create({
|
||||
text: {
|
||||
color: theme.colors.text,
|
||||
},
|
||||
});
|
||||
}
|
||||
export const ThemedText = ({children}: { children: React.ReactNode }) => {
|
||||
const {theme} = useTheme();
|
||||
const styles = createStyles(theme);
|
||||
return <Text style={styles.text}>{children}</Text>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import {StyleSheet, Text, View} from "react-native";
|
||||
import {TextInput as Input} from "react-native-paper";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
import {ThemeType} from "@/src/core/theme";
|
||||
|
||||
|
||||
type Props = {
|
||||
errorText?: string;
|
||||
description?: string;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export default function ThemedTextInput({errorText, description, ...props}: Props) {
|
||||
const {theme} = useTheme();
|
||||
const styles = createStyles(theme);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Input
|
||||
style={styles.input}
|
||||
selectionColor={theme.colors.primary}
|
||||
underlineColor="transparent"
|
||||
mode="outlined"
|
||||
{...props}
|
||||
/>
|
||||
{description && !errorText ? (
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
) : null}
|
||||
{errorText ? <Text style={styles.error}>{errorText}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const createStyles = (theme: ThemeType) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
marginVertical: 12,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
description: {
|
||||
fontSize: 13,
|
||||
color: theme.colors.secondary,
|
||||
paddingTop: 8,
|
||||
},
|
||||
error: {
|
||||
fontSize: 13,
|
||||
color: theme.colors.error,
|
||||
paddingTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {ThemeType} from "@/src/core/theme";
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
|
||||
const createStyles = (theme: ThemeType) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: theme.colors.background
|
||||
},
|
||||
});
|
||||
}
|
||||
export const ThemedView = ({children}: { children: React.ReactNode }) => {
|
||||
const {theme} = useTheme();
|
||||
const styles = createStyles(theme);
|
||||
return <View style={styles.container}>{children}</View>;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, {createContext, useContext, useEffect, useState} from "react";
|
||||
import {getFromStorage, JWT_TOKEN_KEY, removeFromStorage, saveInStorage} from "@/src/utils/secureStorage";
|
||||
import {RestService} from "@/src/services/RestService";
|
||||
|
||||
type AuthContextType = {
|
||||
isAuthenticated: boolean | null;
|
||||
token: string | null;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
error: { message: string; id: "username" | "password" } | null;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children}) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [error, setError] = useState<{ message: string; id: "username" | "password" } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
const storedToken = await getFromStorage(JWT_TOKEN_KEY);
|
||||
if (storedToken) {
|
||||
setToken(storedToken);
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuthStatus();
|
||||
}, []);
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
if (isAuthenticated) {
|
||||
console.log("Already authenticated");
|
||||
return;
|
||||
}
|
||||
const response = await RestService.login(username, password);
|
||||
if (!response.success) {
|
||||
console.error("Login failed:", response.message);
|
||||
setError({
|
||||
message: response.message,
|
||||
id: response.id,
|
||||
});
|
||||
setIsAuthenticated(false);
|
||||
return;
|
||||
}
|
||||
await saveInStorage(JWT_TOKEN_KEY, response.token);
|
||||
setToken(response.token);
|
||||
setIsAuthenticated(true);
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await removeFromStorage(JWT_TOKEN_KEY);
|
||||
setToken(null);
|
||||
setIsAuthenticated(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{isAuthenticated, token, login, logout, error}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, {createContext, ReactNode, useContext, useEffect, useState} from "react";
|
||||
import {getThemeType, themes, ThemeType} from "@/src/core/theme";
|
||||
import {getFromStorage, saveInStorage, THEME_KEY} from "@/src/utils/secureStorage";
|
||||
|
||||
type ThemeContextType = {
|
||||
theme: ThemeType;
|
||||
toggleTheme: () => void;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({children}) => {
|
||||
const [theme, setTheme] = useState<ThemeType>(themes.light);
|
||||
|
||||
useEffect(() => {
|
||||
const loadTheme = async () => {
|
||||
const storedTheme = await getFromStorage(THEME_KEY);
|
||||
if (storedTheme) {
|
||||
setTheme(getThemeType(storedTheme));
|
||||
}
|
||||
};
|
||||
loadTheme();
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === themes.light ? themes.dark : themes.light;
|
||||
saveInStorage(THEME_KEY, newTheme.name).then(() => setTheme(newTheme));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{theme, toggleTheme}}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {StyleSheet, Text, View} from "react-native";
|
||||
import {createStackNavigator} from "@react-navigation/stack";
|
||||
import LoginScreen from "@/app/screens/LoginScreen";
|
||||
import AuthenticatedWrapper from "@/src/components/AuthenticatedWrapper";
|
||||
import HomeScreen from "@/app/screens/HomeScreen";
|
||||
import ProtectedScreen from "@/app/screens/ProtectedScreen";
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const AppNavigator = () => {
|
||||
const {isAuthenticated} = useAuth(); // Auth-Status prüfen
|
||||
const [initialRoute, setInitialRoute] = useState<null | string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated === null) return;
|
||||
setInitialRoute(isAuthenticated ? "HomeScreen" : "LoginScreen");
|
||||
}, [isAuthenticated]);
|
||||
|
||||
if (initialRoute === null) {
|
||||
// Ladebildschirm während der Berechnung
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>Checking authentication...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Initial route:", initialRoute)
|
||||
console.log("isAuthenticated:", isAuthenticated)
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
initialRouteName={initialRoute}
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="LoginScreen" component={LoginScreen}/>
|
||||
<Stack.Screen name="HomeScreen">
|
||||
{() => (
|
||||
<AuthenticatedWrapper>
|
||||
<HomeScreen/>
|
||||
</AuthenticatedWrapper>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
<Stack.Screen name="ProtectedScreen">
|
||||
{() => (
|
||||
<AuthenticatedWrapper>
|
||||
<ProtectedScreen/>
|
||||
</AuthenticatedWrapper>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppNavigator;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#f2f2f2", // Optional: Anpassen an dein Theme
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 18,
|
||||
color: "#333",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
export type ThemeType = {
|
||||
colors: {
|
||||
background: string;
|
||||
text: string;
|
||||
primary: string;
|
||||
secondary: string;
|
||||
error: string;
|
||||
};
|
||||
spacing: (factor: number) => number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const lightTheme: ThemeType = {
|
||||
colors: {
|
||||
background: "#ffffff",
|
||||
text: "#000000",
|
||||
primary: "#6200ee",
|
||||
secondary: "#03dac6",
|
||||
error: "#b00020",
|
||||
},
|
||||
spacing: (factor: number) => factor * 8, // Beispiel für dynamische Abstände+
|
||||
name: "light",
|
||||
};
|
||||
|
||||
const darkTheme: ThemeType = {
|
||||
colors: {
|
||||
background: "#121212",
|
||||
text: "#ffffff",
|
||||
primary: "#bb86fc",
|
||||
secondary: "#03dac6",
|
||||
error: "#cf6679",
|
||||
},
|
||||
spacing: (factor: number) => factor * 8,
|
||||
name: "dark",
|
||||
};
|
||||
|
||||
export const themes = {light: lightTheme, dark: darkTheme};
|
||||
|
||||
export function getThemeType(theme: string): ThemeType {
|
||||
if (theme === "light") {
|
||||
return lightTheme;
|
||||
}
|
||||
return darkTheme;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
type AsyncCallback<T> = () => Promise<T>;
|
||||
const useService = <T>(callback: AsyncCallback<T>): { data: T | undefined; loading: boolean; error: Error | undefined } => {
|
||||
const [data, setData] = useState<T>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
useEffect(() => {
|
||||
const executeCallback = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await callback();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
executeCallback();
|
||||
}, [callback]);
|
||||
|
||||
return {data, loading, error};
|
||||
}
|
||||
|
||||
export default useService;
|
||||
@@ -1,6 +1,6 @@
|
||||
import {useEffect} from "react";
|
||||
import {makeRedirectUri, useAuthRequest} from "expo-auth-session";
|
||||
import {RestService, Token} from "@/services/RestService";
|
||||
import {RestService, Token} from "@/src/services/RestService";
|
||||
|
||||
const discovery = {
|
||||
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
|
||||
@@ -34,7 +34,7 @@ export const useSpotifyAuth = (
|
||||
if (response?.type === 'success') {
|
||||
try {
|
||||
const {code} = response.params;
|
||||
const token = (await RestService.exchangeSpotifyCodeForToken(code)).token;
|
||||
const token = (await RestService.exchangeSpotifyCodeForToken(code,"TODO")).token;
|
||||
console.log('Token:', token);
|
||||
onAuthSuccess(token);
|
||||
} catch (error) {
|
||||
@@ -0,0 +1,45 @@
|
||||
export interface User {
|
||||
name: string,
|
||||
uuid: string,
|
||||
_id: string,
|
||||
config: UserConfig,
|
||||
lastState: MatrixState,
|
||||
spotifyConfig: SpotifyConfig
|
||||
}
|
||||
|
||||
export interface UserConfig {
|
||||
isVisible: boolean,
|
||||
canBeModified: boolean,
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
export interface MatrixState {
|
||||
global: {
|
||||
mode: 'image' | 'text' | "idle" | "music" | "clock";
|
||||
brightness: number;
|
||||
};
|
||||
text: {
|
||||
text: string;
|
||||
align: 'left' | 'center' | 'right';
|
||||
speed: number;
|
||||
size: number;
|
||||
color: [number, number, number];
|
||||
};
|
||||
image: {
|
||||
image: string; // Der Name der Bilddatei
|
||||
};
|
||||
clock: {
|
||||
color: [number, number, number]; // RGB-Werte
|
||||
};
|
||||
music: {
|
||||
fullscreen: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SpotifyConfig {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expirationDate: Date;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import axios from 'axios';
|
||||
import {makeRedirectUri} from "expo-auth-session";
|
||||
import User from "@/model/User";
|
||||
import {User} from "@/src/model/User";
|
||||
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL;
|
||||
const JWT_TOKEN = process.env.EXPO_PUBLIC_JWT_TOKEN;
|
||||
|
||||
export interface Token {
|
||||
access_token: string,
|
||||
@@ -14,9 +13,8 @@ export interface Token {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const RestService = {
|
||||
exchangeSpotifyCodeForToken: async (code: string) => {
|
||||
exchangeSpotifyCodeForToken: async (code: string, jwtToken: string) => {
|
||||
try {
|
||||
const redirectUri = makeRedirectUri({
|
||||
scheme: 'led.matrix',
|
||||
@@ -25,7 +23,7 @@ const RestService = {
|
||||
const response = await axios.get<{ token: Token }>(
|
||||
`${API_URL}/spotify/token/generate/code/${code}/redirect-uri/${encodeURIComponent(redirectUri)}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${JWT_TOKEN}`,
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -36,11 +34,11 @@ const RestService = {
|
||||
}
|
||||
},
|
||||
|
||||
fetchAllUser: async () => {
|
||||
fetchAllUser: async (jwtToken: string) => {
|
||||
try {
|
||||
const response = await axios.get<{ users: User[] }>(`${API_URL}/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${JWT_TOKEN}`,
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
@@ -50,14 +48,28 @@ const RestService = {
|
||||
}
|
||||
},
|
||||
|
||||
sendPayloadToSocket: async (userId: string, payload: object) => {
|
||||
fetchUserById: async (id: string, jwtToken: string) => {
|
||||
try {
|
||||
const response = await axios.get<User>(`${API_URL}/user/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching user by id:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
sendPayloadToSocket: async (userId: string, payload: object, jwtToken: string) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${API_URL}/websocket/send-message`,
|
||||
{users: [userId], payload},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${JWT_TOKEN}`,
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
@@ -69,14 +81,14 @@ const RestService = {
|
||||
}
|
||||
},
|
||||
|
||||
broadcast: async (payload: object) => {
|
||||
broadcast: async (payload: object, jwtToken: String) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${API_URL}/websocket/broadcast`,
|
||||
{payload},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${JWT_TOKEN}`,
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
@@ -87,6 +99,42 @@ const RestService = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateUser:
|
||||
async (user: User, jwtToken: string) => {
|
||||
const {_id, ...rest} = user;
|
||||
try {
|
||||
const response = await axios.put<User>(
|
||||
`${API_URL}/user/${_id}`,
|
||||
rest,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error updating user:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
login: async (username: string, password: string) => {
|
||||
const response = await axios.post<{
|
||||
success: boolean, token: string, message: string,
|
||||
id: "username" | "password"
|
||||
}>(
|
||||
`${API_URL}/auth/login`, {
|
||||
username,
|
||||
password,
|
||||
}, {
|
||||
validateStatus: (status) => status === 200 || status === 401 || status === 404,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export {RestService};
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
const isWeb = Platform.OS === "web";
|
||||
|
||||
export const JWT_TOKEN_KEY = "jwtToken";
|
||||
export const THEME_KEY = "theme";
|
||||
|
||||
export const saveInStorage = async (key: string, value: string) => {
|
||||
if (isWeb) {
|
||||
localStorage.setItem(key, value); // Web: localStorage
|
||||
} else {
|
||||
await SecureStore.setItemAsync(key, value); // Mobile: SecureStore
|
||||
}
|
||||
};
|
||||
|
||||
export const getFromStorage = async (key: string): Promise<string | null> => {
|
||||
if (isWeb) {
|
||||
const item = localStorage.getItem(key);
|
||||
console.log("Item:", item);
|
||||
return item; // Web: localStorage
|
||||
} else {
|
||||
const item = await SecureStore.getItemAsync(key);
|
||||
console.log("Item:", item);
|
||||
return item; // Mobile: SecureStore
|
||||
}
|
||||
};
|
||||
|
||||
export const removeFromStorage = async (key: string) => {
|
||||
if (isWeb) {
|
||||
localStorage.removeItem(key); // Web: localStorage
|
||||
} else {
|
||||
await SecureStore.deleteItemAsync(key); // Mobile: SecureStore
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user