Commit 669f6c24 authored by Simen Kristoffersen's avatar Simen Kristoffersen
Browse files

Lagt til filtrering av filmer

parent 39ad8fea
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider } from 'react-redux';
import { persistStore } from 'redux-persist';
import { PersistGate } from 'redux-persist/integration/react';
import AppLoading from "expo-app-loading";
import { StatusBar } from "expo-status-bar";
import React, { useEffect, useState } from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Provider } from "react-redux";
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";
import useCachedResources from './hooks/useCachedResources';
import useColorScheme from './hooks/useColorScheme';
import Navigation from './navigation';
import configureStore from './store';
import useCachedResources from "./hooks/useCachedResources";
import useColorScheme from "./hooks/useColorScheme";
import Navigation from "./navigation";
import configureStore from "./store";
// Setup Redux store
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......@@ -20,7 +21,7 @@ export default function App() {
const colorScheme = useColorScheme();
if (!isLoadingComplete) {
return null;
return <AppLoading />;
} else {
return (
<SafeAreaProvider>
......
import React from "react";
import { StyleSheet } from "react-native";
import { View } from "../components/Themed";
import { Picker } from "@react-native-picker/picker";
import {
FilterKeys,
FilterValues,
Genre,
SortDirection,
SortKeys,
} from "../constants/filterOptions/interface";
import { genres, sortDirections, sortValues } from "../constants/filterOptions";
import { capitalize } from "../utils/textTransform";
export type FilterPaneProps = {
onFilterChange: (key: FilterKeys, value: FilterValues) => void;
onSortChange: (value: SortKeys) => void;
onSortDirectionChange: (value: SortDirection) => void;
};
const FilterPane: React.FC<FilterPaneProps> = ({
onFilterChange,
onSortChange,
onSortDirectionChange,
}: FilterPaneProps) => {
return (
<View style={styles.container}>
<View>
<Picker
style={styles.input}
onValueChange={(value: Genre) => onFilterChange("genres", [value])}
>
{genres.length > 0 &&
genres.map((genre) => (
<Picker.Item
key={genre}
label={capitalize(genre)}
value={genre.toLowerCase()}
/>
))}
</Picker>
</View>
<View style={styles.inlineRow}>
<Picker
style={styles.input}
onValueChange={(value: SortKeys) => onSortChange(value)}
>
{sortValues.length > 0 &&
sortValues.map((sortValue) => (
<Picker.Item
key={sortValue}
label={capitalize(sortValue)}
value={sortValue}
/>
))}
</Picker>
<Picker
style={styles.input}
onValueChange={(value: SortDirection) => onSortDirectionChange(value)}
>
{sortDirections.length > 0 &&
sortDirections.map((direction) => (
<Picker.Item
key={direction}
label={capitalize(direction)}
value={direction}
/>
))}
</Picker>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginTop: "3rem",
flex: 1,
paddingTop: "1rem",
paddingBottom: "1rem",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
},
inlineRow: {
display: "flex",
flexDirection: "row",
},
input: {
borderRadius: 15,
marginLeft: "0.25rem",
marginRight: "0.25rem",
padding: "0.25rem",
},
});
export default FilterPane;
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useDispatch, useSelector } from 'react-redux';
import { fetchMovies } from '../store/ducks/movies/actions';
import { FetchMovieParams, MovieEntity } from '../store/ducks/movies/types';
import { ApplicationState } from '../store/interface';
import MovieCard from './MovieCard';
import React, { useEffect, useState } from "react";
import { FlatList, StyleSheet, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useSelector } from "react-redux";
import { FetchMovieParams, MovieEntity } from "../store/ducks/movies/types";
import { ApplicationState } from "../store/interface";
import MovieCard from "./MovieCard";
const baseQuery: FetchMovieParams = {
perPage: 20,
page: 0,
orderBy: 'title',
order: 'desc',
filters: {
title: 'Al',
},
export type MovieTableProps = {
path: string;
query: FetchMovieParams;
movies: MovieEntity[];
currentPage: number;
onPageScroll: () => void;
};
const MovieTable = ({ path }: { path: string }): JSX.Element => {
const dispatch = useDispatch();
const MovieTable: React.FC<MovieTableProps> = ({
path,
query,
movies,
currentPage,
onPageScroll,
}: MovieTableProps) => {
const { data, documentCount, loading, error } = useSelector(
({ movies }: ApplicationState) => movies
);
const [movies, setMovies] = useState<MovieEntity[]>([]);
const [prevPageLoaded, setPageLoaded] = useState(0);
const fetchByPage = (page: number) => {
dispatch(fetchMovies({ ...baseQuery, page }));
};
// Fetch movies on mount
useEffect(() => {
dispatch(fetchMovies(baseQuery));
setPageLoaded(baseQuery.page);
}, []);
// Fetch movies on page change
useEffect(() => {
if (!error && !loading) {
// Ensure that we don't add duplicate movies to the array
const moviesToAdd = [...movies];
data.forEach((movie: MovieEntity) => {
if (!moviesToAdd.some((m) => m.id === movie.id)) {
moviesToAdd.push(movie);
}
});
setMovies([...moviesToAdd]);
}
}, [data]);
return (
<View style={styles.movieList}>
<FlatList
style={{ width: '100%', height: '100%' }}
columnWrapperStyle={{ flex: 1, justifyContent: 'space-around' }}
style={{ width: "100%", height: "100%" }}
columnWrapperStyle={{ flex: 1, justifyContent: "space-around" }}
contentContainerStyle={styles.movieItems}
data={movies}
numColumns={2}
......@@ -65,17 +38,12 @@ const MovieTable = ({ path }: { path: string }): JSX.Element => {
// Prevent bug where onEndReached is called multiple times
if (distanceFromEnd < 0) return;
// Prevent fetching if all movies are loaded
if (
prevPageLoaded + 1 >=
Math.ceil(documentCount / baseQuery.perPage)
)
return;
fetchByPage(prevPageLoaded + 1);
setPageLoaded(prevPageLoaded + 1);
if (currentPage >= Math.ceil(documentCount / query.perPage)) return;
// Dispatch scroll event to parent component
onPageScroll();
}}
onEndReachedThreshold={1.5}
initialNumToRender={baseQuery.perPage}
initialNumToRender={query.perPage}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => <MovieCard movie={item} />}
ListFooterComponent={() => (
......@@ -88,20 +56,20 @@ const MovieTable = ({ path }: { path: string }): JSX.Element => {
const styles = StyleSheet.create({
movieItems: {
justifyContent: 'center',
justifyContent: "center",
flexGrow: 1 / 2,
backgroundColor: 'white',
backgroundColor: "white",
},
movieList: {
marginTop: 4,
width: '95%',
height: '100%',
display: 'flex',
alignItems: 'center',
textAlign: 'center',
width: "95%",
height: "95%",
display: "flex",
alignItems: "center",
textAlign: "center",
},
loading: {
fontWeight: 'bold',
fontWeight: "bold",
marginBottom: 8,
},
});
......
......@@ -3,11 +3,11 @@
* https://docs.expo.io/guides/color-schemes/
*/
import * as React from 'react';
import { Text as DefaultText, View as DefaultView } from 'react-native';
import * as React from "react";
import { Text as DefaultText, View as DefaultView } from "react-native";
import Colors from '../constants/Colors';
import useColorScheme from '../hooks/useColorScheme';
import Colors from "../constants/Colors";
import useColorScheme from "../hooks/useColorScheme";
export function useThemeColor(
props: { light?: string; dark?: string },
......@@ -28,19 +28,22 @@ type ThemeProps = {
darkColor?: string;
};
export type TextProps = ThemeProps & DefaultText['props'];
export type ViewProps = ThemeProps & DefaultView['props'];
export type TextProps = ThemeProps & DefaultText["props"];
export type ViewProps = ThemeProps & DefaultView["props"];
export function Text(props: TextProps) {
const { style, lightColor, darkColor, ...otherProps } = props;
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
const color = useThemeColor({ light: lightColor, dark: darkColor }, "text");
return <DefaultText style={[{ color }, style]} {...otherProps} />;
}
export function View(props: ViewProps) {
const { style, lightColor, darkColor, ...otherProps } = props;
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
const backgroundColor = useThemeColor(
{ light: lightColor, dark: darkColor },
"background"
);
return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />;
}
import { Genre, SortDirection, SortKeys } from "./interface";
export const genres: Genre[] = [
"Action",
"Adventure",
"Comedy",
"Drama",
"Fantasy",
"Horror",
"Mystery",
"Romance",
"Science Fiction",
"Thriller",
"Western",
];
type DefaultValue = [string];
export const sortValues: SortKeys[] = ["", "title", "year", "rating"];
export const sortDirections: SortDirection[] = ["asc", "desc"];
import { MovieEntity } from "../../store/ducks/movies/types";
export type FilterInterval<T> = {
start?: T;
end?: T;
};
export type Filters = {
title?: string;
genres?: Genre[];
actors?: string[];
year?: FilterInterval<number>;
rating?: FilterInterval<number>;
};
export type FilterKeys = keyof Filters;
export type FilterValues = Filters[FilterKeys];
export type SortKeys =
| keyof Pick<MovieEntity, "title" | "year" | "rating">
| "";
export type SortDirection = "asc" | "desc";
export type Genre =
| "Action"
| "Comedy"
| "Drama"
| "Fantasy"
| "Horror"
| "Mystery"
| "Romance"
| "Thriller"
| "Western"
| "Science Fiction"
| "Adventure";
export enum Genre {
ACTION = "Action",
COMEDY = "Comedy",
DRAMA = "Drama",
FANTASY = "Fantasy",
HORROR = "Horror",
MYSTERY = "Mystery",
ROMANCE = "Romance",
THRILLER = "Thriller",
WESTERN = "Western",
SCI_FI = "Science Fiction",
ADVENTURE = "Adventure",
}
// export enum Genre {
// ACTION = "Action",
// COMEDY = "Comedy",
// DRAMA = "Drama",
// FANTASY = "Fantasy",
// HORROR = "Horror",
// MYSTERY = "Mystery",
// ROMANCE = "Romance",
// THRILLER = "Thriller",
// WESTERN = "Western",
// SCI_FI = "Science Fiction",
// ADVENTURE = "Adventure",
// }
export interface Pagination {
current: number;
pageSize: number;
total: number;
}
// export interface Pagination {
// current: number;
// pageSize: number;
// total: number;
// }
export interface Sort {
sortColumn: string;
sortOrder: SortOrder;
}
// export interface Sort {
// sortColumn: string;
// sortOrder: SortOrder;
// }
export enum SortOrder {
ASC = "asc",
DESC = "desc",
}
// export enum SortOrder {
// ASC = "asc",
// DESC = "desc",
// }
......@@ -4,36 +4,36 @@
* https://reactnavigation.org/docs/configuring-links
*/
import { LinkingOptions } from '@react-navigation/native';
import * as Linking from 'expo-linking';
import { LinkingOptions } from "@react-navigation/native";
import * as Linking from "expo-linking";
import { RootStackParamList } from '../types';
import { RootStackParamList } from "../types";
const linking: LinkingOptions<RootStackParamList> = {
prefixes: [Linking.makeUrl('/')],
prefixes: [Linking.makeUrl("/")],
config: {
screens: {
Root: {
screens: {
TabOne: {
screens: {
TabOneScreen: 'one',
TabOneScreen: "one",
},
},
TabTwo: {
screens: {
TabTwoScreen: 'two',
TabTwoScreen: "two",
},
},
MovieTableTab: {
screens: {
MovieTableTabScreen: 'movieTable',
MovieTableTabScreen: "movieTable",
},
},
},
},
Modal: 'modal',
NotFound: '*',
Modal: "modal",
NotFound: "*",
},
},
};
......
......@@ -3,33 +3,31 @@
* https://reactnavigation.org/docs/getting-started
*
*/
import { FontAwesome } from '@expo/vector-icons';
import { MaterialIcons } from '@expo/vector-icons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { FontAwesome } from "@expo/vector-icons";
import { MaterialIcons } from "@expo/vector-icons";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import {
NavigationContainer,
DefaultTheme,
DarkTheme,
} from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import * as React from 'react';
import { ColorSchemeName, Pressable } from 'react-native';
} from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import * as React from "react";
import { ColorSchemeName, Pressable } from "react-native";
import Colors from '../constants/Colors';
import useColorScheme from '../hooks/useColorScheme';
import ModalScreen from '../screens/ModalScreen';
import MovieTableScreen from '../screens/MovieTableScreen';
import NotFoundScreen from '../screens/NotFoundScreen';
import TabOneScreen from '../screens/TabOneScreen';
import Profile from '../screens/Profile';
import Colors from "../constants/Colors";
import useColorScheme from "../hooks/useColorScheme";
import ModalScreen from "../screens/ModalScreen";
import MovieTableScreen from "../screens/MovieTableScreen";
import NotFoundScreen from "../screens/NotFoundScreen";
import TabOneScreen from "../screens/TabOneScreen";
import Profile from "../screens/Profile";
import {
RootStackParamList,
RootTabParamList,
RootTabScreenProps,
} from '../types';
import LinkingConfiguration from './LinkingConfiguration';
} from "../types";
import LinkingConfiguration from "./LinkingConfiguration";
export default function Navigation({
colorScheme,
......@@ -38,8 +36,8 @@ export default function Navigation({
}) {
return (
<NavigationContainer
linking={LinkingConfiguration}
theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}
linking={LinkingConfiguration}
theme={colorScheme === "dark" ? DarkTheme : DefaultTheme}
>
<RootNavigator />
</NavigationContainer>
......@@ -59,13 +57,13 @@ function RootNavigator() {
name="Root"
component={BottomTabNavigator}
options={{ headerShown: false }}
/>
<Stack.Screen
/>
{/* <Stack.Screen
name="NotFound"
component={NotFoundScreen}
options={{ title: 'Oops!' }}
/>
<Stack.Group screenOptions={{ presentation: 'modal' }}>
/> */}
<Stack.Group screenOptions={{ presentation: "modal" }}>
<Stack.Screen name="Modal" component={ModalScreen} />
</Stack.Group>
</Stack.Navigator>
......@@ -79,28 +77,42 @@ function RootNavigator() {
const BottomTab = createBottomTabNavigator<RootTabParamList>();
function BottomTabNavigator() {
const colorScheme = useColorScheme();
const colorScheme = useColorScheme();
return (
<BottomTab.Navigator
initialRouteName="Movies"
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme].tint,
initialRouteName="Movies"
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme].tint,
}}
>
>
<BottomTab.Screen
name="Movies"
component={MovieTableScreen}
options={{
title: 'Movies',
tabBarIcon: ({color}) => <MaterialIcons name="movie" size={30} style={{ marginBottom: -3 }} color={color} />,
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 }) => <MaterialIcons name="person-outline" size={30} style={{ marginBottom: -3 }} color={color} />,
title: "Profile",
tabBarIcon: ({ color }) => (
<MaterialIcons
name="person-outline"
size={30}
style={{ marginBottom: -3 }}
color={color}
/>
),
headerRight: () => (
<Pressable
onPress={() =>