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": {
|
"expo": {
|
||||||
"name": "matrix-frontend",
|
"name": "matrix",
|
||||||
"slug": "matrix-frontend",
|
"slug": "matrix",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
@@ -20,11 +20,10 @@
|
|||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"bundler": "metro",
|
"bundler": "metro",
|
||||||
"output": "static",
|
"output": "server",
|
||||||
"favicon": "./assets/images/favicon.png"
|
"favicon": "./assets/images/favicon.png"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
|
||||||
[
|
[
|
||||||
"expo-splash-screen",
|
"expo-splash-screen",
|
||||||
{
|
{
|
||||||
@@ -33,7 +32,8 @@
|
|||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-secure-store"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"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,
|
"developmentClient": true,
|
||||||
"distribution": "internal",
|
"distribution": "internal",
|
||||||
"env": {
|
"env": {
|
||||||
"EXPO_PUBLIC_API_URL": "https://led-matrix.onrender.com/api"
|
"EXPO_PUBLIC_API_URL": "https://starappeal.tech/api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"production": {
|
"production": {
|
||||||
"autoIncrement": true,
|
"autoIncrement": true,
|
||||||
"env": {
|
"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",
|
"name": "matrix-frontend",
|
||||||
"main": "expo-router/entry",
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||||
|
"main": "node_modules/expo/AppEntry.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
|
||||||
"android": "expo start --android",
|
"android": "expo start --android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo start --ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
"test": "jest --watchAll",
|
"test": "jest --watchAll",
|
||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
@@ -16,46 +18,51 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/ngrok": "^4.1.3",
|
"@expo/ngrok": "^4.1.3",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.4",
|
||||||
"@react-navigation/bottom-tabs": "^7.0.0",
|
"@react-navigation/bottom-tabs": "^7.1.3",
|
||||||
"@react-navigation/native": "^7.0.0",
|
"@react-navigation/native": "^7.0.13",
|
||||||
"axios": "^1.7.7",
|
"@react-navigation/stack": "^7.0.18",
|
||||||
"expo": "^52.0.10",
|
"axios": "^1.7.8",
|
||||||
"expo-auth-session": "^6.0.0",
|
"expo": "^52.0.14",
|
||||||
|
"expo-auth-session": "^6.0.1",
|
||||||
"expo-blur": "~14.0.1",
|
"expo-blur": "~14.0.1",
|
||||||
"expo-constants": "~17.0.3",
|
"expo-constants": "~17.0.3",
|
||||||
"expo-dev-client": "~5.0.4",
|
|
||||||
"expo-font": "~13.0.1",
|
"expo-font": "~13.0.1",
|
||||||
"expo-haptics": "~14.0.0",
|
"expo-haptics": "~14.0.0",
|
||||||
"expo-image": "~2.0.2",
|
"expo-image": "~2.0.3",
|
||||||
"expo-image-picker": "~16.0.3",
|
"expo-image-picker": "~16.0.3",
|
||||||
"expo-linking": "~7.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-splash-screen": "~0.29.13",
|
||||||
"expo-status-bar": "~2.0.0",
|
"expo-status-bar": "~2.0.0",
|
||||||
"expo-symbols": "~0.2.0",
|
"expo-symbols": "~0.2.0",
|
||||||
"expo-system-ui": "~4.0.3",
|
"expo-system-ui": "~4.0.5",
|
||||||
"expo-web-browser": "~14.0.1",
|
"expo-web-browser": "~14.0.1",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.2",
|
"react-native": "0.76.3",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.21.2",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-paper": "^5.12.5",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-reanimated": "~3.16.3",
|
||||||
"react-native-screens": "^4.0.0",
|
"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-web": "~0.19.13",
|
||||||
"react-native-webview": "13.12.2"
|
"react-native-webview": "13.12.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.26.0",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-test-renderer": "^18.3.0",
|
"@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",
|
"jest-expo": "~52.0.2",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
"react-test-renderer": "18.3.1",
|
"react-test-renderer": "18.3.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.7.2"
|
||||||
},
|
}
|
||||||
"private": true,
|
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
height: 440,
|
||||||
borderRadius: 18,
|
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 React from 'react';
|
||||||
import {Button} from 'react-native';
|
import {useSpotifyAuth} from '@/src/hooks/useSpotifyAuth';
|
||||||
import {useSpotifyAuth} from '@/hooks/useSpotifyAuth';
|
import {Token} from "@/src/services/RestService";
|
||||||
import {Token} from "@/services/RestService";
|
import ThemedButton from "@/src/components/themed/ThemedButton";
|
||||||
|
|
||||||
interface SpotifyAuthButtonProps {
|
interface SpotifyAuthButtonProps {
|
||||||
onAuthSuccess: (token: Token) => void;
|
onAuthSuccess: (token: Token) => void;
|
||||||
@@ -15,11 +15,11 @@ const SpotifyAuthButton = ({onAuthSuccess}: SpotifyAuthButtonProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<ThemedButton
|
||||||
disabled={!isReady}
|
|
||||||
title={'Login mit Spotify'}
|
|
||||||
onPress={promptAuth}
|
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 {useEffect} from "react";
|
||||||
import {makeRedirectUri, useAuthRequest} from "expo-auth-session";
|
import {makeRedirectUri, useAuthRequest} from "expo-auth-session";
|
||||||
import {RestService, Token} from "@/services/RestService";
|
import {RestService, Token} from "@/src/services/RestService";
|
||||||
|
|
||||||
const discovery = {
|
const discovery = {
|
||||||
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
|
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
|
||||||
@@ -34,7 +34,7 @@ export const useSpotifyAuth = (
|
|||||||
if (response?.type === 'success') {
|
if (response?.type === 'success') {
|
||||||
try {
|
try {
|
||||||
const {code} = response.params;
|
const {code} = response.params;
|
||||||
const token = (await RestService.exchangeSpotifyCodeForToken(code)).token;
|
const token = (await RestService.exchangeSpotifyCodeForToken(code,"TODO")).token;
|
||||||
console.log('Token:', token);
|
console.log('Token:', token);
|
||||||
onAuthSuccess(token);
|
onAuthSuccess(token);
|
||||||
} catch (error) {
|
} 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 axios from 'axios';
|
||||||
import {makeRedirectUri} from "expo-auth-session";
|
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 API_URL = process.env.EXPO_PUBLIC_API_URL;
|
||||||
const JWT_TOKEN = process.env.EXPO_PUBLIC_JWT_TOKEN;
|
|
||||||
|
|
||||||
export interface Token {
|
export interface Token {
|
||||||
access_token: string,
|
access_token: string,
|
||||||
@@ -14,9 +13,8 @@ export interface Token {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const RestService = {
|
const RestService = {
|
||||||
exchangeSpotifyCodeForToken: async (code: string) => {
|
exchangeSpotifyCodeForToken: async (code: string, jwtToken: string) => {
|
||||||
try {
|
try {
|
||||||
const redirectUri = makeRedirectUri({
|
const redirectUri = makeRedirectUri({
|
||||||
scheme: 'led.matrix',
|
scheme: 'led.matrix',
|
||||||
@@ -25,7 +23,7 @@ const RestService = {
|
|||||||
const response = await axios.get<{ token: Token }>(
|
const response = await axios.get<{ token: Token }>(
|
||||||
`${API_URL}/spotify/token/generate/code/${code}/redirect-uri/${encodeURIComponent(redirectUri)}`, {
|
`${API_URL}/spotify/token/generate/code/${code}/redirect-uri/${encodeURIComponent(redirectUri)}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${JWT_TOKEN}`,
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -36,11 +34,11 @@ const RestService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchAllUser: async () => {
|
fetchAllUser: async (jwtToken: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<{ users: User[] }>(`${API_URL}/user`, {
|
const response = await axios.get<{ users: User[] }>(`${API_URL}/user`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${JWT_TOKEN}`,
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return response.data;
|
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 {
|
try {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${API_URL}/websocket/send-message`,
|
`${API_URL}/websocket/send-message`,
|
||||||
{users: [userId], payload},
|
{users: [userId], payload},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${JWT_TOKEN}`,
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -69,14 +81,14 @@ const RestService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
broadcast: async (payload: object) => {
|
broadcast: async (payload: object, jwtToken: String) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${API_URL}/websocket/broadcast`,
|
`${API_URL}/websocket/broadcast`,
|
||||||
{payload},
|
{payload},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${JWT_TOKEN}`,
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -87,6 +99,42 @@ const RestService = {
|
|||||||
throw error;
|
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};
|
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