diff --git a/App.js b/App.js index 19e81fd..0b996c0 100644 --- a/App.js +++ b/App.js @@ -1,20 +1,21 @@ import "@expo/metro-runtime"; -import { useState, useEffect } from "react"; -import { ActivityIndicator } from "react-native"; -import { StatusBar } from "expo-status-bar"; +import AsyncStorage from "@react-native-async-storage/async-storage"; import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import AsyncStorage from "@react-native-async-storage/async-storage"; +import { StatusBar } from "expo-status-bar"; +import { useEffect, useState } from "react"; +import { ActivityIndicator } from "react-native"; -import colors from "./src/constants/colors"; -import HomeScreen from "./src/screens/HomeScreen"; import AccountScreen from "./src/screens/AccountScreen"; +import EmbeddedFormScreen from "./src/screens/EmbeddedFormScreen"; +import EndScreen from "./src/screens/EndScreen"; +import HomeScreen from "./src/screens/HomeScreen"; import SignInScreen from "./src/screens/SignInScreen"; import VolunteerFormScreen from "./src/screens/VolunteerFormScreen"; -import EmbeddedFormScreen from "./src/screens/EmbeddedFormScreen"; import VolunteerOpportunityScreen from "./src/screens/VolunteerOpportunityScreen"; -import EndScreen from "./src/screens/EndScreen"; + import HomeHeader from "./src/components/HomeHeader"; +import colors from "./src/constants/colors"; import { alertError } from "./src/utils"; const Stack = createNativeStackNavigator(); diff --git a/package-lock.json b/package-lock.json index f4284c5..8bc75f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "react": "18.2.0", "react-native": "0.74.5", "react-native-animated-dots-carousel": "^1.0.2", + "react-native-date-picker": "^5.0.4", "react-native-document-picker": "^9.3.0", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.16.1", @@ -12772,6 +12773,16 @@ "react-native": "*" } }, + "node_modules/react-native-date-picker": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/react-native-date-picker/-/react-native-date-picker-5.0.4.tgz", + "integrity": "sha512-UycNfXGd4ipgdU2a+oZGj7h1nvp8Gz49f/Ko+YdWh6nrBKB49MEp0n9eF1QngbxMKqdK0AdY4udb4IdVVWji4g==", + "license": "MIT", + "peerDependencies": { + "react": ">= 17.0.1", + "react-native": ">= 0.64.3" + } + }, "node_modules/react-native-document-picker": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-9.3.0.tgz", diff --git a/package.json b/package.json index 74b6406..2038359 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react": "18.2.0", "react-native": "0.74.5", "react-native-animated-dots-carousel": "^1.0.2", + "react-native-date-picker": "^5.0.4", "react-native-document-picker": "^9.3.0", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.16.1", diff --git a/src/components/CarouselPage.js b/src/components/CarouselPage.js index 0fcdcc0..205c9b6 100644 --- a/src/components/CarouselPage.js +++ b/src/components/CarouselPage.js @@ -1,10 +1,11 @@ import { useState } from "react"; -import { Alert, Pressable, StyleSheet, View, Dimensions } from "react-native"; +import { Alert, Dimensions, Pressable, StyleSheet, View } from "react-native"; import AnimatedDotsCarousel from "react-native-animated-dots-carousel"; -import VolunteerOpportunity from "./VolunteerOpportunity"; import Carousel from "react-native-reanimated-carousel"; + import Heading from "./Heading"; import RefreshButton from "./RefreshButton"; +import VolunteerOpportunity from "./VolunteerOpportunity"; export default function CarouselPage({ navigation, data, onRefresh }) { const [dotIndex, setDotIndex] = useState(0); diff --git a/src/components/CheckBoxQuery.js b/src/components/CheckBoxQuery.js index 663ec94..62c325c 100644 --- a/src/components/CheckBoxQuery.js +++ b/src/components/CheckBoxQuery.js @@ -1,5 +1,6 @@ -import { StyleSheet, Text, View } from "react-native"; import { Checkbox } from "expo-checkbox"; +import { StyleSheet, Text, View } from "react-native"; + import colors from "../constants/colors"; export default function CheckBoxQuery({ question, state, setState }) { @@ -13,18 +14,23 @@ export default function CheckBoxQuery({ question, state, setState }) { })); }} > - + {question} {" *"} { setState((prevState) => ({ ...prevState, - value: true, + value: "Yes", })); }} style={{ borderRadius: 20, transform: [{ scale: 1.3 }] }} @@ -40,11 +46,11 @@ export default function CheckBoxQuery({ question, state, setState }) { { setState((prevState) => ({ ...prevState, - value: false, + value: "No", })); }} style={{ borderRadius: 20, transform: [{ scale: 1.3 }] }} @@ -66,13 +72,10 @@ const styles = StyleSheet.create({ container: { flexGrow: 1, marginBottom: 25, - justifyContent: "center", - alignItems: "center", - alignSelf: "center", + alignSelf: "flex-start", }, checkBoxContainer: { flexDirection: "row", - alignItems: "center", paddingTop: 15, }, text: { diff --git a/src/components/FullWidthButton.js b/src/components/FullWidthButton.js index 4cdd0e9..65113ce 100644 --- a/src/components/FullWidthButton.js +++ b/src/components/FullWidthButton.js @@ -1,4 +1,4 @@ -import { StyleSheet, Text, Pressable } from "react-native"; +import { Pressable, StyleSheet, Text } from "react-native"; export default function FullWidthButton({ buttonStyle, diff --git a/src/components/Heading.js b/src/components/Heading.js index e8b3e28..723dc98 100644 --- a/src/components/Heading.js +++ b/src/components/Heading.js @@ -1,11 +1,11 @@ import { StyleSheet, Text } from "react-native"; export default function Heading({ children }) { - return {children}; + return {children}; } const styles = StyleSheet.create({ - heading: { + container: { fontSize: 18, fontWeight: "bold", marginBottom: 5, diff --git a/src/components/HomeHeader.js b/src/components/HomeHeader.js index 8469802..a59779e 100644 --- a/src/components/HomeHeader.js +++ b/src/components/HomeHeader.js @@ -1,13 +1,13 @@ -import { useState, useEffect } from "react"; +import { LinearGradient } from "expo-linear-gradient"; +import { useEffect, useState } from "react"; import { + ActivityIndicator, Image, - Text, - StyleSheet, Pressable, SafeAreaView, - ActivityIndicator, + StyleSheet, + Text, } from "react-native"; -import { LinearGradient } from "expo-linear-gradient"; import colors from "../constants/colors"; import { getUser } from "../utils"; diff --git a/src/components/MultiSelect.js b/src/components/MultiSelect.js new file mode 100644 index 0000000..6a9945b --- /dev/null +++ b/src/components/MultiSelect.js @@ -0,0 +1,77 @@ +import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; +import { useState } from "react"; +import { Pressable, StyleSheet, Text, View } from "react-native"; + +export default function MultiSelect({ state, setState, title, options }) { + const [selected, setSelected] = useState( + new Array(options.length).fill(false), + ); + + return ( + { + setState((prevState) => ({ + ...prevState, + y: event.nativeEvent.layout.y, + })); + }} + style={styles.container} + > + + {title} + + {options.map((option, index) => ( + { + setState((previous) => { + const newValue = selected[index] + ? previous.value.filter((item) => item !== option) + : [...previous.value, option]; + return { ...previous, value: newValue }; + }); + + setSelected((previous) => { + const next = [...previous]; // Create a new copy of the array + next[index] = !previous[index]; + return next; + }); + }} + > + + + + {option} + + + + ))} + + ); +} + +const styles = StyleSheet.create({ + container: { + marginBottom: 20, + }, + + title: { + fontSize: 18, + fontWeight: "600", + marginBottom: 10, + flexWrap: "wrap", + }, + + optionText: { + fontSize: 16, + marginLeft: 10, + flexWrap: "wrap", + }, +}); diff --git a/src/components/MultipleChoice.js b/src/components/MultipleChoice.js index 01c61c5..687bfbc 100644 --- a/src/components/MultipleChoice.js +++ b/src/components/MultipleChoice.js @@ -1,4 +1,4 @@ -import { View, Text, Pressable, StyleSheet } from "react-native"; +import { Pressable, StyleSheet, Text, View } from "react-native"; import colors from "../constants/colors"; export default function MultipleChoice({ @@ -18,28 +18,28 @@ export default function MultipleChoice({ })); }} > - + {title} * - {mapObject(options, (key) => ( + {options.map((value) => ( onSelect(key)} + onPress={() => onSelect(value)} > - {state.value === key && } + {state.value === value && } - {key} + {value} ))} @@ -48,7 +48,7 @@ export default function MultipleChoice({ } const styles = StyleSheet.create({ - label: { + title: { fontSize: 18, fontWeight: "600", marginBottom: 10, @@ -83,11 +83,3 @@ const styles = StyleSheet.create({ fontWeight: "condensedBold", }, }); - -function mapObject(obj, callback) { - const items = []; - for (const key in obj) { - items.push(callback(key)); - } - return items; -} diff --git a/src/components/NextButton.js b/src/components/NextButton.js index 23ffcbb..ee02333 100644 --- a/src/components/NextButton.js +++ b/src/components/NextButton.js @@ -1,9 +1,9 @@ -import { StyleSheet, Text, View, Image } from "react-native"; import Feather from "@expo/vector-icons/Feather"; +import { StyleSheet, Text, View } from "react-native"; export default function NextButton() { return ( - + {"Next"} @@ -11,7 +11,7 @@ export default function NextButton() { } const styles = StyleSheet.create({ - back: { + container: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", diff --git a/src/components/OtherOpportunities.js b/src/components/OtherOpportunities.js index 89b9ad5..4da7565 100644 --- a/src/components/OtherOpportunities.js +++ b/src/components/OtherOpportunities.js @@ -1,17 +1,21 @@ -import { View, Text, StyleSheet, Image, Pressable, Alert } from "react-native"; +import { Pressable, StyleSheet, Text, View } from "react-native"; + import Entypo from "@expo/vector-icons/Entypo"; -import Ionicons from "@expo/vector-icons/Ionicons"; import FontAwesome from "@expo/vector-icons/FontAwesome"; +import Ionicons from "@expo/vector-icons/Ionicons"; +import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; -export default function OtherOpportunities() { +export default function OtherOpportunities({ navigation }) { return ( - + - Alert.alert( - "Request a Concert is not available yet. Please check back soon for more updates!", - ) + navigation.navigate("Volunteer Form", { + title: "Request a Concert", + location: null, + date: null, + }) } > + + navigation.navigate("Volunteer Form", { + title: "Audacity Dance Club", + location: null, + date: null, + }) + } + > + + Sign Up for Audacity Dance Club + + @@ -38,7 +66,7 @@ export default function OtherOpportunities() { } const styles = StyleSheet.create({ - cardContainer: { + container: { backgroundColor: "#f5f5f5", borderRadius: 10, marginBottom: 10, diff --git a/src/components/Profile.js b/src/components/Profile.js index 5a1216d..f5bc00c 100644 --- a/src/components/Profile.js +++ b/src/components/Profile.js @@ -1,5 +1,5 @@ -import { useState, useEffect } from "react"; -import { StyleSheet, Text, View, Image, ActivityIndicator } from "react-native"; +import { useEffect, useState } from "react"; +import { ActivityIndicator, Image, StyleSheet, Text, View } from "react-native"; import { getUser } from "../utils"; @@ -11,7 +11,7 @@ export default function Profile() { }, []); return ( - + {user?.photo ? ( ); } - -const styles = StyleSheet.create({}); diff --git a/src/components/SignUpButton.js b/src/components/SignUpButton.js index 4148042..90f91f5 100644 --- a/src/components/SignUpButton.js +++ b/src/components/SignUpButton.js @@ -1,10 +1,10 @@ -import { StyleSheet, Text, View, Image } from "react-native"; -import colors from "../constants/colors"; import FontAwesome from "@expo/vector-icons/FontAwesome"; +import { StyleSheet, Text, View } from "react-native"; +import colors from "../constants/colors"; export default function SignUpButton() { return ( - + {"\n\n\n"} Sign Up @@ -13,7 +13,7 @@ export default function SignUpButton() { } const styles = StyleSheet.create({ - signUp: { + container: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", diff --git a/src/components/Tag.js b/src/components/Tag.js index 770eca4..bee59d5 100644 --- a/src/components/Tag.js +++ b/src/components/Tag.js @@ -1,15 +1,15 @@ -import { StyleSheet, Text, View, Image } from "react-native"; +import { StyleSheet, Text, View } from "react-native"; export default function Tag(props) { return ( - + {props.text} ); } const styles = StyleSheet.create({ - back: { + container: { alignItems: "center", justifyContent: "center", borderRadius: 15, diff --git a/src/components/TextField.js b/src/components/TextField.js index 6ea1210..13c74cb 100644 --- a/src/components/TextField.js +++ b/src/components/TextField.js @@ -8,6 +8,8 @@ export default function TextField({ maxLength = 32000, // Limit of chars on Google Forms state, setState, + extraMargin = true, + valid = null, }) { return ( - {title} - {title.slice(-10) == "(optional)" ? null : ( + + {title} + + {title == "" || title.slice(-10) == "(optional)" ? null : ( * )} @@ -28,7 +36,10 @@ export default function TextField({ { setState((prevState) => ({ @@ -61,6 +72,5 @@ const styles = StyleSheet.create({ borderWidth: 1.5, borderColor: colors.secondary, textAlign: "center", - marginBottom: 20, }, }); diff --git a/src/components/TimeSlotList.js b/src/components/TimeSlotList.js new file mode 100644 index 0000000..ae56710 --- /dev/null +++ b/src/components/TimeSlotList.js @@ -0,0 +1,406 @@ +import { useState } from "react"; +import { Pressable, StyleSheet, Text, View } from "react-native"; +import DatePicker from "react-native-date-picker"; + +import EvilIcons from "@expo/vector-icons/EvilIcons"; +import Ionicons from "@expo/vector-icons/Ionicons"; + +import colors from "../constants/colors"; + +function timeFormatter(date) { + const hour = date.getHours() % 12 == 0 ? 12 : date.getHours() % 12; + const minute = (date.getMinutes() < 10 ? "0" : "") + date.getMinutes(); + const period = date.getHours() >= 12 ? "PM" : "AM"; + return `${hour}:${minute} ${period}`; +} + +function dateFormatter(date) { + const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + return `${days[date.getDay()]} ${date.getMonth() + 1}/${date.getDate() < 10 ? "0" + date.getDate() : date.getDate()}/${date.getFullYear() - 2000} ${timeFormatter(date)}`; +} + +function timeCompare(hour1, minute1, hour2, minute2) { + if (hour1 < hour2) { + return -1; + } else if (hour1 > hour2) { + return 1; + } else { + if (minute1 < minute2) { + return -1; + } else if (minute1 > minute2) { + return 1; + } else { + return 0; + } + } +} + +export class TimeSlot { + constructor(start = new Date(), end = new Date()) { + this.start = start; + this.end = end; + this.valid = true; + } + + validate() { + const start_hour = this.start.getHours(); + const start_minute = this.start.getMinutes(); + const end_hour = this.end.getHours(); + const end_minute = this.end.getMinutes(); + + if (this.start >= this.end) { + this.valid = false; + } else if ( + timeCompare(start_hour, start_minute, 10, 30) < 0 || + timeCompare(start_hour, start_minute, 17, 0) > 0 + ) { + this.valid = false; + } else if (timeCompare(end_hour, end_minute, 18, 0) > 0) { + this.valid = false; + } else { + this.valid = true; + } + + return this.valid; + } + + toString() { + return `${dateFormatter(this.start)} - ${timeFormatter(this.end)}`; + } + + compareTo(other) { + if (this.start < other.start) { + return -1; + } else if (this.start > other.start) { + return 1; + } else { + if (this.end < other.end) { + return -1; + } else if (this.end > other.end) { + return 1; + } else { + return 0; + } + } + } + + render(state, setState, index, setIndex, setIsOpen, setIsAdded, setIsStart) { + return ( + + + { + setIsOpen(true); + setIsAdded(false); + setIsStart(true); + setIndex(index); + }} + > + + {dateFormatter(this.start)} + + + + {" "} + -{" "} + + { + setIsOpen(true); + setIsAdded(false); + setIsStart(false); + setIndex(index); + }} + > + + {timeFormatter(this.end)} + + + + + + ); + } +} + +function RemoveButton({ state, setState, index }) { + return ( + { + setState((previous) => { + return { + ...previous, + value: previous.value + .slice(0, index) + .concat(previous.value.slice(index + 1, state.value.length)), + }; + }); + }} + > + + + ); +} + +function AddButton({ + state, + setState, + setIndex, + setIsAdded, + setIsOpen, + setIsStart, +}) { + return ( + { + setIsAdded(true); + setIsStart(true); + let length = state.value.length; + setIndex(length); + setState((previous) => { + return { + ...previous, + value: previous.value.concat([new TimeSlot()]), + }; + }); + setIsOpen(true); + }} + > + + Add Time Slot + + ); +} + +export function Select({ + state, + setState, + index, + isAdded, + isStart, + setIsStart, + isOpen, + setIsOpen, +}) { + const now = new Date(); + + let start = state.value[index].start; + let end = state.value[index].end; + + return ( + { + if (isStart) { + setState((previous) => { + let next = previous.value; + next[index].start = date; + return { ...previous, value: next }; + }); + setIsStart(false); + + setIsOpen(false); + + if (isAdded) { + setIsOpen(true); + } + } else { + setState((previous) => { + let next = previous.value; + next[index].end = date; + return { ...previous, value: next }; + }); + setIsOpen(false); + } + }} + onCancel={() => { + if (isAdded && isStart) { + setState((previous) => { + return { ...previous, value: previous.value.slice(0, index - 1) }; + }); + } + + setIsOpen(false); + }} + /> + ); +} + +export default function TimeSlotList({ title, state, setState }) { + const [open, setIsOpen] = useState(false); + const [start, setIsStart] = useState(true); + const [added, setIsAdded] = useState(false); + const [index, setIndex] = useState(0); + + return ( + { + setState((prevState) => ({ + ...prevState, + y: event.nativeEvent.layout.y, + })); + }} + > + + {title} + * + + + { + "Each time slot must start between 10:30 am and 5 pm and end before 6 pm." + } + + {state.value.map((slot, index) => + slot == null || (index == state.value.length - 1 && open && added) + ? null + : slot.render( + state, + setState, + index, + setIndex, + setIsOpen, + setIsAdded, + setIsStart, + ), + )} + + {open ? ( +