Skip to content

Commit

Permalink
Merge pull request #134 from eurofurence/feature/profile-and-events-nav
Browse files Browse the repository at this point in the history
feat: Profile view and event top level route
  • Loading branch information
lukashaertel authored Aug 24, 2024
2 parents e098c17 + 1ec9a76 commit 4a4e8e5
Show file tree
Hide file tree
Showing 28 changed files with 523 additions and 230 deletions.
172 changes: 172 additions & 0 deletions src/components/ProfileContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { captureException } from "@sentry/react-native";
import React, { FC, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Linking, StyleSheet, View } from "react-native";

import { Image } from "./generic/atoms/Image";
import { Label } from "./generic/atoms/Label";
import { Section } from "./generic/atoms/Section";
import { Badge } from "./generic/containers/Badge";
import { Button } from "./generic/containers/Button";
import { authSettingsUrl, conName } from "../configuration";
import { Claims, useAuthContext } from "../context/AuthContext";
import { useThemeBackground } from "../hooks/themes/useThemeHooks";
import { UserRecord } from "../store/eurofurence/types";
import { assetSource } from "../util/assets";

/**
* User role pill.
* @param role The role name.
* @constructor
*/
const UserRole: FC<{ role: string }> = ({ role }) => {
const bg = useThemeBackground("primary");
const text = useMemo(() => {
return role.replaceAll(/[A-Z]/g, (s) => " " + s);
}, [role]);

return (
<View style={[bg, styles.pill]}>
<Label type="minor" variant="middle" color="white">
{text}
</Label>
</View>
);
};

/**
* User registration pill. Usually only one is displayed.
* @param id The user's ID.
* @param status The registration status, e.g., paid.
* @constructor
*/
const UserRegistration: FC<{ id: string; status: string }> = ({ id, status }) => {
const { t } = useTranslation("Profile");
const { t: tStatus } = useTranslation("Profile", { keyPrefix: "status_names" });
const bg = useThemeBackground("secondary");
return (
<View style={[bg, styles.pill]}>
<Label type="strong" color="white">
{t("registration_nr")} {id} | {tStatus(status)}
</Label>
</View>
);
};

export type ProfileContentProps = {
claims: Claims;
user: UserRecord;
parentPad?: number;
};

/**
* User profile page.
* @param claims The IDP user claims.
* @param user The backend user info.
* @param parentPad The padding that the parent will apply.
* @constructor
*/
export const ProfileContent: FC<ProfileContentProps> = ({ claims, user, parentPad = 0 }) => {
const { t } = useTranslation("Profile");
const avatarBackground = useThemeBackground("primary");
const { logout } = useAuthContext();

const isAttendee = user.Roles.includes("Attendee");
const isCheckedIn = user.Roles.includes("AttendeeCheckedIn");
const roleComplex = Boolean(user.Roles.find((role) => role !== "Attendee" && role !== "AttendeeCheckedIn"));
return (
<>
{isCheckedIn ? (
<Badge unpad={parentPad} badgeColor="primary" textColor="invText">
{t("roles_simple_checked_in")}
</Badge>
) : isAttendee ? (
<Badge unpad={parentPad} badgeColor="warning" textColor="invText">
{t("roles_simple_attendee")}
</Badge>
) : null}
<View style={styles.avatarContainer}>
<Image
style={[avatarBackground, styles.avatarCircle]}
source={claims.avatar ?? assetSource("ych")}
contentFit="contain"
placeholder="ych"
transition={60}
cachePolicy="memory"
priority="high"
/>
</View>

<Label type="h1" variant="middle">
{claims.name as string}
</Label>

<Label type="caption" variant="middle" mb={20}>
{claims.email as string}
</Label>

<View style={styles.registrations}>
{user.Registrations.map((r) => (
<UserRegistration key={r.Id} id={r.Id} status={r.Status} />
))}
</View>

<Label mt={20} type="para">
{t("login_description", { conName })}
</Label>

<Button style={styles.idpButton} outline icon="web" onPress={() => Linking.openURL(authSettingsUrl).catch(captureException)}>
{t("idp_settings")}
</Button>

{roleComplex && (
<>
<Section icon="account-group" title={t("roles")} subtitle={t("roles_subtitle", { conName })} />
<View style={styles.roles}>
{user.Roles.map((r) => (
<UserRole key={r} role={r} />
))}
</View>
</>
)}

<Button style={styles.logoutButton} icon="logout" onPress={() => logout().catch(captureException)}>
{t("logout")}
</Button>
</>
);
};

const styles = StyleSheet.create({
avatarContainer: {
margin: 25,
alignSelf: "center",
},
avatarCircle: {
width: 200,
height: 200,
aspectRatio: 1,
borderRadius: 100,
},
registrations: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "center",
gap: 10,
},
idpButton: {
marginTop: 20,
},
roles: {
flexDirection: "row",
flexWrap: "wrap",
gap: 10,
},
logoutButton: {
marginTop: 100,
},
pill: {
padding: 10,
borderRadius: 10,
},
});
16 changes: 15 additions & 1 deletion src/components/feedback/FeedbackForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { StyleSheet } from "react-native";

import { feedbackSchema, FeedbackSchema } from "./FeedbackForm.schema";
import { useAuthContext } from "../../context/AuthContext";
import { useAppNavigation, useAppRoute } from "../../hooks/nav/useAppNavigation";
import { useTheme } from "../../hooks/themes/useThemeHooks";
import { useAppSelector } from "../../store";
Expand All @@ -18,6 +19,7 @@ import { ManagedTextInput } from "../generic/forms/ManagedTextInput";
export const FeedbackForm = () => {
const theme = useTheme();
const navigation = useAppNavigation("EventFeedback");

const [submitFeedback, feedbackResult] = useSubmitEventFeedbackMutation();
const { t } = useTranslation("EventFeedback");
const form = useForm<FeedbackSchema>({
Expand All @@ -28,6 +30,12 @@ export const FeedbackForm = () => {
},
});

const { loggedIn, user } = useAuthContext();
const attending = Boolean(user?.Roles?.includes("Attendee"));

const disabled = !loggedIn || !attending;
const disabledReason = (!loggedIn && t("disabled_not_logged_in")) || (!attending && t("disabled_not_attending"));

const { params } = useAppRoute("EventFeedback");
const event = useAppSelector((state) => eventsSelector.selectById(state, params.id));

Expand All @@ -52,10 +60,16 @@ export const FeedbackForm = () => {

<ManagedTextInput<FeedbackSchema> name="message" label={t("message_title")} placeholder={t("message_placeholder")} numberOfLines={8} multiline />

<Button onPress={form.handleSubmit(submit)} disabled={feedbackResult.isLoading}>
<Button onPress={form.handleSubmit(submit)} disabled={feedbackResult.isLoading || disabled}>
{t("submit")}
</Button>

{disabledReason && (
<Label type="caption" color="important" variant="middle" mt={16}>
{disabledReason}
</Label>
)}

{feedbackResult.isError && (
<Label style={styles.error} mt={16}>
{t("submit_failed")}
Expand Down
4 changes: 2 additions & 2 deletions src/components/generic/atoms/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const styles = StyleSheet.create({
const types = StyleSheet.create({
lead: {
fontSize: 20,
lineHeight: 20,
lineHeight: 24,
fontWeight: "100",
},
h1: {
Expand Down Expand Up @@ -138,7 +138,7 @@ const types = StyleSheet.create({
},
caption: {
fontSize: 14,
lineHeight: 14,
lineHeight: 18,
fontWeight: "600",
opacity: 0.666,
},
Expand Down
7 changes: 4 additions & 3 deletions src/components/generic/containers/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FC, ReactElement, ReactNode } from "react";
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";

import { useThemeBackground, useThemeColorValue } from "../../../hooks/themes/useThemeHooks";
import { useThemeBackground, useThemeBorder, useThemeColorValue } from "../../../hooks/themes/useThemeHooks";
import { Icon, IconNames } from "../atoms/Icon";
import { Label, LabelProps } from "../atoms/Label";

Expand Down Expand Up @@ -60,6 +60,7 @@ export const Button: FC<ButtonProps> = ({ containerStyle, style, labelType, labe
// Computed styles.
const baseStyle = outline ? styles.containerOutline : styles.containerFill;
const disabledStyle = disabled ? styles.disabled : null;
const borderStyle = useThemeBorder("inverted");
const fillStyle = useThemeBackground(outline ? "transparent" : "inverted");
const color = useThemeColorValue(outline ? "important" : "invImportant");

Expand All @@ -78,7 +79,7 @@ export const Button: FC<ButtonProps> = ({ containerStyle, style, labelType, labe
return (
<TouchableOpacity
containerStyle={containerStyle}
style={[styles.container, baseStyle, fillStyle, disabledStyle, style]}
style={[styles.container, baseStyle, fillStyle, outline && borderStyle, disabledStyle, style]}
onPress={onPress}
onLongPress={onLongPress}
disabled={disabled}
Expand Down Expand Up @@ -111,7 +112,7 @@ const styles = StyleSheet.create({
borderWidth: border,
},
disabled: {
opacity: 0.6,
opacity: 0.5,
},
content: {
flexDirection: "row",
Expand Down
59 changes: 34 additions & 25 deletions src/components/generic/containers/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { useNavigation } from "@react-navigation/core";
import React, { FC, PropsWithChildren } from "react";
import { StyleSheet, View, ViewStyle } from "react-native";
import { StyleSheet, ViewStyle } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";

import { Row } from "./Row";
import { useThemeBackground, useThemeBorder, useThemeColorValue } from "../../../hooks/themes/useThemeHooks";
import { Continuous } from "../atoms/Continuous";
import { Icon, IconNames } from "../atoms/Icon";
import { Label } from "../atoms/Label";

const iconSize = 32;
const iconSize = 26;
const iconPad = 6;

export type HeaderProps = PropsWithChildren<
| {
style?: ViewStyle;
loading?: boolean;
}
| {
style?: ViewStyle;
secondaryIcon: IconNames;
secondaryPress: () => void;
loading?: boolean;
}
>;

Expand All @@ -30,21 +34,31 @@ export const Header: FC<HeaderProps> = (props) => {

return (
<Row style={[styles.container, styleBackground, styleBorder, props.style]} type="center" variant="spaced">
<Icon name="chevron-left" size={iconSize} color={colorValue} />
<TouchableOpacity hitSlop={{ right: 50 }} containerStyle={styles.back} onPress={() => navigation.goBack()}>
<Icon name="chevron-left" size={iconSize} color={colorValue} />
</TouchableOpacity>

<Label style={styles.text} type="lead" ellipsizeMode="tail" numberOfLines={1}>
{props.children}
</Label>

<View style={styles.placeholder} />

{/* Optional secondary action. */}
{!("secondaryIcon" in props) ? null : <Icon name={props.secondaryIcon} size={iconSize} color={colorValue} />}

<TouchableOpacity containerStyle={styles.back} onPress={() => navigation.goBack()} />
{!("secondaryIcon" in props) ? null : (
<TouchableOpacity hitSlop={{ left: 50 }} containerStyle={styles.secondary} onPress={() => props.secondaryPress()}>
<Icon name={props.secondaryIcon} size={iconSize} color={colorValue} />
</TouchableOpacity>
)}

{/* Optional secondary touchable, placed over icon. */}
{!("secondaryIcon" in props) ? null : <TouchableOpacity containerStyle={styles.secondary} onPress={() => props.secondaryPress()} />}
{
// Loading header. Explicitly given as false, not loading.
props.loading === false ? (
<Continuous style={styles.loading} active={false} />
) : // Explicitly given as true, loading.
props.loading === true ? (
<Continuous style={styles.loading} active={true} />
) : // Not given, therefore no element.
null
}
</Row>
);
};
Expand All @@ -58,26 +72,21 @@ const styles = StyleSheet.create({
},
text: {
flex: 1,
},
placeholder: {
width: iconSize,
height: iconSize,
justifyContent: "center",
},
back: {
position: "absolute",
top: 0,
left: 0,
bottom: 0,
width: "30%",
marginLeft: -iconPad,
width: iconSize + iconPad,
height: iconSize + iconPad,
justifyContent: "center",
},
secondary: {
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: "30%",
width: iconSize + iconPad,
height: iconSize + iconPad,
marginRight: -iconPad,
justifyContent: "center",
},
activity: {
loading: {
position: "absolute",
left: 0,
bottom: 0,
Expand Down
Loading

0 comments on commit 4a4e8e5

Please sign in to comment.