Commit 78c5001b authored by Peter Skaar Nordby's avatar Peter Skaar Nordby
Browse files

Merge branch '9-forbedre-profilsiden'

parents 058d4fab 63162c1b
......@@ -48,7 +48,7 @@ const styles = StyleSheet.create({
flex: 1,
marginHorizontal: 4,
minWidth: 175,
boxShadow: '0 2px 4px 0 rgba(0,0,0,0.2)',
//boxShadow: '0 2px 4px 0 rgba(0,0,0,0.2)',
borderRadius: 5,
textAlign: 'center',
},
......
......@@ -40,7 +40,6 @@ const MovieTable = ({ path }: { path: string }): JSX.Element => {
// Fetch movies on page change
useEffect(() => {
if (!error && !loading) {
console.log(movies);
setMovies([...movies, ...data]);
}
}, [data]);
......@@ -80,7 +79,7 @@ const styles = StyleSheet.create({
display: 'flex',
flex: 1,
flexDirection: 'column',
gap: 8,
//gap: 8,
},
loading: {
fontWeight: 'bold',
......
import * as WebBrowser from 'expo-web-browser';
import React from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { StyleSheet } from 'react-native';
import Colors from '../constants/Colors';
import { UserEntity } from '../store/ducks/auth/types';
import { MonoText } from './StyledText';
import { Text, View } from './Themed';
export default function ProfileInfo({ user }: { user: UserEntity }) {
return (
<View>
<View style={styles.getStartedContainer}>
<View style={styles.profileInfoContainer}>
<Text
style={styles.profileName}
style={styles.profileInfoText}
lightColor="rgba(0,0,0,0.8)"
darkColor="rgba(255,255,255,0.8)">
{user.username}
</Text>
<Text
style={styles.getStartedText}
style={styles.profileInfoText}
lightColor="rgba(0,0,0,0.8)"
darkColor="rgba(255,255,255,0.8)">
{user.email}
......@@ -29,14 +26,8 @@ export default function ProfileInfo({ user }: { user: UserEntity }) {
);
}
function handleHelpPress() {
WebBrowser.openBrowserAsync(
'https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet'
);
}
const styles = StyleSheet.create({
getStartedContainer: {
profileInfoContainer: {
alignItems: 'center',
marginHorizontal: 50,
},
......@@ -47,15 +38,10 @@ const styles = StyleSheet.create({
borderRadius: 3,
paddingHorizontal: 4,
},
profileName: {
fontSize: 20,
lineHeight: 24,
textAlign: 'center',
},
getStartedText: {
profileInfoText: {
fontSize: 17,
lineHeight: 24,
textAlign: 'center',
textAlign: 'left',
},
helpContainer: {
marginTop: 15,
......
import React from 'react';
import { Image, StyleSheet } from 'react-native';
import { Text, View } from '../components/Themed';
import { AirbnbRating } from 'react-native-ratings';
import { UserRating } from '../store/ducks/auth/types';
import { MovieEntity } from '../store/ducks/movies/types';
import { buildExecutionContext } from 'graphql/execution/execute';
interface UserRatingCardProps {
userRating: UserRating
}
/**
* Movie Card component
*/
const UserRatingCard = (props: UserRatingCardProps): JSX.Element => {
const { userRating } = props;
const { movie, rating, comment, date } = userRating;
return (
<View style={styles.card} lightColor="#eee" darkColor="rgba(255,255,255,0.1)">
<View style={styles.image}>
<Image
source={{
uri: movie.poster,
}}
style={styles.poster}
resizeMode="contain"
/>
</View>
<View style={styles.cardInfo}>
<AirbnbRating
count={5}
defaultRating={rating}
showRating={false}
isDisabled={true}
size={20}
/>
<Text style={styles.titleText} lightColor="rgba(0,0,0,0.8)" darkColor="rgba(255,255,255,0.8)">{movie.title}</Text>
<Text style={styles.comment} lightColor="rgba(0,0,0,0.8)" darkColor="rgba(255,255,255,0.8)">{comment}</Text>
<Text style={styles.date} lightColor="rgba(0,0,0,0.6)" darkColor="rgba(255,255,255,0.6)">{date.split('T')[0]}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
poster: {
aspectRatio: 300 / 443,
borderRadius: 5
},
card: {
borderRadius: 5,
textAlign: 'center',
flexDirection: 'row',
marginVertical: 10,
},
titleText: {
marginHorizontal: 5,
fontSize: 18,
fontWeight: 'bold',
},
comment: {
marginHorizontal: 5,
fontSize: 12,
},
date: {
marginHorizontal: 5,
fontSize: 12,
position: 'absolute',
bottom: 10,
left: 5
},
cardInfo: {
flex: 2,
textAlign: 'left',
},
image: {
flex: 1,
minWidth: 150
},
});
export default UserRatingCard;
import * as React from 'react';
import { useEffect, useState } from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { AirbnbRating } from 'react-native-ratings';
import { useDispatch, useSelector } from 'react-redux';
import { Text, View } from '../components/Themed';
import { Genre } from '../models/movieData.model';
import { getRatings } from '../store/ducks/auth/actions';
import { AuthState } from '../store/ducks/auth/types';
import { ApplicationState } from '../store/interface';
import UserRatingCard from './UserRatingCard';
export default function UserRatings(): JSX.Element {
const dispatch = useDispatch();
const auth: AuthState = useSelector(({ auth }: ApplicationState) => auth);
useEffect(() => {
if (auth.token) {
console.log(auth.token);
dispatch(getRatings({token: auth.token}));
}
}, []);
return (
<FlatList
contentContainerStyle={styles.movieList}
data={auth.ratings}
renderItem={({item}) => <UserRatingCard userRating={item} />}
keyExtractor={item => item.movie.id}
showsVerticalScrollIndicator={false}
/>
)
}
const styles = StyleSheet.create({
movieList: {
padding: 5,
textAlign: 'center',
},
item: {
padding: 5,
marginVertical: 8,
},
title: {
fontSize: 20,
textAlign: 'left',
paddingBottom: 10
},
});
\ No newline at end of file
......@@ -4,6 +4,7 @@
*
*/
import { FontAwesome } from '@expo/vector-icons';
import { MaterialIcons } from '@expo/vector-icons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import {
NavigationContainer,
......@@ -28,6 +29,8 @@ import {
} from '../types';
import LinkingConfiguration from './LinkingConfiguration';
export default function Navigation({
colorScheme,
}: {
......@@ -35,8 +38,8 @@ export default function Navigation({
}) {
return (
<NavigationContainer
linking={LinkingConfiguration}
theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}
linking={LinkingConfiguration}
theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}
>
<RootNavigator />
</NavigationContainer>
......@@ -56,12 +59,12 @@ function RootNavigator() {
name="Root"
component={BottomTabNavigator}
options={{ headerShown: false }}
/>
/>
<Stack.Screen
name="NotFound"
component={NotFoundScreen}
options={{ title: 'Oops!' }}
/>
/>
<Stack.Group screenOptions={{ presentation: 'modal' }}>
<Stack.Screen name="Modal" component={ModalScreen} />
</Stack.Group>
......@@ -76,44 +79,28 @@ function RootNavigator() {
const BottomTab = createBottomTabNavigator<RootTabParamList>();
function BottomTabNavigator() {
const colorScheme = useColorScheme();
const colorScheme = useColorScheme();
return (
<BottomTab.Navigator
initialRouteName="TabOne"
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme].tint,
initialRouteName="Movies"
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme].tint,
}}
>
>
<BottomTab.Screen
name="TabOne"
component={TabOneScreen}
options={({ navigation }: RootTabScreenProps<'TabOne'>) => ({
title: 'Tab One',
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
headerRight: () => (
<Pressable
onPress={() => navigation.navigate('Modal')}
style={({ pressed }) => ({
opacity: pressed ? 0.5 : 1,
})}
>
<FontAwesome
name="info-circle"
size={25}
color={Colors[colorScheme].text}
style={{ marginRight: 15 }}
/>
</Pressable>
),
})}
/>
name="Movies"
component={MovieTableScreen}
options={{
title: 'Movies',
tabBarIcon: ({color}) => <MaterialIcons name="movie" size={30} style={{ marginBottom: -3 }} color={color} />,
}}
/>
<BottomTab.Screen
name="Profile"
component={Profile}
options={{
title: 'Profile',
tabBarIcon: ({ color }) => <TabBarIcon name="user" color={color} />,
tabBarIcon: ({ color }) => <MaterialIcons name="person-outline" size={30} style={{ marginBottom: -3 }} color={color} />,
headerRight: () => (
<Pressable
onPress={() => 0}
......@@ -131,24 +118,6 @@ function BottomTabNavigator() {
),
}}
/>
<BottomTab.Screen
name="MovieTableTab"
component={MovieTableScreen}
options={{
title: 'Movie Table Tab',
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
}}
/>
</BottomTab.Navigator>
);
}
/**
* You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
*/
function TabBarIcon(props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) {
return <FontAwesome size={30} style={{ marginBottom: -3 }} {...props} />;
}
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as React from 'react';
import { StyleSheet } from 'react-native';
import { useSelector } from 'react-redux';
import { useEffect } from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import Login from '../components/LoginComponent';
import ProfileInfo from '../components/ProfileInfo';
import { Text, View } from '../components/Themed';
import UserRatings from '../components/UserRatings';
import { setToken } from '../store/ducks/auth/actions';
import { AuthState } from '../store/ducks/auth/types';
import { ApplicationState } from '../store/interface';
export default function Profile() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(setToken())
}, [])
const auth: AuthState = useSelector(({ auth }: ApplicationState) => auth);
if (!auth.loggedIn) {
return (
......@@ -36,11 +45,11 @@ const styles = StyleSheet.create({
justifyContent: 'center',
},
title: {
fontSize: 20,
fontSize: 30,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
marginVertical: 20,
height: 1,
width: '80%',
},
......
import { action } from 'typesafe-actions';
import { Dispatch } from 'react';
import { action, PayloadAction } from 'typesafe-actions';
import { ApolloClientAction, LocalAction } from '../../interface';
import { removeToken } from './helpers';
import { getToken, removeToken } from './helpers';
import { getUserRatingsQuery, giveRatingQuery, loginQuery, signupQuery } from './queries';
import {
AuthActionTypes,
GetUserRatingsParams,
RateMovieParams,
SetTokenParams,
SignInParams,
SignUpParams,
Token,
} from './types';
export const signIn = (params: SignInParams): ApolloClientAction =>
......@@ -16,6 +19,14 @@ export const signIn = (params: SignInParams): ApolloClientAction =>
query: loginQuery,
});
export const setToken = () => async (dispatch: Dispatch<PayloadAction<string, Token | undefined>>) => {
const token = await getToken();
dispatch({
type: AuthActionTypes.SET_TOKEN,
payload: token,
});
}
export const signUp = (params: SignUpParams): ApolloClientAction =>
action(AuthActionTypes.SIGN_UP.START, params, {
query: signupQuery,
......
import cookie from 'js-cookie';
import jwtDecode from 'jwt-decode';
import moment from 'moment';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { DecodedToken, EncodedToken, Token } from './types';
......@@ -10,39 +12,32 @@ const STORAGE_KEY = 'STORAGE';
* Stores a JWT Auth Token in the web storage
* @param {string} token the token to be stored
*/
export function saveToken(token: EncodedToken): void {
const twoDays: number = 3600 * 48000;
const decoded: DecodedToken = jwtDecode(token);
const expires = moment.unix(decoded.iat + twoDays);
cookie.set(STORAGE_KEY, token, {
path: '/',
expires: expires.toDate(),
});
export async function saveToken(token: EncodedToken): Promise<void> {
return await AsyncStorage.setItem(STORAGE_KEY, token);
}
/**
* Removes JWT Auth token from the web storage
*/
export function removeToken(): void {
return cookie.remove(STORAGE_KEY, { path: '/' });
export async function removeToken(): Promise<void> {
return await AsyncStorage.removeItem(STORAGE_KEY);
}
/**
* Retrieves JWT Auth Token from the web storage
*/
export function getToken(): Token | undefined {
const encodedToken = cookie.get(STORAGE_KEY);
if (!encodedToken) return;
export async function getToken(): Promise<Token | undefined> {
try {
const decoded: DecodedToken = jwtDecode(encodedToken);
return {
...decoded,
encodedToken,
};
} catch (err) {
console.log('No cookie found');
const encodedToken = await AsyncStorage.getItem(STORAGE_KEY)
if (encodedToken !== null) {
const decoded: DecodedToken = jwtDecode(encodedToken);
return {
...decoded,
encodedToken,
};
}
} catch (e) {
alert('Failed to fetch the data from storage')
}
}
......@@ -35,6 +35,7 @@ export const getUserRatingsQuery = gql`
movie {
id
title
poster
}
}
}
......
import { Action, PayloadAction, TypeConstant } from 'typesafe-actions';
import { Action, PayloadAction, TypeConstant } from "typesafe-actions";
import { getToken } from './helpers';
import { AuthState, AuthActionTypes } from './types';
import { getToken } from "./helpers";
import { AuthState, AuthActionTypes } from "./types";
// Fetch JWT-token from storage
const jwtToken = getToken();
......@@ -15,24 +15,26 @@ export const signedOutState: AuthState = {
ratings: [],
};
export const initialState: AuthState = jwtToken
? {
token: jwtToken.encodedToken,
user: {
id: jwtToken.id,
email: jwtToken.email,
username: jwtToken.username,
},
loggedIn: true,
error: null,
loading: false,
ratings: [],
}
: signedOutState;
export const initialState: AuthState = signedOutState;
// export const initialState: AuthState = jwtToken
// ? {
// token: jwtToken.encodedToken,
// user: {
// id: jwtToken.id,
// email: jwtToken.email,
// username: jwtToken.username,
// },
// loggedIn: true,
// error: null,
// loading: false,
// ratings: [],
// }
// : signedOutState;
export const authReducer = (
state: AuthState = initialState,
action: Action<TypeConstant> & PayloadAction<TypeConstant, any>,
action: Action<TypeConstant> & PayloadAction<TypeConstant, any>
): AuthState => {
switch (action.type) {
case AuthActionTypes.SIGN_IN.START:
......@@ -56,10 +58,25 @@ export const authReducer = (
return { ...state, ratings: action.payload, loading: false };
}
case AuthActionTypes.RATE_MOVIE.SUCCESS: {
return { ...state, ratings: [...state.ratings, action.payload], loading: false };
return {
...state,
ratings: [...state.ratings, action.payload],
loading: false,
};
}
case AuthActionTypes.SIGN_OUT:
return signedOutState;
case AuthActionTypes.SET_TOKEN:
return {
...state,
token: action.payload.encodedToken,
user: {
id: action.payload.userID,
email: action.payload.email,
username: action.payload.username,
},
loggedIn: !!action.payload,
};
case AuthActionTypes.SIGN_IN.ERROR:
case AuthActionTypes.SIGN_UP.ERROR:
case AuthActionTypes.GET_RATINGS.ERROR:
......
......@@ -22,11 +22,13 @@ export type UserRating = {
movie: {
id: string;
title: string;
poster: string;
};
date: string;
};
export const AuthActionTypes = {
SET_TOKEN: '@@auth.SET_TOKEN',
SIGN_IN: generateAsyncAction('@@auth.SIGN_IN'),
SIGN_UP: generateAsyncAction('@@auth.SIGN_UP'),
GET_RATINGS: generateAsyncAction('@@auth.GET_RATINGS'),
......@@ -34,6 +36,7 @@ export const AuthActionTypes = {
SIGN_OUT: '@@auth.SIGN_OUT',
};
export type SetTokenParams = { token: string;}
export type SignInParams = { email: string; password: string };
<