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, + }, + }, + }, +};