Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Setup boilerplate for the home screen #14 #35

Merged
merged 5 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { I18nProvider } from "@/common/i18n";
import { IdentityProvider } from "@/common/identity";
import { ProviderList, useFontLoader } from "@/common/util";
import { ThemeProvider } from "@/components/ThemeProvider";
import { View } from "@/components/Themed";

export { ErrorBoundary } from "expo-router";

Expand All @@ -20,7 +21,10 @@ export default function RootLayout() {
<ProviderList
providers={[I18nProvider, IdentityProvider, PaperProvider, ThemeProvider]}
>
<Stack />
{/* Fixes white flicker during navigation */}
<View style={{ flex: 1 }}>
<Stack />
</View>
</ProviderList>
);
}
5 changes: 3 additions & 2 deletions app/auth/login.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link, Stack } from "expo-router";
import { Link } from "expo-router";
import { Formik } from "formik";
import { useState } from "react";
import {
Expand All @@ -18,6 +18,7 @@ import {
import { POST } from "@/common/api";
import { useI18n } from "@/common/i18n";
import { useIdentity, RedirectIfLoggedIn } from "@/common/identity";
import { Header } from "@/components/Header";

const Page = () => {
const identity = useIdentity();
Expand Down Expand Up @@ -69,7 +70,7 @@ const Page = () => {
}) => (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={styles.container}>
<Stack.Screen options={{ title: t("login.pageTitle") }} />
<Header title={t("login.pageTitle")} />
<Text variant="titleLarge" style={styles.title}>
{t("login.description")}
</Text>
Expand Down
5 changes: 3 additions & 2 deletions app/auth/register.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link, Stack } from "expo-router";
import { Link } from "expo-router";
import { Formik } from "formik";
import { useState } from "react";
import {
Expand All @@ -20,6 +20,7 @@ import {
import { POST } from "@/common/api";
import { useI18n } from "@/common/i18n";
import { useIdentity, RedirectIfLoggedIn } from "@/common/identity";
import { Header } from "@/components/Header";

interface IRegisterForm {
email: string;
Expand Down Expand Up @@ -110,7 +111,7 @@ const Page = () => {
>
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={styles.container}>
<Stack.Screen options={{ title: t("register.pageTitle") }} />
<Header title={t("register.pageTitle")} />
<Text variant="titleLarge" style={styles.title}>
{t("register.description")}
</Text>
Expand Down
61 changes: 55 additions & 6 deletions app/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,72 @@
import { Button } from "react-native-paper";
import { useRouter } from "expo-router";
import { FlatList, StyleSheet } from "react-native";
import { FAB } from "react-native-paper";

import { actions, ActionKey } from "@/common/actions";
import { useI18n } from "@/common/i18n";
import { useIdentity, RedirectIfNoProfile } from "@/common/identity";
import { MonoText } from "@/components/StyledText";
import { DashboardGadget } from "@/components/DashboardGadget";
import { Header } from "@/components/Header";
import { View } from "@/components/Themed";

const mockDashboardGadgets: ActionKey[] = [
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Placeholder - this will be downloaded from the backend.

"logOut",
"profileList",
"profileList",
"logOut",
"logOut",
"profileList",
];

const Page = () => {
const { t } = useI18n();
const identity = useIdentity();
const router = useRouter();

if (!identity.hasProfile) {
return <RedirectIfNoProfile identity={identity} />;
}

const ctx = { identity, router };

return (
<View>
{/* TODO: This shouldn't be here */}
<MonoText>{JSON.stringify(identity.profile, null, 2)}</MonoText>
<Button onPress={() => identity.logOut()}>LOG OUT</Button>
<View style={styles.container}>
<Header
left={{ icon: "menu", onPress: () => router.push("/menu") }}
title={t("dashboard.pageTitle")}
/>
<FlatList
data={mockDashboardGadgets}
numColumns={2}
renderItem={({ item }) => (
<DashboardGadget action={actions[item]} ctx={ctx} />
)}
style={styles.list}
/>
<FAB
icon="alarm-light"
style={styles.fab}
onPress={() => {
/* TODO */
}}
/>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
},
list: {
padding: 8,
},
fab: {
position: "absolute",
bottom: 32,
right: 32,
backgroundColor: "#991b1b",
},
});

export default Page;
11 changes: 7 additions & 4 deletions app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { Link, Stack } from "expo-router";
import { Link } from "expo-router";
import { StyleSheet, View } from "react-native";
import { ActivityIndicator, Button, Text } from "react-native-paper";

import { BASE_URL, useApi } from "@/common/api";
import { useI18n } from "@/common/i18n";
import { useIdentity, RedirectIfLoggedIn } from "@/common/identity";
import { Header } from "@/components/Header";
import { MonoText } from "@/components/StyledText";

const HEALTHZ_PATH = "/api/v1/healthz";

export const Landing = ({ debug = false }: { debug?: boolean }) => {
const { data, isLoading, error, mutate } = useApi({
url: "/api/v1/healthz",
url: HEALTHZ_PATH,
});
const { t } = useI18n();
const identity = useIdentity();
Expand All @@ -34,7 +37,7 @@ export const Landing = ({ debug = false }: { debug?: boolean }) => {
{debug ? (
<>
<MonoText style={styles.debug}>
{new URL("/healthz", BASE_URL).toString()}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was broken after the API change 😨

{new URL(HEALTHZ_PATH, BASE_URL).toString()}
{"\n"}
{JSON.stringify(data)}
{"\n"}
Expand Down Expand Up @@ -72,7 +75,7 @@ const Page = () => {
const { t } = useI18n();
return (
<View style={styles.container}>
<Stack.Screen options={{ title: t("landing.pageTitle") }} />
<Header title={t("landing.pageTitle")} />
<Landing debug={__DEV__} />
</View>
);
Expand Down
64 changes: 64 additions & 0 deletions app/menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useRouter } from "expo-router";
import { ScrollView, StyleSheet } from "react-native";
import { Banner, List, MD3Theme, useTheme, Text } from "react-native-paper";

import { actions } from "@/common/actions";
import { useI18n } from "@/common/i18n";
import {
useIdentity,
RedirectIfNoProfile,
isCaretaker,
} from "@/common/identity";
import { Header } from "@/components/Header";
import { MenuItem } from "@/components/MenuItem";
import { View } from "@/components/Themed";

const Page = () => {
const { t } = useI18n();
const router = useRouter();
const identity = useIdentity();
const theme = useTheme();
const styles = makeStyles(theme);

if (!identity.hasProfile) {
return <RedirectIfNoProfile identity={identity} />;
}

const ctx = { identity, router };

return (
<View style={styles.container}>
<Header left="back" title={t("menu.pageTitle")} />
{isCaretaker(identity.profile) ? (
<Banner visible style={styles.banner}>
<Text style={styles.bannerText}>
{t("menu.caretakerBanner", { alias: identity.profile.seniorAlias })}
</Text>
</Banner>
) : null}
<ScrollView>
<List.Section>
<List.Subheader>{t("menu.account")}</List.Subheader>
<MenuItem action={actions.profileList} ctx={ctx} />
<MenuItem action={actions.logOut} ctx={ctx} />
</List.Section>
</ScrollView>
</View>
);
};

const makeStyles = (theme: MD3Theme) =>
StyleSheet.create({
container: {
flex: 1,
},
banner: {
backgroundColor: theme.colors.primary,
paddingBottom: 8,
},
bannerText: {
color: theme.colors.onPrimary,
},
});

export default Page;
10 changes: 4 additions & 6 deletions app/profile/list.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link, Stack, router } from "expo-router";
import { Link } from "expo-router";
import { ScrollView, StyleSheet, View } from "react-native";
import { Avatar, Button, List } from "react-native-paper";

Expand All @@ -10,6 +10,7 @@ import {
useIdentity,
RedirectIfLoggedOut,
} from "@/common/identity";
import { Header } from "@/components/Header";

const mockApiResponse = {
profiles: [
Expand All @@ -31,14 +32,11 @@ const ProfilesList = () => {
const seniorProfile = profiles.find(isSenior);
const caretakerProfiles = profiles.filter(isCaretaker);

const handleItemPress = (profile: Profile) => {
identity.selectProfile(profile);
router.push("/dashboard");
};
const handleItemPress = (profile: Profile) => identity.selectProfile(profile);

return (
<View style={styles.container}>
<Stack.Screen options={{ title: t("profileList.pageTitle") }} />
<Header left="back" title={t("profileList.pageTitle")} />
<List.Section>
{caretakerProfiles.length > 0 && (
<List.Subheader style={styles.listSubheader}>
Expand Down
24 changes: 24 additions & 0 deletions assets/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,29 @@
"profileList.noProfiles": {
"en": "You don't have any profiles yet.",
"pl": "Nie masz jeszcze żadnych profili."
},
"dashboard.pageTitle": {
"en": "Dashboard",
"pl": "Panel"
},
"menu.pageTitle": {
"en": "Menu",
"pl": "Menu"
},
"menu.account": {
"en": "Account",
"pl": "Konto"
},
"menu.caretakerBanner": {
"en": "You are currently managing {alias}.",
"pl": "Obecnie zarządzasz profilem {alias}."
},
"actions.profileList": {
"en": "Select profile",
"pl": "Zmień profil"
},
"actions.logOut": {
"en": "Log out",
"pl": "Wyloguj się"
}
}
2 changes: 2 additions & 0 deletions common/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { actions, type ActionKey } from "./list";
export type { Action, ActionContext } from "./types";
16 changes: 16 additions & 0 deletions common/actions/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Action } from "./types";

export const actions = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: #36

profileList: {
displayName: (t) => t("actions.profileList"),
icon: "account-switch",
handler: ({ router }) => router.push("/profile/list"),
},
logOut: {
displayName: (t) => t("actions.logOut"),
icon: "account-arrow-right",
handler: ({ identity }) => identity.logOut(),
},
} satisfies Record<string, Action>;

export type ActionKey = keyof typeof actions;
20 changes: 20 additions & 0 deletions common/actions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { useRouter } from "expo-router";
import type { ComponentProps } from "react";
import type { IconButton } from "react-native-paper";

import type { Translator } from "../i18n";
import type { IdentityProfileKnown } from "../identity/types";

type IconSource = ComponentProps<typeof IconButton>["icon"];
type Router = ReturnType<typeof useRouter>;

export type ActionContext = {
identity: IdentityProfileKnown;
router: Router;
};

export type Action = {
displayName: (t: Translator) => string;
icon: IconSource;
handler: (ctx: ActionContext) => void;
};
8 changes: 6 additions & 2 deletions common/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const DEFAULT_LANGUAGE = "en";
type TranslationKey = keyof typeof data;
type Language = "en" | "pl";
type I18nData = { language: Language };
export type Translator = (
key: TranslationKey,
substitutions?: Record<string, any>,
) => string;

const I18nContext = createContext<I18nData>({ language: DEFAULT_LANGUAGE });

Expand All @@ -33,8 +37,8 @@ const I18nContext = createContext<I18nData>({ language: DEFAULT_LANGUAGE });
export const useI18n = () => {
const { language } = useContext(I18nContext);

const t = (key: TranslationKey, substitutions: Record<string, any> = {}) => {
const prop = data[key] as Record<Language, string | undefined>;
const t: Translator = (key, substitutions = {}) => {
const prop = (data[key] ?? {}) as Record<Language, string | undefined>;
let value = prop[language] ?? prop[DEFAULT_LANGUAGE] ?? null;

if (!value) {
Expand Down
Loading