Skip to content

Commit

Permalink
Finish adding saved setlists, persist saved setlist state, add list v…
Browse files Browse the repository at this point in the history
…iew for saved, snackbar upon saving setlist, typed selector fn
  • Loading branch information
dylmye committed Sep 11, 2024
1 parent b7c12d0 commit c1590b8
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 36 deletions.
35 changes: 23 additions & 12 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useColorScheme } from "react-native";
import { Provider } from "react-redux";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import {
Expand All @@ -12,8 +11,14 @@ import {
adaptNavigationTheme,
MD3DarkTheme as DarkPaperTheme,
MD3LightTheme as LightPaperTheme,
ActivityIndicator,
} from "react-native-paper";
import { useMaterial3Theme } from "@pchmn/expo-material3-theme";
import { Provider } from "react-redux";
import {
persistStore,
} from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";

import { store } from "../store";
import PaperNavigationBar from "../components/PaperNavigationBar";
Expand All @@ -25,25 +30,31 @@ const { LightTheme, DarkTheme } = adaptNavigationTheme({

const AppLayout = () => {
const systemTheme = useColorScheme();
const { theme: m3Theme } = useMaterial3Theme({ fallbackSourceColor: "#C8E6C9" });
const { theme: m3Theme } = useMaterial3Theme({
fallbackSourceColor: "#C8E6C9",
});

const paperTheme =
systemTheme === "dark"
? { ...DarkPaperTheme, colors: m3Theme.dark }
: { ...LightPaperTheme, colors: m3Theme.light };

const persistor = persistStore(store);

return (
<Provider store={store}>
<ThemeProvider value={systemTheme === "dark" ? DarkTheme : LightTheme}>
<PaperProvider theme={paperTheme}>
<StatusBar style="auto" />
<Stack
screenOptions={{
header: (props) => <PaperNavigationBar {...props} />,
}}
/>
</PaperProvider>
</ThemeProvider>
<PersistGate loading={<ActivityIndicator />} persistor={persistor}>
<ThemeProvider value={systemTheme === "dark" ? DarkTheme : LightTheme}>
<PaperProvider theme={paperTheme}>
<StatusBar style="auto" />
<Stack
screenOptions={{
header: (props) => <PaperNavigationBar {...props} />,
}}
/>
</PaperProvider>
</ThemeProvider>
</PersistGate>
</Provider>
);
};
Expand Down
20 changes: 15 additions & 5 deletions app/saved/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { Text } from "react-native-paper";
import { FlatList, View } from "react-native";
import { Stack } from "expo-router";

import { useAppSelector } from "../../hooks/store";
import { selectSavedSetlists } from "../../store/saved/slice";
import SetlistListItem from "../../components/SetlistListItem";
import NoSavedSetlistsCard from "../../components/NoSavedSetlistsCard";

/** List of saved setlists marked by user */
const SavedSetlists = () => {
const setlists = useAppSelector(selectSavedSetlists);

// @TODO: add view for saved setlists with save/unsave button, navigation to setlist, title

return (
<Text variant="bodyMedium">slay!</Text>
<View>
<Stack.Screen
options={{ title: "Saved setlists" }}
/>
<FlatList
data={setlists}
renderItem={({ item }) => <SetlistListItem {...item} showDate />}
ListEmptyComponent={<NoSavedSetlistsCard />}
/>
</View>
)
};

Expand Down
64 changes: 60 additions & 4 deletions app/setlist/[setlistId].tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { Share, StyleSheet, View } from "react-native";
import {
ActivityIndicator,
Appbar,
Divider,
FAB,
List,
Snackbar,
Text,
} from "react-native-paper";
import { Link, Stack, useLocalSearchParams } from "expo-router";
import { Link, router, Stack, useLocalSearchParams } from "expo-router";
import * as Linking from "expo-linking";
import { isAfter, parse } from "date-fns";
import { getNetworkStateAsync } from "expo-network";
import { Image } from "expo-image";
import { openBrowserAsync } from "expo-web-browser";

import { useGet10SetlistBySetlistIdQuery } from "../../store/services/setlistFm";
import SetlistEmptyCard from "../../components/SetlistEmptyCard";
import SetlistSectionList from "../../components/SetlistSectionList";
import SetlistMetadataList from "../../components/SetlistMetadataList";
import AddToPlaylistAppbarAction from "../../components/AddToPlaylistAppbarAction";
import { openBrowserAsync } from "expo-web-browser";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
import {
unsaveSetlistById,
selectSetlistIsSaved,
saveSetlist,
} from "../../store/saved/slice";

/** View for setlist set, metadata, links */
const SetlistDetails = () => {
const dispatch = useAppDispatch();
const { setlistId } = useLocalSearchParams<{ setlistId: string }>();

const { data: setlist, isLoading } = useGet10SetlistBySetlistIdQuery({
setlistId: setlistId!,
});
const [networkIsAvailable, setNetworkState] = useState(false);

const [savedSnackbarVisible, setSavedSnackbarVisible] = useState(false);
const isSaved = useAppSelector((store) =>
selectSetlistIsSaved(store, setlistId!),
);
const setlistEmpty = !isLoading && !setlist?.sets?.set?.length;
const setlistInPast =
setlist?.eventDate &&
Expand Down Expand Up @@ -126,6 +139,23 @@ const SetlistDetails = () => {
</View>
);

const toggleSaveState = useCallback(() => {
if (isSaved) {
dispatch(unsaveSetlistById(setlistId!));
setSavedSnackbarVisible(true);
return;
}
dispatch(
saveSetlist({
id: setlistId!,
artist: setlist?.artist,
eventDate: setlist?.eventDate,
venue: setlist?.venue,
}),
);
setSavedSnackbarVisible(true);
}, [isSaved, setlistId, setlist]);

useEffect(() => {
const setNetworkStatus = async () => {
const state = await getNetworkStateAsync();
Expand All @@ -143,7 +173,15 @@ const SetlistDetails = () => {
headerRight: () =>
setlist && (
<>
<Appbar.Action icon="star" accessibilityLabel="Add this setlist to your favourites" />
<Appbar.Action
icon={isSaved ? "star" : "star-outline"}
accessibilityLabel={
isSaved
? "This setlist is in your saved list"
: "Save this setlist to your saved list"
}
onPress={toggleSaveState}
/>
<AddToPlaylistAppbarAction
setlist={setlist}
show={!isLoading && networkIsAvailable && !setlistEmpty}
Expand Down Expand Up @@ -182,6 +220,24 @@ const SetlistDetails = () => {
style={styles.floatingButton}
/>
)}
<Snackbar
visible={savedSnackbarVisible}
onDismiss={() => setSavedSnackbarVisible(false)}
action={
isSaved
? {
label: "View",
onPress: () => {
router.navigate("/saved");
},
}
: undefined
}
>
{isSaved
? "Added to your saved setlists"
: "Removed from your saved setlists"}
</Snackbar>
</View>
);
};
Expand Down
31 changes: 31 additions & 0 deletions components/NoSavedSetlistsCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { StyleSheet, StyleProp, ViewStyle } from "react-native";
import { Card, Text } from "react-native-paper";

/** Message to display when no setlist data has been detected */
const NoSavedSetlistsCard = () => (
<Card style={styles.container} mode="contained">
<Card.Title
title="You haven't saved any setlists yet."
titleVariant="titleMedium"
titleStyle={styles.title}
titleNumberOfLines={0}
/>
<Card.Content>
<Text variant="bodyMedium">
Search for your favourite setlists, then add them by clicking on the {`\u2b50`} button.
</Text>
</Card.Content>
</Card>
);

const styles = StyleSheet.create({
container: {
marginHorizontal: 12,
},
title: {
fontWeight: "bold",
paddingTop: 8,
},
});

export default NoSavedSetlistsCard;
2 changes: 1 addition & 1 deletion components/SearchHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const SearchHeader = ({
loading,
}: SearchHeaderProps) => (
<View style={styles.container}>
<Appbar.BackAction onPress={() => router.back()} />
<Appbar.BackAction onPress={() => router.back()} isLeading />
<SetlistSearchbar
style={styles.searchInput}
initialQuery={initialQuery}
Expand Down
2 changes: 2 additions & 0 deletions hooks/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useDispatch, useSelector } from "react-redux";
import type { TypedUseSelectorHook } from "react-redux";
import type { RootState, AppDispatch } from "../store/types";
import { createSelector } from "@reduxjs/toolkit";

export const useAppDispatch: () => AppDispatch = useDispatch;
export const createAppSelector = createSelector.withTypes<RootState>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@lomray/react-native-apple-music": "^1.2.1",
"@pchmn/expo-material3-theme": "^1.3.2",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/elements": "^1.3.21",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
Expand Down Expand Up @@ -50,7 +51,8 @@
"react-native-screens": "3.31.1",
"react-native-svg": "15.2.0",
"react-native-vector-icons": "^10.0.2",
"react-redux": "^9.1.0"
"react-redux": "^9.1.0",
"redux-persist": "^6.0.0"
},
"devDependencies": {
"@babel/core": "^7.24.0",
Expand Down
39 changes: 31 additions & 8 deletions store/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,43 @@
import { configureStore } from "@reduxjs/toolkit";
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import {
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist";
import AsyncStorage from "@react-native-async-storage/async-storage";

import { setlistFmApi } from "./services/setlistFm";
import { spotifyApi } from "./services/spotify";
import { appleMusicApi } from "./services/appleMusic";
import { savedSlice } from "./saved/slice";

const persistConfig = {
key: 'root',
version: 1,
storage: AsyncStorage,
whitelist: ['saved'],
}

const rootReducer = combineReducers({
[setlistFmApi.reducerPath]: setlistFmApi.reducer,
[spotifyApi.reducerPath]: spotifyApi.reducer,
[appleMusicApi.reducerPath]: appleMusicApi.reducer,
saved: savedSlice.reducer,
});

export const store = configureStore({
reducer: {
[setlistFmApi.reducerPath]: setlistFmApi.reducer,
[spotifyApi.reducerPath]: spotifyApi.reducer,
[appleMusicApi.reducerPath]: appleMusicApi.reducer,
saved: savedSlice.reducer,
},
reducer: persistReducer(persistConfig, rootReducer),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
})
.concat(setlistFmApi.middleware)
.concat(spotifyApi.middleware)
.concat(appleMusicApi.middleware),
Expand Down
15 changes: 10 additions & 5 deletions store/saved/slice.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Setlist } from "../services/setlistFm";
import { parse } from "date-fns";
import { RootState } from "../types";
import { createAppSelector } from "../../hooks/store";

type PartialSetlist = Pick<Setlist, 'id' | 'artist' | 'eventDate'>;
type PartialSetlist = Pick<Setlist, 'id' | 'artist' | 'eventDate' | 'venue'>;

interface SavedState {
setlists: PartialSetlist[];
Expand All @@ -19,7 +20,7 @@ export const savedSlice = createSlice({
name: "saved",
initialState,
reducers: {
addSetlist: (state, action: PayloadAction<PartialSetlist>) => {
saveSetlist: (state, action: PayloadAction<PartialSetlist>) => {
if (state.setlistIds.includes(action.payload.id!)) {
return;
}
Expand All @@ -28,7 +29,7 @@ export const savedSlice = createSlice({
state.setlists.sort((a, b) => (parse(b.eventDate!, "d-M-y", new Date()).valueOf() - parse(a.eventDate!, "d-M-y", new Date()).valueOf()));
state.setlistIds = state.setlists.map(({ id }) => id!);
},
removeSetlistById: (state, action: PayloadAction<string>) => {
unsaveSetlistById: (state, action: PayloadAction<string>) => {
const indexToRemove = state.setlistIds.findIndex(x => x === action.payload);

if (indexToRemove === -1) {
Expand All @@ -47,8 +48,12 @@ export const savedSlice = createSlice({
}
});

export const { addSetlist, clearList, removeSetlistById } = savedSlice.actions;
export const { saveSetlist, clearList, unsaveSetlistById } = savedSlice.actions;

export const selectSavedSetlists = (state: RootState) => state.saved.setlists;
export const selectSetlistIsSaved = createAppSelector(
[(state) => state.saved.setlistIds, (_, setlistId: string) => setlistId],
(savedSetlistIds, currentId) => savedSetlistIds.includes(currentId)
);

export default savedSlice.reducer;
Loading

0 comments on commit c1590b8

Please sign in to comment.