Path based routing for index

This commit is contained in:
Niklas Korz 2024-08-22 19:43:04 +02:00
parent 581d28a6df
commit 9921da7265
10 changed files with 358 additions and 324 deletions

View file

@ -0,0 +1,29 @@
import React from "react";
import { Stack } from "expo-router";
import TaskNavigationBar from "@/components/navigation/TaskNavigationBar";
import ListNavigationBar from "@/components/navigation/ListNavigationBar";
export default function HomeLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{
header: (props) => <ListNavigationBar {...props} />,
}}
/>
<Stack.Screen
name="lists/[listId]"
options={{
header: (props) => <ListNavigationBar {...props} />,
}}
/>
<Stack.Screen
name="tasks/[taskId]"
options={{
header: (props) => <TaskNavigationBar {...props} />,
}}
/>
</Stack>
);
}

View file

@ -0,0 +1,3 @@
import ListContent from "./lists/[listId]";
export default ListContent;

View file

@ -0,0 +1,157 @@
import { StyleSheet, ScrollView, View } from "react-native";
import { Text, Avatar, Card, List, useTheme } from "react-native-paper";
import useSWR, { preload } from "swr";
import { ListData, listFetcher, taskFetcher } from "@/api/fetcher";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useEffect } from "react";
function ListItem(props: { data: ListData["lists"][0]; onPress(): void }) {
const { name, listCount, taskCount } = props.data;
let subtitle = "";
if (listCount) {
subtitle += `${listCount} lists`;
}
if (listCount && taskCount) {
subtitle += " • ";
}
if (taskCount) {
subtitle += `${taskCount} tasks`;
}
if (!listCount && !taskCount) {
subtitle += "Empty";
}
return (
<Card
style={{ marginHorizontal: 8, marginBottom: 8 }}
onPress={props.onPress}
>
<Card.Title
title={name}
subtitle={subtitle}
left={(props) => <Avatar.Icon {...props} icon="clipboard-list" />}
/*right={(props) => (
<IconButton {...props} icon="dots-vertical" onPress={() => {}} />
)}*/
/>
</Card>
);
}
function TaskItem(props: { data: ListData["tasks"][0]; onPress(): void }) {
const { id, name, schedule } = props.data;
const date = new Date(schedule);
const now = new Date();
const ready = date <= now;
return (
<List.Item
title={name}
description={ready ? "Ready" : "Not ready"}
left={(props) => <List.Icon {...props} icon="clipboard-check-outline" />}
onPress={props.onPress}
onPressIn={() => preload(id, taskFetcher)}
/>
);
}
export default function ListContent() {
const theme = useTheme();
const { listId, listName } = useLocalSearchParams<{
listId: string;
listName?: string;
}>();
const navigation = useNavigation();
const { isLoading, data, error } = useSWR(listId || "all", listFetcher);
useEffect(() => {
let title = "";
if (isLoading) {
title = listName || "Loading...";
} else if (data) {
title = data.name;
} else if (error) {
title = "Error";
}
navigation.setOptions({ title });
}, [isLoading, data?.name, error, navigation]);
if (isLoading) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Loading...</Text>
</View>
);
}
if (error || !data) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Something went wrong: {error.toString()}</Text>
</View>
);
}
const { name, parentId, lists, tasks } = data;
return (
<ScrollView
style={[styles.container, { backgroundColor: theme.colors.background }]}
>
{lists.map((l) => (
<ListItem
key={l.id}
data={l}
onPress={() =>
router.push({
pathname: "/lists/[listId]",
params: { listId: l.id, listName: l.name },
})
}
/>
))}
{tasks.length > 0 && (
<List.Section>
<List.Subheader>Tasks</List.Subheader>
{tasks.map((t) => (
<TaskItem
key={t.id}
data={t}
onPress={() =>
router.push({
pathname: "/tasks/[taskId]",
params: { taskId: t.id, taskName: t.name },
})
}
/>
))}
</List.Section>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingVertical: 8,
//backgroundColor: "#fff",
/*alignItems: "center",
justifyContent: "center",*/
},
padded: {
padding: 16,
flex: 1,
},
centered: {
alignItems: "center",
},
image: {
width: 300,
height: 300,
borderRadius: 15,
},
});

View file

@ -0,0 +1,99 @@
import { taskFetcher } from "@/api/fetcher";
import React, { useEffect } from "react";
import { useWindowDimensions, View, Image, StyleSheet } from "react-native";
import { useTheme, Text, Button } from "react-native-paper";
import useSWR from "swr";
import RenderHtml from "react-native-render-html";
import { useLocalSearchParams, useNavigation } from "expo-router";
export default function TaskContent() {
const dimensions = useWindowDimensions();
const theme = useTheme();
const { taskId, taskName } = useLocalSearchParams<{
taskId: string;
taskName?: string;
}>();
const { isLoading, data, error } = useSWR(taskId, taskFetcher);
const navigation = useNavigation();
useEffect(() => {
let title = "";
if (isLoading) {
title = taskName || "Loading...";
} else if (data) {
title = data.name;
} else if (error) {
title = "Error";
}
navigation.setOptions({ title });
}, [isLoading, data?.name, error, navigation]);
if (isLoading) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Loading...</Text>
</View>
);
}
if (error || !data) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Something went wrong: {error?.toString()}</Text>
</View>
);
}
const { name, description, schedule, imageSource } = data;
const date = new Date(schedule);
const now = new Date();
const ready = date <= now;
return (
<View
style={[
styles.padded,
styles.centered,
{ backgroundColor: theme.colors.background },
]}
>
<Image style={styles.image} source={{ uri: imageSource }} />
<RenderHtml
contentWidth={dimensions.width}
source={{ html: description }}
baseStyle={{
color: theme.colors.onBackground,
fontSize: "1.1em",
}}
/>
<Button disabled={!ready} mode="contained">
{ready ? "Start" : "Not ready"}
</Button>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingVertical: 8,
//backgroundColor: "#fff",
/*alignItems: "center",
justifyContent: "center",*/
},
padded: {
padding: 16,
flex: 1,
},
centered: {
alignItems: "center",
},
image: {
width: 300,
height: 300,
borderRadius: 15,
},
});

View file

@ -19,7 +19,7 @@ export default function TabLayout() {
}}
>
<Tabs.Screen
name="index"
name="(home)"
options={{
title: "Home",
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,

View file

@ -1,295 +0,0 @@
import {
StyleSheet,
ScrollView,
View,
useWindowDimensions,
Image,
} from "react-native";
import { Appbar, Text, Avatar, Card, List, useTheme, Button } from "react-native-paper";
import {
StackHeaderProps,
createStackNavigator,
} from "@react-navigation/stack";
import RenderHtml from "react-native-render-html";
import { HomeScreenNavigationProp } from "./types";
import useSWR, { preload } from "swr";
import { getHeaderTitle } from "@react-navigation/elements";
import { useEffect } from "react";
import { ListData, listFetcher, randomTask, taskFetcher } from "@/api/fetcher";
function ListItem(props: { data: ListData["lists"][0]; onPress(): void }) {
const { name, listCount, taskCount } = props.data;
let subtitle = "";
if (listCount) {
subtitle += `${listCount} lists`;
}
if (listCount && taskCount) {
subtitle += " • "
}
if (taskCount) {
subtitle += `${taskCount} tasks`;
}
if (!listCount && !taskCount) {
subtitle += "Empty";
}
return (
<Card
style={{ marginHorizontal: 8, marginBottom: 8 }}
onPress={props.onPress}
>
<Card.Title
title={name}
subtitle={subtitle}
left={(props) => <Avatar.Icon {...props} icon="clipboard-list" />}
/*right={(props) => (
<IconButton {...props} icon="dots-vertical" onPress={() => {}} />
)}*/
/>
</Card>
);
}
function TaskItem(props: { data: ListData["tasks"][0]; onPress(): void }) {
const { id, name, schedule } = props.data;
const date = new Date(schedule);
const now = new Date();
const ready = date <= now;
return (
<List.Item
title={name}
description={ready ? "Ready" : "Not ready"}
left={(props) => <List.Icon {...props} icon="clipboard-check-outline" />}
onPress={props.onPress}
onPressIn={() => preload(id, taskFetcher)}
/>
);
}
function ListContent({ route, navigation }: HomeScreenNavigationProp<"List">) {
const theme = useTheme();
const { listId, listName } = route.params ?? {};
const { isLoading, data, error } = useSWR(listId || "all", listFetcher);
useEffect(() => {
let title = "";
if (isLoading) {
title = listName || "Loading...";
} else if (data) {
title = data.name;
} else if (error) {
title = "Error";
}
navigation.setOptions({ title });
}, [isLoading, data?.name, error, navigation]);
if (isLoading) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Loading...</Text>
</View>
);
}
if (error || !data) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Something went wrong: {error.toString()}</Text>
</View>
);
}
const { name, parentId, lists, tasks } = data;
return (
<ScrollView
style={[styles.container, { backgroundColor: theme.colors.background }]}
>
{lists.map((l) => (
<ListItem
key={l.id}
data={l}
onPress={() =>
navigation.push("List", { listId: l.id, listName: l.name })
}
/>
))}
{tasks.length > 0 && (
<List.Section>
<List.Subheader>Tasks</List.Subheader>
{tasks.map((t) => (
<TaskItem
key={t.id}
data={t}
onPress={() =>
navigation.push("Task", { taskId: t.id, taskName: t.name })
}
/>
))}
</List.Section>
)}
</ScrollView>
);
}
function TaskContent({ route, navigation }: HomeScreenNavigationProp<"Task">) {
const dimensions = useWindowDimensions();
const theme = useTheme();
const { taskId, taskName } = route.params;
const { isLoading, data, error } = useSWR(taskId, taskFetcher);
useEffect(() => {
let title = "";
if (isLoading) {
title = taskName || "Loading...";
} else if (data) {
title = data.name;
} else if (error) {
title = "Error";
}
navigation.setOptions({ title });
}, [isLoading, data?.name, error, navigation]);
if (isLoading) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Loading...</Text>
</View>
);
}
if (error || !data) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Something went wrong: {error?.toString()}</Text>
</View>
);
}
const { name, description, schedule, imageSource } = data;
const date = new Date(schedule);
const now = new Date();
const ready = date <= now;
return (
<View
style={[
styles.padded,
styles.centered,
{ backgroundColor: theme.colors.background },
]}
>
<Image style={styles.image} source={{ uri: imageSource }} />
<RenderHtml
contentWidth={dimensions.width}
source={{ html: description }}
baseStyle={{
color: theme.colors.onBackground,
fontSize: "1.1em",
}}
/>
<Button disabled={!ready} mode="contained">{ready ? "Start" : "Not ready"}</Button>
</View>
);
}
function ListNavigationBar({
navigation,
route,
options,
back,
}: StackHeaderProps) {
const params = route.params as any | undefined;
const title = getHeaderTitle(options, params?.listName || route.name);
return (
<Appbar.Header elevated>
{(back || params?.listId) ? <Appbar.BackAction
onPress={() => (back ? navigation.pop() : navigation.replace("List"))}
/> : null}
<Appbar.Content title={title} />
<Appbar.Action
icon="dice-multiple-outline"
onPress={() =>
randomTask(params?.listId || null).then((data) =>
navigation.push("Task", { taskId: data.id, taskName: data.name }),
)
}
/>
{/*<Appbar.Action icon="magnify" onPress={() => {}} />*/}
</Appbar.Header>
);
}
function TaskNavigationBar({
navigation,
route,
options,
back,
}: StackHeaderProps) {
const title = getHeaderTitle(
options,
(route.params as any)?.taskName || route.name,
);
return (
<Appbar.Header elevated>
<Appbar.BackAction
onPress={() => (back ? navigation.pop() : navigation.replace("List"))}
/>
<Appbar.Content title={title} />
{/*<Appbar.Action icon="run-fast" onPress={() => {}} />*/}
{/*<Appbar.Action icon="square-edit-outline" onPress={() => {}} />*/}
</Appbar.Header>
);
}
const Stack = createStackNavigator();
export default function ScreenList() {
return (
<Stack.Navigator initialRouteName="List">
<Stack.Screen
name="List"
component={ListContent as any}
options={{
header: (props) => <ListNavigationBar {...props} />,
}}
/>
<Stack.Screen
name="Task"
component={TaskContent as any}
options={{
header: (props) => <TaskNavigationBar {...props} />,
}}
/>
</Stack.Navigator>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingVertical: 8,
//backgroundColor: "#fff",
/*alignItems: "center",
justifyContent: "center",*/
},
padded: {
padding: 16,
flex: 1,
},
centered: {
alignItems: "center",
},
image: {
width: 300,
height: 300,
borderRadius: 15,
},
});

View file

@ -1,26 +0,0 @@
import type {
NavigatorScreenParams,
ParamListBase,
} from "@react-navigation/native";
import type { StackScreenProps } from "@react-navigation/stack";
export interface RootBottomParamList extends ParamListBase {
Home: NavigatorScreenParams<HomeStackParamList>;
History: undefined;
Profile: undefined;
}
interface HomeStackParamList extends ParamListBase {
List: { listId: string | null, listName?: string };
Task: { taskId: string | null, taskName?: string };
}
export type HomeScreenNavigationProp<
RouteName extends keyof HomeStackParamList = keyof HomeStackParamList,
> = StackScreenProps<HomeStackParamList, RouteName>;
declare global {
namespace ReactNavigation {
interface RootParamList extends RootBottomParamList {}
}
}

View file

@ -1,10 +1,9 @@
import {
DarkTheme as NavigationDarkTheme,
DefaultTheme as NavigationDefaultTheme,
DefaultTheme,
ThemeProvider,
} from "@react-navigation/native";
import { PaperProvider, Text, adaptNavigationTheme } from "react-native-paper";
import { PaperProvider, adaptNavigationTheme } from "react-native-paper";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";

View file

@ -0,0 +1,40 @@
import { randomTask } from "@/api/fetcher";
import { getHeaderTitle } from "@react-navigation/elements";
import type { NativeStackHeaderProps } from "@react-navigation/native-stack";
import { router, useGlobalSearchParams } from "expo-router";
import { Appbar } from "react-native-paper";
export default function ListNavigationBar({
options,
route,
}: NativeStackHeaderProps) {
const { listId, listName } = useGlobalSearchParams<{
listId: string;
listName?: string;
}>();
const title = getHeaderTitle(options, listName || route.name);
const back = router.canGoBack();
return (
<Appbar.Header elevated>
{back || (route.path !== "/") ? (
<Appbar.BackAction
onPress={() => (back ? router.back() : router.replace("/"))}
/>
) : null}
<Appbar.Content title={title} />
<Appbar.Action
icon="dice-multiple-outline"
onPress={() =>
randomTask(listId || null).then((data) =>
router.push({
pathname: "/tasks/[taskId]",
params: { taskId: data.id, taskName: data.name },
})
)
}
/>
{/*<Appbar.Action icon="magnify" onPress={() => {}} />*/}
</Appbar.Header>
);
}

View file

@ -0,0 +1,28 @@
import { router, useGlobalSearchParams } from "expo-router";
import type { NativeStackHeaderProps } from "@react-navigation/native-stack";
import { Appbar } from "react-native-paper";
import { getHeaderTitle } from "@react-navigation/elements";
export default function TaskNavigationBar({
options,
route,
}: NativeStackHeaderProps) {
const { taskId, taskName } = useGlobalSearchParams<{
taskId: string;
taskName?: string;
}>();
const title = getHeaderTitle(options, taskName || route.name);
return (
<Appbar.Header elevated>
<Appbar.BackAction
onPress={() =>
router.canGoBack() ? router.back() : router.replace("/")
}
/>
<Appbar.Content title={title} />
{/*<Appbar.Action icon="run-fast" onPress={() => {}} />*/}
{/*<Appbar.Action icon="square-edit-outline" onPress={() => {}} />*/}
</Appbar.Header>
);
}