From ddb0d5d16847898e08d05ad7eb0839fbe30b0868 Mon Sep 17 00:00:00 2001 From: Pramad712 Date: Thu, 15 Aug 2024 16:05:09 -0700 Subject: [PATCH 01/22] add time slot component --- package-lock.json | 11 +++ package.json | 1 + src/components/Slot.js | 162 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 src/components/Slot.js diff --git a/package-lock.json b/package-lock.json index be34a22..7ce50f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react": "18.2.0", "react-native": "0.74.3", "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.17.1", @@ -12522,6 +12523,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 f358747..57f0cb6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react": "18.2.0", "react-native": "0.74.3", "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.17.1", diff --git a/src/components/Slot.js b/src/components/Slot.js new file mode 100644 index 0000000..d9fa5f9 --- /dev/null +++ b/src/components/Slot.js @@ -0,0 +1,162 @@ +import { useState } from 'react'; +import { View, Pressable, Text, StyleSheet } from 'react-native' +import DatePicker from 'react-native-date-picker' +import Ionicons from '@expo/vector-icons/Ionicons'; + +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)}`; +} + +export class TimeSlot { + constructor(start=new Date(), end=new Date()) { + this.start = start; + this.end = end; + } + + 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(list, setList, index, setIndex, setOpen, setAdded, setStart) { + return ( + + + {setOpen(true); setAdded(false); setStart(true); setIndex(index);}}> + {dateFormatter(this.start)} + + - + {setOpen(true); setAdded(false); setStart(false); setIndex(index);}}> + {timeFormatter(this.end)} + + + + + ); + } +} + +function RemoveButton({list, setList, index}) { + return ( + {setList(previous => {return previous.slice(0, index).concat(previous.slice(index + 1, list.length))})}}> + + Delete + + ); +} + +function AddButton({list, setList, setIndex, setAdded, setOpen, setStart}) { + return ( + { + setAdded(true); + setStart(true); + let length = list.length; + setIndex(length); + setList((previous) => previous.concat([new TimeSlot()])); + setOpen(true); + }}> + + Add Time Slot + + ); +} + +export function Select({list, setList, index, added, start, setStart, open, setOpen}) { + const now = new Date(); + console.log(list); + + return ( + { + if (start) { + setList((previous) => {let next = previous; next[index].start = date; return next}); + setStart(false); + + setOpen(false); + + if (added) { + setOpen(true); + } + } + + else { + setList((previous) => {let next = previous; next[index].end = date; return next}) + setOpen(false); + } + }} + + onCancel={() => { + if (added && start) { + setList((previous) => {return previous.slice(0, index - 1)}); + } + + setOpen(false) + }} + /> + ); +} + +export default function SlotList({slots, setSlots}) { + const [open, setOpen] = useState(false); + const [start, setStart] = useState(true); + const [added, setAdded] = useState(false); + const [index, setIndex] = useState(0); + + return ( + + {slots.map((slot, index) => (slot == null) || (((index == slots.length - 1) && open && added)) ? null : slot.render(slots, setSlots, index, setIndex, setOpen, setAdded, setStart))} + + + + {open ? : null} + { + setState((prevState) => ({ + ...prevState, + y: event.nativeEvent.layout.y, + })); + }} + > + + {state.value.map((slot, index) => (slot == null) || (((index == state.value.length - 1) && open && added)) ? null : slot.render(state, setState, index, setIndex, setOpen, setAdded, setStart))} + + {open ? : null} @@ -160,6 +206,19 @@ export default function SlotList({state, setState}) { } const styles = StyleSheet.create({ + title: { + fontSize: 18, + fontWeight: "600", + marginBottom: 10, + flexWrap: "wrap", + }, + + requirements: { + fontSize: 15, + color: colors.secondary, + flexWrap: "wrap", + }, + container: { flex: 1, marginTop: 50 diff --git a/src/components/Tag.js b/src/components/Tag.js index 770eca4..105e246 100644 --- a/src/components/Tag.js +++ b/src/components/Tag.js @@ -2,14 +2,14 @@ import { StyleSheet, Text, View, Image } 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/UploadButton.js b/src/components/UploadButton.js index 76883dd..778a924 100644 --- a/src/components/UploadButton.js +++ b/src/components/UploadButton.js @@ -53,7 +53,7 @@ export default function UploadButton({ })); }} > - + {title} {required ? * : null} @@ -158,7 +158,7 @@ export default function UploadButton({ } const styles = StyleSheet.create({ - label: { + title: { fontSize: 18, fontWeight: "700", paddingBottom: 10, diff --git a/src/components/Websites.js b/src/components/Websites.js index 7ebd0d2..17a8ca0 100644 --- a/src/components/Websites.js +++ b/src/components/Websites.js @@ -15,7 +15,7 @@ export default function Websites() { return ( - + openLink("https://eternityband.org/")} @@ -81,7 +81,7 @@ export default function Websites() { } const styles = StyleSheet.create({ - cardContainer: { + container: { backgroundColor: "#f5f5f5", borderRadius: 10, }, diff --git a/src/constants/questions.js b/src/constants/questions.js new file mode 100644 index 0000000..b6318ca --- /dev/null +++ b/src/constants/questions.js @@ -0,0 +1,626 @@ +import { useState, useEffect } from "react"; +import { getUser, Question, emptyQuestionState } from "../utils"; +import TextField from "../components/TextField"; +import CheckBoxQuery from "../components/CheckBoxQuery"; +import UploadButton from "../components/UploadButton"; +import NextButton from "../components/NextButton"; +import MultipleChoice from "../components/MultipleChoice"; +import MultiSelect from "../components/MultiSelect"; +import SlotList from "../components/Slot"; + +const isAtLeast = (value, len) => value?.trim().length >= len; +const isNotEmpty = (value) => isAtLeast(value, 1); + +function MusicHour(title, navigation) { + const [fullName, setFullName] = emptyQuestionState(); + + useEffect(() => { + (async () => { + try { + const user = await getUser(); + setFullName((prevState) => ({ ...prevState, value: user?.name })); + } catch (error) { + console.error(error); + } + })(); + }, []); + + const [city, setCity] = emptyQuestionState(); + const [phoneNumber, setPhoneNumber] = emptyQuestionState(); + const [age, setAge] = emptyQuestionState(); + const [musicPiece, setMusicPiece] = emptyQuestionState(); + const [composer, setComposer] = emptyQuestionState(); + const [instrument, setInstrument] = emptyQuestionState(); + const [performanceType, setPerformanceType] = emptyQuestionState(); + const [length, setLength] = emptyQuestionState(); + const [recordingLink, setRecordingLink] = emptyQuestionState(); + const [publicPermission, setPublicPermission] = emptyQuestionState(); + const [parentalConsent, setParentalConsent] = emptyQuestionState(); + const [pianoAccompaniment, setPianoAccompaniment] = emptyQuestionState(); + const [ensembleProfile, setEnsembleProfile] = emptyQuestionState(); + const [otherInfo, setOtherInfo] = emptyQuestionState(); + const [timeLimit, setTimeLimit] = useState( + title == "Library Music Hour" ? 0 : 10, + ); + + const performanceOptions = { + "Individual performance only": 8, + "Individual performance and music instrument presentation": 12, + "Group performance only": 15, + "Group performance and music instrument presentation": 20, + "Library Band Ensemble (Band, Orchestra, or Choir)": 60, + }; + + let questions = [ + new Question({ + component: ( + + ), + validate: (value) => value.trim().split(" ").length >= 2, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: (value) => isAtLeast(value, 10), + }), + + new Question({ + component: ( + + ), + validate(value) { + const age = Number(value); + if (isNaN(age)) { + return false; + } + return age >= 5 && age <= 125; + }, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + title == "Library Music Hour" + ? new Question({ + component: ( + { + setPerformanceType((prevState) => ({ + ...prevState, + value: option, + })); + setTimeLimit(performanceOptions[option]); + }} + key="performanceType" + state={performanceType} + setState={setPerformanceType} + /> + ), + validate: isNotEmpty, + }) + : null, + + new Question({ + component: ( + + ), + validate(value) { + const time = Number(value); + if (isNaN(time)) { + return false; + } + return time > 0 && time <= timeLimit; + }, + }), + + new Question({ + component: ( + + ), + validate(value) { + try { + new URL(value); + } catch { + return false; + } + return true; + }, + }), + + new Question({ + component: ( + + ), + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ), + validate: (value) => value, + isVisible: () => age.value < 18, + }), + + new Question({ + component: ( + + ), + // Only PDF files can be uploaded + // Optional + validate: () => + pianoAccompaniment.value == null || + pianoAccompaniment.value[1] <= 104857600, // There are 104,857,600 bytes in 100 MB + }), + + title == "Library Music Hour" + ? new Question({ + component: ( + + ), + + isVisible: () => performanceType.value?.includes("Ensemble"), + + // Only PDF files can be uploaded + // Required only if visible (selected ensemble option) + validate: () => + ensembleProfile.value != null && + ensembleProfile.value[1] <= 104857600, // There are 104,857,600 bytes in 100 MB + }) + : null, + + new Question({ + component: ( + + ), + }), + ]; + + questions = questions.filter((question) => question != null); + return questions; +} + +function RequestConcert() { + const [phoneNumber, setPhoneNumber] = emptyQuestionState(); + const [organization, setOrganization] = emptyQuestionState(); + const [eventInfo, setEventInfo] = emptyQuestionState(); + const [venue, setVenue] = emptyQuestionState(); + const [publicity, setPublicity] = emptyQuestionState(); + const [stipend, setStipend] = emptyQuestionState(); + const [donatable, setDonatable] = emptyQuestionState(); + const [slots, setSlots] = emptyQuestionState([]); + const [audience, setAudience] = emptyQuestionState(); + const [distance, setDistance] = emptyQuestionState(); + const [provided, setProvided] = emptyQuestionState([]); + const [donationBox, setDonationBox] = emptyQuestionState(); + const [extraAudience, setExtraAudience] = emptyQuestionState(); + const [otherInfo, setOtherInfo] = emptyQuestionState(); + + return [ + new Question({ + component: ( + + ), + validate: (value) => isAtLeast(value, 10), + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + { + setPublicity((prevState) => ({ ...prevState, value: option })); + }} + key="publicity" + state={publicity} + setState={setPublicity} + /> + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ), + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ), + + validate: (value) => { + if (value.length == 0) { + return false; + } + + for (const slot of value) { + if (!slot.validate()) { + return false; + } + } + + return true; + } + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ), + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ) + }) + ] +} + +function DanceClub() { + const [fullName, setFullName] = emptyQuestionState(); + + useEffect(() => { + (async () => { + try { + const user = await getUser(); + setFullName((prevState) => ({ ...prevState, value: user?.name })); + } catch (error) { + console.error(error); + } + })(); + }, []); + + const [phoneNumber, setPhoneNumber] = emptyQuestionState(); + const [favoritePieces, setFavoritePieces] = emptyQuestionState(); + const [age, setAge] = emptyQuestionState(); + const [favoriteDanceStyles, setFavoriteDanceStyles] = emptyQuestionState(); + const [consent, setConsent] = emptyQuestionState(); + const [recording, setRecording] = emptyQuestionState(); + + return [ + new Question({ + component: ( + + ), + validate: (value) => value.trim().split(" ").length >= 2, + }), + + new Question({ + component: ( + + ), + validate: (value) => isAtLeast(value, 10), + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + {setAge((prevState) => ({ ...prevState, value: value }))}} + key="age" + state={age} + setState={setAge} + /> + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + {setFavoriteDanceStyles((prevState) => ({ ...prevState, value: value }))}} + key="favoriteDanceStyles" + state={favoriteDanceStyles} + setState={setFavoriteDanceStyles} + /> + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: (value) => value, + }), + + new Question({ + component: ( + + ), + validate: (value) => value, + }), + ] +} + +export default function questions(navigation) { + return { + "Library Music Hour": MusicHour("Library Music Hour", navigation), + "Music by the Tracks": MusicHour("Music by the Tracks", navigation), + "Request Concert": RequestConcert(), + "Audacity Dance Club": DanceClub(), + }; +} \ No newline at end of file diff --git a/src/screens/VolunteerFormScreen.js b/src/screens/VolunteerFormScreen.js index e4b11a6..003a0ae 100644 --- a/src/screens/VolunteerFormScreen.js +++ b/src/screens/VolunteerFormScreen.js @@ -13,7 +13,7 @@ import { } from "react-native"; import forms from "../constants/forms"; -import { getUser, FormString, submitForm, hashForm } from "../utils"; +import { getUser, Question, emptyQuestionState, FormString, submitForm, hashForm } from "../utils"; import TextField from "../components/TextField"; import CheckBoxQuery from "../components/CheckBoxQuery"; @@ -23,24 +23,9 @@ import MultipleChoice from "../components/MultipleChoice"; import AsyncStorage from "@react-native-async-storage/async-storage"; -class Question { - constructor({ component, validate = (_) => true, isVisible = () => true }) { - this.component = component; - this.validate = () => !isVisible() || validate(component.props.state.value); - this.isVisible = isVisible; - this.state = component.props.state; - this.setState = component.props.setState; - this.y = component.props.state.y; - } -} - export default function VolunteerFormScreen({ navigation, route }) { const { title, location, date } = route.params; - function emptyQuestionState(initial = null) { - return useState({ value: initial, y: null, valid: true }); - } - const [fullName, setFullName] = emptyQuestionState(); useEffect(() => { diff --git a/src/utils/index.js b/src/utils/index.js index 9dcb51b..bcd354a 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,3 +1,4 @@ +import { useState } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; export async function getUser() { @@ -12,6 +13,21 @@ export async function getUser() { } } +export class Question { + constructor({ component, validate = (_) => true, isVisible = () => true }) { + this.component = component; + this.validate = () => !isVisible() || validate(component.props.state.value); + this.isVisible = isVisible; + this.state = component.props.state; + this.setState = component.props.setState; + this.y = component.props.state.y; + } +} + +export function emptyQuestionState(initial = null) { + return useState({ value: initial, y: null, valid: true }); +} + export function hashForm(title, location, date) { return title + location + date; } From f4d28cea41c33e3a5e5622c70eda87cea6563ccd Mon Sep 17 00:00:00 2001 From: Pramad712 Date: Sat, 24 Aug 2024 20:26:10 -0700 Subject: [PATCH 07/22] add questions for all forms, refractor them into index.js, and add request concert form screen navigate (todo: fix cannot read property map of undefined when request concert button pressed) --- src/components/OtherOpportunities.js | 13 +- src/constants/forms.js | 40 -- src/constants/questions.js | 626 -------------------- src/screens/HomeScreen.js | 2 +- src/screens/VolunteerFormScreen.js | 389 +------------ src/utils/index.js | 824 ++++++++++++++++++++++++++- 6 files changed, 840 insertions(+), 1054 deletions(-) delete mode 100644 src/constants/forms.js delete mode 100644 src/constants/questions.js diff --git a/src/components/OtherOpportunities.js b/src/components/OtherOpportunities.js index 98b9bcc..7630ced 100644 --- a/src/components/OtherOpportunities.js +++ b/src/components/OtherOpportunities.js @@ -1,9 +1,18 @@ import { View, Text, StyleSheet, Image, Pressable } from "react-native"; -export default function OtherOpportunities() { +export default function OtherOpportunities({navigation}) { return ( - + + navigation.navigate("Volunteer Opportunity", { + title: "Request Concert", + location: null, + date: null, + }) + } + > value?.trim().length >= len; -const isNotEmpty = (value) => isAtLeast(value, 1); - -function MusicHour(title, navigation) { - const [fullName, setFullName] = emptyQuestionState(); - - useEffect(() => { - (async () => { - try { - const user = await getUser(); - setFullName((prevState) => ({ ...prevState, value: user?.name })); - } catch (error) { - console.error(error); - } - })(); - }, []); - - const [city, setCity] = emptyQuestionState(); - const [phoneNumber, setPhoneNumber] = emptyQuestionState(); - const [age, setAge] = emptyQuestionState(); - const [musicPiece, setMusicPiece] = emptyQuestionState(); - const [composer, setComposer] = emptyQuestionState(); - const [instrument, setInstrument] = emptyQuestionState(); - const [performanceType, setPerformanceType] = emptyQuestionState(); - const [length, setLength] = emptyQuestionState(); - const [recordingLink, setRecordingLink] = emptyQuestionState(); - const [publicPermission, setPublicPermission] = emptyQuestionState(); - const [parentalConsent, setParentalConsent] = emptyQuestionState(); - const [pianoAccompaniment, setPianoAccompaniment] = emptyQuestionState(); - const [ensembleProfile, setEnsembleProfile] = emptyQuestionState(); - const [otherInfo, setOtherInfo] = emptyQuestionState(); - const [timeLimit, setTimeLimit] = useState( - title == "Library Music Hour" ? 0 : 10, - ); - - const performanceOptions = { - "Individual performance only": 8, - "Individual performance and music instrument presentation": 12, - "Group performance only": 15, - "Group performance and music instrument presentation": 20, - "Library Band Ensemble (Band, Orchestra, or Choir)": 60, - }; - - let questions = [ - new Question({ - component: ( - - ), - validate: (value) => value.trim().split(" ").length >= 2, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: (value) => isAtLeast(value, 10), - }), - - new Question({ - component: ( - - ), - validate(value) { - const age = Number(value); - if (isNaN(age)) { - return false; - } - return age >= 5 && age <= 125; - }, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - title == "Library Music Hour" - ? new Question({ - component: ( - { - setPerformanceType((prevState) => ({ - ...prevState, - value: option, - })); - setTimeLimit(performanceOptions[option]); - }} - key="performanceType" - state={performanceType} - setState={setPerformanceType} - /> - ), - validate: isNotEmpty, - }) - : null, - - new Question({ - component: ( - - ), - validate(value) { - const time = Number(value); - if (isNaN(time)) { - return false; - } - return time > 0 && time <= timeLimit; - }, - }), - - new Question({ - component: ( - - ), - validate(value) { - try { - new URL(value); - } catch { - return false; - } - return true; - }, - }), - - new Question({ - component: ( - - ), - validate: (value) => value != null, - }), - - new Question({ - component: ( - - ), - validate: (value) => value, - isVisible: () => age.value < 18, - }), - - new Question({ - component: ( - - ), - // Only PDF files can be uploaded - // Optional - validate: () => - pianoAccompaniment.value == null || - pianoAccompaniment.value[1] <= 104857600, // There are 104,857,600 bytes in 100 MB - }), - - title == "Library Music Hour" - ? new Question({ - component: ( - - ), - - isVisible: () => performanceType.value?.includes("Ensemble"), - - // Only PDF files can be uploaded - // Required only if visible (selected ensemble option) - validate: () => - ensembleProfile.value != null && - ensembleProfile.value[1] <= 104857600, // There are 104,857,600 bytes in 100 MB - }) - : null, - - new Question({ - component: ( - - ), - }), - ]; - - questions = questions.filter((question) => question != null); - return questions; -} - -function RequestConcert() { - const [phoneNumber, setPhoneNumber] = emptyQuestionState(); - const [organization, setOrganization] = emptyQuestionState(); - const [eventInfo, setEventInfo] = emptyQuestionState(); - const [venue, setVenue] = emptyQuestionState(); - const [publicity, setPublicity] = emptyQuestionState(); - const [stipend, setStipend] = emptyQuestionState(); - const [donatable, setDonatable] = emptyQuestionState(); - const [slots, setSlots] = emptyQuestionState([]); - const [audience, setAudience] = emptyQuestionState(); - const [distance, setDistance] = emptyQuestionState(); - const [provided, setProvided] = emptyQuestionState([]); - const [donationBox, setDonationBox] = emptyQuestionState(); - const [extraAudience, setExtraAudience] = emptyQuestionState(); - const [otherInfo, setOtherInfo] = emptyQuestionState(); - - return [ - new Question({ - component: ( - - ), - validate: (value) => isAtLeast(value, 10), - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - { - setPublicity((prevState) => ({ ...prevState, value: option })); - }} - key="publicity" - state={publicity} - setState={setPublicity} - /> - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - - validate: (value) => value != null, - }), - - new Question({ - component: ( - - ), - validate: (value) => value != null, - }), - - new Question({ - component: ( - - ), - - validate: (value) => { - if (value.length == 0) { - return false; - } - - for (const slot of value) { - if (!slot.validate()) { - return false; - } - } - - return true; - } - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: (value) => value != null, - }), - - new Question({ - component: ( - - ), - validate: (value) => value != null, - }), - - new Question({ - component: ( - - ) - }) - ] -} - -function DanceClub() { - const [fullName, setFullName] = emptyQuestionState(); - - useEffect(() => { - (async () => { - try { - const user = await getUser(); - setFullName((prevState) => ({ ...prevState, value: user?.name })); - } catch (error) { - console.error(error); - } - })(); - }, []); - - const [phoneNumber, setPhoneNumber] = emptyQuestionState(); - const [favoritePieces, setFavoritePieces] = emptyQuestionState(); - const [age, setAge] = emptyQuestionState(); - const [favoriteDanceStyles, setFavoriteDanceStyles] = emptyQuestionState(); - const [consent, setConsent] = emptyQuestionState(); - const [recording, setRecording] = emptyQuestionState(); - - return [ - new Question({ - component: ( - - ), - validate: (value) => value.trim().split(" ").length >= 2, - }), - - new Question({ - component: ( - - ), - validate: (value) => isAtLeast(value, 10), - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - {setAge((prevState) => ({ ...prevState, value: value }))}} - key="age" - state={age} - setState={setAge} - /> - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - {setFavoriteDanceStyles((prevState) => ({ ...prevState, value: value }))}} - key="favoriteDanceStyles" - state={favoriteDanceStyles} - setState={setFavoriteDanceStyles} - /> - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: (value) => value, - }), - - new Question({ - component: ( - - ), - validate: (value) => value, - }), - ] -} - -export default function questions(navigation) { - return { - "Library Music Hour": MusicHour("Library Music Hour", navigation), - "Music by the Tracks": MusicHour("Music by the Tracks", navigation), - "Request Concert": RequestConcert(), - "Audacity Dance Club": DanceClub(), - }; -} \ No newline at end of file diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index 5218182..e2b0ecb 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -123,7 +123,7 @@ export default function HomeScreen({ navigation, route }) { onRefresh={onRefresh} /> Other Opportunities - + Websites diff --git a/src/screens/VolunteerFormScreen.js b/src/screens/VolunteerFormScreen.js index 003a0ae..91ac8fa 100644 --- a/src/screens/VolunteerFormScreen.js +++ b/src/screens/VolunteerFormScreen.js @@ -12,396 +12,17 @@ import { KeyboardAvoidingView, } from "react-native"; -import forms from "../constants/forms"; -import { getUser, Question, emptyQuestionState, FormString, submitForm, hashForm } from "../utils"; - -import TextField from "../components/TextField"; -import CheckBoxQuery from "../components/CheckBoxQuery"; -import UploadButton from "../components/UploadButton"; +import { getUser, Question, emptyQuestionState, FormString, submitForm, hashForm, questions } from "../utils"; import NextButton from "../components/NextButton"; -import MultipleChoice from "../components/MultipleChoice"; + import AsyncStorage from "@react-native-async-storage/async-storage"; export default function VolunteerFormScreen({ navigation, route }) { const { title, location, date } = route.params; - - const [fullName, setFullName] = emptyQuestionState(); - - useEffect(() => { - (async () => { - try { - const user = await getUser(); - setFullName((prevState) => ({ ...prevState, value: user?.name })); - } catch (error) { - console.error(error); - } - })(); - }, []); - - const [city, setCity] = emptyQuestionState(); - const [phoneNumber, setPhoneNumber] = emptyQuestionState(); - const [age, setAge] = emptyQuestionState(); - const [musicPiece, setMusicPiece] = emptyQuestionState(); - const [composer, setComposer] = emptyQuestionState(); - const [instrument, setInstrument] = emptyQuestionState(); - const [performanceType, setPerformanceType] = emptyQuestionState(); - const [length, setLength] = emptyQuestionState(); - const [recordingLink, setRecordingLink] = emptyQuestionState(); - const [publicPermission, setPublicPermission] = emptyQuestionState(); - const [parentalConsent, setParentalConsent] = emptyQuestionState(); - const [pianoAccompaniment, setPianoAccompaniment] = emptyQuestionState(); - const [ensembleProfile, setEnsembleProfile] = emptyQuestionState(); - const [otherInfo, setOtherInfo] = emptyQuestionState(); - const [scrollObject, setScrollObject] = useState(null); - const [timeLimit, setTimeLimit] = useState( - title == "Library Music Hour" ? 0 : 10, - ); - - const performanceOptions = { - "Individual performance only": 8, - "Individual performance and music instrument presentation": 12, - "Group performance only": 15, - "Group performance and music instrument presentation": 20, - "Library Band Ensemble (Band, Orchestra, or Choir)": 60, - }; - - const isAtLeast = (value, len) => value?.trim().length >= len; - const isNotEmpty = (value) => isAtLeast(value, 1); - - let questions = [ - new Question({ - component: ( - - ), - validate: (value) => value.trim().split(" ").length >= 2, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: (value) => isAtLeast(value, 10), - }), - - new Question({ - component: ( - - ), - validate(value) { - const age = Number(value); - if (isNaN(age)) { - return false; - } - return age >= 5 && age <= 125; - }, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - new Question({ - component: ( - - ), - validate: isNotEmpty, - }), - - title == "Library Music Hour" - ? new Question({ - component: ( - { - setPerformanceType((prevState) => ({ - ...prevState, - value: option, - })); - setTimeLimit(performanceOptions[option]); - }} - key="performanceType" - state={performanceType} - setState={setPerformanceType} - /> - ), - validate: isNotEmpty, - }) - : null, - - new Question({ - component: ( - - ), - validate(value) { - const time = Number(value); - if (isNaN(time)) { - return false; - } - return time > 0 && time <= timeLimit; - }, - }), - - new Question({ - component: ( - - ), - validate(value) { - try { - new URL(value); - } catch { - return false; - } - return true; - }, - }), - - new Question({ - component: ( - - ), - validate: (value) => value != null, - }), - - new Question({ - component: ( - - ), - validate: (value) => value, - isVisible: () => age.value < 18, - }), - - new Question({ - component: ( - - ), - // Only PDF files can be uploaded - // Optional - validate: () => - pianoAccompaniment.value == null || - pianoAccompaniment.value[1] <= 104857600, // There are 104,857,600 bytes in 100 MB - }), - - title == "Library Music Hour" - ? new Question({ - component: ( - - ), - - isVisible: () => performanceType.value?.includes("Ensemble"), - - // Only PDF files can be uploaded - // Required only if visible (selected ensemble option) - validate: () => - ensembleProfile.value != null && - ensembleProfile.value[1] <= 104857600, // There are 104,857,600 bytes in 100 MB - }) - : null, - - new Question({ - component: ( - - ), - }), - ]; - - questions = questions.filter((q) => q != null); - - async function submit() { - let allValid = true; - let minInvalidY = Infinity; - - for (const question of questions) { - const isValid = question.validate(); - question.setState((prevState) => ({ - ...prevState, - valid: isValid, - })); - - if (!isValid) { - allValid = false; - if (question.y < minInvalidY) { - minInvalidY = question.y; - } - } - } - - if (!allValid) { - scrollObject.scrollTo({ - x: 0, - y: minInvalidY, - animated: true, - }); - return; - } - - if (!forms.hasOwnProperty(title)) { - Alert.alert(`Form "${title}" is not implemented`); - return; - } - - const form = forms[title]; - const formData = new FormString(); - - if (title == "Library Music Hour" || title == "Music by the Tracks") { - formData.append(form.location, location); - formData.append(form.date, date); - formData.append(form.fullName, fullName.value); - formData.append(form.city, city.value); - formData.append(form.phoneNumber, phoneNumber.value); - formData.append(form.age, age.value); - formData.append(form.musicPiece, musicPiece.value); - formData.append(form.composer, composer.value); - formData.append(form.instrument, instrument.value); - formData.append(form.length, length.value); - formData.append(form.recordingLink, recordingLink.value); - if (title == "Library Music Hour") { - formData.append(form.performanceType, performanceType.value); - } - formData.append( - form.publicPermission, - publicPermission.value ? "Yes" : "No", - ); - formData.append( - form.parentalConsent, - parentalConsent.value == null - ? "" - : parentalConsent.value - ? "Yes" - : "No", - ); - formData.append( - form.pianoAccompaniment, - pianoAccompaniment.value ? pianoAccompaniment.value[0] : "", - ); - if (title == "Library Music Hour") { - formData.append( - form.ensembleProfile, - ensembleProfile.value ? ensembleProfile.value[0] : "", - ); - } - formData.append(form.otherInfo, otherInfo.value ?? ""); - } - - if (!submitForm(form.id, formData)) { - navigation.navigate("End", { isSuccess: false }); - return; - } - - try { - const submittedForms = await AsyncStorage.getItem("submittedForms"); - const hash = hashForm(title, location, date); - if (submittedForms == null) { - await AsyncStorage.setItem("submittedForms", JSON.stringify([hash])); - } else { - const newForms = JSON.parse(submittedForms); - newForms.push(hash); - await AsyncStorage.setItem("submittedForms", JSON.stringify(newForms)); - } - } catch (error) { - console.error(`Unable to get/save submittedForms: ${error}`); - } - navigation.navigate("End", { isSuccess: true }); - } + const form = questions(date, location, navigation, scrollObject)[title]; return ( @@ -428,11 +49,11 @@ export default function VolunteerFormScreen({ navigation, route }) { - {questions + {form.questions() .filter((question) => question?.isVisible()) .map((question) => question.component)} - submit()}> + form.submit()}> diff --git a/src/utils/index.js b/src/utils/index.js index bcd354a..153268b 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,4 +1,12 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { Alert } from "react-native"; +import TextField from "../components/TextField"; +import CheckBoxQuery from "../components/CheckBoxQuery"; +import UploadButton from "../components/UploadButton"; +import NextButton from "../components/NextButton"; +import MultipleChoice from "../components/MultipleChoice"; +import MultiSelect from "../components/MultiSelect"; +import SlotList from "../components/Slot"; import AsyncStorage from "@react-native-async-storage/async-storage"; export async function getUser() { @@ -53,6 +61,7 @@ export class FormString { export async function submitForm(formId, formData) { const formUrl = `https://docs.google.com/forms/d/e/${formId}/formResponse`; console.log("Form Data: " + formData); + try { console.log(formData); const response = await fetch(formUrl, { @@ -73,3 +82,816 @@ export async function submitForm(formId, formData) { return false; } } + +const isAtLeast = (value, len) => !value ? false : value.trim().length >= len; +const isNotEmpty = (value) => isAtLeast(value, 1); + +class Form { + constructor(scrollObject) { + this.scrollObject = scrollObject; + } + + questions() { + return []; + } + + validate() { + let allValid = true; + let minInvalidY = Infinity; + + for (const question of this.questions()) { + const isValid = question.validate(); + question.setState((prevState) => ({ + ...prevState, + valid: isValid, + })); + + if (!isValid) { + allValid = false; + if (question.y < minInvalidY) { + minInvalidY = question.y; + } + } + } + + if (!allValid) { + this.scrollObject.scrollTo({ + x: 0, + y: minInvalidY, + animated: true, + }); + return false; + } + + return true; + } +} + +class MusicHour extends Form { + constructor(title, date, location, navigation, scrollObject) { + super(scrollObject); + + this.title = title; + this.form = formEntryIDs[title]; + this.navigation = navigation; + this.date = date; + this.location = location; + + this.fullName = emptyQuestionState(); + + useEffect(() => { + (async () => { + try { + const user = await getUser(); + this.fullName[1]((prevState) => ({ ...prevState, value: user?.name })); + } catch (error) { + console.error(error); + } + })(); + }, []); + + this.city = emptyQuestionState(); + this.phoneNumber = emptyQuestionState(); + this.age = emptyQuestionState(); + this.musicPiece = emptyQuestionState(); + this.composer = emptyQuestionState(); + this.instrument = emptyQuestionState(); + this.performanceType = emptyQuestionState(); + this.length = emptyQuestionState(); + this.recordingLink = emptyQuestionState(); + this.publicPermission = emptyQuestionState(); + this.parentalConsent = emptyQuestionState(); + this.pianoAccompaniment = emptyQuestionState(); + this.ensembleProfile = emptyQuestionState(); + this.otherInfo = emptyQuestionState(); + this.timeLimit = useState( + title == "Library Music Hour" ? 0 : 10, + ); + + this.performanceOptions = { + "Individual performance only": 8, + "Individual performance and music instrument presentation": 12, + "Group performance only": 15, + "Group performance and music instrument presentation": 20, + "Library Band Ensemble (Band, Orchestra, or Choir)": 60, + }; + } + + questions() { + let questions = [ + new Question({ + component: ( + + ), + validate: (value) => value.trim().split(" ").length >= 2, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: (value) => isAtLeast(value, 10), + }), + + new Question({ + component: ( + + ), + validate(value) { + const age = Number(value); + if (isNaN(age)) { + return false; + } + return age >= 5 && age <= 125; + }, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + this.title == "Library Music Hour" + ? new Question({ + component: ( + { + this.performanceType[1]((prevState) => ({ + ...prevState, + value: option, + })); + this.timeLimit[1](this.performanceOptions[option]); + }} + key="performanceType" + state={this.performanceType[0]} + setState={this.performanceType[1]} + /> + ), + validate: isNotEmpty, + }) + : null, + + new Question({ + component: ( + + ), + validate(value) { + const time = Number(value); + if (isNaN(time)) { + return false; + } + // return time > 0 && time <= this.timeLimit[0]; + return time > 0; + }, + }), + + new Question({ + component: ( + + ), + validate(value) { + try { + new URL(value); + } catch { + return false; + } + return true; + }, + }), + + new Question({ + component: ( + + ), + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ), + validate: (value) => value, + isVisible: () => this.age[0].value < 18, + }), + + new Question({ + component: ( + + ), + // Only PDF files can be uploaded + // Optional + validate: () => + this.pianoAccompaniment[0].value == null || + this.pianoAccompaniment[0].value[1] <= 104857600, // There are 104,857,600 bytes in 100 MB + }), + + this.title == "Library Music Hour" + ? new Question({ + component: ( + + ), + + isVisible: () => this.performanceType[0].value?.includes("Ensemble"), + + // Only PDF files can be uploaded + // Required only if visible (selected ensemble option) + validate: () => + this.ensembleProfile[0].value != null && + this.ensembleProfile[0].value <= 104857600, // There are 104,857,600 bytes in 100 MB + }) + : null, + + new Question({ + component: ( + + ), + }), + ]; + + questions = questions.filter((question) => question != null); + return questions; + } + + async submit() { + if (!super.validate()) { + return; + } + + const formData = new FormString(); + + formData.append(this.form.location, this.location); + formData.append(this.form.date, this.date); + formData.append(this.form.fullName, this.fullName[0].value); + formData.append(this.form.city, this.city[0].value); + formData.append(this.form.phoneNumber, this.phoneNumber[0].value); + formData.append(this.form.age, this.age[0].value); + formData.append(this.form.musicPiece, this.musicPiece[0].value); + formData.append(this.form.composer, this.composer[0].value); + formData.append(this.form.instrument, this.instrument[0].value); + formData.append(this.form.length, this.length[0].value); + formData.append(this.form.recordingLink, this.recordingLink[0].value); + + if (this.title == "Library Music Hour") { + formData.append(this.form.performanceType, this.performanceType[0].value); + } + + formData.append( + this.form.publicPermission, + this.publicPermission[0].value ? "Yes" : "No", + ); + + formData.append( + this.form.parentalConsent, + this.parentalConsent[0].value == null + ? "" + : this.parentalConsent[0].value + ? "Yes" + : "No", + ); + + formData.append( + this.form.pianoAccompaniment, + this.pianoAccompaniment[0].value ? this.pianoAccompaniment[0].value[0] : "", + ); + + if (this.title == "Library Music Hour") { + formData.append( + this.form.ensembleProfile, + this.ensembleProfile[0].value ? this.ensembleProfile[0].value[0] : "", + ); + } + + if (!submitForm(this.form.id, formData)) { + this.navigation.navigate("End", { isSuccess: false }); + return; + } + + try { + const submittedForms = await AsyncStorage.getItem("submittedForms"); + const hash = hashForm(this.title, this.location, this.date); + if (submittedForms == null) { + await AsyncStorage.setItem("submittedForms", JSON.stringify([hash])); + } else { + const newForms = JSON.parse(submittedForms); + newForms.push(hash); + await AsyncStorage.setItem("submittedForms", JSON.stringify(newForms)); + } + } catch (error) { + console.error(`Unable to get/save submittedForms: ${error}`); + } + + this.navigation.navigate("End", { isSuccess: true }); + } +} + +class RequestConcert extends Form { + constructor(scrollObject, navigation) { + super(scrollObject); + this.navigation = navigation; + + this.phoneNumber = emptyQuestionState(); + this.organization = emptyQuestionState(); + this.eventInfo = emptyQuestionState(); + this.venue = emptyQuestionState(); + this.publicity = emptyQuestionState(); + this.stipend = emptyQuestionState(); + this.donatable = emptyQuestionState(); + this.slots = emptyQuestionState([]); + this.audience = emptyQuestionState(); + this.distance = emptyQuestionState(); + this.provided = emptyQuestionState([]); + this.donationBox = emptyQuestionState(); + this.extraAudience = emptyQuestionState(); + this.otherInfo = emptyQuestionState(); + } + + questions() { + return [ + new Question({ + component: ( + + ), + validate: (value) => isAtLeast(value, 10), + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + { + this.publicity[1]((prevState) => ({ ...prevState, value: option })); + }} + key="publicity" + state={this.publicity[0]} + setState={this.publicity[1]} + /> + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ), + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ), + + validate: (value) => { + if (value.length == 0) { + return false; + } + + for (const slot of value) { + if (!slot.validate()) { + return false; + } + } + + return true; + } + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ), + validate: (value) => value != null, + }), + + new Question({ + component: ( + + ) + }) + ]; + } + + submit() { + if (!super.validate()) { + return; + } + + Alert.alert("Not Implemented", "The Request Concert form's submission has not been implemented yet. Please contact the IT Team."); + } +} + +class DanceClub extends Form{ + constructor(scrollObject, navigation) { + super(scrollObject); + this.navigation = navigation; + + this.fullName = emptyQuestionState(); + + useEffect(() => { + (async () => { + try { + const user = await getUser(); + this.fullName[1]((prevState) => ({ ...prevState, value: user?.name })); + } catch (error) { + console.error(error); + } + })(); + }, []); + + this.phoneNumber = emptyQuestionState(); + this.favoritePieces = emptyQuestionState(); + this.age = emptyQuestionState(); + this.favoriteDanceStyles = emptyQuestionState(); + this.consent = emptyQuestionState(); + this.recording = emptyQuestionState(); + } + + questions() { + return [ + new Question({ + component: ( + + ), + validate: (value) => value.trim().split(" ").length >= 2, + }), + + new Question({ + component: ( + + ), + validate: (value) => isAtLeast(value, 10), + }), + + new Question({ + component: ( + + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + {this.age[1]((prevState) => ({ ...prevState, value: value }))}} + key="age" + state={this.age[0]} + setState={this.age[1]} + /> + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + {this.favoriteDanceStyles[1]((prevState) => ({ ...prevState, value: value }))}} + key="favoriteDanceStyles" + state={this.favoriteDanceStyles[0]} + setState={this.favoriteDanceStyles[1]} + /> + ), + validate: isNotEmpty, + }), + + new Question({ + component: ( + + ), + validate: (value) => value, + }), + + new Question({ + component: ( + + ), + validate: (value) => value, + }), + ]; + } + + submit() { + if (!super.validate()) { + return; + } + + Alert.alert("Not Implemented", "The Dance Club Signup form's submission has not been implemented yet. Please contact the IT Team."); + } +} + +export function questions(date, location, navigation, scrollObject) { + return { + "Library Music Hour": new MusicHour("Library Music Hour", date, location, navigation, scrollObject), + "Music by the Tracks": new MusicHour("Music by the Tracks", date, location, navigation, scrollObject), + "Request Concert": new RequestConcert(scrollObject, navigation), + "Audacity Dance Club": new DanceClub(scrollObject, navigation), + }; +} + +export const formEntryIDs = { + "Library Music Hour": { + id: "1FAIpQLSf7SfdPH1WRNscth7BTy9hZpFwhm-CLRlToryqsQjSFU1EPlg", + location: "476670975", + date: "785137847", + fullName: "194705108", + city: "171289053", + phoneNumber: "1427621981", + age: "2098203720", + musicPiece: "762590043", + composer: "979503979", + instrument: "1774708872", + length: "44358071", + recordingLink: "687629199", + performanceType: "1453633813", + publicPermission: "1607100323", + parentalConsent: "1363593782", + pianoAccompaniment: "1307690728", + ensembleProfile: "1413514639", + otherInfo: "995802706", + }, + "Music by the Tracks": { + id: "1FAIpQLSfjPUdis44wJBjAXgegrKVaUzzvG-kRgErcV5td76BDU-dSSg", + location: "476670975", + date: "785137847", + fullName: "194705108", + city: "171289053", + phoneNumber: "1427621981", + age: "2098203720", + musicPiece: "762590043", + composer: "979503979", + instrument: "1774708872", + length: "44358071", + recordingLink: "687629199", + publicPermission: "1607100323", + parentalConsent: "1363593782", + pianoAccompaniment: "1307690728", + otherInfo: "995802706", + }, +}; From 2dd5b6013184860c38e8c87f3de7c5164e070892 Mon Sep 17 00:00:00 2001 From: Pramad712 Date: Sun, 1 Sep 2024 14:23:12 -0700 Subject: [PATCH 08/22] Add support for Request Concert and Dance Club. Note that as of right now they are in the volunteer forms spreadsheet, not yet a button in other opportunities --- src/components/CheckBoxQuery.js | 5 +-- src/components/MultiSelect.js | 5 +++ src/components/MultipleChoice.js | 12 +++---- src/components/Slot.js | 35 ++++++++++++-------- src/utils/index.js | 56 +++++++++++++++++++++++++++----- 5 files changed, 80 insertions(+), 33 deletions(-) diff --git a/src/components/CheckBoxQuery.js b/src/components/CheckBoxQuery.js index 663ec94..69d1ba4 100644 --- a/src/components/CheckBoxQuery.js +++ b/src/components/CheckBoxQuery.js @@ -66,13 +66,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/MultiSelect.js b/src/components/MultiSelect.js index df4003b..af98ee2 100644 --- a/src/components/MultiSelect.js +++ b/src/components/MultiSelect.js @@ -13,6 +13,7 @@ export default function MultiSelect({ state, setState, title, options }) { y: event.nativeEvent.layout.y, })); }} + style={styles.container} > {title} {options.map((option, index) => ( @@ -41,6 +42,10 @@ export default function MultiSelect({ state, setState, title, options }) { } const styles = StyleSheet.create({ + container: { + marginBottom: 20, + }, + title: { fontSize: 18, fontWeight: "600", diff --git a/src/components/MultipleChoice.js b/src/components/MultipleChoice.js index 45a0bac..346b2a8 100644 --- a/src/components/MultipleChoice.js +++ b/src/components/MultipleChoice.js @@ -24,22 +24,22 @@ export default function MultipleChoice({ * - {mapObject(options, (key) => ( + {options.map((value, index) => ( onSelect(key)} + onPress={() => onSelect(value)} > - {state.value === key && } + {state.value === value && } - {key} + {value} ))} diff --git a/src/components/Slot.js b/src/components/Slot.js index ad8dd13..075d111 100644 --- a/src/components/Slot.js +++ b/src/components/Slot.js @@ -2,6 +2,7 @@ import { useState } from 'react'; import { View, Pressable, Text, StyleSheet } from 'react-native' import DatePicker from 'react-native-date-picker' import Ionicons from '@expo/vector-icons/Ionicons'; +import FontAwesome6 from '@expo/vector-icons/FontAwesome6'; import colors from '../constants/colors'; function timeFormatter(date) { @@ -63,6 +64,10 @@ export class TimeSlot { this.valid = false; } + else { + this.valid = true; + } + return this.valid; } @@ -90,18 +95,18 @@ export class TimeSlot { return ( {setOpen(true); setAdded(false); setStart(true); setIndex(index);}}> - {dateFormatter(this.start)} + {dateFormatter(this.start)} - - + - {setOpen(true); setAdded(false); setStart(false); setIndex(index);}}> - {timeFormatter(this.end)} + {timeFormatter(this.end)} - + ); } @@ -110,8 +115,7 @@ export class TimeSlot { 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))}})}}> - - Delete + ); } @@ -126,8 +130,8 @@ function AddButton({state, setState, setIndex, setAdded, setOpen, setStart}) { setState((previous) => {return {...previous, value: previous.value.concat([new TimeSlot()])}}); setOpen(true); }}> - - Add Time Slot + + Add Time Slot ); } @@ -194,8 +198,8 @@ export default function SlotList({title, state, setState}) { })); }} > - {title} - + {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, setOpen, setAdded, setStart))} @@ -211,21 +215,24 @@ const styles = StyleSheet.create({ fontWeight: "600", marginBottom: 10, flexWrap: "wrap", + color: "black", }, requirements: { - fontSize: 15, + fontSize: 17, color: colors.secondary, flexWrap: "wrap", + marginBottom: 10, }, container: { flex: 1, - marginTop: 50 + marginBottom: 10 }, button: { flexDirection: 'row', - alignItems: 'center' + alignItems: 'center', + marginBottom: 10, } }); diff --git a/src/utils/index.js b/src/utils/index.js index 153268b..748f74f 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -3,7 +3,6 @@ import { Alert } from "react-native"; import TextField from "../components/TextField"; import CheckBoxQuery from "../components/CheckBoxQuery"; import UploadButton from "../components/UploadButton"; -import NextButton from "../components/NextButton"; import MultipleChoice from "../components/MultipleChoice"; import MultiSelect from "../components/MultiSelect"; import SlotList from "../components/Slot"; @@ -22,7 +21,7 @@ export async function getUser() { } export class Question { - constructor({ component, validate = (_) => true, isVisible = () => true }) { + constructor({ name, optional=false, component, validate = (_) => true, isVisible = () => true }) { this.component = component; this.validate = () => !isVisible() || validate(component.props.state.value); this.isVisible = isVisible; @@ -37,7 +36,7 @@ export function emptyQuestionState(initial = null) { } export function hashForm(title, location, date) { - return title + location + date; + return title + "&&&" + location + "&&&" + date; } export class FormString { @@ -83,12 +82,13 @@ export async function submitForm(formId, formData) { } } -const isAtLeast = (value, len) => !value ? false : value.trim().length >= len; +const isAtLeast = (value, len) => value ? (value.hasOwnProperty("trim") ? value.trim() : value).length >= len : false; const isNotEmpty = (value) => isAtLeast(value, 1); class Form { constructor(scrollObject) { this.scrollObject = scrollObject; + this.form = null; } questions() { @@ -180,6 +180,7 @@ class MusicHour extends Form { questions() { let questions = [ new Question({ + name: "fullName", component: ( @@ -627,6 +652,7 @@ class RequestConcert extends Form { }), new Question({ + name: 'audience', component: ( ), - validate: isNotEmpty, + validate: () => true, }), new Question({ + name: 'donationBox', component: ( {this.favoriteDanceStyles[1]((prevState) => ({ ...prevState, value: value }))}} key="favoriteDanceStyles" state={this.favoriteDanceStyles[0]} setState={this.favoriteDanceStyles[1]} @@ -805,6 +841,7 @@ class DanceClub extends Form{ }), new Question({ + name: 'consent', component: ( Date: Sun, 1 Sep 2024 16:57:11 -0700 Subject: [PATCH 09/22] Fix Bugs --- src/components/CheckBoxQuery.js | 2 +- src/components/Slot.js | 6 ++++-- src/utils/index.js | 17 +++++++++-------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/CheckBoxQuery.js b/src/components/CheckBoxQuery.js index 69d1ba4..5c2b014 100644 --- a/src/components/CheckBoxQuery.js +++ b/src/components/CheckBoxQuery.js @@ -13,7 +13,7 @@ export default function CheckBoxQuery({ question, state, setState }) { })); }} > - + {question} {" *"} diff --git a/src/components/Slot.js b/src/components/Slot.js index 075d111..fc7ae8b 100644 --- a/src/components/Slot.js +++ b/src/components/Slot.js @@ -2,6 +2,7 @@ import { useState } from 'react'; import { View, Pressable, Text, StyleSheet } from 'react-native' import DatePicker from 'react-native-date-picker' import Ionicons from '@expo/vector-icons/Ionicons'; +import EvilIcons from '@expo/vector-icons/EvilIcons'; import FontAwesome6 from '@expo/vector-icons/FontAwesome6'; import colors from '../constants/colors'; @@ -115,7 +116,8 @@ export class TimeSlot { 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))}})}}> - + {/* */} + ); } @@ -152,7 +154,7 @@ export function Select({state, setState, index, added, start, setStart, open, se mode={start ? "datetime" : "time"} title={start ? "Select Start Time" : "Select End Time"} minimumDate={start ? new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0) : new Date(start_.getFullYear(), start_.getMonth(), start_.getDate(), start_.getHours(), start_.getMinutes())} - maximumDate={start ? new Date(now.getFullYear(), now.getMonth() + 3, now.getDate(), 23, 59) : new Date(start_.getFullYear(), start_.getMonth(), start_.getDate(), 23, 59, 59)} + maximumDate={start ? new Date(now.getFullYear(), now.getMonth() + 3, now.getDate(), 23, 59, 59) : new Date(start_.getFullYear(), start_.getMonth(), start_.getDate(), 23, 59, 59)} onConfirm={(date) => { if (start) { setState((previous) => {let next = previous.value; next[index].start = date; return {...previous, value: next}}); diff --git a/src/utils/index.js b/src/utils/index.js index 748f74f..b57f3b9 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -84,6 +84,7 @@ export async function submitForm(formId, formData) { const isAtLeast = (value, len) => value ? (value.hasOwnProperty("trim") ? value.trim() : value).length >= len : false; const isNotEmpty = (value) => isAtLeast(value, 1); +const isExactly = (value, len) => !isAtLeast(value, len + 1) && isAtLeast(value, len); class Form { constructor(scrollObject) { @@ -211,7 +212,7 @@ class MusicHour extends Form { ), - validate: isNotEmpty, + validate: (value) => isExactly(value, 4), }), new Question({ @@ -857,12 +858,12 @@ class DanceClub extends Form{ name: 'consent', component: ( Date: Sun, 1 Sep 2024 20:44:45 -0700 Subject: [PATCH 10/22] Make 4 favorite music pieces question in dance club form a list of 4 input boxes. Bugs: validation (only for this form, probably this specific question), margin 20 instead of 0 --- src/components/TextField.js | 6 +++-- src/screens/VolunteerFormScreen.js | 5 +---- src/utils/index.js | 36 +++++++++++++++++++----------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/components/TextField.js b/src/components/TextField.js index 6ea1210..9a99364 100644 --- a/src/components/TextField.js +++ b/src/components/TextField.js @@ -8,10 +8,12 @@ export default function TextField({ maxLength = 32000, // Limit of chars on Google Forms state, setState, + margin=true, }) { return ( { + console.log(title, state); setState((prevState) => ({ ...prevState, y: event.nativeEvent.layout.y, @@ -20,7 +22,7 @@ export default function TextField({ > {title} - {title.slice(-10) == "(optional)" ? null : ( + {(title == "" || title.slice(-10) == "(optional)") ? null : ( * )} @@ -28,7 +30,7 @@ export default function TextField({ { setState((prevState) => ({ diff --git a/src/screens/VolunteerFormScreen.js b/src/screens/VolunteerFormScreen.js index 91ac8fa..d76586c 100644 --- a/src/screens/VolunteerFormScreen.js +++ b/src/screens/VolunteerFormScreen.js @@ -12,12 +12,9 @@ import { KeyboardAvoidingView, } from "react-native"; -import { getUser, Question, emptyQuestionState, FormString, submitForm, hashForm, questions } from "../utils"; +import { questions } from "../utils"; import NextButton from "../components/NextButton"; - -import AsyncStorage from "@react-native-async-storage/async-storage"; - export default function VolunteerFormScreen({ navigation, route }) { const { title, location, date } = route.params; const [scrollObject, setScrollObject] = useState(null); diff --git a/src/utils/index.js b/src/utils/index.js index b57f3b9..754c4d6 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { Alert } from "react-native"; +import { Alert, View } from "react-native"; import TextField from "../components/TextField"; import CheckBoxQuery from "../components/CheckBoxQuery"; import UploadButton from "../components/UploadButton"; @@ -21,13 +21,14 @@ export async function getUser() { } export class Question { - constructor({ name, optional=false, component, validate = (_) => true, isVisible = () => true }) { + constructor({ name, component, validate = (_) => true, isVisible = () => true, state=null, setState=null, y=null}) { + this.name = name; this.component = component; this.validate = () => !isVisible() || validate(component.props.state.value); this.isVisible = isVisible; - this.state = component.props.state; - this.setState = component.props.setState; - this.y = component.props.state.y; + this.state = (state ? state : component.props.state); + this.setState = (setState ? setState : component.props.setState); + this.y = (y ? y : component.props.state.y); } } @@ -761,7 +762,8 @@ class DanceClub extends Form{ }, []); this.phoneNumber = emptyQuestionState(); - this.favoritePieces = emptyQuestionState(); + this.favoritePieces = [emptyQuestionState(), emptyQuestionState(), emptyQuestionState(), emptyQuestionState()]; + console.log(this.favoritePieces); this.age = emptyQuestionState(); this.favoriteDanceStyles = emptyQuestionState([]); console.log("Values: " + this.favoriteDanceStyles[0].value); @@ -802,14 +804,22 @@ class DanceClub extends Form{ new Question({ name: 'favoritePieces', component: ( - + + {this.favoritePieces.map((piece, index) => ( + + ))} + ), - validate: isNotEmpty, + state: [...this.favoritePieces.map((piece) => piece[0])], + setState: (value) => {this.favoritePieces = [...value]}, + y: -1, + validate: (value) => value.every(isNotEmpty), }), new Question({ From afa5d15df734871d1cdea43613fa162138b852fd Mon Sep 17 00:00:00 2001 From: Pramad712 Date: Mon, 2 Sep 2024 12:04:02 -0700 Subject: [PATCH 11/22] make submit forms concise --- src/components/CheckBoxQuery.js | 4 +-- src/components/OtherOpportunities.js | 22 +++++++++++- src/utils/index.js | 52 +++++----------------------- 3 files changed, 32 insertions(+), 46 deletions(-) diff --git a/src/components/CheckBoxQuery.js b/src/components/CheckBoxQuery.js index 5c2b014..6195ab4 100644 --- a/src/components/CheckBoxQuery.js +++ b/src/components/CheckBoxQuery.js @@ -24,7 +24,7 @@ export default function CheckBoxQuery({ question, state, setState }) { onValueChange={() => { setState((prevState) => ({ ...prevState, - value: true, + value: "Yes", })); }} style={{ borderRadius: 20, transform: [{ scale: 1.3 }] }} @@ -44,7 +44,7 @@ export default function CheckBoxQuery({ question, state, setState }) { onValueChange={() => { setState((prevState) => ({ ...prevState, - value: false, + value: "No", })); }} style={{ borderRadius: 20, transform: [{ scale: 1.3 }] }} diff --git a/src/components/OtherOpportunities.js b/src/components/OtherOpportunities.js index 7630ced..cdf063e 100644 --- a/src/components/OtherOpportunities.js +++ b/src/components/OtherOpportunities.js @@ -6,7 +6,7 @@ export default function OtherOpportunities({navigation}) { - navigation.navigate("Volunteer Opportunity", { + navigation.navigate("Volunteer Form", { title: "Request Concert", location: null, date: null, @@ -23,6 +23,26 @@ export default function OtherOpportunities({navigation}) { source={require("./../assets/caret-left.png")} /> + + navigation.navigate("Volunteer Form", { + title: "Audacity Dance Club", + location: null, + date: null, + }) + } + > + + Request a Concert + + diff --git a/src/utils/index.js b/src/utils/index.js index 754c4d6..a8dbea8 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -370,7 +370,7 @@ class MusicHour extends Form { setState={this.parentalConsent[1]} /> ), - validate: (value) => value, + validate: (value) => value == "Yes", isVisible: () => this.age[0].value < 18, }), @@ -442,44 +442,10 @@ class MusicHour extends Form { formData.append(this.form.location, this.location); formData.append(this.form.date, this.date); - formData.append(this.form.fullName, this.fullName[0].value); - formData.append(this.form.city, this.city[0].value); - formData.append(this.form.phoneNumber, this.phoneNumber[0].value); - formData.append(this.form.age, this.age[0].value); - formData.append(this.form.musicPiece, this.musicPiece[0].value); - formData.append(this.form.composer, this.composer[0].value); - formData.append(this.form.instrument, this.instrument[0].value); - formData.append(this.form.length, this.length[0].value); - formData.append(this.form.recordingLink, this.recordingLink[0].value); - - if (this.title == "Library Music Hour") { - formData.append(this.form.performanceType, this.performanceType[0].value); - } - - formData.append( - this.form.publicPermission, - this.publicPermission[0].value ? "Yes" : "No", - ); - - formData.append( - this.form.parentalConsent, - this.parentalConsent[0].value == null - ? "" - : this.parentalConsent[0].value - ? "Yes" - : "No", - ); - - formData.append( - this.form.pianoAccompaniment, - this.pianoAccompaniment[0].value ? this.pianoAccompaniment[0].value[0] : "", - ); - - if (this.title == "Library Music Hour") { - formData.append( - this.form.ensembleProfile, - this.ensembleProfile[0].value ? this.ensembleProfile[0].value[0] : "", - ); + + for (const question of this.questions()) { + const value = this[question.name][0].value; + formData.append(this.form[question.name], (value == null) ? "" : ((value.constructor === Array) ? value : value[0])); } if (!submitForm(this.form.id, formData)) { @@ -734,7 +700,7 @@ class RequestConcert extends Form { ]; } - submit() { + async submit() { if (!super.validate()) { return; } @@ -861,7 +827,7 @@ class DanceClub extends Form{ setState={this.consent[1]} /> ), - validate: (value) => value, + validate: (value) => value == "Yes", }), new Question({ @@ -881,12 +847,12 @@ class DanceClub extends Form{ setState={this.recording[1]} /> ), - validate: (value) => value, + validate: (value) => value == "Yes", }), ]; } - submit() { + async submit() { if (!super.validate()) { return; } From 12fdffbb392c0f9ac2649d336debad33e34e809f Mon Sep 17 00:00:00 2001 From: Pramad712 Date: Mon, 2 Sep 2024 13:31:47 -0700 Subject: [PATCH 12/22] merge more --- src/utils/index.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utils/index.js b/src/utils/index.js index a8dbea8..8892bc4 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { Alert, View } from "react-native"; +import { Alert, View, Platform } from "react-native"; import TextField from "../components/TextField"; import CheckBoxQuery from "../components/CheckBoxQuery"; import UploadButton from "../components/UploadButton"; @@ -8,6 +8,16 @@ import MultiSelect from "../components/MultiSelect"; import SlotList from "../components/Slot"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import Constants from "expo-constants"; + +export function alertError(error) { + console.error(error); + Alert.alert( + "Error", + `Your request was not processed successfully due to an unexpected error. We apologize for the inconvenience. To help us identify and fix this error, please take a screenshot of this alert and send a bug report to ${Constants.expoConfig.extra.email}. Thank you!\n\nPlatform: ${Platform.OS} with v${Platform.Version}\n\n${error}`, + ); +} + export async function getUser() { try { const userString = await AsyncStorage.getItem("user"); From 274ee18e960fda67b9db3c232eb6e1adc8ef521a Mon Sep 17 00:00:00 2001 From: Pramad712 Date: Mon, 2 Sep 2024 13:52:25 -0700 Subject: [PATCH 13/22] add request concert and dance club functionality to other opportunities --- src/components/OtherOpportunities.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/OtherOpportunities.js b/src/components/OtherOpportunities.js index 889c203..9729c0f 100644 --- a/src/components/OtherOpportunities.js +++ b/src/components/OtherOpportunities.js @@ -2,6 +2,7 @@ import { View, Text, StyleSheet, Image, Pressable, Alert } 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 MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; export default function OtherOpportunities({navigation}) { return ( @@ -16,14 +17,18 @@ export default function OtherOpportunities({navigation}) { }) } > - Request a Concert - - - Request a Concert + Sign Up for Audacity Dance Club Date: Mon, 2 Sep 2024 15:01:05 -0700 Subject: [PATCH 14/22] fix checkboxquery, fixing favoritePieces question in dance form attempt 1 --- src/components/CheckBoxQuery.js | 4 ++-- src/components/TextField.js | 5 +++-- src/utils/index.js | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/CheckBoxQuery.js b/src/components/CheckBoxQuery.js index 6195ab4..7a47dba 100644 --- a/src/components/CheckBoxQuery.js +++ b/src/components/CheckBoxQuery.js @@ -20,7 +20,7 @@ export default function CheckBoxQuery({ question, state, setState }) { { setState((prevState) => ({ ...prevState, @@ -40,7 +40,7 @@ export default function CheckBoxQuery({ question, state, setState }) { { setState((prevState) => ({ ...prevState, diff --git a/src/components/TextField.js b/src/components/TextField.js index 9a99364..5b6d64c 100644 --- a/src/components/TextField.js +++ b/src/components/TextField.js @@ -8,7 +8,7 @@ export default function TextField({ maxLength = 32000, // Limit of chars on Google Forms state, setState, - margin=true, + extraMargin=true, }) { return ( { setState((prevState) => ({ diff --git a/src/utils/index.js b/src/utils/index.js index 8892bc4..fc819d5 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -34,11 +34,11 @@ export class Question { constructor({ name, component, validate = (_) => true, isVisible = () => true, state=null, setState=null, y=null}) { this.name = name; this.component = component; - this.validate = () => !isVisible() || validate(component.props.state.value); - this.isVisible = isVisible; this.state = (state ? state : component.props.state); this.setState = (setState ? setState : component.props.setState); this.y = (y ? y : component.props.state.y); + this.isVisible = isVisible; + this.validate = () => !isVisible() || validate((state ? state : component.props.state).value); } } @@ -787,15 +787,15 @@ class DanceClub extends Form{ key={`favoritePieces${index}`} state={piece[0]} setState={piece[1]} - margin={index < 3 ? false : true} + extraMargin={index == 3} /> ))} ), - state: [...this.favoritePieces.map((piece) => piece[0])], + state: {value: this.favoritePieces}, setState: (value) => {this.favoritePieces = [...value]}, y: -1, - validate: (value) => value.every(isNotEmpty), + validate: (value) => value.every((piece) => isNotEmpty(piece[0].value)), }), new Question({ From da902d8516062701a76abe9e57cecf7aa98f68e5 Mon Sep 17 00:00:00 2001 From: Pramad712 Date: Mon, 2 Sep 2024 17:15:40 -0700 Subject: [PATCH 15/22] 'bout to do pr --- src/components/TextField.js | 7 +++---- src/utils/index.js | 40 +++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/components/TextField.js b/src/components/TextField.js index 5b6d64c..0b60049 100644 --- a/src/components/TextField.js +++ b/src/components/TextField.js @@ -9,6 +9,7 @@ export default function TextField({ state, setState, extraMargin=true, + valid=null, }) { return ( - {title} + {title} {(title == "" || title.slice(-10) == "(optional)") ? null : ( * )} @@ -30,8 +31,7 @@ export default function TextField({ { setState((prevState) => ({ @@ -64,6 +64,5 @@ const styles = StyleSheet.create({ borderWidth: 1.5, borderColor: colors.secondary, textAlign: "center", - marginBottom: 20, }, }); diff --git a/src/utils/index.js b/src/utils/index.js index fc819d5..53679df 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -53,27 +53,34 @@ export function hashForm(title, location, date) { export class FormString { constructor() { this.string = ""; + this.repr = ""; } append(key, value) { if (this.string.length > 0) { this.string += "&"; + this.repr += "\n"; } this.string += `entry.${key}=${encodeURIComponent(value)}`; + this.repr += `entry.${key}=${value}`; } toString() { return this.string; } + + log() { + return this.repr; + } } export async function submitForm(formId, formData) { const formUrl = `https://docs.google.com/forms/d/e/${formId}/formResponse`; - console.log("Form Data: " + formData); + console.log("Form Data: " + formData.log()); try { - console.log(formData); + console.log(formData.log()); const response = await fetch(formUrl, { method: "POST", body: formData.toString(), @@ -84,8 +91,8 @@ export async function submitForm(formId, formData) { if (response.ok) { return true; - } - console.error(response); + }; + console.log(response); return false; } catch (error) { console.error(error); @@ -455,9 +462,11 @@ class MusicHour extends Form { for (const question of this.questions()) { const value = this[question.name][0].value; - formData.append(this.form[question.name], (value == null) ? "" : ((value.constructor === Array) ? value : value[0])); + formData.append(this.form[question.name], (value == null) ? "" : ((value.constructor === Array) ? value[0] : value)); } + console.log(formData.log()); + if (!submitForm(this.form.id, formData)) { this.navigation.navigate("End", { isSuccess: false }); return; @@ -494,7 +503,6 @@ class RequestConcert extends Form { this.stipend = emptyQuestionState(); this.donatable = emptyQuestionState(); this.slots = emptyQuestionState([]); - console.log(this.slots[0].value); this.audience = emptyQuestionState(); this.distance = emptyQuestionState(); this.provided = emptyQuestionState([]); @@ -739,10 +747,8 @@ class DanceClub extends Form{ this.phoneNumber = emptyQuestionState(); this.favoritePieces = [emptyQuestionState(), emptyQuestionState(), emptyQuestionState(), emptyQuestionState()]; - console.log(this.favoritePieces); this.age = emptyQuestionState(); this.favoriteDanceStyles = emptyQuestionState([]); - console.log("Values: " + this.favoriteDanceStyles[0].value); this.consent = emptyQuestionState(); this.recording = emptyQuestionState(); } @@ -788,14 +794,26 @@ class DanceClub extends Form{ state={piece[0]} setState={piece[1]} extraMargin={index == 3} + valid={this.favoritePieces.every((piece) => piece[0].valid)} /> ))} ), - state: {value: this.favoritePieces}, - setState: (value) => {this.favoritePieces = [...value]}, + state: {value: this.favoritePieces, y: -1, valid: true}, + setState: (value) => {this.favoritePieces = value}, y: -1, - validate: (value) => value.every((piece) => isNotEmpty(piece[0].value)), + validate: (value) => { + let good = true; + + for (const [index, piece] of value.entries()) { + const result = isNotEmpty(piece[0].value); + const original = this.favoritePieces[index][0]; + this.favoritePieces[index][1]({...original, valid: result}); + good = good && result; + }; + + return good; + } }), new Question({ From 5f193f0807e19cabbdc04dd82302848907a7f92c Mon Sep 17 00:00:00 2001 From: Pramad712 Date: Mon, 2 Sep 2024 17:22:10 -0700 Subject: [PATCH 16/22] styling --- src/components/CheckBoxQuery.js | 7 +- src/components/MultiSelect.js | 116 +++--- src/components/OtherOpportunities.js | 14 +- src/components/Slot.js | 560 +++++++++++++++++---------- src/components/TextField.js | 19 +- src/screens/HomeScreen.js | 2 +- src/screens/VolunteerFormScreen.js | 5 +- src/utils/index.js | 296 ++++++++------ 8 files changed, 642 insertions(+), 377 deletions(-) diff --git a/src/components/CheckBoxQuery.js b/src/components/CheckBoxQuery.js index 7a47dba..4e83b6e 100644 --- a/src/components/CheckBoxQuery.js +++ b/src/components/CheckBoxQuery.js @@ -13,7 +13,12 @@ export default function CheckBoxQuery({ question, state, setState }) { })); }} > - + {question} {" *"} diff --git a/src/components/MultiSelect.js b/src/components/MultiSelect.js index af98ee2..9cd0fbd 100644 --- a/src/components/MultiSelect.js +++ b/src/components/MultiSelect.js @@ -1,61 +1,77 @@ import { useState } from "react"; import { View, Pressable, Text, StyleSheet } from "react-native"; -import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; +import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; export default function MultiSelect({ state, setState, title, options }) { - const [selected, setSelected] = useState(new Array(options.length).fill(false)); + 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 }; - }); + 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} - - - ))} - - ); + 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, - }, + container: { + marginBottom: 20, + }, - title: { - fontSize: 18, - fontWeight: "600", - marginBottom: 10, - flexWrap: "wrap", - }, + title: { + fontSize: 18, + fontWeight: "600", + marginBottom: 10, + flexWrap: "wrap", + }, - optionText: { - fontSize: 16, - marginLeft: 10, - flexWrap: "wrap", - } -}); \ No newline at end of file + optionText: { + fontSize: 16, + marginLeft: 10, + flexWrap: "wrap", + }, +}); diff --git a/src/components/OtherOpportunities.js b/src/components/OtherOpportunities.js index 9729c0f..c169a82 100644 --- a/src/components/OtherOpportunities.js +++ b/src/components/OtherOpportunities.js @@ -4,12 +4,12 @@ import Ionicons from "@expo/vector-icons/Ionicons"; import FontAwesome from "@expo/vector-icons/FontAwesome"; import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; -export default function OtherOpportunities({navigation}) { +export default function OtherOpportunities({ navigation }) { return ( - + navigation.navigate("Volunteer Form", { title: "Request Concert", location: null, @@ -31,9 +31,9 @@ export default function OtherOpportunities({navigation}) { style={styles.arrow} /> - + navigation.navigate("Volunteer Form", { title: "Audacity Dance Club", location: null, diff --git a/src/components/Slot.js b/src/components/Slot.js index fc7ae8b..12a0ae8 100644 --- a/src/components/Slot.js +++ b/src/components/Slot.js @@ -1,240 +1,398 @@ -import { useState } from 'react'; -import { View, Pressable, Text, StyleSheet } from 'react-native' -import DatePicker from 'react-native-date-picker' -import Ionicons from '@expo/vector-icons/Ionicons'; -import EvilIcons from '@expo/vector-icons/EvilIcons'; -import FontAwesome6 from '@expo/vector-icons/FontAwesome6'; -import colors from '../constants/colors'; +import { useState } from "react"; +import { View, Pressable, Text, StyleSheet } from "react-native"; +import DatePicker from "react-native-date-picker"; +import Ionicons from "@expo/vector-icons/Ionicons"; +import EvilIcons from "@expo/vector-icons/EvilIcons"; +import FontAwesome6 from "@expo/vector-icons/FontAwesome6"; +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}`; + 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)}`; + 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; + if (hour1 < hour2) { + return -1; + } else if (hour1 > hour2) { + return 1; + } else { + if (minute1 < minute2) { + return -1; + } else if (minute1 > minute2) { + return 1; } else { - if (minute1 < minute2) { - return -1; - } else if (minute1 > minute2) { - return 1; - } else { - return 0; - } + 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(); + constructor(start = new Date(), end = new Date()) { + this.start = start; + this.end = end; + this.valid = true; + } - if (this.start >= this.end) { - this.valid = false; - } + 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(); - 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; + 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; } - toString() { - return `${dateFormatter(this.start)} - ${timeFormatter(this.end)}`; - } + return this.valid; + } - compareTo(other) { - if (this.start < other.start) { + 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.start > other.start) { + } else if (this.end > other.end) { return 1; - } else { - if (this.end < other.end) { - return -1; - } else if (this.end > other.end) { - return 1; - } else { - return 0; - } - } + } else { + return 0; + } } + } - render(state, setState, index, setIndex, setOpen, setAdded, setStart) { - return ( - + + { + setOpen(true); + setAdded(false); + setStart(true); + setIndex(index); + }} + > + - - {setOpen(true); setAdded(false); setStart(true); setIndex(index);}}> - {dateFormatter(this.start)} - - - - {setOpen(true); setAdded(false); setStart(false); setIndex(index);}}> - {timeFormatter(this.end)} - - - - - ); - } + {dateFormatter(this.start)} + + + + {" "} + -{" "} + + { + setOpen(true); + setAdded(false); + setStart(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 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, setAdded, setOpen, setStart}) { - return ( - { - setAdded(true); - setStart(true); - let length = state.value.length; - setIndex(length); - setState((previous) => {return {...previous, value: previous.value.concat([new TimeSlot()])}}); - setOpen(true); - }}> - - Add Time Slot - - ); +function AddButton({ state, setState, setIndex, setAdded, setOpen, setStart }) { + return ( + { + setAdded(true); + setStart(true); + let length = state.value.length; + setIndex(length); + setState((previous) => { + return { + ...previous, + value: previous.value.concat([new TimeSlot()]), + }; + }); + setOpen(true); + }} + > + + Add Time Slot + + ); } -export function Select({state, setState, index, added, start, setStart, open, setOpen}) { - const now = new Date(); - console.log(state); - console.log(index); +export function Select({ + state, + setState, + index, + added, + start, + setStart, + open, + setOpen, +}) { + const now = new Date(); + console.log(state); + console.log(index); - let start_ = state.value[index].start; - let end_ = state.value[index].end; + let start_ = state.value[index].start; + let end_ = state.value[index].end; - return ( - { - if (start) { - setState((previous) => {let next = previous.value; next[index].start = date; return {...previous, value: next}}); - setStart(false); - - setOpen(false); - - if (added) { - setOpen(true); - } - } - - else { - setState((previous) => {let next = previous.value; next[index].end = date; return {...previous, value: next}}) - setOpen(false); - } - }} + return ( + { + if (start) { + setState((previous) => { + let next = previous.value; + next[index].start = date; + return { ...previous, value: next }; + }); + setStart(false); - onCancel={() => { - if (added && start) { - setState((previous) => {return {...previous, value: previous.value.slice(0, index - 1)}}); - } - - setOpen(false) - }} - /> - ); + setOpen(false); + + if (added) { + setOpen(true); + } + } else { + setState((previous) => { + let next = previous.value; + next[index].end = date; + return { ...previous, value: next }; + }); + setOpen(false); + } + }} + onCancel={() => { + if (added && start) { + setState((previous) => { + return { ...previous, value: previous.value.slice(0, index - 1) }; + }); + } + + setOpen(false); + }} + /> + ); } -export default function SlotList({title, state, setState}) { - const [open, setOpen] = useState(false); - const [start, setStart] = useState(true); - const [added, setAdded] = useState(false); - const [index, setIndex] = useState(0); - - return ( - { - setState((prevState) => ({ - ...prevState, - y: event.nativeEvent.layout.y, - })); - }} +export default function SlotList({ title, state, setState }) { + const [open, setOpen] = useState(false); + const [start, setStart] = useState(true); + const [added, setAdded] = 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, setOpen, setAdded, setStart))} - - {open ? + ) : null} + + ); } const styles = StyleSheet.create({ - title: { - fontSize: 18, - fontWeight: "600", - marginBottom: 10, - flexWrap: "wrap", - color: "black", - }, - - requirements: { - fontSize: 17, - color: colors.secondary, - flexWrap: "wrap", - marginBottom: 10, - }, - - container: { - flex: 1, - marginBottom: 10 - }, - - button: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 10, - } + title: { + fontSize: 18, + fontWeight: "600", + marginBottom: 10, + flexWrap: "wrap", + color: "black", + }, + + requirements: { + fontSize: 17, + color: colors.secondary, + flexWrap: "wrap", + marginBottom: 10, + }, + + container: { + flex: 1, + marginBottom: 10, + }, + + button: { + flexDirection: "row", + alignItems: "center", + marginBottom: 10, + }, }); diff --git a/src/components/TextField.js b/src/components/TextField.js index 0b60049..bcf8760 100644 --- a/src/components/TextField.js +++ b/src/components/TextField.js @@ -8,8 +8,8 @@ export default function TextField({ maxLength = 32000, // Limit of chars on Google Forms state, setState, - extraMargin=true, - valid=null, + extraMargin = true, + valid = null, }) { return ( - {title} - {(title == "" || title.slice(-10) == "(optional)") ? null : ( + + {title} + + {title == "" || title.slice(-10) == "(optional)" ? null : ( * )} @@ -31,7 +37,10 @@ export default function TextField({ { setState((prevState) => ({ diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index fa1cc6c..039bf5c 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -146,7 +146,7 @@ export default function HomeScreen({ navigation, route }) { onRefresh={onRefresh} /> Other Opportunities - + Websites diff --git a/src/screens/VolunteerFormScreen.js b/src/screens/VolunteerFormScreen.js index 8e72cc6..6e3be65 100644 --- a/src/screens/VolunteerFormScreen.js +++ b/src/screens/VolunteerFormScreen.js @@ -46,7 +46,8 @@ export default function VolunteerFormScreen({ navigation, route }) { - {form.questions() + {form + .questions() .filter((question) => question?.isVisible()) .map((question) => question.component)} @@ -108,4 +109,4 @@ const styles = StyleSheet.create({ justifyContent: "flex-end", marginBottom: 50, }, -}); \ No newline at end of file +}); diff --git a/src/utils/index.js b/src/utils/index.js index 53679df..7ef27fd 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -31,14 +31,23 @@ export async function getUser() { } export class Question { - constructor({ name, component, validate = (_) => true, isVisible = () => true, state=null, setState=null, y=null}) { + constructor({ + name, + component, + validate = (_) => true, + isVisible = () => true, + state = null, + setState = null, + y = null, + }) { this.name = name; this.component = component; - this.state = (state ? state : component.props.state); - this.setState = (setState ? setState : component.props.setState); - this.y = (y ? y : component.props.state.y); + this.state = state ? state : component.props.state; + this.setState = setState ? setState : component.props.setState; + this.y = y ? y : component.props.state.y; this.isVisible = isVisible; - this.validate = () => !isVisible() || validate((state ? state : component.props.state).value); + this.validate = () => + !isVisible() || validate((state ? state : component.props.state).value); } } @@ -91,7 +100,7 @@ export async function submitForm(formId, formData) { if (response.ok) { return true; - }; + } console.log(response); return false; } catch (error) { @@ -100,9 +109,13 @@ export async function submitForm(formId, formData) { } } -const isAtLeast = (value, len) => value ? (value.hasOwnProperty("trim") ? value.trim() : value).length >= len : false; +const isAtLeast = (value, len) => + value + ? (value.hasOwnProperty("trim") ? value.trim() : value).length >= len + : false; const isNotEmpty = (value) => isAtLeast(value, 1); -const isExactly = (value, len) => !isAtLeast(value, len + 1) && isAtLeast(value, len); +const isExactly = (value, len) => + !isAtLeast(value, len + 1) && isAtLeast(value, len); class Form { constructor(scrollObject) { @@ -117,14 +130,14 @@ class Form { validate() { let allValid = true; let minInvalidY = Infinity; - + for (const question of this.questions()) { const isValid = question.validate(); question.setState((prevState) => ({ ...prevState, valid: isValid, })); - + if (!isValid) { allValid = false; if (question.y < minInvalidY) { @@ -132,7 +145,7 @@ class Form { } } } - + if (!allValid) { this.scrollObject.scrollTo({ x: 0, @@ -141,7 +154,7 @@ class Form { }); return false; } - + return true; } } @@ -162,7 +175,10 @@ class MusicHour extends Form { (async () => { try { const user = await getUser(); - this.fullName[1]((prevState) => ({ ...prevState, value: user?.name })); + this.fullName[1]((prevState) => ({ + ...prevState, + value: user?.name, + })); } catch (error) { console.error(error); } @@ -175,18 +191,16 @@ class MusicHour extends Form { this.musicPiece = emptyQuestionState(); this.composer = emptyQuestionState(); this.instrument = emptyQuestionState(); - this.performanceType = emptyQuestionState(); + this.performanceType = emptyQuestionState(); this.length = emptyQuestionState(); this.recordingLink = emptyQuestionState(); this.publicPermission = emptyQuestionState(); this.parentalConsent = emptyQuestionState(); - this.pianoAccompaniment = emptyQuestionState(); + this.pianoAccompaniment = emptyQuestionState(); this.ensembleProfile = emptyQuestionState(); this.otherInfo = emptyQuestionState(); - this.timeLimit = useState( - title == "Library Music Hour" ? 0 : 10, - ); - + this.timeLimit = useState(title == "Library Music Hour" ? 0 : 10); + this.performanceOptions = { "Individual performance only": 8, "Individual performance and music instrument presentation": 12, @@ -210,7 +224,7 @@ class MusicHour extends Form { ), validate: (value) => value.trim().split(" ").length >= 2, }), - + new Question({ name: "city", component: ( @@ -223,7 +237,7 @@ class MusicHour extends Form { ), validate: isNotEmpty, }), - + new Question({ name: "phoneNumber", component: ( @@ -238,7 +252,7 @@ class MusicHour extends Form { ), validate: (value) => isAtLeast(value, 10), }), - + new Question({ name: "age", component: ( @@ -258,7 +272,7 @@ class MusicHour extends Form { return age >= 5 && age <= 125; }, }), - + new Question({ name: "musicPiece", component: ( @@ -271,7 +285,7 @@ class MusicHour extends Form { ), validate: isNotEmpty, }), - + new Question({ name: "composer", component: ( @@ -284,7 +298,7 @@ class MusicHour extends Form { ), validate: isNotEmpty, }), - + new Question({ name: "instrument", component: ( @@ -297,7 +311,7 @@ class MusicHour extends Form { ), validate: isNotEmpty, }), - + this.title == "Library Music Hour" ? new Question({ name: "performanceType", @@ -320,9 +334,9 @@ class MusicHour extends Form { validate: isNotEmpty, }) : null, - + new Question({ - name: 'length', + name: "length", component: ( 0; }, }), - + new Question({ - name: 'recordingLink', + name: "recordingLink", component: ( value != null, }), - + new Question({ name: "parentalConsent", component: ( @@ -390,7 +404,7 @@ class MusicHour extends Form { validate: (value) => value == "Yes", isVisible: () => this.age[0].value < 18, }), - + new Question({ name: "pianoAccompaniment", component: ( @@ -408,7 +422,7 @@ class MusicHour extends Form { this.pianoAccompaniment[0].value == null || this.pianoAccompaniment[0].value[1] <= 104857600, // There are 104,857,600 bytes in 100 MB }), - + this.title == "Library Music Hour" ? new Question({ name: "ensembleProfile", @@ -422,9 +436,10 @@ class MusicHour extends Form { required={true} /> ), - - isVisible: () => this.performanceType[0].value?.includes("Ensemble"), - + + isVisible: () => + this.performanceType[0].value?.includes("Ensemble"), + // Only PDF files can be uploaded // Required only if visible (selected ensemble option) validate: () => @@ -432,7 +447,7 @@ class MusicHour extends Form { this.ensembleProfile[0].value <= 104857600, // There are 104,857,600 bytes in 100 MB }) : null, - + new Question({ name: "otherInfo", component: ( @@ -445,9 +460,9 @@ class MusicHour extends Form { ), }), ]; - + questions = questions.filter((question) => question != null); - return questions; + return questions; } async submit() { @@ -459,10 +474,13 @@ class MusicHour extends Form { formData.append(this.form.location, this.location); formData.append(this.form.date, this.date); - + for (const question of this.questions()) { const value = this[question.name][0].value; - formData.append(this.form[question.name], (value == null) ? "" : ((value.constructor === Array) ? value[0] : value)); + formData.append( + this.form[question.name], + value == null ? "" : value.constructor === Array ? value[0] : value, + ); } console.log(formData.log()); @@ -527,7 +545,7 @@ class RequestConcert extends Form { ), validate: (value) => isAtLeast(value, 10), }), - + new Question({ name: "organization", component: ( @@ -540,7 +558,7 @@ class RequestConcert extends Form { ), validate: isNotEmpty, }), - + new Question({ name: "eventInfo", component: ( @@ -553,7 +571,7 @@ class RequestConcert extends Form { ), validate: isNotEmpty, }), - + new Question({ name: "venue", component: ( @@ -566,15 +584,18 @@ class RequestConcert extends Form { ), validate: isNotEmpty, }), - + new Question({ - name: 'publicity', + name: "publicity", component: ( { - this.publicity[1]((prevState) => ({ ...prevState, value: option })); + this.publicity[1]((prevState) => ({ + ...prevState, + value: option, + })); }} key="publicity" state={this.publicity[0]} @@ -583,9 +604,9 @@ class RequestConcert extends Form { ), validate: isNotEmpty, }), - + new Question({ - name: 'stipend', + name: "stipend", component: ( ), - + validate: (value) => value != null, }), - + new Question({ - name: 'donatable', + name: "donatable", component: ( ), - + validate: (value) => { if (value.length == 0) { return false; } - + for (const slot of value) { if (!slot.validate()) { return false; } } - + return true; - } + }, }), - + new Question({ - name: 'audience', + name: "audience", component: ( true, }), - + new Question({ - name: 'donationBox', + name: "donationBox", component: ( value != null, }), - + new Question({ - name: 'extraAudience', + name: "extraAudience", component: ( value != null, }), - + new Question({ - name: 'otherInfo', + name: "otherInfo", component: ( - ) - }) + ), + }), ]; } @@ -723,11 +752,14 @@ class RequestConcert extends Form { return; } - Alert.alert("Not Implemented", "The Request Concert form's submission has not been implemented yet. Please contact the IT Team."); + Alert.alert( + "Not Implemented", + "The Request Concert form's submission has not been implemented yet. Please contact the IT Team.", + ); } } -class DanceClub extends Form{ +class DanceClub extends Form { constructor(scrollObject, navigation) { super(scrollObject); this.navigation = navigation; @@ -738,7 +770,10 @@ class DanceClub extends Form{ (async () => { try { const user = await getUser(); - this.fullName[1]((prevState) => ({ ...prevState, value: user?.name })); + this.fullName[1]((prevState) => ({ + ...prevState, + value: user?.name, + })); } catch (error) { console.error(error); } @@ -746,7 +781,12 @@ class DanceClub extends Form{ }, []); this.phoneNumber = emptyQuestionState(); - this.favoritePieces = [emptyQuestionState(), emptyQuestionState(), emptyQuestionState(), emptyQuestionState()]; + this.favoritePieces = [ + emptyQuestionState(), + emptyQuestionState(), + emptyQuestionState(), + emptyQuestionState(), + ]; this.age = emptyQuestionState(); this.favoriteDanceStyles = emptyQuestionState([]); this.consent = emptyQuestionState(); @@ -756,7 +796,7 @@ class DanceClub extends Form{ questions() { return [ new Question({ - name: 'fullName', + name: "fullName", component: ( value.trim().split(" ").length >= 2, }), - + new Question({ - name: 'phoneNumber', + name: "phoneNumber", component: ( isAtLeast(value, 10), }), - + new Question({ - name: 'favoritePieces', + name: "favoritePieces", component: ( {this.favoritePieces.map((piece, index) => ( @@ -799,30 +839,42 @@ class DanceClub extends Form{ ))} ), - state: {value: this.favoritePieces, y: -1, valid: true}, - setState: (value) => {this.favoritePieces = value}, + state: { value: this.favoritePieces, y: -1, valid: true }, + setState: (value) => { + this.favoritePieces = value; + }, y: -1, validate: (value) => { let good = true; for (const [index, piece] of value.entries()) { - const result = isNotEmpty(piece[0].value); - const original = this.favoritePieces[index][0]; - this.favoritePieces[index][1]({...original, valid: result}); + const result = isNotEmpty(piece[0].value); + const original = this.favoritePieces[index][0]; + this.favoritePieces[index][1]({ ...original, valid: result }); good = good && result; - }; + } return good; - } + }, }), - + new Question({ - name: 'age', + name: "age", component: ( {this.age[1]((prevState) => ({ ...prevState, value: value }))}} + options={[ + "Under 10", + "10-15", + "16-20", + "21-40", + "41-60", + "61-80", + "81-100", + ]} + onSelect={(value) => { + this.age[1]((prevState) => ({ ...prevState, value: value })); + }} key="age" state={this.age[0]} setState={this.age[1]} @@ -830,13 +882,21 @@ class DanceClub extends Form{ ), validate: isNotEmpty, }), - + new Question({ - name: 'favoriteDanceStyles', + name: "favoriteDanceStyles", component: ( isExactly(value, 4), }), - + new Question({ - name: 'consent', + name: "consent", component: ( value == "Yes", }), - + new Question({ - name: 'consent', + name: "consent", component: ( Date: Sat, 7 Sep 2024 18:15:26 -0700 Subject: [PATCH 17/22] do requested changes --- src/components/MultipleChoice.js | 4 +- src/components/Slot.js | 120 +++++++++++++++---------------- src/components/TextField.js | 1 - src/utils/index.js | 12 ++-- 4 files changed, 67 insertions(+), 70 deletions(-) diff --git a/src/components/MultipleChoice.js b/src/components/MultipleChoice.js index 346b2a8..e9c2ed3 100644 --- a/src/components/MultipleChoice.js +++ b/src/components/MultipleChoice.js @@ -24,9 +24,9 @@ export default function MultipleChoice({ * - {options.map((value, index) => ( + {mapObject(options, (value) => ( onSelect(value)} > diff --git a/src/components/Slot.js b/src/components/Slot.js index 12a0ae8..bb715ea 100644 --- a/src/components/Slot.js +++ b/src/components/Slot.js @@ -83,7 +83,7 @@ export class TimeSlot { } } - render(state, setState, index, setIndex, setOpen, setAdded, setStart) { + render(state, setState, index, setIndex, setIsOpen, setIsAdded, setIsStart) { return ( { - setOpen(true); - setAdded(false); - setStart(true); + setIsOpen(true); + setIsAdded(false); + setIsStart(true); setIndex(index); }} > @@ -122,9 +122,9 @@ export class TimeSlot { { - setOpen(true); - setAdded(false); - setStart(false); + setIsOpen(true); + setIsAdded(false); + setIsStart(false); setIndex(index); }} > @@ -173,13 +173,13 @@ function RemoveButton({ state, setState, index }) { ); } -function AddButton({ state, setState, setIndex, setAdded, setOpen, setStart }) { +function AddButton({ state, setState, setIndex, setIsAdded, setIsOpen, setIsStart }) { return ( { - setAdded(true); - setStart(true); + setIsAdded(true); + setIsStart(true); let length = state.value.length; setIndex(length); setState((previous) => { @@ -188,7 +188,7 @@ function AddButton({ state, setState, setIndex, setAdded, setOpen, setStart }) { value: previous.value.concat([new TimeSlot()]), }; }); - setOpen(true); + setIsOpen(true); }} > @@ -201,57 +201,55 @@ export function Select({ state, setState, index, - added, - start, - setStart, - open, - setOpen, + isAdded, + isStart, + setIsStart, + isOpen, + setIsOpen, }) { const now = new Date(); - console.log(state); - console.log(index); - let start_ = state.value[index].start; - let end_ = state.value[index].end; + let start = state.value[index].start; + let end = state.value[index].end; return ( { - if (start) { + if (isStart) { setState((previous) => { let next = previous.value; next[index].start = date; return { ...previous, value: next }; }); - setStart(false); + setIsStart(false); - setOpen(false); + setIsOpen(false); - if (added) { - setOpen(true); + if (isAdded) { + setIsOpen(true); } } else { setState((previous) => { @@ -289,26 +287,26 @@ export function Select({ next[index].end = date; return { ...previous, value: next }; }); - setOpen(false); + setIsOpen(false); } }} onCancel={() => { - if (added && start) { + if (isAdded && isStart) { setState((previous) => { return { ...previous, value: previous.value.slice(0, index - 1) }; }); } - setOpen(false); + setIsOpen(false); }} /> ); } export default function SlotList({ title, state, setState }) { - const [open, setOpen] = useState(false); - const [start, setStart] = useState(true); - const [added, setAdded] = useState(false); + const [open, setIsOpen] = useState(false); + const [start, setIsStart] = useState(true); + const [added, setIsAdded] = useState(false); const [index, setIndex] = useState(0); return ( @@ -340,29 +338,29 @@ export default function SlotList({ title, state, setState }) { setState, index, setIndex, - setOpen, - setAdded, - setStart, + setIsOpen, + setIsAdded, + setIsStart, ), )} {open ? (