i forgor 💀

This commit is contained in:
2024-12-04 03:22:05 +01:00
parent 7f9f057a21
commit 97902ce502
47 changed files with 4919 additions and 1026 deletions
+5
View File
@@ -0,0 +1,5 @@
// https://docs.expo.dev/guides/using-eslint/
module.exports = {
extends: 'expo',
ignorePatterns: ['/dist/*'],
};
+18
View File
@@ -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>
);
}
+5 -5
View File
@@ -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
-42
View File
@@ -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>
);
}
-37
View File
@@ -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',
},
});
-65
View File
@@ -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',
},
});
-30
View File
@@ -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',
},
});
-10
View File
@@ -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>
);
}
+44
View File
@@ -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>
);
}
+84
View File
@@ -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>
);
}
+35
View File
@@ -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>
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File
BIN
View File
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

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

-61
View File
@@ -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,
},
});
+2 -2
View File
@@ -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"
}
}
},
-35
View File
@@ -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;
-16
View File
@@ -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
) {}
}
+3921 -677
View File
File diff suppressed because it is too large Load Diff
+32 -25
View File
@@ -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"
}
}
+41
View File
@@ -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}`);
});
+15
View File
@@ -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;
+30
View File
@@ -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,
},
});
});
+19
View File
@@ -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,
},
});
+28
View File
@@ -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",
},
});
}
+46
View File
@@ -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,
},
});
}
+26
View File
@@ -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,
},
});
}
+28
View File
@@ -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,
},
});
}
+16
View File
@@ -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>;
}
+55
View File
@@ -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,
},
});
}
+16
View File
@@ -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>;
}
+73
View File
@@ -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;
};
+43
View File
@@ -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;
};
+73
View File
@@ -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",
},
});
+44
View File
@@ -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;
}
+28
View File
@@ -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) {
+45
View File
@@ -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};
+35
View File
@@ -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
}
};