diff --git a/thi-app/app.json b/thi-app/app.json
index 873b5c9..88254b2 100644
--- a/thi-app/app.json
+++ b/thi-app/app.json
@@ -13,9 +13,7 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
- "assetBundlePatterns": [
- "**/*"
- ],
+ "assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"userInterfaceStyle": "automatic",
@@ -36,14 +34,18 @@
},
"plugins": [
"expo-router",
- "expo-font",
[
- "expo-screen-orientation",
+ "expo-font",
{
- "initialOrientation": "DEFAULT"
+ "fonts": ["./assets/fonts/Jost-VariableFont_wght.tff"]
}
],
- "expo-font"
+ [
+ "expo-screen-orientation",
+ {
+ "initialOrientation": "LANDSCAPE_LEFT"
+ }
+ ]
],
"experiments": {
"tsconfigPaths": true,
diff --git a/thi-app/app/(drawer)/_layout.tsx b/thi-app/app/(drawer)/_layout.tsx
index fb0df97..53028fc 100644
--- a/thi-app/app/(drawer)/_layout.tsx
+++ b/thi-app/app/(drawer)/_layout.tsx
@@ -1,47 +1,54 @@
-import React, { createContext, useContext, useState } from 'react';
-import { View, Dimensions, SafeAreaView } from 'react-native';
+import React, { useState } from "react";
+import { View, Dimensions, SafeAreaView } from "react-native";
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
- Easing,
-} from 'react-native-reanimated';
-import { GestureDetector, Gesture } from 'react-native-gesture-handler';
-import { Slot } from 'expo-router';
-import Sidebar, { SidebarContext, useSidebarContext } from '../../components/Sidebar';
+ runOnJS,
+} from "react-native-reanimated";
+import { GestureDetector, Gesture } from "react-native-gesture-handler";
+import { Slot } from "expo-router";
+import Sidebar, {
+ SidebarContext,
+ useSidebarContext,
+ useTransitionCustomization,
+} from "@/components/Sidebar";
-// Customize transition settings
-const TransitionCustomization = createContext({
- transitionEasing: Easing.out(Easing.cubic),
- transitionDuration: 400, // in ms
- // Add more
-})
-
-export const useTransitionCustomization = () => useContext(TransitionCustomization);
-
-export default function Layout() {
- // Sidebar state, dimensions
+const DrawerLayout = () => {
+ // Sidebar state, dimensions, and transition settings
+ const { transitionEasing, transitionDuration } = useTransitionCustomization();
+ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isTransitioning, setIsTransitioning] = useState(false);
- const { openSidebarWidth, closedSidebarWidth } = useSidebarContext();
+ const {
+ openSidebarWidth,
+ closedSidebarWidth,
+ activeIconTextColor,
+ defaultIconTextColor,
+ activeTabColor,
+ defaultTabColor,
+ buttonColor,
+ buttonSize,
+ } = useSidebarContext();
// Tie animations to initial sidebar state (currently set to open)
const sidebarAnimatedValue = useSharedValue(isSidebarOpen ? 1 : 0);
const mainScreenWidth = useSharedValue(
- Dimensions.get('window').width - (openSidebarWidth + closedSidebarWidth));
- // Transition settings
- const { transitionEasing, transitionDuration } = useTransitionCustomization();
- const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+ Dimensions.get("window").width - (openSidebarWidth + closedSidebarWidth)
+ );
- // Toggle sidebar state
const toggleSidebar = async () => {
- // Perform transitions
- setIsTransitioning(true);
- setIsSidebarOpen(!isSidebarOpen);
- transitionMainScreen();
- transitionSidebar();
- // Allow swipes after 400ms duration
- await delay(transitionDuration);
- setIsTransitioning(false);
+ if (
+ (sidebarAnimatedValue.value === 1 && isSidebarOpen) ||
+ (sidebarAnimatedValue.value === 0 && !isSidebarOpen)
+ ) {
+ setIsTransitioning(true);
+ setIsSidebarOpen(!isSidebarOpen);
+ transitionSidebar();
+ transitionMainScreen();
+ // Allow swipes after complete transition
+ await delay(transitionDuration);
+ setIsTransitioning(false);
+ }
};
// Set main screen dynamic width
@@ -49,69 +56,79 @@ export default function Layout() {
return { width: mainScreenWidth.value };
});
+ // Transitions sidebar for 400ms duration
+ const transitionSidebar = () => {
+ sidebarAnimatedValue.value = withTiming(isSidebarOpen ? 0 : 1, {
+ duration: transitionDuration,
+ easing: transitionEasing,
+ });
+ };
+
// Transitions main screen for 400ms duration
const transitionMainScreen = () => {
mainScreenWidth.value = withTiming(
- (mainScreenWidth.value === Dimensions.get('window').width - (openSidebarWidth + closedSidebarWidth) && isSidebarOpen) ?
- Dimensions.get('window').width - closedSidebarWidth :
- Dimensions.get('window').width - (openSidebarWidth + closedSidebarWidth),
- {
- duration: transitionDuration,
- easing: transitionEasing,
- }
- );
- };
-
- // Transitions sidebar for 400ms duration
- const transitionSidebar = () => {
- sidebarAnimatedValue.value = withTiming(
- sidebarAnimatedValue.value === 1 && isSidebarOpen ? 0 : 1, {
+ isSidebarOpen
+ ? Dimensions.get("window").width - closedSidebarWidth
+ : Dimensions.get("window").width - (openSidebarWidth + closedSidebarWidth),
+ {
duration: transitionDuration,
easing: transitionEasing,
}
);
};
- // Horizontal swipes trigger
- const swipeGesture = Gesture.Pan()
- .onUpdate((event) => {
+ // Horizontal swipe triggers sidebar toggle
+ const swipeGesture = Gesture.Pan().onUpdate((event) => {
// Ignore swipes mid-transition
if (isTransitioning) return;
- // Threshold horizontal distance in px for swipe to trigger
- if (event.translationX > 50 && !isSidebarOpen) {
- // Swipe right opens sidebar
- toggleSidebar();
- } else if (event.translationX < -50 && isSidebarOpen) {
- // Swipe left closes sidebar
- toggleSidebar();
+ // Threshold horizontal distance is 50 px to trigger
+ if (
+ (event.translationX > 50 && !isSidebarOpen) ||
+ (event.translationX < -50 && isSidebarOpen)
+ ) {
+ runOnJS(toggleSidebar)();
}
});
return (
-
+
-
{/* Sidebar */}
-
+
{/* Main screen in ./(drawer)/ */}
-
-
+
+
-
);
-}
\ No newline at end of file
+};
+export default DrawerLayout;
diff --git a/thi-app/app/(drawer)/schedule.tsx b/thi-app/app/(drawer)/schedule.tsx
new file mode 100644
index 0000000..3dac1fc
--- /dev/null
+++ b/thi-app/app/(drawer)/schedule.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import { View, Text } from "react-native";
+
+export default function SchedulePage() {
+ return (
+
+ Placeholder schedule page
+
+ );
+}
diff --git a/thi-app/app/_layout.tsx b/thi-app/app/_layout.tsx
index 85231a9..cb88785 100644
--- a/thi-app/app/_layout.tsx
+++ b/thi-app/app/_layout.tsx
@@ -1,10 +1,9 @@
import "../global.css";
-import FontAwesome from "@expo/vector-icons/FontAwesome";
-import { useFonts } from "expo-font";
-import { SplashScreen, Stack } from "expo-router";
import { useEffect } from "react";
import { View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
+import { useFonts } from "expo-font";
+import { SplashScreen, Stack } from "expo-router";
export {
// Catch any errors thrown by the Layout component.
@@ -21,22 +20,16 @@ SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
- Jost: require("../assets/fonts/Jost-VariableFont_wght.ttf"),
- ...FontAwesome.font,
+ 'Jost': require("../assets/fonts/Jost-VariableFont_wght.ttf")
});
- // Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => {
- if (error) throw error;
- }, [error]);
-
- useEffect(() => {
- if (loaded) {
+ if (loaded || error) {
SplashScreen.hideAsync();
}
- }, [loaded]);
+ }, [loaded, error]);
- if (!loaded) {
+ if (!loaded && !error) {
return null;
}
@@ -54,4 +47,4 @@ function RootLayoutNav() {
);
-}
\ No newline at end of file
+}
diff --git a/thi-app/app/index.tsx b/thi-app/app/index.tsx
index 679891b..30a1524 100644
--- a/thi-app/app/index.tsx
+++ b/thi-app/app/index.tsx
@@ -1,4 +1,4 @@
-import Login from '@/components/login';
+import Login from '@/components/Login';
import React from 'react';
import { View } from 'react-native';
diff --git a/thi-app/assets/icon.png b/thi-app/assets/images/icon.png
similarity index 100%
rename from thi-app/assets/icon.png
rename to thi-app/assets/images/icon.png
diff --git a/thi-app/assets/images/students_icon.png b/thi-app/assets/images/students_icon.png
deleted file mode 100644
index 2d983c5..0000000
Binary files a/thi-app/assets/images/students_icon.png and /dev/null differ
diff --git a/thi-app/components/ExternalLink.tsx b/thi-app/components/ExternalLink.tsx
deleted file mode 100644
index 05b76a5..0000000
--- a/thi-app/components/ExternalLink.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Link } from 'expo-router';
-import * as WebBrowser from 'expo-web-browser';
-import React from 'react';
-import { Platform } from 'react-native';
-
-export function ExternalLink(props: React.ComponentProps) {
- return (
- {
- if (Platform.OS !== 'web') {
- // Prevent the default behavior of linking to the default browser on native.
- e.preventDefault();
- // Open the link in an in-app browser.
- WebBrowser.openBrowserAsync(props.href as string);
- }
- }}
- />
- );
-}
diff --git a/thi-app/components/Sidebar.tsx b/thi-app/components/Sidebar.tsx
index a6b5fea..97f201c 100644
--- a/thi-app/components/Sidebar.tsx
+++ b/thi-app/components/Sidebar.tsx
@@ -1,236 +1,195 @@
-import React, { createContext, useContext, useState } from 'react';
-import { View, Text, TouchableOpacity, Image, Dimensions } from 'react-native';
-import Animated, { SharedValue, useAnimatedStyle, interpolate } from 'react-native-reanimated';
-import { useFocusEffect } from '@react-navigation/native';
-import { useRouter, useSegments } from 'expo-router';
-import { Entypo, FontAwesome, FontAwesome6, MaterialIcons, MaterialCommunityIcons } from '@expo/vector-icons';
-
-// Context for current screen
-export const ScreenContext = createContext({
- currentScreen: "",
- setCurrentScreen: () => {},
+import React, { createContext, useContext } from "react";
+import { View, Image, Dimensions, Pressable, ViewStyle } from "react-native";
+import Animated, {
+ SharedValue,
+ useAnimatedStyle,
+ interpolate,
+ FadeInLeft,
+ FadeOutLeft,
+ Easing,
+ AnimatedStyle,
+} from "react-native-reanimated";
+import {
+ Entypo,
+ FontAwesome,
+ FontAwesome6,
+ MaterialIcons,
+ MaterialCommunityIcons,
+} from "@expo/vector-icons";
+import SidebarTab, { dynamicIconSize } from "@/components/SidebarTab";
+
+// Sidebar state, dimensions, and customization
+export const SidebarContext = createContext({
+ isSidebarOpen: true,
+ toggleSidebar: () => {},
+ openSidebarWidth: Dimensions.get("window").width * 0.18,
+ closedSidebarWidth: Dimensions.get("window").width * 0.02,
+ activeIconTextColor: "white",
+ defaultIconTextColor: "black",
+ activeTabColor: "#10536699",
+ defaultTabColor: "transparent",
+ buttonColor: "#105366",
+ buttonSize: 2.3, // in rem (1 rem = 16px)
});
-// Context for sidebar state and dimensions
-export const SidebarContext = createContext({
- isSidebarOpen: true,
- toggleSidebar: () => {},
- openSidebarWidth: Dimensions.get('window').width * 0.18,
- closedSidebarWidth: Dimensions.get('window').width * 0.02,
+// Customize transition settings
+const TransitionCustomization = createContext({
+ transitionEasing: Easing.out(Easing.cubic),
+ transitionDuration: 400, // in ms
});
-// Customize sidebar
-const SidebarCustomization = createContext({
- currentIconTextColor: 'white',
- defaultIconTextColor: 'black',
- currentDrawerColor: '#10536699',
- defaultDrawerColor: 'transparent',
- buttonColor: '#105366',
- buttonSize: 2.5, // in rem (1 rem = 16px)
- // Add more
-})
-
-export const useScreenContext = () => useContext(ScreenContext);
export const useSidebarContext = () => useContext(SidebarContext);
-export const useSidebarCustomization = () => useContext(SidebarCustomization);
-
-export default function Sidebar({ animatedValue }: { animatedValue: SharedValue }) {
- // Sidebar state and screen details
- const router = useRouter();
- const { toggleSidebar, openSidebarWidth, closedSidebarWidth } = useSidebarContext();
- const [currentScreen, setCurrentScreen] = useState("");
- const segments: string[] = useSegments();
- // Sidebar customizations
- const {
- currentIconTextColor,
- defaultIconTextColor,
- currentDrawerColor,
- defaultDrawerColor,
- buttonColor,
- buttonSize,
- } = useSidebarCustomization();
-
- /* Animation interpolations */
-
- // Sidebar slide
- const sidebarStyle = useAnimatedStyle(() => ({
- transform: [
- { translateX: interpolate(animatedValue.value, [0, 1], [-openSidebarWidth, 0]) }
- ],
- }));
- // Button rotation (> when closed, < when open)
- const buttonStyle = useAnimatedStyle(() => ({
- transform: [
- { rotate: `${interpolate(animatedValue.value, [0, 0.5, 1], [0, -90, -180])}deg` }
- ],
- }));
- // Move current screen highlight off-screen when closed
- const currentHighlightStyle = useAnimatedStyle(() => ({
- transform: [
- { translateX: interpolate(animatedValue.value, [0, 1], [-closedSidebarWidth * 0.5, 0]) }
- ],
- }));
-
- // Set current screen name (e.g. "students" for (drawer)/students)
- useFocusEffect(
- React.useCallback(() => {
- if (segments.length > 0) {
- // Set screen name to last segment of URL
- const screenName = segments[segments.length - 1];
- setCurrentScreen(screenName);
- }
- }, [segments])
- );
-
- return (
- // Animated sidebar container
-
-
-
- {/* Open/collapse button */}
-
-
-
-
-
-
-
-
- {/* Sidebar */}
-
-
- {/* App icon */}
-
-
-
-
- {/* Navigable screens */}
-
-
-
- {/* Home */}
-
- router.push('/(drawer)/home')}>
-
-
-
-
- Home
-
-
-
-
- {/* Students */}
-
- router.push('/students')}>
-
-
-
-
- Students
-
-
-
-
- {/* Games */}
-
- router.push('/games')}>
-
-
-
-
- Games
-
-
-
-
- {/* Timer */}
-
- router.push('/timer')}>
-
-
-
-
- Timer
-
-
-
-
- {/* Settings */}
-
- router.push('/settings')}>
-
-
-
-
- Settings
-
-
-
-
-
-
-
- {/* Sign out */}
-
-
- router.push('/')}>
-
-
-
-
- Sign Out
-
-
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
+export const useTransitionCustomization = () => useContext(TransitionCustomization);
+
+// Open/close sidebar button
+const SidebarButton = ({ animatedStyle }: { animatedStyle: AnimatedStyle }) => {
+ const { toggleSidebar, openSidebarWidth, closedSidebarWidth, buttonColor, buttonSize } =
+ useSidebarContext();
+
+ return (
+
+
+
+
+
+ );
+};
+
+const Sidebar = ({ animatedValue }: { animatedValue: SharedValue }) => {
+ // Sidebar and transition details
+ const { transitionDuration, transitionEasing } = useTransitionCustomization();
+ const { isSidebarOpen, openSidebarWidth, closedSidebarWidth } = useSidebarContext();
+ const enteringAnimation = FadeInLeft.duration(transitionDuration / 1.5).easing(transitionEasing);
+ const exitingAnimation = FadeOutLeft.duration(transitionDuration).easing(transitionEasing);
+
+ // Sidebar slide interpolation
+ const sidebarStyle = useAnimatedStyle(() => ({
+ transform: [{ translateX: interpolate(animatedValue.value, [0, 1], [-openSidebarWidth, 0]) }],
+ }));
+
+ // Button rotation interpolation (> when closed, < when open)
+ const buttonStyle = useAnimatedStyle(() => ({
+ transform: [{ rotate: `${interpolate(animatedValue.value, [0, 0.5, 1], [0, -90, -180])}deg` }],
+ }));
+
+ return (
+
+
+ {/* Open/collapse button */}
+
+
+ {/* Sidebar contents */}
+
+ {isSidebarOpen && (
+
+ {/* App icon */}
+
+
+
+
+ {/* Navigable screens */}
+
+ {/* Home */}
+
+
+ {/* Students */}
+
+
+ {/* Schedule */}
+
+
+ {/* Games */}
+
+
+ {/* Timer */}
+
+
+ {/* Settings */}
+
+
+
+ )}
+
+ {/* Spacer */}
+
+ {isSidebarOpen && (
+
+ {/* Sign out */}
+
+
+ )}
+
+
+ {/* Bottom padding */}
+
+
+
+
+ );
+};
+export default Sidebar;
diff --git a/thi-app/components/SidebarTab.tsx b/thi-app/components/SidebarTab.tsx
new file mode 100644
index 0000000..d637075
--- /dev/null
+++ b/thi-app/components/SidebarTab.tsx
@@ -0,0 +1,98 @@
+import React, { useState } from "react";
+import { useFocusEffect } from "@react-navigation/native";
+import { RelativePathString, useRouter, useSegments } from "expo-router";
+import { View, Text, TouchableOpacity, Dimensions, DimensionValue } from "react-native";
+import { Icon } from "@expo/vector-icons/build/createIconSet";
+import { useSidebarContext } from "@/components/Sidebar";
+
+// Dynamic icon resizing
+export function dynamicIconSize(): number {
+ const { closedSidebarWidth } = useSidebarContext();
+ let size;
+ const height = Dimensions.get("window").height;
+ const outerContainerHeight = closedSidebarWidth;
+
+ if (height >= 800 && height > outerContainerHeight) {
+ size = 32; // lg:text-2xl
+ } else if (height >= 600 && height > outerContainerHeight) {
+ size = 24; // md:text-xl
+ } else {
+ size = 16; // default for small phones
+ }
+ return size;
+}
+
+// Define props for sidebar tab
+interface SidebarTabProps {
+ iconSet: Icon;
+ iconName: string;
+ iconSize?: number;
+ label: string;
+ useActiveColor?: boolean;
+ tabWidth?: DimensionValue;
+}
+
+// Sidebar navigation tab
+const SidebarTab = ({
+ iconSet: Icon,
+ iconName,
+ iconSize = dynamicIconSize(),
+ label,
+ useActiveColor = true,
+ tabWidth = "100%",
+}: SidebarTabProps) => {
+ // Sidebar details
+ const {
+ closedSidebarWidth,
+ activeIconTextColor,
+ defaultIconTextColor,
+ activeTabColor,
+ defaultTabColor,
+ } = useSidebarContext();
+ // Screen routing details
+ const router = useRouter();
+ const [currentScreen, setCurrentScreen] = useState("");
+ const segments: string[] = useSegments();
+ const tabName = label === "Sign out" ? "" : label.toLowerCase();
+ const directory = "/" + tabName;
+ const isActive = currentScreen.includes(tabName);
+ // Sidebar tab details
+ const tabHeight = Dimensions.get("window").height * 0.08;
+ const tabIconTextColor = useActiveColor && isActive ? activeIconTextColor : defaultIconTextColor;
+ const tabButtonColor = useActiveColor && isActive ? activeTabColor : defaultTabColor;
+
+ // Set current screen name (e.g. "students" for (drawer)/students)
+ useFocusEffect(
+ React.useCallback(() => {
+ if (segments.length > 0) {
+ // Set screen name to last segment of URL
+ const screenName = segments[segments.length - 1];
+ setCurrentScreen(screenName);
+ }
+ }, [segments])
+ );
+
+ return (
+
+ router.push(directory as RelativePathString)}
+ >
+
+
+
+
+
+ {label}
+
+
+
+
+ );
+};
+export default SidebarTab;
diff --git a/thi-app/components/StyledText.tsx b/thi-app/components/StyledText.tsx
deleted file mode 100644
index aa3977c..0000000
--- a/thi-app/components/StyledText.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Text, TextProps } from './Themed';
-
-export function MonoText(props: TextProps) {
- return ;
-}
diff --git a/thi-app/index.js b/thi-app/index.js
index 0671ee3..3a75b38 100644
--- a/thi-app/index.js
+++ b/thi-app/index.js
@@ -1,4 +1,4 @@
-import './gesture-handler';
+import "react-native-gesture-handler";
import ReactNativeFeatureFlags from "react-native/Libraries/ReactNative/ReactNativeFeatureFlags";
// enable the JS-side of the w3c PointerEvent implementation
@@ -6,7 +6,6 @@ ReactNativeFeatureFlags.shouldEmitW3CPointerEvents = () => true;
// enable hover events in Pressibility to be backed by the PointerEvent implementation.
// shouldEmitW3CPointerEvents should also be true
-ReactNativeFeatureFlags.shouldPressibilityUseW3CPointerEventsForHover = () =>
- true;
+ReactNativeFeatureFlags.shouldPressibilityUseW3CPointerEventsForHover = () => true;
-import "expo-router/entry";
\ No newline at end of file
+import "expo-router/entry";
diff --git a/thi-app/metro.config.js b/thi-app/metro.config.js
index 4180728..4aecfdc 100644
--- a/thi-app/metro.config.js
+++ b/thi-app/metro.config.js
@@ -1,5 +1,7 @@
const path = require("path");
const { getDefaultConfig } = require("expo/metro-config");
+const { withNativeWind } = require("nativewind/metro");
+const { wrapWithReanimatedMetroConfig } = require("react-native-reanimated/metro-config");
// 1. Enable CSS for Expo.
const config = getDefaultConfig(__dirname, {
@@ -17,8 +19,7 @@ config.resolver.nodeModulesPaths = [
];
// 2. Enable NativeWind
-const { withNativeWind } = require("nativewind/metro");
-module.exports = withNativeWind(config, {
+const nativeWindConfig = withNativeWind(config, {
// 3. Set `input` to your CSS file with the Tailwind at-rules
input: "global.css",
// This is optional
@@ -27,4 +28,7 @@ module.exports = withNativeWind(config, {
features: {
transformPercentagePolyfill: true,
},
-});
\ No newline at end of file
+});
+
+// 4. Enable Reanimated
+module.exports = wrapWithReanimatedMetroConfig(nativeWindConfig);
diff --git a/thi-app/tailwind.config.js b/thi-app/tailwind.config.js
index 122838a..a451f77 100644
--- a/thi-app/tailwind.config.js
+++ b/thi-app/tailwind.config.js
@@ -4,4 +4,20 @@ module.exports = {
content: ["app/**/*.{js,jsx,ts,tsx}", "components/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
plugins: [],
-};
\ No newline at end of file
+ theme: {
+ extend: {
+ fontFamily: {
+ jost: ["Jost-VariableFont_wght", "sans-serif"],
+ },
+ fontWeight: {
+ thin: 100,
+ light: 300,
+ regular: 400,
+ medium: 500,
+ semibold: 600,
+ bold: 700,
+ extrabold: 800,
+ },
+ },
+ },
+};