From d7efb873e3518334740df527afc5339c2273304a Mon Sep 17 00:00:00 2001 From: elysseaa Date: Sat, 28 Feb 2026 13:52:31 -0500 Subject: [PATCH 1/8] First Commit --- Api/MeetingsApi.ts | 94 +++++++++++ Api/Models/Attendance.ts | 10 ++ AppEnvironment.tsx | 5 + Attendance/AttendableSelect.tsx | 81 +++++++++ Attendance/AttendanceScreen.tsx | 95 ++++++++++- Attendance/BuzzCardPrompt.tsx | 271 +++++++++++++++++++++++++++++++ Attendance/TapABuzzCard.tsx | 124 ++++++++++++++ Auth/EnvironmentSelect.tsx | 4 + Auth/InsufficientPermissions.tsx | 5 +- Components/ActionPrompt.tsx | 47 ++++++ Components/MenuHeader.tsx | 26 +++ Components/MenuLink.tsx | 59 +++++++ Navigation/RootStack.tsx | 52 +++--- Nfc/NfcScanModal.tsx | 2 +- 14 files changed, 839 insertions(+), 36 deletions(-) create mode 100644 Api/MeetingsApi.ts create mode 100644 Api/Models/Attendance.ts create mode 100644 Attendance/AttendableSelect.tsx create mode 100644 Attendance/BuzzCardPrompt.tsx create mode 100644 Attendance/TapABuzzCard.tsx create mode 100644 Components/ActionPrompt.tsx create mode 100644 Components/MenuHeader.tsx create mode 100644 Components/MenuLink.tsx diff --git a/Api/MeetingsApi.ts b/Api/MeetingsApi.ts new file mode 100644 index 0000000..8e9beef --- /dev/null +++ b/Api/MeetingsApi.ts @@ -0,0 +1,94 @@ +import { AxiosInstance } from 'axios'; + +export type TeamInfo> = { + id: number; + name: string; + self_serviceable: boolean; + visible: boolean; + visible_on_kiosk: boolean; + attendable: boolean; + description: string; + mailing_list_name: string | null; + slack_channel_id: string; + slack_channel_name: string; + slack_private_channel_id: string; + google_group: string; + created_at: string; + updated_at: string; + deleted_at: string | null; +} & T; + +export type EventInfo> = { + id: number; + name: string; + allow_anonymous_rsvp: boolean; + location: string; + start_time: string; + end_time: string; + created_at: string; + updated_at: string; + deleted_at: string | null; +} & T; + +export type AttendanceInfo> = { + attendable_type: string; + attendable_id: number; + gtid: number; + source: string; +} & T; + +// TODO: Fill this in with proper attendance response params +export type AttendanceResponse = { + attendance: { + attendee?: { + name: string; + }; + }; +}; + +export async function getTeamInfo(api: AxiosInstance): Promise { + try { + const teams = await api.get('/api/v1/teams'); + //TODO: Incorporate Sentry + const teamInfos: TeamInfo[] = teams.data.teams; + return teamInfos; + } catch (error) { + //TODO: incorporate logging + } + return null; +} + +export async function getEventInfo(api: AxiosInstance): Promise { + try { + const events = await api.get('/api/v1/events'); + //TODO: Incorporate Sentry + const eventInfos: EventInfo[] = events.data.events; + return eventInfos; + } catch (error) { + //TODO: incorporate logging + } + return null; +} + +export async function postAttendance( + api: AxiosInstance, + props: AttendanceInfo +): Promise<{ success: true; data: AttendanceResponse } | { success: false; error: string }> { + try { + console.log("Posting ", props); + const response = await api.post('/api/v1/attendance?include=attendee', props); + console.log('Attendance response:', response.data); + + return { + success: true, + data: response.data + }; + } catch (error) { + //TODO: incorporate logging + console.error(error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } +} \ No newline at end of file diff --git a/Api/Models/Attendance.ts b/Api/Models/Attendance.ts new file mode 100644 index 0000000..15df6df --- /dev/null +++ b/Api/Models/Attendance.ts @@ -0,0 +1,10 @@ +export enum NfcSource { + NFC = 'Nfc', + KEYBOARD = 'Keyboard' +} + +export enum AttendableType { + TEAM = 'team', + EVENT = 'event', + NONE = 'none' +} \ No newline at end of file diff --git a/AppEnvironment.tsx b/AppEnvironment.tsx index 5be8a69..ded22b9 100644 --- a/AppEnvironment.tsx +++ b/AppEnvironment.tsx @@ -16,6 +16,11 @@ export const APP_ENVIRONMENTS: AppEnvironmentList = { production: true, baseUrl: 'https://my.robojackets.org', }, + test: { + name: 'Test', + production: false, + baseUrl: 'https://apiary-test.robojackets.org', + }, }; type EnvironmentContextType = { diff --git a/Attendance/AttendableSelect.tsx b/Attendance/AttendableSelect.tsx new file mode 100644 index 0000000..d9c4186 --- /dev/null +++ b/Attendance/AttendableSelect.tsx @@ -0,0 +1,81 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + SafeAreaView, + ScrollView, + StyleSheet +} from 'react-native'; +import { useApi } from '../Api/ApiContextProvider'; +import { EventInfo, getEventInfo, getTeamInfo, TeamInfo } from '../Api/MeetingsApi'; +import { AttendableType } from '../Api/Models/Attendance'; +import LoadingScreen from '../Components/LoadingScreen'; +import MenuHeader from '../Components/MenuHeader'; +import MenuLink from '../Components/MenuLink'; +import TapABuzzCard from './TapABuzzCard'; + +type AttendanceProps = { + attendanceType: AttendableType; + setAttendanceType: (state: AttendableType) => void; +}; + +function AttendableSelect({attendanceType, setAttendanceType} : AttendanceProps) { + const api = useApi(); + const [attendables, setAttendables] = useState(undefined); + const [attendable, setAttendable] = useState(undefined); + + async function onRefreshAttendables(forceRefresh: boolean = false) { + if (forceRefresh) { + if (attendanceType === AttendableType.TEAM) { + setAttendables(await getTeamInfo(api)); + } else { + setAttendables(await getEventInfo(api)); + } + } + } + + useEffect(() => { + onRefreshAttendables(true); + }, []); + + function AttendableList () { + if (attendables) { + const attendableList = []; + for (const attendable of attendables) { + attendableList.push( + {setAttendable(attendable)}}> + ); + } + return attendableList; + } else { + return ( + + ) + } + } + + return ( + <> + { + attendable ? + + : + + + + + Last attendee: {lastAttendee?.name || "None"} + + ); + } + + return ( + + {/* {enterGTID && } */} + + + + Total attendees: {totalAttendees} + { + totalAttendees in AttendanceGoals && + {AttendanceGoals[totalAttendees]} + } + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + margin: 10 + }, + topPanelText: { + fontSize: 20, + paddingHorizontal: 10, + }, + bottomPanel: { + alignSelf: "flex-end", + marginTop: "auto", + paddingHorizontal: 10 + }, + bottomPanelText: { + fontSize: 20, + textAlign: "right" + }, + modalButton: { + alignItems: 'flex-end', + marginVertical: 20, + }, + modalText: { + fontSize: 20, + marginVertical: 20, + }, + modalInput: { + borderWidth: 1, + borderColor: "gray", + padding: 10, + }, + modalView: { + backgroundColor: 'white', + borderRadius: 20, + elevation: 5, + margin: 20, + padding: 20, + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + width: '75%', + }, + viewContainer: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + margin: 10, + }, + backButton: { + marginHorizontal: 5, + alignSelf: 'flex-start' + }, +}); + +export default TapABuzzCard; diff --git a/Auth/EnvironmentSelect.tsx b/Auth/EnvironmentSelect.tsx index 9513016..e81ae22 100644 --- a/Auth/EnvironmentSelect.tsx +++ b/Auth/EnvironmentSelect.tsx @@ -44,6 +44,10 @@ export default function EnvironmentSelect({ visible, onDismiss }: EnvironmentSel Production + + + Test + Other diff --git a/Auth/InsufficientPermissions.tsx b/Auth/InsufficientPermissions.tsx index 69085ec..13563cd 100644 --- a/Auth/InsufficientPermissions.tsx +++ b/Auth/InsufficientPermissions.tsx @@ -1,13 +1,14 @@ import MaterialIcons from '@react-native-vector-icons/material-icons'; import React, { useState } from 'react'; import { Button, Modal, StyleSheet, Text, View } from 'react-native'; +import { Permission } from '../Api/Models/Permission'; import { useTheme } from '../Themes/ThemeContextProvider'; type InsufficientPermissionsProps = { featureName: string; onRetry: () => void; - missingPermissions: string[]; - requiredPermissions: string[]; + missingPermissions: Permission[]; + requiredPermissions: Permission[]; }; function InsufficientPermissions({ diff --git a/Components/ActionPrompt.tsx b/Components/ActionPrompt.tsx new file mode 100644 index 0000000..9202609 --- /dev/null +++ b/Components/ActionPrompt.tsx @@ -0,0 +1,47 @@ +import MaterialIcons from '@react-native-vector-icons/material-icons'; +import React from 'react'; +import { ColorValue, StyleSheet, Text, View } from 'react-native'; + +type ActionPromptProps = { + icon: React.ComponentProps['name']; + color?: ColorValue; + title: string; + subtitle?: string; + subtitle2?: string; +}; + +export const ActionPrompt = (props : ActionPromptProps) => { + return ( + + + {props.title} + {props.subtitle && {props.subtitle}} + {props.subtitle2 && {props.subtitle2}} + + ) +} + +const styles = StyleSheet.create({ + icon: { + margin: 10, + }, + mainText: { + fontSize: 30, + marginBottom: 20, + textAlign: 'center', + }, + subText: { + fontSize: 20, + marginBottom: 20, + textAlign: 'center', + }, + viewContainer: { + alignItems: 'center', + justifyContent: 'center', + }, +}); \ No newline at end of file diff --git a/Components/MenuHeader.tsx b/Components/MenuHeader.tsx new file mode 100644 index 0000000..4fb49cc --- /dev/null +++ b/Components/MenuHeader.tsx @@ -0,0 +1,26 @@ +import { + StyleSheet, + Text, + View, +} from 'react-native'; + +type MenuHeaderProps = { title: string }; + +const MenuHeader = ({ title }: MenuHeaderProps) => ( + + {title} + +); + +const styles = StyleSheet.create({ + headerContainer: { + paddingHorizontal: 10, + paddingVertical: 20, + }, + headerText: { + fontSize: 20, + fontWeight: 'bold', + }, +}); + +export default MenuHeader; \ No newline at end of file diff --git a/Components/MenuLink.tsx b/Components/MenuLink.tsx new file mode 100644 index 0000000..5583a76 --- /dev/null +++ b/Components/MenuLink.tsx @@ -0,0 +1,59 @@ +import MaterialIcons from '@react-native-vector-icons/material-icons'; +import { + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; + +type MenuLinkProps = { + icon: React.ComponentProps['name']; + title: string; + onClick: () => void; +}; + +const MenuLink = ({ icon, title, onClick }: MenuLinkProps) => ( + + + + + + {title} + + + + +); + +const styles = StyleSheet.create({ + menuIcon: { + paddingHorizontal: 10, + paddingVertical: 5, + }, + menuLinkContainer: { + borderBlockColor: 'gray', + borderBottomWidth: 0, + borderTopWidth: 1, + }, + menuLinkRow: { + alignItems: 'center', + flexDirection: 'row', + }, + menuLinkTouchable: { + paddingHorizontal: 10, + paddingVertical: 20, + }, + menuSubtitle: { + color: 'gray', + fontSize: 15, + }, + menuTextContainer: { + flexShrink: 1, + padding: 5, + }, + menuTitle: { + fontSize: 20, + }, +}); + +export default MenuLink; \ No newline at end of file diff --git a/Navigation/RootStack.tsx b/Navigation/RootStack.tsx index cd6f03a..5f2b607 100644 --- a/Navigation/RootStack.tsx +++ b/Navigation/RootStack.tsx @@ -1,7 +1,5 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import React, { useContext, useEffect, useState } from 'react'; -import { Platform } from 'react-native'; -import NfcManager from 'react-native-nfc-manager'; +import React, { useContext, useState } from 'react'; import { AuthContext } from '../Auth/AuthContextProvider'; import { AuthenticationState } from '../Auth/Authentication'; import AuthenticationScreen from '../Auth/AuthenticationScreen'; @@ -16,31 +14,31 @@ type NfcState = 'enabled' | 'disabled' | 'unsupported'; function RootStack() { const { currentTheme } = useTheme(); const auth = useContext(AuthContext); - const [nfcEnabled, setNfcEnabled] = useState('disabled'); + const [nfcEnabled, setNfcEnabled] = useState('enabled'); - useEffect(() => { - const isEnabled = async () => { - try { - const isSupported = await NfcManager.isSupported(); - if (!isSupported) { - setNfcEnabled('unsupported'); - return; - } - if (Platform.OS === 'android') { - const enabled = await NfcManager.isEnabled(); - setNfcEnabled(enabled ? 'enabled' : 'disabled'); - } else if (Platform.OS === 'ios') { - setNfcEnabled('enabled'); - } else { - // Platform.OS === "windows" | "macos" | "web" - console.log('Not a valid platform for NFC'); - } - } catch (error) { - console.error('Error fetching data:', error); - } - }; - isEnabled(); - }, []); + // useEffect(() => { + // const isEnabled = async () => { + // try { + // const isSupported = await NfcManager.isSupported(); + // if (!isSupported) { + // setNfcEnabled('unsupported'); + // return; + // } + // if (Platform.OS === 'android') { + // const enabled = await NfcManager.isEnabled(); + // setNfcEnabled(enabled ? 'enabled' : 'disabled'); + // } else if (Platform.OS === 'ios') { + // setNfcEnabled('enabled'); + // } else { + // // Platform.OS === "windows" | "macos" | "web" + // console.log('Not a valid platform for NFC'); + // } + // } catch (error) { + // console.error('Error fetching data:', error); + // } + // }; + // isEnabled(); + // }, []); return ( = (props: NfcScanModalProps) => }, [props]); return ( - + From 5f69848b121b341106999f0fe5fecf8d4fef041d Mon Sep 17 00:00:00 2001 From: elysseaa Date: Sat, 28 Feb 2026 14:15:32 -0500 Subject: [PATCH 2/8] Appease Linter --- Api/MeetingsApi.ts | 24 +-- Api/Models/Attendance.ts | 6 +- Attendance/AttendableSelect.tsx | 51 +++--- Attendance/AttendanceScreen.tsx | 78 +++++---- Attendance/BuzzCardPrompt.tsx | 275 ++++++++++++++++++-------------- Attendance/TapABuzzCard.tsx | 110 +++++-------- Components/ActionPrompt.tsx | 15 +- Components/MenuHeader.tsx | 9 +- Components/MenuLink.tsx | 26 ++- Navigation/RootStack.tsx | 52 +++--- 10 files changed, 326 insertions(+), 320 deletions(-) diff --git a/Api/MeetingsApi.ts b/Api/MeetingsApi.ts index 8e9beef..8758936 100644 --- a/Api/MeetingsApi.ts +++ b/Api/MeetingsApi.ts @@ -10,7 +10,7 @@ export type TeamInfo> = { description: string; mailing_list_name: string | null; slack_channel_id: string; - slack_channel_name: string; + slack_channel_name: string; slack_private_channel_id: string; google_group: string; created_at: string; @@ -71,24 +71,24 @@ export async function getEventInfo(api: AxiosInstance): Promise { try { - console.log("Posting ", props); + console.log('Posting ', props); const response = await api.post('/api/v1/attendance?include=attendee', props); console.log('Attendance response:', response.data); - - return { - success: true, - data: response.data + + return { + success: true, + data: response.data, }; } catch (error) { //TODO: incorporate logging console.error(error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', }; } -} \ No newline at end of file +} diff --git a/Api/Models/Attendance.ts b/Api/Models/Attendance.ts index 15df6df..c0a1936 100644 --- a/Api/Models/Attendance.ts +++ b/Api/Models/Attendance.ts @@ -1,10 +1,10 @@ export enum NfcSource { NFC = 'Nfc', - KEYBOARD = 'Keyboard' + KEYBOARD = 'Keyboard', } export enum AttendableType { TEAM = 'team', EVENT = 'event', - NONE = 'none' -} \ No newline at end of file + NONE = 'none', +} diff --git a/Attendance/AttendableSelect.tsx b/Attendance/AttendableSelect.tsx index d9c4186..5be5864 100644 --- a/Attendance/AttendableSelect.tsx +++ b/Attendance/AttendableSelect.tsx @@ -1,10 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { - Button, - SafeAreaView, - ScrollView, - StyleSheet -} from 'react-native'; +import { Button, SafeAreaView, ScrollView, StyleSheet } from 'react-native'; import { useApi } from '../Api/ApiContextProvider'; import { EventInfo, getEventInfo, getTeamInfo, TeamInfo } from '../Api/MeetingsApi'; import { AttendableType } from '../Api/Models/Attendance'; @@ -18,9 +13,11 @@ type AttendanceProps = { setAttendanceType: (state: AttendableType) => void; }; -function AttendableSelect({attendanceType, setAttendanceType} : AttendanceProps) { +function AttendableSelect({ attendanceType, setAttendanceType }: AttendanceProps) { const api = useApi(); - const [attendables, setAttendables] = useState(undefined); + const [attendables, setAttendables] = useState( + undefined, + ); const [attendable, setAttendable] = useState(undefined); async function onRefreshAttendables(forceRefresh: boolean = false) { @@ -37,36 +34,50 @@ function AttendableSelect({attendanceType, setAttendanceType} : AttendanceProps) onRefreshAttendables(true); }, []); - function AttendableList () { + function AttendableList() { if (attendables) { const attendableList = []; for (const attendable of attendables) { attendableList.push( - {setAttendable(attendable)}}> + { + setAttendable(attendable); + }} + >, ); } return attendableList; } else { - return ( - - ) + return ; } } return ( <> - { - attendable ? - - : + {attendable ? ( + + ) : ( - + - Last attendee: {lastAttendee?.name || "None"} + Last attendee: {lastAttendee?.name || 'None'} ); } @@ -50,74 +55,41 @@ function TapABuzzCard({attendanceType, setAttendanceType, attendable, setAttenda {/* {enterGTID && } */} - + Total attendees: {totalAttendees} - { - totalAttendees in AttendanceGoals && + {totalAttendees in AttendanceGoals && ( {AttendanceGoals[totalAttendees]} - } + )} ); } const styles = StyleSheet.create({ - container: { - flex: 1, - margin: 10 - }, - topPanelText: { - fontSize: 20, - paddingHorizontal: 10, - }, bottomPanel: { - alignSelf: "flex-end", - marginTop: "auto", - paddingHorizontal: 10 + alignSelf: 'flex-end', + marginTop: 'auto', + paddingHorizontal: 10, }, bottomPanelText: { fontSize: 20, - textAlign: "right" + textAlign: 'right', }, - modalButton: { - alignItems: 'flex-end', - marginVertical: 20, - }, - modalText: { - fontSize: 20, - marginVertical: 20, - }, - modalInput: { - borderWidth: 1, - borderColor: "gray", - padding: 10, - }, - modalView: { - backgroundColor: 'white', - borderRadius: 20, - elevation: 5, - margin: 20, - padding: 20, - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 4, - width: '75%', - }, - viewContainer: { - alignItems: 'center', + container: { flex: 1, - justifyContent: 'center', margin: 10, }, - backButton: { - marginHorizontal: 5, - alignSelf: 'flex-start' + topPanelText: { + fontSize: 20, + paddingHorizontal: 10, }, }); diff --git a/Components/ActionPrompt.tsx b/Components/ActionPrompt.tsx index 9202609..eaebb9c 100644 --- a/Components/ActionPrompt.tsx +++ b/Components/ActionPrompt.tsx @@ -10,21 +10,16 @@ type ActionPromptProps = { subtitle2?: string; }; -export const ActionPrompt = (props : ActionPromptProps) => { +export const ActionPrompt = (props: ActionPromptProps) => { return ( - + {props.title} {props.subtitle && {props.subtitle}} {props.subtitle2 && {props.subtitle2}} - ) -} + ); +}; const styles = StyleSheet.create({ icon: { @@ -44,4 +39,4 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, -}); \ No newline at end of file +}); diff --git a/Components/MenuHeader.tsx b/Components/MenuHeader.tsx index 4fb49cc..03de23e 100644 --- a/Components/MenuHeader.tsx +++ b/Components/MenuHeader.tsx @@ -1,8 +1,5 @@ -import { - StyleSheet, - Text, - View, -} from 'react-native'; +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; type MenuHeaderProps = { title: string }; @@ -23,4 +20,4 @@ const styles = StyleSheet.create({ }, }); -export default MenuHeader; \ No newline at end of file +export default MenuHeader; diff --git a/Components/MenuLink.tsx b/Components/MenuLink.tsx index 5583a76..bb9e014 100644 --- a/Components/MenuLink.tsx +++ b/Components/MenuLink.tsx @@ -1,10 +1,6 @@ import MaterialIcons from '@react-native-vector-icons/material-icons'; -import { - StyleSheet, - Text, - TouchableOpacity, - View, -} from 'react-native'; +import React from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; type MenuLinkProps = { icon: React.ComponentProps['name']; @@ -15,13 +11,13 @@ type MenuLinkProps = { const MenuLink = ({ icon, title, onClick }: MenuLinkProps) => ( - - - - {title} - + + + + {title} - + + ); @@ -43,10 +39,6 @@ const styles = StyleSheet.create({ paddingHorizontal: 10, paddingVertical: 20, }, - menuSubtitle: { - color: 'gray', - fontSize: 15, - }, menuTextContainer: { flexShrink: 1, padding: 5, @@ -56,4 +48,4 @@ const styles = StyleSheet.create({ }, }); -export default MenuLink; \ No newline at end of file +export default MenuLink; diff --git a/Navigation/RootStack.tsx b/Navigation/RootStack.tsx index 5f2b607..cd6f03a 100644 --- a/Navigation/RootStack.tsx +++ b/Navigation/RootStack.tsx @@ -1,5 +1,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import React, { useContext, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; +import { Platform } from 'react-native'; +import NfcManager from 'react-native-nfc-manager'; import { AuthContext } from '../Auth/AuthContextProvider'; import { AuthenticationState } from '../Auth/Authentication'; import AuthenticationScreen from '../Auth/AuthenticationScreen'; @@ -14,31 +16,31 @@ type NfcState = 'enabled' | 'disabled' | 'unsupported'; function RootStack() { const { currentTheme } = useTheme(); const auth = useContext(AuthContext); - const [nfcEnabled, setNfcEnabled] = useState('enabled'); + const [nfcEnabled, setNfcEnabled] = useState('disabled'); - // useEffect(() => { - // const isEnabled = async () => { - // try { - // const isSupported = await NfcManager.isSupported(); - // if (!isSupported) { - // setNfcEnabled('unsupported'); - // return; - // } - // if (Platform.OS === 'android') { - // const enabled = await NfcManager.isEnabled(); - // setNfcEnabled(enabled ? 'enabled' : 'disabled'); - // } else if (Platform.OS === 'ios') { - // setNfcEnabled('enabled'); - // } else { - // // Platform.OS === "windows" | "macos" | "web" - // console.log('Not a valid platform for NFC'); - // } - // } catch (error) { - // console.error('Error fetching data:', error); - // } - // }; - // isEnabled(); - // }, []); + useEffect(() => { + const isEnabled = async () => { + try { + const isSupported = await NfcManager.isSupported(); + if (!isSupported) { + setNfcEnabled('unsupported'); + return; + } + if (Platform.OS === 'android') { + const enabled = await NfcManager.isEnabled(); + setNfcEnabled(enabled ? 'enabled' : 'disabled'); + } else if (Platform.OS === 'ios') { + setNfcEnabled('enabled'); + } else { + // Platform.OS === "windows" | "macos" | "web" + console.log('Not a valid platform for NFC'); + } + } catch (error) { + console.error('Error fetching data:', error); + } + }; + isEnabled(); + }, []); return ( Date: Sat, 28 Feb 2026 14:34:31 -0500 Subject: [PATCH 3/8] Modify Nfc Scanning to Not Use Modal --- Attendance/BuzzCardPrompt.tsx | 59 +++++++++++++++++++++++++---------- Nfc/NfcScanModal.tsx | 2 +- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/Attendance/BuzzCardPrompt.tsx b/Attendance/BuzzCardPrompt.tsx index e43ad8b..1c502fb 100644 --- a/Attendance/BuzzCardPrompt.tsx +++ b/Attendance/BuzzCardPrompt.tsx @@ -1,10 +1,19 @@ -import React, { useState } from 'react'; -import { Button, Modal, Platform, StyleSheet, Text, TextInput, View } from 'react-native'; +import React, { useEffect, useState } from 'react'; +import { + Button, + Modal, + NativeModules, + Platform, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import NfcManager, { NfcTech } from 'react-native-nfc-manager'; import { useApi } from '../Api/ApiContextProvider'; import { EventInfo, postAttendance, TeamInfo } from '../Api/MeetingsApi'; import { AttendableType, NfcSource } from '../Api/Models/Attendance'; import { ActionPrompt } from '../Components/ActionPrompt'; -import NfcScanModal from '../Nfc/NfcScanModal'; import { useTheme } from '../Themes/ThemeContextProvider'; import { LastAttendeeProps } from './TapABuzzCard'; @@ -42,13 +51,38 @@ const BuzzCardPrompt: React.FC = ({ const { currentTheme } = useTheme(); const [buzzCardState, setBuzzCardState] = useState('Ready'); const [enterGTIDManually, setEnterGTIDManually] = useState(false); - const [scanIos, setScanIos] = useState(true); + + useEffect(() => { + beginScan(); + }, []); + + const beginScan = async () => { + if (Platform.OS === 'ios') { + const { BuzzCardReader } = NativeModules; + BuzzCardReader.sendCommand(selectApp, readFile, handleNfcResult); + } else if (Platform.OS === 'android') { + try { + await NfcManager.start(); + await NfcManager.requestTechnology(NfcTech.IsoDep); + await NfcManager.getTag(); + await NfcManager.transceive(selectApp); + const result = await NfcManager.transceive(readFile); + handleNfcResult(null, result); + } catch (error: unknown) { + handleNfcResult(error instanceof Error ? error : new Error(String(error)), null); + } finally { + NfcManager.cancelTechnologyRequest(); + } + } else { + console.log('Not a valid platform for NFC'); + handleNfcResult(null, null); + } + }; // Callback function for NfcScanModal const handleNfcResult = async (error: Error | null, result: number[] | null) => { if (error) { // TODO: Validate that error.message actually contains these formats - setScanIos(false); // closes modal so you can see the error switch (error.message) { case 'Wrong CLA': setBuzzCardState('NotABuzzCard'); @@ -68,6 +102,7 @@ const BuzzCardPrompt: React.FC = ({ if (!result) { // User cancelled setBuzzCardState('Ready'); + beginScan(); return; } @@ -94,6 +129,7 @@ const BuzzCardPrompt: React.FC = ({ await onBuzzCardTap(gtidNumber, NfcSource.NFC); setBuzzCardState('Ready'); + beginScan(); } catch (error) { setBuzzCardState('InvalidBuzzCardData'); } @@ -125,8 +161,6 @@ const BuzzCardPrompt: React.FC = ({ id: gtid, name: attendeeName, }); - - setBuzzCardState('Ready'); } catch (error) { setBuzzCardState('BadInternet'); } @@ -167,15 +201,6 @@ const BuzzCardPrompt: React.FC = ({ return ( - {scanIos && ( - - )} {enterGTIDManually && } {buzzCardState === 'Ready' ? ( @@ -229,7 +254,7 @@ const BuzzCardPrompt: React.FC = ({ + /> Last attendee: {lastAttendee?.name || 'None'} @@ -53,7 +54,6 @@ function TapABuzzCard({ return ( - {/* {enterGTID && } */} void; + color?: string; + textColor?: string; + borderColor?: string; }; function RoundedButton(props: RoundedButtonProps) { return ( - - {props.title} + + {props.title} ); } @@ -17,14 +27,12 @@ function RoundedButton(props: RoundedButtonProps) { const styles = StyleSheet.create({ button: { alignItems: 'center', - backgroundColor: '#007AFF', borderRadius: 30, justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 12, }, text: { - color: '#fff', fontSize: 16, fontWeight: '600', }, From 56e64156b5030bba7461b2ef97329402450a9850 Mon Sep 17 00:00:00 2001 From: elysseaa Date: Sat, 11 Apr 2026 14:46:55 -0400 Subject: [PATCH 7/8] Implement Dark Mode - Introduced component called ThemedText which is has the same usage as Text except adapts to dark/light mode - Side note: To get around linting flagging raw text in non-Text components, I had to use inside , which may not be the best implementation - Refactored Attendance components slightly to make usage of SafeAreaView more efficient --- Attendance/AttendableSelect.tsx | 31 +++++++------------ Attendance/AttendanceScreen.tsx | 17 +++++------ Attendance/BuzzCardPrompt.tsx | 19 +++++++++--- Attendance/TapABuzzCard.tsx | 23 +++++++------- Auth/AuthenticationScreen.tsx | 15 ++++++---- Auth/EnvironmentSelect.tsx | 27 +++++++++++++---- Components/ActionPrompt.tsx | 9 +++--- Components/LoadingScreen.tsx | 14 ++++++--- Components/MenuHeader.tsx | 15 ++++++---- Components/MenuLink.tsx | 32 ++++++++++++-------- Components/ThemedText.tsx | 21 +++++++++++++ Navigation/NavBar.tsx | 8 ++++- Navigation/RootStack.tsx | 53 ++++++++++++++++----------------- Settings/SettingsScreen.tsx | 17 +++++++---- 14 files changed, 187 insertions(+), 114 deletions(-) create mode 100644 Components/ThemedText.tsx diff --git a/Attendance/AttendableSelect.tsx b/Attendance/AttendableSelect.tsx index b890952..b43dfa4 100644 --- a/Attendance/AttendableSelect.tsx +++ b/Attendance/AttendableSelect.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { SafeAreaView, ScrollView, StyleSheet } from 'react-native'; +import { ScrollView } from 'react-native'; import { useApi } from '../Api/ApiContextProvider'; import { EventInfo, getEventInfo, getTeamInfo, TeamInfo } from '../Api/MeetingsApi'; import { AttendableType } from '../Api/Models/Attendance'; @@ -66,28 +66,19 @@ function AttendableSelect({ attendanceType, setAttendanceType }: AttendanceProps setAttendable={setAttendable} > ) : ( - - - { - setAttendanceType(AttendableType.NONE); - }} - /> - - - - + + { + setAttendanceType(AttendableType.NONE); + }} + /> + + + )} ); } -const styles = StyleSheet.create({ - container: { - flex: 1, - margin: 10, - }, -}); - export default AttendableSelect; diff --git a/Attendance/AttendanceScreen.tsx b/Attendance/AttendanceScreen.tsx index f208a41..f7d7c3b 100644 --- a/Attendance/AttendanceScreen.tsx +++ b/Attendance/AttendanceScreen.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { StyleSheet, View } from 'react-native'; +import { SafeAreaView, StyleSheet } from 'react-native'; import { useApi } from '../Api/ApiContextProvider'; import { AttendableType } from '../Api/Models/Attendance'; import { Permission } from '../Api/Models/Permission'; @@ -8,6 +8,7 @@ import InsufficientPermissions from '../Auth/InsufficientPermissions'; import LoadingScreen from '../Components/LoadingScreen'; import MenuHeader from '../Components/MenuHeader'; import MenuLink from '../Components/MenuLink'; +import { useTheme } from '../Themes/ThemeContextProvider'; import AttendableSelect from './AttendableSelect'; const requiredPermissions: Permission[] = [Permission.CREATE_ATTENDANCE, Permission.READ_USERS]; @@ -20,6 +21,7 @@ function AttendanceScreen() { const api = useApi(); const [user, setUser] = useState(undefined); const [missingPermissions, setMissingPermissions] = useState(undefined); + const { currentTheme } = useTheme(); async function onRefreshUser(forceRefresh: boolean = false) { if (!user || forceRefresh) { @@ -45,9 +47,9 @@ function AttendanceScreen() { const [attendanceType, setAttendanceType] = useState(AttendableType.NONE); return ( - <> + {attendanceType === AttendableType.NONE ? ( - + <> - + ) : ( )} - + ); } @@ -95,10 +97,7 @@ function AttendanceScreen() { } const styles = StyleSheet.create({ - container: { - flex: 1, - margin: 10, - }, + container: { flex: 1, padding: 10 }, }); export default AttendanceScreen; diff --git a/Attendance/BuzzCardPrompt.tsx b/Attendance/BuzzCardPrompt.tsx index 29fe1aa..5b18df2 100644 --- a/Attendance/BuzzCardPrompt.tsx +++ b/Attendance/BuzzCardPrompt.tsx @@ -6,6 +6,7 @@ import { EventInfo, postAttendance, TeamInfo } from '../Api/MeetingsApi'; import { AttendableType, NfcSource } from '../Api/Models/Attendance'; import { ActionPrompt } from '../Components/ActionPrompt'; import RoundedButton from '../Components/RoundedButton'; +import ThemedText from '../Components/ThemedText'; import { useTheme } from '../Themes/ThemeContextProvider'; import { LastAttendeeProps } from './TapABuzzCard'; @@ -180,8 +181,10 @@ const BuzzCardPrompt: React.FC = ({ return ( - - Type the entire 9-digit GTID, starting with 90. + + + Type the entire 9-digit GTID, starting with 90. + = ({ {enterGTIDManually && } {buzzCardState === 'Ready' ? ( - + ) : buzzCardState === 'TagLost' ? ( = ({ > ) : ( // buzzCardState === 'Processing' - + )} {Platform.OS === 'ios' && ( // only relevant to ios, after modal closed to display error diff --git a/Attendance/TapABuzzCard.tsx b/Attendance/TapABuzzCard.tsx index 03bd92a..801c427 100644 --- a/Attendance/TapABuzzCard.tsx +++ b/Attendance/TapABuzzCard.tsx @@ -1,9 +1,10 @@ import React, { useState } from 'react'; -import { SafeAreaView, StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, Text, View } from 'react-native'; import { EventInfo, TeamInfo } from '../Api/MeetingsApi'; import { AttendableType } from '../Api/Models/Attendance'; import MenuHeader from '../Components/MenuHeader'; import RoundedButton from '../Components/RoundedButton'; +import ThemedText from '../Components/ThemedText'; import BuzzCardPrompt from './BuzzCardPrompt'; type AttendanceProps = { @@ -47,13 +48,15 @@ function TapABuzzCard({ }} /> - Last attendee: {lastAttendee?.name || 'None'} + + Last attendee: {lastAttendee?.name || 'None'} + ); } return ( - + <> - Total attendees: {totalAttendees} + + Total attendees: {totalAttendees} + {totalAttendees in AttendanceGoals && ( - {AttendanceGoals[totalAttendees]} + + {AttendanceGoals[totalAttendees]} + )} - + ); } @@ -81,10 +88,6 @@ const styles = StyleSheet.create({ fontSize: 20, textAlign: 'right', }, - container: { - flex: 1, - margin: 10, - }, topPanelText: { fontSize: 20, paddingHorizontal: 10, diff --git a/Auth/AuthenticationScreen.tsx b/Auth/AuthenticationScreen.tsx index b052029..d7d1f03 100644 --- a/Auth/AuthenticationScreen.tsx +++ b/Auth/AuthenticationScreen.tsx @@ -3,14 +3,17 @@ import { Alert, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useAppEnvironment } from '../AppEnvironment'; import RoundedButton from '../Components/RoundedButton'; +import ThemedText from '../Components/ThemedText'; import TransparentButton from '../Components/TransparentButton'; import RoboBuzzSvg from '../icons/ic_robobuzz_white_outline.svg'; +import { useTheme } from '../Themes/ThemeContextProvider'; import { AuthContext } from './AuthContextProvider'; import * as Authentication from './Authentication'; import { authError } from './Authentication'; import EnvironmentSelect from './EnvironmentSelect'; function AuthenticationScreen() { + const { currentTheme } = useTheme(); const auth = useContext(AuthContext); const { environment } = useAppEnvironment(); const [envChangeVisible, setEnvChangeVisible] = useState(false); @@ -39,23 +42,25 @@ function AuthenticationScreen() { }, [auth?.authenticated]); return ( - + setEnvChangeVisible(true)} /> - - Server: {environment.name} ({environment.baseUrl}) - + + + Server: {environment.name} ({environment.baseUrl}) + + setEnvChangeVisible(false)} /> ); } const styles = StyleSheet.create({ - container: { backgroundColor: '#fff', flex: 1 }, + container: { flex: 1 }, lower: { alignItems: 'center', flexShrink: 1, padding: 10 }, upper: { alignItems: 'center', flex: 1, justifyContent: 'space-around' }, }); diff --git a/Auth/EnvironmentSelect.tsx b/Auth/EnvironmentSelect.tsx index e81ae22..c291abd 100644 --- a/Auth/EnvironmentSelect.tsx +++ b/Auth/EnvironmentSelect.tsx @@ -3,6 +3,8 @@ import { StyleSheet, Text, TextInput, View } from 'react-native'; import { Modal, RadioButton } from 'react-native-paper'; import { APP_ENVIRONMENTS, useAppEnvironment } from '../AppEnvironment'; import RoundedButton from '../Components/RoundedButton'; +import ThemedText from '../Components/ThemedText'; +import { useTheme } from '../Themes/ThemeContextProvider'; type EnvironmentSelectProps = { visible: boolean; @@ -10,6 +12,7 @@ type EnvironmentSelectProps = { }; export default function EnvironmentSelect({ visible, onDismiss }: EnvironmentSelectProps) { + const { currentTheme } = useTheme(); const { environment, setEnvironment } = useAppEnvironment(); const [selectedEnv, setSelectedEnv] = useState(environment.name.toLowerCase()); const [customUrl, setCustomUrl] = useState(''); @@ -37,20 +40,32 @@ export default function EnvironmentSelect({ visible, onDismiss }: EnvironmentSel } return ( - - Change Server + + + Change Server + - Production + + Production + - Test + + Test + - Other + + Other + @@ -64,7 +79,7 @@ export default function EnvironmentSelect({ visible, onDismiss }: EnvironmentSel /> )} - {error && {error}} + {error && {error}} ['name']; @@ -14,9 +15,9 @@ export const ActionPrompt = (props: ActionPromptProps) => { return ( - {props.title} - {props.subtitle && {props.subtitle}} - {props.subtitle2 && {props.subtitle2}} + {props.title} + {props.subtitle && {props.subtitle}} + {props.subtitle2 && {props.subtitle2}} ); }; diff --git a/Components/LoadingScreen.tsx b/Components/LoadingScreen.tsx index a463ae7..a26c7b1 100644 --- a/Components/LoadingScreen.tsx +++ b/Components/LoadingScreen.tsx @@ -1,12 +1,18 @@ import React from 'react'; -import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, Text } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTheme } from '../Themes/ThemeContextProvider'; +import ThemedText from './ThemedText'; export default function LoadingScreen() { + const { currentTheme } = useTheme(); return ( - + - Loading... - + + {'Loading...'} + + ); } diff --git a/Components/MenuHeader.tsx b/Components/MenuHeader.tsx index 03de23e..8265cf4 100644 --- a/Components/MenuHeader.tsx +++ b/Components/MenuHeader.tsx @@ -1,13 +1,16 @@ import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; +import ThemedText from './ThemedText'; type MenuHeaderProps = { title: string }; -const MenuHeader = ({ title }: MenuHeaderProps) => ( - - {title} - -); +const MenuHeader = ({ title }: MenuHeaderProps) => { + return ( + + {title} + + ); +}; const styles = StyleSheet.create({ headerContainer: { diff --git a/Components/MenuLink.tsx b/Components/MenuLink.tsx index bb9e014..07e4801 100644 --- a/Components/MenuLink.tsx +++ b/Components/MenuLink.tsx @@ -1,6 +1,8 @@ import MaterialIcons from '@react-native-vector-icons/material-icons'; import React from 'react'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { useTheme } from '../Themes/ThemeContextProvider'; +import ThemedText from './ThemedText'; type MenuLinkProps = { icon: React.ComponentProps['name']; @@ -8,18 +10,24 @@ type MenuLinkProps = { onClick: () => void; }; -const MenuLink = ({ icon, title, onClick }: MenuLinkProps) => ( - - - - - - {title} +const MenuLink = ({ icon, title, onClick }: MenuLinkProps) => { + const { currentTheme } = useTheme(); + + return ( + + + + + + + {title} + + - - - -); + + + ); +}; const styles = StyleSheet.create({ menuIcon: { diff --git a/Components/ThemedText.tsx b/Components/ThemedText.tsx new file mode 100644 index 0000000..d75bf3f --- /dev/null +++ b/Components/ThemedText.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { StyleSheet, Text, TextProps } from 'react-native'; +import { useTheme } from '../Themes/ThemeContextProvider'; + +type ThemedTextProps = TextProps & { + children: React.ReactNode; +}; + +const ThemedText = ({ children, style }: ThemedTextProps) => { + const { currentTheme } = useTheme(); + + return {children}; +}; + +const styles = StyleSheet.create({ + text: { + fontSize: 16, + }, +}); + +export default ThemedText; diff --git a/Navigation/NavBar.tsx b/Navigation/NavBar.tsx index e50a8e5..d4fb3a6 100644 --- a/Navigation/NavBar.tsx +++ b/Navigation/NavBar.tsx @@ -5,6 +5,7 @@ import React from 'react'; import AttendanceScreen from '../Attendance/AttendanceScreen'; import MerchandiseScreen from '../Merchandise/MerchandiseScreen'; import SettingsScreen from '../Settings/SettingsScreen'; +import { useTheme } from '../Themes/ThemeContextProvider'; type NavBarProps = { hidden?: boolean | null; }; @@ -12,11 +13,16 @@ type NavBarProps = { const Tab = createBottomTabNavigator(); function NavBar(props: NavBarProps) { + const { currentTheme } = useTheme(); + return ( ('disabled'); + // const [nfcEnabled, setNfcEnabled] = useState('disabled'); + const [nfcEnabled, setNfcEnabled] = useState('enabled'); - useEffect(() => { - const isEnabled = async () => { - try { - const isSupported = await NfcManager.isSupported(); - if (!isSupported) { - setNfcEnabled('unsupported'); - return; - } - if (Platform.OS === 'android') { - const enabled = await NfcManager.isEnabled(); - setNfcEnabled(enabled ? 'enabled' : 'disabled'); - } else if (Platform.OS === 'ios') { - setNfcEnabled('enabled'); - } else { - // Platform.OS === "windows" | "macos" | "web" - console.log('Not a valid platform for NFC'); - } - } catch (error) { - console.error('Error fetching data:', error); - } - }; - isEnabled(); - }, []); + // useEffect(() => { + // const isEnabled = async () => { + // try { + // const isSupported = await NfcManager.isSupported(); + // if (!isSupported) { + // setNfcEnabled('unsupported'); + // return; + // } + // if (Platform.OS === 'android') { + // const enabled = await NfcManager.isEnabled(); + // setNfcEnabled(enabled ? 'enabled' : 'disabled'); + // } else if (Platform.OS === 'ios') { + // setNfcEnabled('enabled'); + // } else { + // // Platform.OS === "windows" | "macos" | "web" + // console.log('Not a valid platform for NFC'); + // } + // } catch (error) { + // console.error('Error fetching data:', error); + // } + // }; + // isEnabled(); + // }, []); return ( ( - {title} + {title} ); @@ -47,8 +50,8 @@ function SettingsScreen() { - {title} - {subtitle && {subtitle}} + {title} + {subtitle && {subtitle}} @@ -56,7 +59,9 @@ function SettingsScreen() { const MadeWithLove = () => ( - {'Made with ♥ by RoboJackets'} + + {'Made with ♥ by RoboJackets'} + ); @@ -101,7 +106,7 @@ function SettingsScreen() { }, []); return ( - + Date: Sat, 11 Apr 2026 15:01:05 -0400 Subject: [PATCH 8/8] Appease linter --- Auth/InsufficientPermissions.tsx | 30 ++++++++++-------- Navigation/RootStack.tsx | 53 ++++++++++++++++---------------- Nfc/NfcEnabledScreen.tsx | 45 ++++++++++++++++++--------- 3 files changed, 74 insertions(+), 54 deletions(-) diff --git a/Auth/InsufficientPermissions.tsx b/Auth/InsufficientPermissions.tsx index 13563cd..008d2b0 100644 --- a/Auth/InsufficientPermissions.tsx +++ b/Auth/InsufficientPermissions.tsx @@ -2,6 +2,7 @@ import MaterialIcons from '@react-native-vector-icons/material-icons'; import React, { useState } from 'react'; import { Button, Modal, StyleSheet, Text, View } from 'react-native'; import { Permission } from '../Api/Models/Permission'; +import ThemedText from '../Components/ThemedText'; import { useTheme } from '../Themes/ThemeContextProvider'; type InsufficientPermissionsProps = { @@ -45,7 +46,7 @@ function InsufficientPermissions({ style={styles.icon} /> )} - {permission} + {permission} )); }; @@ -53,9 +54,11 @@ function InsufficientPermissions({ const PermissionDetailsDialog = () => { return ( - - - Required Permissions + + + + Required Permissions + @@ -68,7 +71,7 @@ function InsufficientPermissions({ }; return ( - + {detailsVisible && } - {featureName} permissions required + + {featureName} permissions required +