From 0d4a59378044417261a7e65df5d1142041e31c73 Mon Sep 17 00:00:00 2001 From: Charlie Lin Date: Sat, 2 Nov 2024 19:37:10 -0400 Subject: [PATCH 1/8] completed calendar form, upcoming events component is not completely finished (filters are not implemented) --- .../calender-component/CalendarEvent.tsx | 494 ++++++++++++++++++ .../calender-component/CalendarForm.tsx | 132 +++++ .../calender-component/UpcomingEvent.tsx | 136 +++++ .../UpcomingEventContainer.tsx | 72 +++ src/components/subcomponents/ProviderInfo.tsx | 174 ++++-- 5 files changed, 956 insertions(+), 52 deletions(-) create mode 100644 src/components/dashboard/calender-component/CalendarEvent.tsx create mode 100644 src/components/dashboard/calender-component/CalendarForm.tsx create mode 100644 src/components/dashboard/calender-component/UpcomingEvent.tsx create mode 100644 src/components/dashboard/calender-component/UpcomingEventContainer.tsx diff --git a/src/components/dashboard/calender-component/CalendarEvent.tsx b/src/components/dashboard/calender-component/CalendarEvent.tsx new file mode 100644 index 0000000..0b683cb --- /dev/null +++ b/src/components/dashboard/calender-component/CalendarEvent.tsx @@ -0,0 +1,494 @@ +import React, { useState, useEffect } from "react"; +import { Button, InputGroup } from "react-bootstrap"; +import Form from "react-bootstrap/Form"; + +export default function CalendarEvent({ + index, + eventData, + handleEventDataChange, + handleDelete, + handleAdd, + handleAllDayUpdate, +}) { + const { + displayNumber, + eventName, + fromDate, + toDate, + fromTime, + toTime, + isAllDay, + isCustom, + address, + description, + buttonText, + buttonLink, + repeatDays, + customEndDate, + customEndOccurrences, + isOn, + isAfter, + } = eventData; + + const renderCustomRecurrance = () => { + const daysOfWeek = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + + const clickedObj = {}; + if (repeatDays) { + repeatDays.forEach((day) => { + clickedObj[day] = true; + }); + } + + const [clickedDays, setClickedDays] = useState({ + Sunday: false, + Monday: false, + Tuesday: false, + Wednesday: false, + Thursday: false, + Friday: false, + Saturday: false, + ...clickedObj, + }); + + const toggleDay = (day) => { + const updatedClickedDays = { + ...clickedDays, + [day]: !clickedDays[day], + }; + setClickedDays(updatedClickedDays); + + const updatedRepeatDaysArr = daysOfWeek.filter( + (currDay) => updatedClickedDays[currDay] + ); + + handleEventDataChange(index, "repeatDays", updatedRepeatDaysArr); + }; + if (isCustom) { + return ( + + Custom Recurrence + + Repeat on + + {daysOfWeek.map((day, i) => ( + + ))} + + + Ends + + + { + handleEventDataChange(index, "isOn", true); + handleEventDataChange( + index, + "isAfter", + false + ); + }} + id="endOn" + /> + + + handleEventDataChange( + index, + "customEndDate", + (e.target as HTMLInputElement).value + ) + } + /> + + + + { + handleEventDataChange(index, "isOn", false); + handleEventDataChange( + index, + "isAfter", + true + ); + }} + /> + + + handleEventDataChange( + index, + "customEndOccurrences", + Number( + (e.target as HTMLInputElement) + .value + ) + ) + } + /> + occurrences + + + + + ); + } else { + return null; + } + }; + + return ( +
+
+ {index === 0 && ( + + Number of events to display: + { + handleEventDataChange( + index, + "displayNumber", + Number((e.target as HTMLInputElement).value) + ); + }} + style={{ maxWidth: "7%" }} + /> + + )} + + {`Event ${index + 1}`} + + + + Event name * + + + handleEventDataChange( + index, + "eventName", + (e.target as HTMLInputElement).value + ) + } + style={{ borderColor: "#D9D9D9" }} + required + /> + + + {/* + Event Duration + */} + + + Event Duration + * + + + + handleEventDataChange( + index, + "fromDate", + (e.target as HTMLInputElement).value + ) + } + style={{ borderColor: "#D9D9D9", maxWidth: "30%" }} + required + /> + {!isAllDay && ( + { + handleEventDataChange( + index, + "fromTime", + (e.target as HTMLInputElement).value + ); + }} + style={{ borderColor: "#D9D9D9" }} + required + /> + )} + to + + handleEventDataChange( + index, + "toDate", + (e.target as HTMLInputElement).value + ) + } + style={{ borderColor: "#D9D9D9", maxWidth: "30%" }} + required + /> + {!isAllDay && ( + + handleEventDataChange( + index, + "toTime", + (e.target as HTMLInputElement).value + ) + } + style={{ borderColor: "#D9D9D9" }} + required + /> + )} + + + {/* + Checkboxes + */} + + { + handleAllDayUpdate(index, e.target.checked); + }} + style={{ borderColor: "#D9D9D9" }} + /> + + { + handleEventDataChange( + index, + "isCustom", + e.target.checked + ); + }} + style={{ borderColor: "#D9D9D9" }} + /> + + + + {renderCustomRecurrance()} + + + Address + + handleEventDataChange( + index, + "address", + (e.target as HTMLInputElement).value + ) + } + style={{ borderColor: "#D9D9D9" }} + /> + + + + Description + + handleEventDataChange( + index, + "description", + (e.target as HTMLInputElement).value + ) + } + style={{ borderColor: "#D9D9D9", height: "148px" }} + /> + + + + + Button text (optional) + + handleEventDataChange( + index, + "buttonText", + (e.target as HTMLInputElement).value + ) + } + style={{ borderColor: "#D9D9D9", width: "100%" }} + /> + + + + Button link (optional) + + handleEventDataChange( + index, + "buttonLink", + (e.target as HTMLInputElement).value + ) + } + style={{ borderColor: "#D9D9D9", width: "100%" }} + /> + + +
+
+
+ +
+ +
+
+ ); +} diff --git a/src/components/dashboard/calender-component/CalendarForm.tsx b/src/components/dashboard/calender-component/CalendarForm.tsx new file mode 100644 index 0000000..2e75665 --- /dev/null +++ b/src/components/dashboard/calender-component/CalendarForm.tsx @@ -0,0 +1,132 @@ +import React, { useState, useEffect } from "react"; +import CalendarEvent from "./CalendarEvent"; + +interface CalendarEvent { + displayNumber: number; + eventName: string; + fromDate: string; + toDate: string; + fromTime: string; + toTime: string; + isAllDay: boolean; + isCustom: boolean; + repeatDays?: ( + | "Sunday" + | "Monday" + | "Tuesday" + | "Wednesday" + | "Thursday" + | "Friday" + | "Saturday" + )[]; + isOn?: boolean; + customEndDate?: string; + isAfter?: boolean; + customEndOccurrences?: number; + address: string; + description: string; + buttonText?: string; + buttonLink?: string; +} + +export default function CalendarForm({ + eventsArray = [], +}: { + eventsArray: CalendarEvent[]; +}) { + const defaultEvent: CalendarEvent = { + displayNumber: 5, + eventName: "", + fromDate: "", + toDate: "", + fromTime: "", + toTime: "", + isAllDay: false, + isCustom: false, + address: "", + description: "", + repeatDays: [], + customEndDate: "", + customEndOccurrences: 1, + isOn: true, + isAfter: false, + }; + + const [events, setEvents] = useState( + eventsArray.length > 0 ? eventsArray : [{ ...defaultEvent }] + ); + + const handleEventDataChange = ( + index: number, + field: keyof CalendarEvent, + value: string | number | boolean | string[], + additionalUpdates: Partial = {} + ) => { + setEvents((prevEvents) => { + return prevEvents.map((event, i) => + i === index + ? { ...event, [field]: value, ...additionalUpdates } + : event + ); + }); + }; + + const handleAllDayUpdate = (index: number, isAllDay: boolean) => { + setEvents((prevEvents) => { + return prevEvents.map((event, i) => + i === index + ? { + ...event, + isAllDay, + fromTime: isAllDay ? "00:00" : "", + toTime: isAllDay ? "23:59" : "", + } + : event + ); + }); + }; + + const handleDelete = (index: number) => { + if (events.length > 1) { + setEvents((prevEvents) => prevEvents.filter((_, i) => i !== index)); + } + }; + + const handleAdd = (index: number) => { + setEvents((prevEvents) => { + const newEvents = [...prevEvents]; + const newEvent = { ...defaultEvent }; + + if (index === newEvents.length - 1) { + newEvents.push(newEvent); + } else { + newEvents.splice(index + 1, 0, newEvent); + } + return newEvents; + }); + }; + + const renderevents = () => { + return events.map((event, i) => ( + + )); + }; + + return ( +
+ {renderevents()} +
+

Current Data:

+
{JSON.stringify(events, null, 2)}
+
+
+ ); +} diff --git a/src/components/dashboard/calender-component/UpcomingEvent.tsx b/src/components/dashboard/calender-component/UpcomingEvent.tsx new file mode 100644 index 0000000..8ab0857 --- /dev/null +++ b/src/components/dashboard/calender-component/UpcomingEvent.tsx @@ -0,0 +1,136 @@ +import React from "react"; +import { Button } from "react-bootstrap"; + +export default function UpcomingEvent({ eventData }) { + const { + displayNumber, + eventName, + fromDate, + toDate, + fromTime, + toTime, + isAllDay, + isCustom, + address, + description, + buttonText, + buttonLink, + repeatDays, + customEndDate, + customEndOccurrences, + isOn, + isAfter, + } = eventData; + + function formatDate(dateStr) { + const date = new Date(dateStr); + const options: Intl.DateTimeFormatOptions = { + weekday: "long", + month: "long", + day: "numeric", + }; + return date.toLocaleDateString("en-US", options); + } + + function formatTime(timeStr) { + let [hours, minutes] = timeStr.split(":").map(Number); + const ampm = hours >= 12 ? "PM" : "AM"; + hours = hours % 12 || 12; + return `${hours}:${minutes.toString().padStart(2, "0")} ${ampm}`; + } + + return ( +
+
+

{`${formatDate(fromDate)} to ${formatDate(toDate)}`}

+

{`${formatTime( + fromTime + )} - ${formatTime(toTime)}`}

+
+
+

+ {eventName} +

+

+ {description} +

+ +
+

+ { + + + + } + {address} +

+
+
+ {buttonLink && buttonText && ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/dashboard/calender-component/UpcomingEventContainer.tsx b/src/components/dashboard/calender-component/UpcomingEventContainer.tsx new file mode 100644 index 0000000..ab464a0 --- /dev/null +++ b/src/components/dashboard/calender-component/UpcomingEventContainer.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import UpcomingEvent from "./UpcomingEvent"; + +interface CalendarEvent { + displayNumber: number; + eventName: string; + fromDate: string; + toDate: string; + fromTime: string; + toTime: string; + isAllDay: boolean; + isCustom: boolean; + repeatDays?: ( + | "Sunday" + | "Monday" + | "Tuesday" + | "Wednesday" + | "Thursday" + | "Friday" + | "Saturday" + )[]; + isOn?: boolean; + customEndDate?: string; + isAfter?: boolean; + customEndOccurrences?: number; + address: string; + description: string; + buttonText?: string; + buttonLink?: string; +} + +const dateComparison = (firstDate, secondDate) => { + const date1 = new Date(firstDate); + const date2 = new Date(secondDate); + const difference = date2.getTime() - date1.getTime(); + + //larger the number the further secondDate is from the firstDate + return difference; +}; + +export default function UpcomingEventsContainer(events) { + const date = new Date(); + const dd = String(date.getDate()).padStart(2, "0"); + const mm = String(date.getMonth() + 1).padStart(2, "0"); //January is 0 + const yyyy = date.getFullYear(); + + const currentDate = `${yyyy}-${mm}-${dd}`; + + let eventsArr = events.events; + const shownEvents = eventsArr.slice(0, events.events[0].displayNumber); + + ///filters out events that are past the current date + eventsArr = eventsArr.filter((event) => { + const eventFromDate = event.fromDate; + const timeDifference = dateComparison(currentDate, eventFromDate); + + /// add if >= 0, since that means the starting date for the event is today or in the future + if (timeDifference >= 0) { + return true; + } else { + return false; + } + }); + + return ( +
+ {shownEvents.map((event) => { + return ; + })} +
+ ); +} diff --git a/src/components/subcomponents/ProviderInfo.tsx b/src/components/subcomponents/ProviderInfo.tsx index 987a781..72e7ef1 100644 --- a/src/components/subcomponents/ProviderInfo.tsx +++ b/src/components/subcomponents/ProviderInfo.tsx @@ -16,17 +16,59 @@ import Collapsible from "components/collapsible"; import Directory from "components/dashboard/Directory"; import EmbedForm from "components/dashboard/embed-component/EmbedForm"; import EmbedComponent from "components/dashboard/embed-component/EmbedComponent"; +import CalendarForm from "components/dashboard/calender-component/CalendarForm"; +import UpcomingEventsContainer from "components/dashboard/calender-component/UpcomingEventContainer"; { /*TO BE DELETED */ } + +const calenderData = [ + { + displayNumber: 5, + eventName: "sladkfjlaskjdf", + fromDate: "2012-12-3", + toDate: "2023-12-4", + fromTime: "00:00", + toTime: "12:00", + isAllDay: false, + isCustom: true, + address: "526 Ponce De Leon Blvd, Atlanta, GA, USA", + description: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", + repeatDays: ["Sunday"], + customEndOccurrences: 5, + isOn: true, + isAfter: false, + }, + { + displayNumber: 5, + eventName: "sladkfjlaskjdf", + fromDate: "2012-12-3", + toDate: "2023-12-4", + fromTime: "00:00", + toTime: "12:00", + isAllDay: false, + isCustom: true, + address: "526 Ponce De Leon Blvd, Atlanta, GA, USA", + description: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", + repeatDays: ["Sunday"], + customEndOccurrences: 5, + buttonText: "RSVP", + buttonLink: "https://www.google.com/", + isOn: true, + isAfter: false, + }, +]; + const galleryData = [ { title: "Urban Tree Fundraiser", description: "Last Friday, we gathered for food, fun, and giving back at Urban Tree cidery. All proceeds from the evening went to our Bereavement fund. Everyone remembered to bring a sweater because the back deck got cold. We enjoyed drinks, games, and more!", imgLink: - "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", + "https://firebasestorage.googleapis.com/v0/b/gtbog-pacts.appspot.com/o/images%2FA5_5_3.png?alt=media&token=9b4befbc-5158-4de6-9f8f-fbe488e84703", }, { title: "testVal2", @@ -71,36 +113,36 @@ const galleryData = [ "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", }, ]; -const directoryData = - [ - { - name: "bob", - description: "firefighter", - details: "bob@gmail.com", - image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", - }, - { - name: "bob", - description: "firefighter", - details: "bob@gmail.com", - image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", - }, - { - name: "bob", - description: "firefighter", - details: "bob@gmail.com", - image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", - }, - { - name: "bob", - description: "firefighter", - details: "bob@gmail.com", - image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", - }, +const directoryData = [ + { + name: "bob", + description: "firefighter", + details: "bob@gmail.com", + image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", + }, + { + name: "bob", + description: "firefighter", + details: "bob@gmail.com", + image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", + }, + { + name: "bob", + description: "firefighter", + details: "bob@gmail.com", + image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", + }, + { + name: "bob", + description: "firefighter", + details: "bob@gmail.com", + image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", + }, ]; const eventInfo = { title: "Tour Our Station", - videoUrl: "https://www.youtube.com/watch?v=oZcKTf4RLQ8&ab_channel=HorizonsHealth", + videoUrl: + "https://www.youtube.com/watch?v=oZcKTf4RLQ8&ab_channel=HorizonsHealth", thumbnail: "https://picsum.photos/200", }; @@ -115,7 +157,7 @@ const ProviderInfo = (props) => { try { const res2 = await fetch( `https://maps.googleapis.com/maps/api/staticmap?center=${props.item.latitude},${props.item.longitude}&zoom=16&scale=2&size=335x250&maptype=roadmap&key=${GOOGLE_API_KEY}&format=png&visual_refresh=true` + - `&markers=${props.item.latitude},${props.item.longitude}`, + `&markers=${props.item.latitude},${props.item.longitude}` ); setStreetView(res2.url); setImage(props.item.imageURL); @@ -193,7 +235,7 @@ const ProviderInfo = (props) => { index === props.item.address.toString().split(",") .length - - 1 + 1 ) { return (
@@ -257,6 +299,36 @@ const ProviderInfo = (props) => { {/* Sample components that in the future should be added dynamically based on the response from firebase */} + + + + {/*TO BE DELETED */} + + + + + + + + {/*TO BE DELETED */} + + + + { style={{ maxWidth: "1000px", marginLeft: "auto", - marginRight: "auto" + marginRight: "auto", }} > {/*TO BE DELETED */} @@ -279,12 +351,10 @@ const ProviderInfo = (props) => { style={{ maxWidth: "1000px", marginLeft: "auto", - marginRight: "auto" + marginRight: "auto", }} > - + @@ -295,7 +365,7 @@ const ProviderInfo = (props) => { style={{ maxWidth: "1000px", marginLeft: "auto", - marginRight: "auto" + marginRight: "auto", }} > @@ -307,7 +377,7 @@ const ProviderInfo = (props) => { .filter( (category) => props.item[category.id] && - props.item[category.id].length, + props.item[category.id].length ) .map((category) => (
@@ -320,7 +390,7 @@ const ProviderInfo = (props) => { if ( index !== props.item[category.id].length - - 1 + 1 ) { return (
@@ -333,7 +403,7 @@ const ProviderInfo = (props) => { {selected}
); - }, + } ) ) : ( @@ -369,9 +439,9 @@ function calculateHours(props) { !props.item.hours[days[i]] || !props.item.hours[days[i - 1]] || props.item.hours[days[i]][0] !== - props.item.hours[days[i - 1]][0] || + props.item.hours[days[i - 1]][0] || props.item.hours[days[i]][1] !== - props.item.hours[days[i - 1]][1] + props.item.hours[days[i - 1]][1] ) { startandFinish.push(i - 1); startandFinish.push(i); @@ -387,7 +457,7 @@ function calculateHours(props) { children.push( {days[startandFinish[i]]} - , + ); } else { const subchild = [ @@ -399,22 +469,22 @@ function calculateHours(props) { children.push( {subchild} - , + ); } children.push( {props.item.hours[days[startandFinish[i]]] ? props.item.hours[days[startandFinish[i]]].map( - (time, index) => - formatTime( - props.item.hours[days[startandFinish[i]]], - time, - index, - ), - ) + (time, index) => + formatTime( + props.item.hours[days[startandFinish[i]]], + time, + index + ) + ) : "CLOSED"} - , + ); rows.push({children}); } @@ -472,5 +542,5 @@ export default compose( connect((state: Storage) => ({ providers: state.firestore.ordered.providers, firebase: state.firebase, - })), + })) )(ProviderInfo); From 5d0908fd1b568727516de1e2f0b22d0c0dc28e7d Mon Sep 17 00:00:00 2001 From: Charlie Lin Date: Sat, 2 Nov 2024 19:47:13 -0400 Subject: [PATCH 2/8] removed target blank from button in upcoming event --- src/components/dashboard/calender-component/UpcomingEvent.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/dashboard/calender-component/UpcomingEvent.tsx b/src/components/dashboard/calender-component/UpcomingEvent.tsx index 8ab0857..5bc90b6 100644 --- a/src/components/dashboard/calender-component/UpcomingEvent.tsx +++ b/src/components/dashboard/calender-component/UpcomingEvent.tsx @@ -126,9 +126,7 @@ export default function UpcomingEvent({ eventData }) { justifyContent: "center", }} > - +
)}
From 24b902baf3bd11aca44fb968ebfa713c2b1bbd82 Mon Sep 17 00:00:00 2001 From: Charlie Lin Date: Sat, 2 Nov 2024 19:50:02 -0400 Subject: [PATCH 3/8] added fix to checker --- src/components/subcomponents/ProviderInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/subcomponents/ProviderInfo.tsx b/src/components/subcomponents/ProviderInfo.tsx index 72e7ef1..a152c19 100644 --- a/src/components/subcomponents/ProviderInfo.tsx +++ b/src/components/subcomponents/ProviderInfo.tsx @@ -310,7 +310,7 @@ const ProviderInfo = (props) => { }} > {/*TO BE DELETED */} - + From d6887eab05fd0e174b903d329088ef871cac4ad7 Mon Sep 17 00:00:00 2001 From: Charlie Lin Date: Sun, 3 Nov 2024 22:55:26 -0500 Subject: [PATCH 4/8] finished filtering logic for custom and upcoming --- .../calender-component/CalendarEvent.tsx | 11 +- .../calender-component/CalendarForm.tsx | 43 +++-- .../calender-component/UpcomingEvent.tsx | 20 +- .../UpcomingEventContainer.tsx | 173 ++++++++++++++---- src/components/subcomponents/ProviderInfo.tsx | 156 +++++++++++----- 5 files changed, 291 insertions(+), 112 deletions(-) diff --git a/src/components/dashboard/calender-component/CalendarEvent.tsx b/src/components/dashboard/calender-component/CalendarEvent.tsx index 0b683cb..706df55 100644 --- a/src/components/dashboard/calender-component/CalendarEvent.tsx +++ b/src/components/dashboard/calender-component/CalendarEvent.tsx @@ -4,14 +4,15 @@ import Form from "react-bootstrap/Form"; export default function CalendarEvent({ index, + displayNumber, eventData, handleEventDataChange, + handleDisplayNumberChange, handleDelete, handleAdd, handleAllDayUpdate, }) { const { - displayNumber, eventName, fromDate, toDate, @@ -229,11 +230,11 @@ export default function CalendarEvent({ max={10} min={1} onChange={(e) => { - handleEventDataChange( - index, - "displayNumber", - Number((e.target as HTMLInputElement).value) + const num = Number( + (e.target as HTMLInputElement).value ); + if (num > 10 || num < 1) return; + handleDisplayNumberChange(num); }} style={{ maxWidth: "7%" }} /> diff --git a/src/components/dashboard/calender-component/CalendarForm.tsx b/src/components/dashboard/calender-component/CalendarForm.tsx index 2e75665..5589602 100644 --- a/src/components/dashboard/calender-component/CalendarForm.tsx +++ b/src/components/dashboard/calender-component/CalendarForm.tsx @@ -1,8 +1,7 @@ import React, { useState, useEffect } from "react"; import CalendarEvent from "./CalendarEvent"; -interface CalendarEvent { - displayNumber: number; +export interface ICalendarEvent { eventName: string; fromDate: string; toDate: string; @@ -29,13 +28,17 @@ interface CalendarEvent { buttonLink?: string; } +export interface ICalendarData { + events: ICalendarEvent[]; + displayNumber: number; +} + export default function CalendarForm({ - eventsArray = [], + calendarData, }: { - eventsArray: CalendarEvent[]; + calendarData: ICalendarData; }) { - const defaultEvent: CalendarEvent = { - displayNumber: 5, + const defaultEvent: ICalendarEvent = { eventName: "", fromDate: "", toDate: "", @@ -50,17 +53,25 @@ export default function CalendarForm({ customEndOccurrences: 1, isOn: true, isAfter: false, + buttonLink: "", + buttonText: "", }; - const [events, setEvents] = useState( - eventsArray.length > 0 ? eventsArray : [{ ...defaultEvent }] + const [events, setEvents] = useState( + calendarData.events.length > 0 + ? calendarData.events + : [{ ...defaultEvent }] + ); + + const [displayNumber, setDisplayNumber] = useState( + calendarData.displayNumber ); const handleEventDataChange = ( index: number, - field: keyof CalendarEvent, + field: keyof ICalendarEvent, value: string | number | boolean | string[], - additionalUpdates: Partial = {} + additionalUpdates: Partial = {} ) => { setEvents((prevEvents) => { return prevEvents.map((event, i) => @@ -71,6 +82,10 @@ export default function CalendarForm({ }); }; + const handleDisplayNumberChange = (value: number) => { + setDisplayNumber(value); + }; + const handleAllDayUpdate = (index: number, isAllDay: boolean) => { setEvents((prevEvents) => { return prevEvents.map((event, i) => @@ -106,13 +121,15 @@ export default function CalendarForm({ }); }; - const renderevents = () => { + const renderEvents = () => { return events.map((event, i) => ( - {renderevents()} + {renderEvents()}

Current Data:

-
{JSON.stringify(events, null, 2)}
+
{JSON.stringify({ displayNumber, events }, null, 2)}
); diff --git a/src/components/dashboard/calender-component/UpcomingEvent.tsx b/src/components/dashboard/calender-component/UpcomingEvent.tsx index 5bc90b6..86e10c0 100644 --- a/src/components/dashboard/calender-component/UpcomingEvent.tsx +++ b/src/components/dashboard/calender-component/UpcomingEvent.tsx @@ -3,27 +3,20 @@ import { Button } from "react-bootstrap"; export default function UpcomingEvent({ eventData }) { const { - displayNumber, + isAllDay, eventName, fromDate, toDate, fromTime, toTime, - isAllDay, - isCustom, address, description, buttonText, buttonLink, - repeatDays, - customEndDate, - customEndOccurrences, - isOn, - isAfter, } = eventData; function formatDate(dateStr) { - const date = new Date(dateStr); + const date = new Date(dateStr + "T00:00:00"); const options: Intl.DateTimeFormatOptions = { weekday: "long", month: "long", @@ -46,7 +39,6 @@ export default function UpcomingEvent({ eventData }) { paddingBottom: "10px", paddingTop: "10px", width: "100%", - gap: "12px", borderBottom: "solid", borderColor: "#DFDFDF", borderWidth: "1px", @@ -64,9 +56,11 @@ export default function UpcomingEvent({ eventData }) {

{`${formatDate(fromDate)} to ${formatDate(toDate)}`}

-

{`${formatTime( - fromTime - )} - ${formatTime(toTime)}`}

+

+ {isAllDay + ? "All Day" + : `${formatTime(fromTime)} - ${formatTime(toTime)}`} +

{ + const date1 = new Date(firstDate); + const date2 = new Date(secondDate); + const difference = date2.getTime() - date1.getTime(); + return difference; +}; + +function countWeekdayOccurrences( + startDate: Date, + endDate: Date, + weekdays: Array< | "Sunday" | "Monday" | "Tuesday" @@ -18,43 +21,110 @@ interface CalendarEvent { | "Thursday" | "Friday" | "Saturday" - )[]; - isOn?: boolean; - customEndDate?: string; - isAfter?: boolean; - customEndOccurrences?: number; - address: string; - description: string; - buttonText?: string; - buttonLink?: string; + > +): number { + const dayMapping: { [key: string]: number } = { + Sunday: 0, + Monday: 1, + Tuesday: 2, + Wednesday: 3, + Thursday: 4, + Friday: 5, + Saturday: 6, + }; + + if (startDate > endDate) { + throw new Error( + "The start date must be before or equal to the end date." + ); + } + + const targetDayNumbers = weekdays.map((day) => dayMapping[day]); + let count = 0; + + startDate = new Date(startDate); + while (startDate <= endDate) { + if (targetDayNumbers.includes(startDate.getDay())) { + count++; + } + startDate.setDate(startDate.getDate() + 1); + } + + return count; } -const dateComparison = (firstDate, secondDate) => { - const date1 = new Date(firstDate); - const date2 = new Date(secondDate); - const difference = date2.getTime() - date1.getTime(); +const checkIfShouldFilterByCustom = (event, currentDate) => { + if (event.isCustom) { + if (event.isOn) { + const customEndDate = event.customEndDate; + const isPast = + calculateDateDifference(currentDate, customEndDate) < 0 + ? true + : false; + return !isPast; + } else if (event.isAfter) { + const startDate = new Date(event.fromDate); + currentDate = new Date(currentDate); + const maxOccurrances = event.customEndOccurrences; + const isUnderMaxOccurances = + countWeekdayOccurrences( + startDate, + currentDate, + event.repeatDays + ) < maxOccurrances + ? true + : false; + return isUnderMaxOccurances; + } + } - //larger the number the further secondDate is from the firstDate - return difference; + return false; }; -export default function UpcomingEventsContainer(events) { +export default function UpcomingEventsContainer(calendarData: ICalendarData) { const date = new Date(); - const dd = String(date.getDate()).padStart(2, "0"); + const day = date.getDate(); + const dd = String(day).padStart(2, "0"); const mm = String(date.getMonth() + 1).padStart(2, "0"); //January is 0 const yyyy = date.getFullYear(); - const currentDate = `${yyyy}-${mm}-${dd}`; - let eventsArr = events.events; - const shownEvents = eventsArr.slice(0, events.events[0].displayNumber); + const weekdays: [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ] = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + + const currentWeekdayName: + | "Sunday" + | "Monday" + | "Tuesday" + | "Wednesday" + | "Thursday" + | "Friday" + | "Saturday" = weekdays[date.getDay()]; - ///filters out events that are past the current date - eventsArr = eventsArr.filter((event) => { - const eventFromDate = event.fromDate; - const timeDifference = dateComparison(currentDate, eventFromDate); + let eventsArr = calendarData.events; - /// add if >= 0, since that means the starting date for the event is today or in the future + //filters out events that are past the current date + let filteredEvents = eventsArr.filter((event) => { + const eventToDate = event.toDate; + const timeDifference = calculateDateDifference( + currentDate, + eventToDate + ); if (timeDifference >= 0) { return true; } else { @@ -62,9 +132,40 @@ export default function UpcomingEventsContainer(events) { } }); + filteredEvents = filteredEvents.filter((event) => { + if (checkIfShouldFilterByCustom(event, currentDate)) { + if (event.repeatDays.includes(currentWeekdayName)) { + return true; + } + return false; + } else if (event.isCustom) { + if ( + calculateDateDifference(currentDate, event.customEndDate) <= + 0 || + countWeekdayOccurrences( + new Date(event.fromDate), + new Date(currentDate), + event.repeatDays + ) >= event.customEndOccurrences + ) { + return true; + } + } else if (event.isCustom === false) { + return true; + } + return false; + }); + + //sorts events from closests to farthest event + filteredEvents.sort((a, b) => { + return calculateDateDifference(b.fromDate, a.fromDate); + }); + //slices to show first {displayNumber} events + filteredEvents = filteredEvents.slice(0, calendarData.displayNumber); + return (
- {shownEvents.map((event) => { + {filteredEvents.map((event) => { return ; })}
diff --git a/src/components/subcomponents/ProviderInfo.tsx b/src/components/subcomponents/ProviderInfo.tsx index a152c19..346e18e 100644 --- a/src/components/subcomponents/ProviderInfo.tsx +++ b/src/components/subcomponents/ProviderInfo.tsx @@ -16,51 +16,114 @@ import Collapsible from "components/collapsible"; import Directory from "components/dashboard/Directory"; import EmbedForm from "components/dashboard/embed-component/EmbedForm"; import EmbedComponent from "components/dashboard/embed-component/EmbedComponent"; -import CalendarForm from "components/dashboard/calender-component/CalendarForm"; +import CalendarForm, { + ICalendarData, +} from "components/dashboard/calender-component/CalendarForm"; import UpcomingEventsContainer from "components/dashboard/calender-component/UpcomingEventContainer"; -{ - /*TO BE DELETED */ -} - -const calenderData = [ - { - displayNumber: 5, - eventName: "sladkfjlaskjdf", - fromDate: "2012-12-3", - toDate: "2023-12-4", - fromTime: "00:00", - toTime: "12:00", - isAllDay: false, - isCustom: true, - address: "526 Ponce De Leon Blvd, Atlanta, GA, USA", - description: - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", - repeatDays: ["Sunday"], - customEndOccurrences: 5, - isOn: true, - isAfter: false, - }, - { - displayNumber: 5, - eventName: "sladkfjlaskjdf", - fromDate: "2012-12-3", - toDate: "2023-12-4", - fromTime: "00:00", - toTime: "12:00", - isAllDay: false, - isCustom: true, - address: "526 Ponce De Leon Blvd, Atlanta, GA, USA", - description: - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", - repeatDays: ["Sunday"], - customEndOccurrences: 5, - buttonText: "RSVP", - buttonLink: "https://www.google.com/", - isOn: true, - isAfter: false, - }, -]; +const calenderData: ICalendarData = { + displayNumber: 5, + events: [ + //NON-CUSTOM, EXPIRED EVENT DATE CASE (SHOULD NOT BE SHOWN) + { + eventName: "Morning Yoga Class", + fromDate: "2024-11-01", // Start date + toDate: "2024-11-01", // End date (same as start date) + fromTime: "07:00", + toTime: "08:30", + isAllDay: false, + isCustom: false, + address: "Community Center, Main Hall", + description: "Start your day with a refreshing yoga class.", + repeatDays: ["Monday", "Wednesday", "Friday"], + customEndDate: "", + customEndOccurrences: 1, + isOn: true, + isAfter: false, + buttonLink: "https://yogaclass.com/register", + buttonText: "Register", + }, + //CUSTOM, ON, NON-EXPIRED CUSTOM DATE, NON-EXPIRED EVENT DATE, NON-CURRENT DAY CASE (may be subject to IRL day rn!!!!) (SHOULD ONLY BE SHOWN ON TUESDAY, THURSDAY) + { + eventName: "Tech Conference 2024", + fromDate: "2024-11-10", // Start date + toDate: "2024-11-29", // End date + fromTime: "09:00", + toTime: "17:00", + isAllDay: false, + isCustom: true, + address: "Tech Park Auditorium", + description: + "A 3-day conference with keynotes and workshops on technology trends.", + repeatDays: ["Tuesday", "Thursday"], + customEndDate: "2024-11-12", + customEndOccurrences: 1, + isOn: true, + isAfter: false, + buttonLink: "https://techconf2024.com", + buttonText: "Get Tickets", + }, + //CUSTOM, ON, EXPIRED CUSTOM END DATE, NON-EXPIRED EVENT END DATE (SHOULD BE SHOWN ON ANY DAY SINCE THE CUSTOM END DATE IS PAST AND NOT EXPIRED EVENT DATE) + { + eventName: "Weekly Community Meetup", + fromDate: "2024-10-15", // Start date + toDate: "2024-12-15", // End date + fromTime: "18:00", + toTime: "20:00", + isAllDay: false, + isCustom: true, + address: "Local Library, Meeting Room 2", + description: + "A weekly gathering for community discussions and activities.", + repeatDays: ["Sunday", "Tuesday"], + customEndDate: "2024-11-2", + customEndOccurrences: 8, + isOn: true, + isAfter: false, + buttonLink: "https://communitymeetup.org", + buttonText: "Join Us", + }, + //NON-CUSTOM, NON-EXPIRED EVENT DATE CASE (SHOULD BE SHOWN AND SHOULD SHOW ALL DAY IN EVENT TILE) + { + eventName: "Art Workshop for Beginners", + fromDate: "2024-11-05", // Start date + toDate: "2024-11-29", // End date (same as start date) + fromTime: "00:00", + toTime: "23:59", + isAllDay: true, + isCustom: false, + address: "Downtown Art Studio", + description: + "Learn the basics of painting in a supportive group setting.", + repeatDays: [], + customEndDate: "", + customEndOccurrences: 1, + isOn: true, + isAfter: false, + buttonLink: "https://artworkshop.com/signup", + buttonText: "Sign Up", + }, + //CUSTOM, AFTER 2 OCCURRANCES, NON-EXPIRED EVENT CASE (SHOULD BE SHOWN ON SET WEEKDAY(S) UNTIL OCCURANCES HAVE BEEN ACCOUNTED FOR) + { + eventName: "Monthly Board Game Night", + fromDate: "2024-10-26", + toDate: "2024-11-29", + fromTime: "19:00", + toTime: "23:00", + isAllDay: false, + isCustom: true, + address: "The Game Lounge", + description: "Join us for an evening of board games and fun!", + repeatDays: ["Saturday"], + customEndDate: "2025-01-18", + customEndOccurrences: 2, + isOn: false, + isAfter: true, + buttonLink: "https://gamenight.com", + buttonText: "Reserve Your Spot", + }, + ], +}; const galleryData = [ { @@ -310,7 +373,7 @@ const ProviderInfo = (props) => { }} > {/*TO BE DELETED */} - + @@ -325,7 +388,10 @@ const ProviderInfo = (props) => { }} > {/*TO BE DELETED */} - + From 5e932468a35d17bd35b08aa6e54ff887de0b6585 Mon Sep 17 00:00:00 2001 From: Charlie Lin Date: Sun, 3 Nov 2024 23:02:15 -0500 Subject: [PATCH 5/8] added target blank to upcoming events button --- src/components/dashboard/calender-component/UpcomingEvent.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/calender-component/UpcomingEvent.tsx b/src/components/dashboard/calender-component/UpcomingEvent.tsx index 86e10c0..12320ed 100644 --- a/src/components/dashboard/calender-component/UpcomingEvent.tsx +++ b/src/components/dashboard/calender-component/UpcomingEvent.tsx @@ -120,7 +120,9 @@ export default function UpcomingEvent({ eventData }) { justifyContent: "center", }} > - + + +
)} From 0e253e13c6b9fbb35b3776646ee96302ea189e28 Mon Sep 17 00:00:00 2001 From: Athanasios Taprantzis Date: Fri, 3 Jan 2025 22:17:39 +0200 Subject: [PATCH 6/8] Integration + UI changes --- src/components/dashboard/ContentForm.tsx | 120 ++++++++++++++++++ .../calender-component/CalendarEvent.tsx | 56 +++++--- .../calender-component/CalendarForm.tsx | 11 +- src/components/subcomponents/ProviderInfo.tsx | 113 +++++++---------- 4 files changed, 213 insertions(+), 87 deletions(-) diff --git a/src/components/dashboard/ContentForm.tsx b/src/components/dashboard/ContentForm.tsx index 7ee2282..d1d22f1 100644 --- a/src/components/dashboard/ContentForm.tsx +++ b/src/components/dashboard/ContentForm.tsx @@ -12,6 +12,111 @@ import ChartComponentForm from "components/subcomponents/chartcomponents/ChartCo import Collapsible from "components/collapsible"; import ProviderGallery from "./ProviderGallery"; import EmbedForm from "./embed-component/EmbedForm"; +import CalendarForm, { ICalendarData } from "components/dashboard/calender-component/CalendarForm"; + +const calenderData: ICalendarData = { + displayNumber: 5, + events: [ + //NON-CUSTOM, EXPIRED EVENT DATE CASE (SHOULD NOT BE SHOWN) + { + eventName: "Morning Yoga Class", + fromDate: "2024-11-01", // Start date + toDate: "2024-11-01", // End date (same as start date) + fromTime: "07:00", + toTime: "08:30", + isAllDay: false, + isCustom: false, + address: "Community Center, Main Hall", + description: "Start your day with a refreshing yoga class.", + repeatDays: ["Monday", "Wednesday", "Friday"], + customEndDate: "", + customEndOccurrences: 1, + isOn: true, + isAfter: false, + buttonLink: "https://yogaclass.com/register", + buttonText: "Register", + }, + //CUSTOM, ON, NON-EXPIRED CUSTOM DATE, NON-EXPIRED EVENT DATE, NON-CURRENT DAY CASE (may be subject to IRL day rn!!!!) (SHOULD ONLY BE SHOWN ON TUESDAY, THURSDAY) + { + eventName: "Tech Conference 2024", + fromDate: "2024-11-10", // Start date + toDate: "2024-11-29", // End date + fromTime: "09:00", + toTime: "17:00", + isAllDay: false, + isCustom: true, + address: "Tech Park Auditorium", + description: + "A 3-day conference with keynotes and workshops on technology trends.", + repeatDays: ["Tuesday", "Thursday"], + customEndDate: "2024-11-12", + customEndOccurrences: 1, + isOn: true, + isAfter: false, + buttonLink: "https://techconf2024.com", + buttonText: "Get Tickets", + }, + //CUSTOM, ON, EXPIRED CUSTOM END DATE, NON-EXPIRED EVENT END DATE (SHOULD BE SHOWN ON ANY DAY SINCE THE CUSTOM END DATE IS PAST AND NOT EXPIRED EVENT DATE) + { + eventName: "Weekly Community Meetup", + fromDate: "2024-10-15", // Start date + toDate: "2024-12-15", // End date + fromTime: "18:00", + toTime: "20:00", + isAllDay: false, + isCustom: true, + address: "Local Library, Meeting Room 2", + description: + "A weekly gathering for community discussions and activities.", + repeatDays: ["Sunday", "Tuesday"], + customEndDate: "2024-11-2", + customEndOccurrences: 8, + isOn: true, + isAfter: false, + buttonLink: "https://communitymeetup.org", + buttonText: "Join Us", + }, + //NON-CUSTOM, NON-EXPIRED EVENT DATE CASE (SHOULD BE SHOWN AND SHOULD SHOW ALL DAY IN EVENT TILE) + { + eventName: "Art Workshop for Beginners", + fromDate: "2024-11-05", // Start date + toDate: "2024-11-29", // End date (same as start date) + fromTime: "00:00", + toTime: "23:59", + isAllDay: true, + isCustom: false, + address: "Downtown Art Studio", + description: + "Learn the basics of painting in a supportive group setting.", + repeatDays: [], + customEndDate: "", + customEndOccurrences: 1, + isOn: true, + isAfter: false, + buttonLink: "https://artworkshop.com/signup", + buttonText: "Sign Up", + }, + //CUSTOM, AFTER 2 OCCURRANCES, NON-EXPIRED EVENT CASE (SHOULD BE SHOWN ON SET WEEKDAY(S) UNTIL OCCURANCES HAVE BEEN ACCOUNTED FOR) + { + eventName: "Monthly Board Game Night", + fromDate: "2024-10-26", + toDate: "2024-11-29", + fromTime: "19:00", + toTime: "23:00", + isAllDay: false, + isCustom: true, + address: "The Game Lounge", + description: "Join us for an evening of board games and fun!", + repeatDays: ["Saturday"], + customEndDate: "2025-01-18", + customEndOccurrences: 2, + isOn: false, + isAfter: true, + buttonLink: "https://gamenight.com", + buttonText: "Reserve Your Spot", + }, + ], +}; const EditableText = ({ text, setText, isEditing, setIsEditing }) => { const inputRef = useRef(null); @@ -223,6 +328,21 @@ const SectionCard = ({ ])}>Embed + + setComponents([...components, + + + + ])}>Calendar diff --git a/src/components/dashboard/calender-component/CalendarEvent.tsx b/src/components/dashboard/calender-component/CalendarEvent.tsx index 706df55..9b448ee 100644 --- a/src/components/dashboard/calender-component/CalendarEvent.tsx +++ b/src/components/dashboard/calender-component/CalendarEvent.tsx @@ -4,6 +4,7 @@ import Form from "react-bootstrap/Form"; export default function CalendarEvent({ index, + length, displayNumber, eventData, handleEventDataChange, @@ -158,7 +159,7 @@ export default function CalendarEvent({ display: "flex", justifyContent: "space-between", alignItems: "center", - maxWidth: "35%", + maxWidth: "43%", }} > 10 || num < 1) return; handleDisplayNumberChange(num); }} - style={{ maxWidth: "7%" }} + style={{ width: "65px" }} /> )} @@ -473,22 +474,41 @@ export default function CalendarEvent({ Delete - + + +
+ + {index == length - 1 && + + } +
); diff --git a/src/components/dashboard/calender-component/CalendarForm.tsx b/src/components/dashboard/calender-component/CalendarForm.tsx index 5589602..7a1ac23 100644 --- a/src/components/dashboard/calender-component/CalendarForm.tsx +++ b/src/components/dashboard/calender-component/CalendarForm.tsx @@ -91,11 +91,11 @@ export default function CalendarForm({ return prevEvents.map((event, i) => i === index ? { - ...event, - isAllDay, - fromTime: isAllDay ? "00:00" : "", - toTime: isAllDay ? "23:59" : "", - } + ...event, + isAllDay, + fromTime: isAllDay ? "00:00" : "", + toTime: isAllDay ? "23:59" : "", + } : event ); }); @@ -128,6 +128,7 @@ export default function CalendarForm({ eventData={{ ...event }} index={i} key={i} + length={events.length} handleEventDataChange={handleEventDataChange} handleDisplayNumberChange={handleDisplayNumberChange} handleAllDayUpdate={handleAllDayUpdate} diff --git a/src/components/subcomponents/ProviderInfo.tsx b/src/components/subcomponents/ProviderInfo.tsx index 346e18e..6ab2fdf 100644 --- a/src/components/subcomponents/ProviderInfo.tsx +++ b/src/components/subcomponents/ProviderInfo.tsx @@ -27,8 +27,8 @@ const calenderData: ICalendarData = { //NON-CUSTOM, EXPIRED EVENT DATE CASE (SHOULD NOT BE SHOWN) { eventName: "Morning Yoga Class", - fromDate: "2024-11-01", // Start date - toDate: "2024-11-01", // End date (same as start date) + fromDate: "2025-01-01", // Start date + toDate: "2025-11-01", // End date (same as start date) fromTime: "07:00", toTime: "08:30", isAllDay: false, @@ -46,8 +46,8 @@ const calenderData: ICalendarData = { //CUSTOM, ON, NON-EXPIRED CUSTOM DATE, NON-EXPIRED EVENT DATE, NON-CURRENT DAY CASE (may be subject to IRL day rn!!!!) (SHOULD ONLY BE SHOWN ON TUESDAY, THURSDAY) { eventName: "Tech Conference 2024", - fromDate: "2024-11-10", // Start date - toDate: "2024-11-29", // End date + fromDate: "2025-01-01", // Start date + toDate: "2025-11-29", // End date fromTime: "09:00", toTime: "17:00", isAllDay: false, @@ -56,7 +56,7 @@ const calenderData: ICalendarData = { description: "A 3-day conference with keynotes and workshops on technology trends.", repeatDays: ["Tuesday", "Thursday"], - customEndDate: "2024-11-12", + customEndDate: "2025-11-12", customEndOccurrences: 1, isOn: true, isAfter: false, @@ -66,8 +66,8 @@ const calenderData: ICalendarData = { //CUSTOM, ON, EXPIRED CUSTOM END DATE, NON-EXPIRED EVENT END DATE (SHOULD BE SHOWN ON ANY DAY SINCE THE CUSTOM END DATE IS PAST AND NOT EXPIRED EVENT DATE) { eventName: "Weekly Community Meetup", - fromDate: "2024-10-15", // Start date - toDate: "2024-12-15", // End date + fromDate: "2025-01-01", // Start date + toDate: "2025-12-15", // End date fromTime: "18:00", toTime: "20:00", isAllDay: false, @@ -76,7 +76,7 @@ const calenderData: ICalendarData = { description: "A weekly gathering for community discussions and activities.", repeatDays: ["Sunday", "Tuesday"], - customEndDate: "2024-11-2", + customEndDate: "2025-11-02", customEndOccurrences: 8, isOn: true, isAfter: false, @@ -86,8 +86,8 @@ const calenderData: ICalendarData = { //NON-CUSTOM, NON-EXPIRED EVENT DATE CASE (SHOULD BE SHOWN AND SHOULD SHOW ALL DAY IN EVENT TILE) { eventName: "Art Workshop for Beginners", - fromDate: "2024-11-05", // Start date - toDate: "2024-11-29", // End date (same as start date) + fromDate: "2025-01-01", // Start date + toDate: "2025-11-29", // End date (same as start date) fromTime: "00:00", toTime: "23:59", isAllDay: true, @@ -176,36 +176,36 @@ const galleryData = [ "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", }, ]; -const directoryData = [ - { - name: "bob", - description: "firefighter", - details: "bob@gmail.com", - image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", - }, - { - name: "bob", - description: "firefighter", - details: "bob@gmail.com", - image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", - }, - { - name: "bob", - description: "firefighter", - details: "bob@gmail.com", - image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", - }, - { - name: "bob", - description: "firefighter", - details: "bob@gmail.com", - image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", - }, +const directoryData = + [ + { + name: "bob", + description: "firefighter", + details: "bob@gmail.com", + image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", + }, + { + name: "bob", + description: "firefighter", + details: "bob@gmail.com", + image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", + }, + { + name: "bob", + description: "firefighter", + details: "bob@gmail.com", + image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", + }, + { + name: "bob", + description: "firefighter", + details: "bob@gmail.com", + image: "https://images.squarespace-cdn.com/content/v1/54822a56e4b0b30bd821480c/45ed8ecf-0bb2-4e34-8fcf-624db47c43c8/Golden+Retrievers+dans+pet+care.jpeg", + }, ]; const eventInfo = { title: "Tour Our Station", - videoUrl: - "https://www.youtube.com/watch?v=oZcKTf4RLQ8&ab_channel=HorizonsHealth", + videoUrl: "https://www.youtube.com/watch?v=oZcKTf4RLQ8&ab_channel=HorizonsHealth", thumbnail: "https://picsum.photos/200", }; @@ -220,7 +220,7 @@ const ProviderInfo = (props) => { try { const res2 = await fetch( `https://maps.googleapis.com/maps/api/staticmap?center=${props.item.latitude},${props.item.longitude}&zoom=16&scale=2&size=335x250&maptype=roadmap&key=${GOOGLE_API_KEY}&format=png&visual_refresh=true` + - `&markers=${props.item.latitude},${props.item.longitude}` + `&markers=${props.item.latitude},${props.item.longitude}`, ); setStreetView(res2.url); setImage(props.item.imageURL); @@ -298,7 +298,7 @@ const ProviderInfo = (props) => { index === props.item.address.toString().split(",") .length - - 1 + 1 ) { return (
@@ -362,21 +362,6 @@ const ProviderInfo = (props) => { {/* Sample components that in the future should be added dynamically based on the response from firebase */} - - - - {/*TO BE DELETED */} - - - - { if ( index !== props.item[category.id].length - - 1 + 1 ) { return (
@@ -505,9 +490,9 @@ function calculateHours(props) { !props.item.hours[days[i]] || !props.item.hours[days[i - 1]] || props.item.hours[days[i]][0] !== - props.item.hours[days[i - 1]][0] || + props.item.hours[days[i - 1]][0] || props.item.hours[days[i]][1] !== - props.item.hours[days[i - 1]][1] + props.item.hours[days[i - 1]][1] ) { startandFinish.push(i - 1); startandFinish.push(i); @@ -542,13 +527,13 @@ function calculateHours(props) { {props.item.hours[days[startandFinish[i]]] ? props.item.hours[days[startandFinish[i]]].map( - (time, index) => - formatTime( - props.item.hours[days[startandFinish[i]]], - time, - index - ) - ) + (time, index) => + formatTime( + props.item.hours[days[startandFinish[i]]], + time, + index + ) + ) : "CLOSED"} ); From 79ad69a43a98d235c3f5df256b454722aeab943e Mon Sep 17 00:00:00 2001 From: Athanasios Taprantzis Date: Sat, 4 Jan 2025 16:35:27 +0200 Subject: [PATCH 7/8] Merge branch 'develop' into feature/calendarComponent --- .eslintrc.js | 14 +- package.json | 37 +- src/assets/styles/main.css | 28 +- src/assets/svg/blockquote.svg | 6 + src/components/chat/Discussion.tsx | 2 +- src/components/chat/index.tsx | 41 +- src/components/collapsible/index.tsx | 16 +- src/components/dashboard/AddProvider.tsx | 216 ++--- .../dashboard/ContentForm.module.css | 16 + src/components/dashboard/ContentForm.tsx | 326 +++++-- src/components/dashboard/Directory.tsx | 103 ++- .../dashboard/DirectoryForm.module.css | 5 +- src/components/dashboard/DirectoryForm.tsx | 106 ++- .../dashboard/EventInfoComponent.css | 17 + .../dashboard/EventInfoComponent.tsx | 14 + src/components/dashboard/GeneralInfo.tsx | 228 +++++ src/components/dashboard/ProviderGallery.tsx | 77 +- .../dashboard/ProviderGalleryCarousel.tsx | 259 +++--- .../dashboard/ProviderGallerySlide.tsx | 25 +- src/components/dashboard/RowForm.tsx | 18 +- .../TextComponent/CustomDropdown.tsx | 92 ++ .../dashboard/{ => TextComponent}/Icons.tsx | 103 +++ .../dashboard/TextComponent/LinkModal.tsx | 46 + .../dashboard/TextComponent/Modal.tsx | 32 + .../dashboard/TextComponent/SimpleEditor.tsx | 396 +++++++++ .../TextColor/ColorPickerButton.tsx | 48 + .../TextComponent/TextColor/TextColor.tsx | 65 ++ .../dashboard/TextComponent/styles.css | 148 ++++ .../calender-component/CalendarEvent.tsx | 36 +- .../calender-component/CalendarForm.tsx | 104 ++- .../embed-component/EmbedComponent.tsx | 5 +- .../dashboard/embed-component/EmbedForm.tsx | 202 +++-- .../dashboard/embed-component/InfoAlert.tsx | 36 - src/components/dashboard/index.tsx | 9 +- src/components/map/GoogleMap.tsx | 3 +- src/components/map/MapMarker.tsx | 41 +- src/components/map/index.tsx | 138 +-- src/components/map/mapStyles.css | 63 +- src/components/navigation/NavBar.tsx | 2 +- src/components/navigation/ProviderRoutes.tsx | 31 +- src/components/subcomponents/ProviderInfo.tsx | 466 ++++------ .../chartcomponents/ChartComponentForm.tsx | 139 +-- .../styles/ChartComponentForm.css | 3 +- src/components/template/CategoryCell.tsx | 2 +- src/components/template/PrimaryCell.tsx | 3 - src/components/template/index.tsx | 259 +++--- src/config/firebase-config.ts | 2 - src/functions/useWindowSize.ts | 10 +- src/tutorial/nextButton.tsx | 2 +- src/types/firestore.ts | 1 + yarn.lock | 832 ++++++++++++++---- 51 files changed, 3410 insertions(+), 1463 deletions(-) create mode 100644 src/assets/svg/blockquote.svg create mode 100644 src/components/dashboard/EventInfoComponent.css create mode 100644 src/components/dashboard/EventInfoComponent.tsx create mode 100644 src/components/dashboard/GeneralInfo.tsx create mode 100644 src/components/dashboard/TextComponent/CustomDropdown.tsx rename src/components/dashboard/{ => TextComponent}/Icons.tsx (52%) create mode 100644 src/components/dashboard/TextComponent/LinkModal.tsx create mode 100644 src/components/dashboard/TextComponent/Modal.tsx create mode 100644 src/components/dashboard/TextComponent/SimpleEditor.tsx create mode 100644 src/components/dashboard/TextComponent/TextColor/ColorPickerButton.tsx create mode 100644 src/components/dashboard/TextComponent/TextColor/TextColor.tsx create mode 100644 src/components/dashboard/TextComponent/styles.css delete mode 100644 src/components/dashboard/embed-component/InfoAlert.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 8d02f49..5ce3c6b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,11 +3,7 @@ module.exports = { browser: true, es6: true, }, - extends: [ - "eslint:recommended", - "plugin:react/recommended", - "airbnb", - ], + extends: ["eslint:recommended", "plugin:react/recommended", "airbnb"], globals: { Atomics: "readonly", SharedArrayBuffer: "readonly", @@ -22,5 +18,11 @@ module.exports = { plugins: ["only-warn", "react"], rules: { "react/jsx-props-no-spreading": ["off"], + "react/jsx-indent": ["error", 4], + indent: ["error", 4], + quotes: ["error", "double"], }, -}; + ignorePatterns: [ + "node_modules/@firebase/**/*.map", // Only ignore source map files in Firebase + ], +} diff --git a/package.json b/package.json index 77f8b31..1e0a670 100644 --- a/package.json +++ b/package.json @@ -33,16 +33,34 @@ "@reactour/tour": "^3.3.0", "@sentry/react": "^5.19.1", "@slack/client": "^5.0.2", - "@tiptap/extension-bullet-list": "^2.8.0", + "@tiptap/core": "^2.9.1", + "@tiptap/extension-blockquote": "^2.9.1", + "@tiptap/extension-bold": "^2.9.1", + "@tiptap/extension-bullet-list": "^2.9.1", + "@tiptap/extension-code": "^2.9.1", "@tiptap/extension-color": "^2.8.0", - "@tiptap/extension-image": "^2.8.0", - "@tiptap/extension-link": "^2.8.0", - "@tiptap/extension-list-item": "^2.8.0", - "@tiptap/extension-ordered-list": "^2.8.0", + "@tiptap/extension-document": "^2.9.1", + "@tiptap/extension-heading": "^2.9.1", + "@tiptap/extension-highlight": "^2.9.1", + "@tiptap/extension-history": "^2.9.1", + "@tiptap/extension-horizontal-rule": "^2.9.1", + "@tiptap/extension-image": "^2.9.1", + "@tiptap/extension-italic": "^2.9.1", + "@tiptap/extension-link": "^2.9.1", + "@tiptap/extension-list-item": "^2.9.1", + "@tiptap/extension-ordered-list": "^2.9.1", + "@tiptap/extension-paragraph": "^2.9.1", + "@tiptap/extension-strike": "^2.9.1", + "@tiptap/extension-subscript": "^2.9.1", + "@tiptap/extension-superscript": "^2.9.1", + "@tiptap/extension-task-item": "^2.9.1", + "@tiptap/extension-task-list": "^2.9.1", + "@tiptap/extension-text": "^2.9.1", + "@tiptap/extension-text-align": "^2.9.1", "@tiptap/extension-text-style": "^2.8.0", - "@tiptap/extension-underline": "^2.8.0", - "@tiptap/react": "^2.8.0", - "@tiptap/starter-kit": "^2.8.0", + "@tiptap/extension-underline": "^2.9.1", + "@tiptap/react": "^2.9.1", + "@tiptap/starter-kit": "^2.9.1", "@types/react": "^17.0.30", "@types/react-modal": "^3.16.3", "autoprefixer": "9.8.0", @@ -106,6 +124,9 @@ "redux-freeze": "^0.1.5", "redux-logger": "^3.0.6", "redux-thunk": "^2.2.0", + "tiptap-extension-resize-image": "^1.1.9", + "tiptap-imagresize": "^1.1.0", + "tiptap-resize-image": "^2.0.1", "typescript": "^4.0.2", "url-loader": "^4.0.0", "uuid": "^7.0.3", diff --git a/src/assets/styles/main.css b/src/assets/styles/main.css index b8f683f..a4021ca 100644 --- a/src/assets/styles/main.css +++ b/src/assets/styles/main.css @@ -134,11 +134,12 @@ h2 { width: 100vw; height: calc(200vh - 70px); z-index: 2; - overflow-y: scroll; background: white; padding-top: 10px; } - +.map-list::-webkit-scrollbar { + display: none; +} /* .map-cell { border-top: solid #e0e0e0; background-color: white; @@ -220,7 +221,9 @@ h2 { padding-left: 12px; padding-right: 12px; } - +.padder::-webkit-scrollbar { + display: none; +} .point { cursor: pointer; background: white; @@ -670,12 +673,17 @@ h2 { flex-direction: row; } +.modal-image-col { + height: 50%; +} + .modal-hours-container { display: flex; flex-direction: column; font-size: 13px; - padding-left: 15px; - width: 30vw; + padding-left: 0px; + width: fit-content; + white-space: nowrap; } .modal-hours-container h5 { @@ -1412,16 +1420,20 @@ h2 { #discussion-root { margin-top: 12px; margin-bottom: 20px; -} + } #discussion-scroll { display: flex; flex-direction: column-reverse; max-height: 440px; - width: 100%; + width: 60vw; overflow-y: scroll; } +#discussion-scroll::-webkit-scrollbar { + display: none; +} + .chat-bubble { border: solid 8px limegreen; padding: 10px 24px; @@ -1730,7 +1742,7 @@ h2 { display: flex; flex-direction: column; align-items: center; - width: 635px; + width: 800px; } .provider-entry { diff --git a/src/assets/svg/blockquote.svg b/src/assets/svg/blockquote.svg new file mode 100644 index 0000000..4668e2f --- /dev/null +++ b/src/assets/svg/blockquote.svg @@ -0,0 +1,6 @@ + + + block-quote-line + + + \ No newline at end of file diff --git a/src/components/chat/Discussion.tsx b/src/components/chat/Discussion.tsx index 460341f..010d80d 100644 --- a/src/components/chat/Discussion.tsx +++ b/src/components/chat/Discussion.tsx @@ -15,7 +15,7 @@ function Discussion({ chatHistory }) { }, []); useEffect(() => { - setData(chatHistory); + setData(chatHistory.slice().reverse()); }, [chatHistory]); return ( diff --git a/src/components/chat/index.tsx b/src/components/chat/index.tsx index 8b766b8..ef74683 100644 --- a/src/components/chat/index.tsx +++ b/src/components/chat/index.tsx @@ -23,28 +23,34 @@ async function sendSlackMessage(email, message) { fetch("https://bit-bot-five.vercel.app/bog/mapscout", requestOptions); } -function Chat({ firebase, newChat, updateNewChat }) { +const addToDo = (newToDo) => { + return chatRef.push().set(newToDo); +}; + +function Chat({firebase}) { const [message, setMessage] = useState(""); - const addToDo = async (newToDo) => { - chatRef.push().set(newToDo); - }; + const [isSubmitted, setIsSubmitted] = useState(false); const inputChange = (e) => { setMessage(e.target.value); }; - - const formSubmit = (e) => { + + const formSubmit = async (e) => { e.preventDefault(); if (message !== "") { const currentdate = new Date(); const datetime = currentdate.toISOString(); - addToDo({ + await addToDo({ message, timestamp: datetime, uid: firebase.auth.uid, username: firebase.auth.email, - }).then(() => setMessage("")); - sendSlackMessage(firebase.auth.email, message); + }); + await sendSlackMessage(firebase.auth.email, message); + // Keep the state updates after all the async functions are done! + // For some reason, updating them first causes them to not update the state + setIsSubmitted(true); + setMessage(""); } }; @@ -58,6 +64,23 @@ function Chat({ firebase, newChat, updateNewChat }) {
+ { isSubmitted && ( +
+
Thanks for your feedback, we have recieved your message, our team will reach out to you shortely!
+
+ )}
{ - const [isOpen, setOpen] = useState(false) + const [isOpen, setOpen] = useState(props?.defaultState ?? true); const contentRef = useRef(null) const toogle = () => { setOpen(!isOpen) } return ( @@ -43,10 +47,10 @@ const Collapsible = (props: PropTypes) => {
-
{props.children}
+
{props.children}
-
- ) -} +
+ ); +}; export default Collapsible; diff --git a/src/components/dashboard/AddProvider.tsx b/src/components/dashboard/AddProvider.tsx index 3727390..f882eb6 100644 --- a/src/components/dashboard/AddProvider.tsx +++ b/src/components/dashboard/AddProvider.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from "react"; import Steps, { Step } from "rc-steps"; import "rc-steps/assets/index.css"; import "rc-steps/assets/iconfont.css"; -import 'bootstrap/dist/css/bootstrap.min.css'; +import "bootstrap/dist/css/bootstrap.min.css"; import Form from "react-bootstrap/Form"; import Row from "react-bootstrap/Row"; import Col from "react-bootstrap/Col"; @@ -25,6 +25,7 @@ import promiseWithTimeout from "../../functions/promiseWithTimeout"; import { GOOGLE_API_KEY } from "../../config/keys"; import { storage } from "../../store"; import { Store } from "reducers/types"; +import { ICalendarEvent } from "./calender-component/CalendarForm"; const { v4: uuidv4 } = require("uuid"); let steps = [ @@ -48,73 +49,13 @@ function AddProvider(props) { const [descriptions, setDescriptions] = useState(null); const [single, setSingle] = useState(null); const [error, setError] = useState(""); - const [content, setContent] = useState('ex. "Changing lives one bit at a time..."'); + const [content, setContent] = useState( + 'ex. "Changing lives one bit at a time..."' + ); const handleUpdate = (updatedContent: string) => { setContent(updatedContent); }; - // const eventInfo2 = { - // title: "Introducing APFF", - // description: - // "Atlanta Professional Fire Foundation supports the firefighters of Atlanta and their families when they need assistance. Due to a growing number of hazards, our brothers & sisters are at greater risk than ever before while protecting the citizens of Atlanta. APFF provides assistance for Illness, Injury, PTSD, Line of Duty Death and Bereavement. APFF also funds Tuition Reimbursement, Tools & Equipment Purchases, Training Opportunities, Living Condition Improvements, Affordable Housing and Fellowship Events.", - // highlight: "Our Foundation is run by Firefighters, for Firefighters!" - // }; - - // async function fetchData() { - // const collections = props.firestore.collection('categories'); - // const f = await collections - // .where('team', '==', props.team.name) - // .where('active', '==', true) - // .where('select_type', '==', 2) - // .get() - // .then((querySnapshot) => { - // const idToData = {}; - // querySnapshot.forEach((doc) => { - // const data = doc.data(); - // idToData[doc.id] = { - // name: data.name, - // options: data.options, - // }; - // }); - // return idToData; - // }); - // const d = await collections - // .where('team', '==', props.team.name) - // .where('active', '==', true) - // .where('select_type', '==', 0) - // .get() - // .then((querySnapshot) => { - // const idToData = {}; - // querySnapshot.forEach((doc) => { - // const data = doc.data(); - // idToData[doc.id] = { - // name: data.name, - // options: data.options, - // }; - // }); - // return idToData; - // }); - // const c = await collections - // .where('team', '==', props.team.name) - // .where('active', '==', true) - // .where('select_type', '==', 1) - // .get() - // .then((querySnapshot) => { - // const idToData = {}; - // querySnapshot.forEach((doc) => { - // const data = doc.data(); - // idToData[doc.id] = { - // name: data.name, - // options: data.options, - // }; - // }); - // return idToData; - // }); - // setFilters(f); - // setDescriptions(d); - // setCategories(c); - // } - useEffect(() => { async function fetchData() { const collections = props.firestore.collection("categories"); @@ -180,36 +121,89 @@ function AddProvider(props) { delIndex !== -1 && steps.splice(delIndex, 1); } + if (filters && Object.keys(filters).length) { + const delIndex = steps.indexOf("Tag"); + delIndex == -1 && steps.push("Tag"); + } + if (descriptions && !Object.keys(descriptions).length) { const delIndex = steps.indexOf("Text"); delIndex !== -1 && steps.splice(delIndex, 1); } + if (descriptions && Object.keys(descriptions).length) { + const delIndex = steps.indexOf("Text"); + delIndex == -1 && steps.push("Text"); + } + if (single && !Object.keys(single).length) { const delIndex = steps.indexOf("Toggle"); delIndex !== -1 && steps.splice(delIndex, 1); } - } + if (single && Object.keys(single).length) { + const delIndex = steps.indexOf("Toggle"); + delIndex == -1 && steps.push("Toggle"); + } + } updateSteps(); }, [filters, descriptions, single]); - // function updateSteps() { - // if (filters && !Object.keys(filters).length) { - // const delIndex = steps.indexOf("Tag"); - // delIndex !== -1 && steps.splice(delIndex, 1); - // } - - // if (descriptions && !Object.keys(descriptions).length) { - // const delIndex = steps.indexOf("Text"); - // delIndex !== -1 && steps.splice(delIndex, 1); - // } + const validateComponents = (i) => { + if (!i.content) { + return true; + } + const components = (i.content?.sections ?? []).flatMap( + (section) => section.components + ); + return components.every((component) => { + return validateComponent(component); + }); + }; - // if (categories && !Object.keys(categories).length) { - // const delIndex = steps.indexOf("Toggle"); - // delIndex !== -1 && steps.splice(delIndex, 1); - // } - // } + const validateComponent = (component) => { + const { type, data } = component; + switch (type) { + case "Calendar": + return data.events.every((event: ICalendarEvent) => { + return ( + event.eventName.length > 0 && + event.fromDate.length > 0 && + event.toDate.length > 0 && + event.fromTime.length > 0 && + event.toTime.length > 0 + ) + }); + case "Chart": + switch (data.type) { + case "donut": + return !!(data.data.donutData?.length > 0); + case "progress": + return ( + data.data.current != null && + !isNaN(data.data.current) && + data.data.total != null && + !isNaN(data.data.total) + ); + case "line": + return !!(data.data.lineData?.length > 0); + default: + return true; + } + case "Gallery": + return data.slidesArray.every((slide) => { + return slide.title !== ""; + }); + case "Directory": + return data.items.every((item) => { + return item.name !== ""; + }); + case "Embed": + return data.embedLink !== ""; + default: + return true; + } + }; async function addFirestore() { setIsLoading(true); @@ -227,9 +221,9 @@ function AddProvider(props) { fetch( `https://maps.googleapis.com/maps/api/geocode/json?address=${i.address[0].replace( /\s/g, - "%20", - )}&key=${GOOGLE_API_KEY}`, - ), + "%20" + )}&key=${GOOGLE_API_KEY}` + ) ); const responseJson = await response.json(); if ( @@ -241,7 +235,7 @@ function AddProvider(props) { } if (!i.imageURL) { const res = await fetch( - `https://maps.googleapis.com/maps/api/streetview?size=500x500&location=${i.latitude},${i.longitude}&fov=80&heading=70&pitch=0&key=${GOOGLE_API_KEY}`, + `https://maps.googleapis.com/maps/api/streetview?size=500x500&location=${i.latitude},${i.longitude}&fov=80&heading=70&pitch=0&key=${GOOGLE_API_KEY}` ); const blob = await res.blob(); const filename = i.facilityName + ".jpeg"; @@ -257,15 +251,15 @@ function AddProvider(props) { } await promiseWithTimeout( 5000, - props.firestore.set( - { collection: "providers", doc: i.facilityName }, - i, - ), + props.firestore.set({ + collection: "providers", + doc: i.id + }, i) ); props.history.push(providerRoute); } catch (e) { setError( - "Failed to save changes. Please check your network connection or try again later.", + "Failed to save changes. Please check your network connection or try again later." ); } finally { setIsLoading(false); @@ -284,8 +278,8 @@ function AddProvider(props) { const response = await fetch( `https://maps.googleapis.com/maps/api/geocode/json?address=${i.address[0].replace( /\s/g, - "%20", - )}&key=${GOOGLE_API_KEY}`, + "%20" + )}&key=${GOOGLE_API_KEY}` ); const responseJson = await response.json(); if ( @@ -297,7 +291,7 @@ function AddProvider(props) { } if (!i.imageURL) { const res = await fetch( - `https://maps.googleapis.com/maps/api/streetview?size=500x500&location=${i.latitude},${i.longitude}&fov=80&heading=70&pitch=0&key=${GOOGLE_API_KEY}`, + `https://maps.googleapis.com/maps/api/streetview?size=500x500&location=${i.latitude},${i.longitude}&fov=80&heading=70&pitch=0&key=${GOOGLE_API_KEY}` ); const blob = await res.blob(); const filename = i.facilityName + ".jpeg"; @@ -319,7 +313,7 @@ function AddProvider(props) { querySnapshot.forEach((doc) => { firestore.update( { collection: "providers", doc: doc.id }, - i, + i ); }); }); @@ -327,7 +321,7 @@ function AddProvider(props) { props.history.push(providerRoute); } catch (e) { setError( - "Failed to save changes. Please check your network connection or try again later.", + "Failed to save changes. Please check your network connection or try again later." ); } finally { setIsLoading(false); @@ -388,13 +382,13 @@ function AddProvider(props) { disabled={!completed} onClick={ props.selected && - props.selected.facilityName + props.selected.facilityName ? updateFirestore : addFirestore } > {props.selected && - props.selected.facilityName + props.selected.facilityName ? "Edit" : "Add"}{" "} Provider @@ -411,7 +405,12 @@ function AddProvider(props) { )} - +
@@ -434,10 +433,10 @@ function AddProvider(props) { - {components.map((v, i) => + {components.map((v, i) => ( - {v} + + {switchRender(v.type, v.data, i)} + - )} - + ))} + + }} + id="dropdown-basic" + > + Add Filter - - setComponents([...components, - - - - ])}>Chart - - setComponents([...components, - - - - ])}>Gallery - - setComponents([...components, - - - - ])}>Directory - - setComponents([...components, - - - - ])}>Embed - - setComponents([...components, - - - - ])}>Calendar + addComponent("Calendar")} + > + Calendar + + addComponent("Chart")} + > + Chart + + addComponent("Directory")} + > + Directory + + addComponent("Embed")}> + Embed + + addComponent("Gallery")}> + Gallery + + addComponent("Text")}> + Text + @@ -426,6 +581,7 @@ const ContentForm = ({ content, onChange }) => { const updateSections = (newSections) => { setSections(newSections); + console.log("Sections:", newSections); onChange(newSections); }; @@ -489,7 +645,7 @@ const ContentForm = ({ content, onChange }) => { style={{ backgroundColor: "#E3E9F5", padding: "16px 16px 28px 16px", - width: "calc(100% - 264px)" + width: "calc(100% - 264px)", }} > {selectedSection === null ? null : ( diff --git a/src/components/dashboard/Directory.tsx b/src/components/dashboard/Directory.tsx index 5dbc7dd..cddb04e 100644 --- a/src/components/dashboard/Directory.tsx +++ b/src/components/dashboard/Directory.tsx @@ -1,23 +1,53 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useState, useEffect } from "react"; -import { DirectoryItem } from "./DirectoryForm"; - -import styles from "./Directory.module.css"; +interface DirectoryItem { + name: string; + image: string; + description: string; + details: string; +} const DirectoryCard = ({ directoryItem }: { directoryItem: DirectoryItem }) => { + const [isMobile, setIsMobile] = useState(window.innerWidth < 768); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + return ( -
- {directoryItem.name} -
+
+ +
{directoryItem.name} @@ -25,8 +55,9 @@ const DirectoryCard = ({ directoryItem }: { directoryItem: DirectoryItem }) => {
{directoryItem.description} @@ -34,33 +65,55 @@ const DirectoryCard = ({ directoryItem }: { directoryItem: DirectoryItem }) => {
{directoryItem.details}
+ {directoryItem.name}
); }; const Directory = ({ directoryItems }: { directoryItems: DirectoryItem[] }) => { const isTwoColumn = directoryItems.length > 3; + const [isMobile, setIsMobile] = useState(window.innerWidth < 768); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + return (
- {directoryItems.map((directoryItem, i) => { - return ( - - ); - })} + {directoryItems.map((directoryItem, i) => ( + + ))}
); }; diff --git a/src/components/dashboard/DirectoryForm.module.css b/src/components/dashboard/DirectoryForm.module.css index e7b60e5..c28e03b 100644 --- a/src/components/dashboard/DirectoryForm.module.css +++ b/src/components/dashboard/DirectoryForm.module.css @@ -1,12 +1,10 @@ .directory::-webkit-scrollbar { width: 5px; } - .directory::-webkit-scrollbar-track { background-color: transparent; } .directory::-webkit-scrollbar-thumb { - /* border: 15px solid transparent; */ border-radius: 4px; background-color: #a9a9a9; } @@ -16,10 +14,9 @@ .addButton { border: 1px solid #226dff; - background: var(--drop-down-fill, #1346aa0f); + background: transparent; color: #226dff; font-size: 16px; - margin-top: 16px; width: 100px; height: 40px; font-weight: 600; diff --git a/src/components/dashboard/DirectoryForm.tsx b/src/components/dashboard/DirectoryForm.tsx index 1b420aa..12ce89e 100644 --- a/src/components/dashboard/DirectoryForm.tsx +++ b/src/components/dashboard/DirectoryForm.tsx @@ -48,6 +48,7 @@ const DirectoryFormItem = ({ .child(filename) .getDownloadURL() .then((url) => { + console.log(url); newItem = { ...newItem, image: url }; updateItem(newItem); }); @@ -56,7 +57,7 @@ const DirectoryFormItem = ({ return (
@@ -121,9 +122,11 @@ const DirectoryFormItem = ({ fontWeight: "500", }} onClick={() => { - setDirectoryItems((directoryItems) => - directoryItems.filter((_, i) => i !== index) + const filterItems = directoryItems.filter( + (_, i) => i !== index ); + // console.log(filterItems); + setDirectoryItems(filterItems); }} > Delete @@ -132,8 +135,19 @@ const DirectoryFormItem = ({ ); }; -const DirectoryForm = ({ items }) => { - const [isOpen, setIsOpen] = useState(false); +interface DirectoryState { + items: DirectoryItem[]; +} + +const DirectoryForm = ({ + directoryState, + setDirectoryState, + deleteComponent, +}: { + directoryState: DirectoryState; + setDirectoryState: (newState: DirectoryState) => void; + deleteComponent: () => void; +}) => { const defaultDirectoryItem: DirectoryItem = { name: "", description: "", @@ -141,7 +155,16 @@ const DirectoryForm = ({ items }) => { image: "", }; //might have to lift this state later to avoid one shared state - const [directoryItems, setDirectoryItems] = useState(items); + // const [directoryItems, setDirectoryItems] = useState( + // data.items + // ); + const { items: directoryItems } = directoryState; + + const setDirectoryItems = (newItems) => { + // console.log(newItems); + setDirectoryState({ ...directoryState, items: newItems }); + }; + /* directoryItem { @@ -156,7 +179,6 @@ const DirectoryForm = ({ items }) => { style={{ width: "100%", height: "100%", - padding: "16px", display: "flex", flexDirection: "column", }} @@ -164,34 +186,54 @@ const DirectoryForm = ({ items }) => {
{/* {directoryItems.length} */} - {directoryItems.map((directoryItem, index) => ( - // console.log() - - ))} - + + +
); diff --git a/src/components/dashboard/EventInfoComponent.css b/src/components/dashboard/EventInfoComponent.css new file mode 100644 index 0000000..7a3c319 --- /dev/null +++ b/src/components/dashboard/EventInfoComponent.css @@ -0,0 +1,17 @@ +.event-info-container { + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + font-family: Arial, sans-serif; + max-width: 600px; + margin: 0 auto; + } + + .event-description { + font-family: Arial, sans-serif; + font-size: 16px; + line-height: 1.6; + margin-bottom: 12px; + } + + \ No newline at end of file diff --git a/src/components/dashboard/EventInfoComponent.tsx b/src/components/dashboard/EventInfoComponent.tsx new file mode 100644 index 0000000..a0d4744 --- /dev/null +++ b/src/components/dashboard/EventInfoComponent.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import './EventInfoComponent.css'; + +interface EventInfoComponentProps { + description: string; +} + +const EventInfoComponent: React.FC = ({ description }) => { + return ( +

+ ); +}; + +export default EventInfoComponent; diff --git a/src/components/dashboard/GeneralInfo.tsx b/src/components/dashboard/GeneralInfo.tsx new file mode 100644 index 0000000..2a42133 --- /dev/null +++ b/src/components/dashboard/GeneralInfo.tsx @@ -0,0 +1,228 @@ +import React, { useEffect, useState } from "react"; +import { Col, Row } from "react-bootstrap"; +import { FaRegClock } from "react-icons/fa"; +import { IoPhonePortraitOutline } from "react-icons/io5"; +import { SlGlobe } from "react-icons/sl"; +import { TbPlaystationCircle } from "react-icons/tb"; + +export default function GeneralInfo(props) { + const [isMobile, setIsMobile] = useState(window.innerWidth < 768); + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + const infoStyle = { + display: "flex", + alignItems: "center", + marginBottom: "10px", + }; + + const iconStyle = { + marginRight: "20px", + verticalAlign: "middle", + color: "#226DFF", + }; + + function calculateHours(props) { + const rows = []; + const startandFinish = [0]; // In pairs, keep track of the starting and ending days with same time + const days = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ]; + const abbrevDays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + for (let i = 1; i < 7; i++) { + // not both undefined + if (props.item.hours[days[i]] !== props.item.hours[days[i - 1]]) { + if ( + !props.item.hours[days[i]] || + !props.item.hours[days[i - 1]] || + props.item.hours[days[i]][0] !== + props.item.hours[days[i - 1]][0] || + props.item.hours[days[i]][1] !== + props.item.hours[days[i - 1]][1] + ) { + startandFinish.push(i - 1); + startandFinish.push(i); + } + } + if (i === 6) { + startandFinish.push(6); + } + } + for (let i = 0; i < startandFinish.length; i += 2) { + const children = []; + if (startandFinish[i] === startandFinish[i + 1]) { + children.push( + + {days[startandFinish[i]]} + , + ); + } else { + const subchild = [ +

+ {abbrevDays[startandFinish[i]]} -{" "} + {abbrevDays[startandFinish[i + 1]]} +
, + ]; + children.push( + + {subchild} + , + ); + } + children.push( + + {props.item.hours[days[startandFinish[i]]] + ? props.item.hours[days[startandFinish[i]]].map( + (time, index) => + formatTime( + props.item.hours[days[startandFinish[i]]], + time, + index, + ), + ) + : "CLOSED"} + , + ); + rows.push({children}); + } + return rows; + } + + function formatTime(arr, time, index) { + if (time == null) { + if (index !== arr.length - 1) { + return
CLOSED -
; + } + return
CLOSED
; + } + const seconds = time; + let hours = Math.floor(seconds / 3600); + let mins: string = ((seconds / 60) % 60).toString(); + const endtime_ending = hours < 12 ? "AM" : "PM"; + hours %= 12; + if (hours === 0) { + hours = 12; + } + if (parseInt(mins) < 10) { + mins = `0${mins}`; + } + const timeformat = `${hours}:${mins}${endtime_ending}`; + if (index !== arr.length - 1) { + return
{timeformat} -
; + } + return
{timeformat}
; + } + + return ( + <> +
+ {props.item.description !== undefined && ( +

+ {props.item.description} +

+ )} +
+
+ +
+ {" "} + {props.item.phoneNum && + props.item.phoneNum.join(", ")} +
+
+
+ {props.item.website && props.item.website[0] && ( + <> + {props.item.website[0].startsWith("http") ? (<> + + + ) : (<> + + )} + + + )} +
+
+ +
+ {" "} + {props.item.address + .toString() + .split(",") + .map((value, index) => { + if (index === 0) { + return ( +
+ {value}, +
+ ); + } + if ( + index === + props.item.address.toString().split(",") + .length - + 1 + ) { + return ( +
+ {value} +
+ ); + } + if (index === 1) { + return ( +
{`${value},`}
+ ); + } + return `${value},`; + })} +
+
+
+ +
+ {props.item.hours && calculateHours(props)} +
+
+ + ); +} diff --git a/src/components/dashboard/ProviderGallery.tsx b/src/components/dashboard/ProviderGallery.tsx index a444f20..af661fe 100644 --- a/src/components/dashboard/ProviderGallery.tsx +++ b/src/components/dashboard/ProviderGallery.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, Provider } from "react"; import ProviderGallerySlide from "./ProviderGallerySlide"; import { storage } from "../../store"; +import { Button } from "react-bootstrap"; interface GallerySlide { title: string; @@ -8,12 +9,24 @@ interface GallerySlide { imgLink: string; } +interface GalleryState { + slidesArray: GallerySlide[]; +} + export default function ProviderGallery({ - slidesArray = [], + galleryState, + setGalleryState, + deleteComponent, }: { - slidesArray?: GallerySlide[]; + galleryState: GalleryState; + setGalleryState: (newState: GalleryState) => void; + deleteComponent: () => void; }) { - const [slides, setSlides] = useState(slidesArray); + const { slidesArray: slides } = galleryState; + + const setSlides = (newSlides) => { + setGalleryState({ ...galleryState, slidesArray: newSlides }); + }; const defaultSlide: GallerySlide = { title: "", @@ -21,12 +34,6 @@ export default function ProviderGallery({ imgLink: "", }; - useEffect(() => { - if (!slides || slides.length === 0) { - setSlides([{ ...defaultSlide }]); - } - }, [slides]); - const handleSlideDataChange = ( index: number, field: keyof GallerySlide, @@ -39,10 +46,8 @@ export default function ProviderGallery({ }; const handleDelete = (index: number) => { - if (slides.length > 1) { - const newSlides = slides.filter((_, i) => i !== index); - setSlides(newSlides); - } + const newSlides = slides.filter((_, i) => i !== index); + setSlides(newSlides); }; const handleAdd = (index: number) => { @@ -85,12 +90,46 @@ export default function ProviderGallery({ }; return ( -
+
{renderSlides()} - {/*TO BE DELETED */} -
-

Current Data:

-
{JSON.stringify(slides, null, 2)}
+
+ +
); diff --git a/src/components/dashboard/ProviderGalleryCarousel.tsx b/src/components/dashboard/ProviderGalleryCarousel.tsx index dd0f1f5..8647fd4 100644 --- a/src/components/dashboard/ProviderGalleryCarousel.tsx +++ b/src/components/dashboard/ProviderGalleryCarousel.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; interface GallerySlide { title: string; @@ -12,6 +12,17 @@ export default function ProviderGalleryCarousel({ slidesArray: GallerySlide[]; }) { const [currentIndex, setCurrentIndex] = useState(0); + const [isMobile, setIsMobile] = useState(window.innerWidth < 768); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); const goToPrevious = () => { setCurrentIndex((prevIndex) => @@ -28,154 +39,166 @@ export default function ProviderGalleryCarousel({ const isActive = (index: number) => currentIndex === index; return ( -
-
- - - {/* Card */} + <> + { + slidesArray.length > 0 &&
- {/*Left-side */}
- {/* Card Title */} + +
-

- {slidesArray[currentIndex].title} -

-
+
+

+ {slidesArray[currentIndex].title} +

+
- {/* Card Description */} -
-

+

+ {slidesArray[currentIndex].description} +

+
+
+ +
- {slidesArray[currentIndex].description} -

+ {slidesArray[currentIndex].title} +
+
- {/*Right-side*/} +
- {slidesArray[currentIndex].title} + {slidesArray.map((_, index) => ( +
+ ))}
- -
- - {/* bubbles */} -
- {slidesArray.map((_, index) => ( -
- ))} -
-
+ } + ); } diff --git a/src/components/dashboard/ProviderGallerySlide.tsx b/src/components/dashboard/ProviderGallerySlide.tsx index a79440a..2f77985 100644 --- a/src/components/dashboard/ProviderGallerySlide.tsx +++ b/src/components/dashboard/ProviderGallerySlide.tsx @@ -64,13 +64,18 @@ export default function ProviderGallerySlide({
-
+
-
); diff --git a/src/components/dashboard/RowForm.tsx b/src/components/dashboard/RowForm.tsx index 0c024c0..bac2008 100644 --- a/src/components/dashboard/RowForm.tsx +++ b/src/components/dashboard/RowForm.tsx @@ -15,7 +15,6 @@ import GoogleSuggest from "./GoogleSuggest"; import ImageModal from "./ImageModal"; import Modal from "react-bootstrap/Modal"; import Button from "react-bootstrap/Button"; - import ActionForm from "./ActionForm"; import ContentForm from "./ContentForm"; @@ -65,6 +64,7 @@ const RowForm = (props) => { address: [], description: "", buildingNum: [], + stationNum: "", childcare: [false], epic: [false], hours: {}, @@ -381,6 +381,17 @@ const RowForm = (props) => { as="textarea" /> + + Station # + + ); @@ -391,6 +402,7 @@ const RowForm = (props) => { onChange={onTimeChange} /> ); + case "Tag": return ( <> @@ -418,6 +430,7 @@ const RowForm = (props) => { )} ); + case "Text": return ( <> @@ -445,6 +458,7 @@ const RowForm = (props) => { )} ); + case "Toggle": return ( <> @@ -471,6 +485,7 @@ const RowForm = (props) => { )} ); + case "Actions": return ( { onChange={onActionTableChange} /> ); + case "Content": return ( void; + options: { label: string; value: string; icon?: React.ReactNode }[]; + defaultValue: string; + labelType?: "icon" | "text"; +} + +const CustomDropdown: React.FC = ({ onChange, options, defaultValue, labelType = "text" }) => { + const [isOpen, setIsOpen] = useState(false); + const [selectedOption, setSelectedOption] = useState( + options.find((option) => option.value === defaultValue) || options[0] + ); + + const handleOptionClick = (option: { label: string; value: string }) => { + setSelectedOption(option); + onChange(option.value); + setIsOpen(false); + }; + + return ( +
+
setIsOpen(!isOpen)} + style={{ + display: "flex", + alignItems: "center", + cursor: "pointer", + padding: "4px 8px", + color: "black", + fontSize: "14px", + }} + > + {labelType === "icon" && selectedOption.icon ? ( + <> + {selectedOption.icon} + + + + + ) : ( + <> + {selectedOption.label} + + + + + )} +
+ {isOpen && ( +
+ {options.map((option) => ( + + ))} +
+ )} +
+ ); +}; + +export default CustomDropdown; diff --git a/src/components/dashboard/Icons.tsx b/src/components/dashboard/TextComponent/Icons.tsx similarity index 52% rename from src/components/dashboard/Icons.tsx rename to src/components/dashboard/TextComponent/Icons.tsx index ef1b5b1..09ff442 100644 --- a/src/components/dashboard/Icons.tsx +++ b/src/components/dashboard/TextComponent/Icons.tsx @@ -148,3 +148,106 @@ export const X = ({ size = 16, color = "currentColor" }) => ( ); + +export const DropdownArrow = () => ( + + + +); + +export const TextIcon = () => ( + + + +); + +export const AlignLeftIcon = () => ( + + + +); + +export const AlignCenterIcon = () => ( + + + +); + +export const AlignRightIcon = () => ( + + + +); + +export const JustifyIcon = () => ( + + + +); + +export const BulletListIcon = ({ size = 16, color = "currentColor" }) => ( + + + + + + +); + +export const NumberedListIcon = ({ size = 16, color = "currentColor" }) => ( + + + + + 1 + 2 + +); + +export const ImageIcon = ({ size = 16, color = "currentColor" }) => ( + + {/* Frame of the image */} + + {/* Circle inside the image to represent an object */} + + {/* Line representing a mountain or shape */} + + +); + + + + + + + diff --git a/src/components/dashboard/TextComponent/LinkModal.tsx b/src/components/dashboard/TextComponent/LinkModal.tsx new file mode 100644 index 0000000..6d039c1 --- /dev/null +++ b/src/components/dashboard/TextComponent/LinkModal.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import ReactModal from "react-modal"; +import * as Icons from "../TextComponent/Icons"; +import { Modal } from "../TextComponent/Modal"; + + +interface IProps extends ReactModal.Props { + url: string; + closeModal: () => void; + onChangeUrl: (e: React.ChangeEvent) => void; + onSaveLink: (e: React.MouseEvent) => void; + onRemoveLink: (e: React.MouseEvent) => void; +} + +export function LinkModal(props: IProps) { + const { + url, + closeModal, + onChangeUrl, + onSaveLink, + onRemoveLink, + ...rest + } = props; + return ( + +

Edit link

+ + +
+ + +
+
+ ); +} diff --git a/src/components/dashboard/TextComponent/Modal.tsx b/src/components/dashboard/TextComponent/Modal.tsx new file mode 100644 index 0000000..0e5abe5 --- /dev/null +++ b/src/components/dashboard/TextComponent/Modal.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import ReactModal, { Props } from "react-modal"; + +const modalStyles = { + overlay: { + zIndex: 10000 + }, + content: { + top: "50%", + left: "50%", + right: "auto", + bottom: "auto", + width: 480, + marginRight: "-50%", + padding: 24, + transform: "translate(-50%, -50%)", + border: "2px solid var(--color-gray-4)", + borderRadius: "4px" + } +}; + +// ReactModal.setAppElement("#root"); + +export function Modal(props: Props) { + const { style, ...rest } = props; + + return ( + + {props.children} + + ); +} diff --git a/src/components/dashboard/TextComponent/SimpleEditor.tsx b/src/components/dashboard/TextComponent/SimpleEditor.tsx new file mode 100644 index 0000000..a9b6f63 --- /dev/null +++ b/src/components/dashboard/TextComponent/SimpleEditor.tsx @@ -0,0 +1,396 @@ +import React, { useCallback, useState } from "react"; +import classNames from "classnames"; +import "./styles.css"; +import { useEditor, EditorContent, Editor, BubbleMenu } from "@tiptap/react"; +import Document from "@tiptap/extension-document"; +import Paragraph from "@tiptap/extension-paragraph"; +import Text from "@tiptap/extension-text"; +import Link from "@tiptap/extension-link"; +import Bold from "@tiptap/extension-bold"; +import Underline from "@tiptap/extension-underline"; +import Italic from "@tiptap/extension-italic"; +import Strike from "@tiptap/extension-strike"; +import Code from "@tiptap/extension-code"; +import History from "@tiptap/extension-history"; +import Heading from "@tiptap/extension-heading"; +import TextAlign from "@tiptap/extension-text-align"; +import * as Icons from "./Icons"; +import { LinkModal } from "./LinkModal"; +import CustomDropdown from "./CustomDropdown"; +import { TextColor } from "./TextColor/TextColor"; +import ColorPickerButton from "./TextColor/ColorPickerButton"; +import BulletList from "@tiptap/extension-bullet-list"; +import ListItem from "@tiptap/extension-list-item"; +import OrderedList from "@tiptap/extension-ordered-list"; +import Image from "@tiptap/extension-image"; +import Blockquote from "@tiptap/extension-blockquote"; +import bqSVG from "../../../assets/svg/blockquote.svg" + +interface EditorState { + title: string; + description: string; +}; + +export function SimpleEditor( + { editorState, setEditorState, deleteComponent }: + { + editorState: EditorState; + setEditorState: (newState: EditorState) => void; + deleteComponent: () => void; + }) { + const CustomImage = Image.extend({ + addAttributes() { + return { + ...this.parent?.(), + class: { + default: "fixed-image", // Assign the CSS class here + }, + }; + }, + }); + + const editor = useEditor({ + extensions: [ + Document, + // History, + Paragraph, + Text, + Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }), + Link.configure({ openOnClick: false }), + Bold, + Underline, + Italic, + Strike, + Code, + TextAlign.configure({ types: ["heading", "paragraph"] }), + TextColor, + BulletList, + ListItem, + OrderedList, + CustomImage, + Blockquote + ], + content: editorState.description, + onUpdate: ({ editor }) => { + setEditorState({ + ...editorState, + description: editor.getHTML() + }); + } + }) as Editor; + + const [modalIsOpen, setIsOpen] = useState(false); + const [url, setUrl] = useState(""); + + const changeTextStyle = useCallback((value: string) => { + editor.chain().focus().setParagraph().run(); + if (value === "heading1") { + editor.chain().focus().toggleHeading({ level: 1 }).run(); + } else if (value === "heading2") { + editor.chain().focus().toggleHeading({ level: 2 }).run(); + } else if (value === "heading3") { + editor.chain().focus().toggleHeading({ level: 3 }).run(); + } + }, [editor]); + + const changeTextAlignment = useCallback((alignment: string) => { + editor.chain().focus().setTextAlign(alignment).run(); + }, [editor]); + + const openModal = useCallback(() => { + setUrl(editor.getAttributes("link").href); + setIsOpen(true); + }, [editor]); + + const closeModal = useCallback(() => { + setIsOpen(false); + setUrl(""); + }, []); + + const saveLink = useCallback(() => { + if (url) { + editor.chain().focus().extendMarkRange("link").setLink({ href: url, target: "_blank" }).run(); + } else { + editor.chain().focus().extendMarkRange("link").unsetLink().run(); + } + closeModal(); + }, [editor, url, closeModal]); + + const removeLink = useCallback(() => { + editor.chain().focus().extendMarkRange("link").unsetLink().run(); + closeModal(); + }, [editor, closeModal]); + + const toggleBold = useCallback(() => { + editor.chain().focus().toggleBold().run(); + }, [editor]); + + const toggleUnderline = useCallback(() => { + editor.chain().focus().toggleUnderline().run(); + }, [editor]); + + const toggleItalic = useCallback(() => { + editor.chain().focus().toggleItalic().run(); + }, [editor]); + + const toggleStrike = useCallback(() => { + editor.chain().focus().toggleStrike().run(); + }, [editor]); + + const toggleCode = useCallback(() => { + editor.chain().focus().toggleCode().run(); + }, [editor]); + + + const addImage = useCallback(() => { + const imageUrl: string | null = prompt("Enter the image URL"); + if (imageUrl) { + editor.chain().focus().setImage({ src: imageUrl }).run(); + } + }, [editor]); + + if (!editor) { + return null; + } + + + return ( +
+ editor.isActive("link")} + > + + + + +
+ +
+ {/* + + */} + + + + + + }, + { label: "Center", value: "center", icon: }, + { label: "Right", value: "right", icon: }, + { label: "Justify", value: "justify", icon: }, + ]} + defaultValue="left" + labelType="icon" + /> + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + setUrl(e.target.value)} + onSaveLink={saveLink} + onRemoveLink={removeLink} + /> + +
+ +
+
+ ); +} diff --git a/src/components/dashboard/TextComponent/TextColor/ColorPickerButton.tsx b/src/components/dashboard/TextComponent/TextColor/ColorPickerButton.tsx new file mode 100644 index 0000000..6cc7e7a --- /dev/null +++ b/src/components/dashboard/TextComponent/TextColor/ColorPickerButton.tsx @@ -0,0 +1,48 @@ +import React, { useState } from "react"; +import * as Icons from "../Icons"; +import { SketchPicker, ColorResult } from "react-color"; +import { Editor } from "@tiptap/react"; + +interface ColorPickerButtonProps { + editor: Editor; +} + +const ColorPickerButton: React.FC = ({ editor }) => { + const [isOpen, setIsOpen] = useState(false); + const [color, setColor] = useState("#000000"); + + const handleColorChange = (selectedColor: ColorResult) => { + setColor(selectedColor.hex); + editor.chain().focus().setColor(selectedColor.hex).run(); + }; + + return ( +
+ + + + + {isOpen && ( +
+ +
+ )} +
+ ); +}; + +export default ColorPickerButton; diff --git a/src/components/dashboard/TextComponent/TextColor/TextColor.tsx b/src/components/dashboard/TextComponent/TextColor/TextColor.tsx new file mode 100644 index 0000000..48f7300 --- /dev/null +++ b/src/components/dashboard/TextComponent/TextColor/TextColor.tsx @@ -0,0 +1,65 @@ +import { Mark, mergeAttributes } from "@tiptap/core"; + +declare module "@tiptap/core" { + interface Commands { + textColor: { + setColor: (color: string) => ReturnType; + unsetColor: () => ReturnType; + }; + } +} + +export const TextColor = Mark.create({ + name: "textColor", + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + addAttributes() { + return { + color: { + default: null, + parseHTML: (element) => element.style.color, + renderHTML: (attributes) => { + if (!attributes.color) { + return {}; + } + return { + style: `color: ${attributes.color}`, + }; + }, + }, + }; + }, + + parseHTML() { + return [ + { + tag: "span", + getAttrs: (element) => (element as HTMLElement).style.color && null, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["span", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + }, + + addCommands() { + return { + setColor: + (color: string) => + ({ chain }) => { + return chain().setMark(this.name, { color }).run(); + }, + unsetColor: + () => + ({ chain }) => { + return chain().unsetMark(this.name).run(); + }, + }; + }, +}); diff --git a/src/components/dashboard/TextComponent/styles.css b/src/components/dashboard/TextComponent/styles.css new file mode 100644 index 0000000..6ce6565 --- /dev/null +++ b/src/components/dashboard/TextComponent/styles.css @@ -0,0 +1,148 @@ +:root { + --color-pink: #ee415f; + --color-green: #58ce97; + --color-orange: #fc7d4a; + --color-red: #d14758; + --color-pale-pink: #ef6e85; + --color-coral: #f3907e; + --color-peach: #ecb38d; + --color-aqua: #a0ded0; + --color-pale-aqua: #c0ebe1; + --color-lightest-blue: #f8fcff; + --color-blue: #4c88e9; + --color-black: #242e39; + --color-gray-1: #3b424b; + --color-gray-2: #68707d; + --color-gray-3: #949fab; + --color-gray-4: #c7cdd4; + --color-gray-5: #edf1f6; + --color-gray-6: #f7f9fb; + --color-white: #fff; +} + +body { + color: var(--color-black); + font-size: 16px; + font-family: sans-serif; + line-height: 1.6; +} + +.menu { + position: relative; + top: 0; + left: 0; + width: 100%; + height: 40px; + display: flex; + align-items: center; + gap: 8px; + background-color: var(--color-white); + z-index: 1; + margin-bottom: 20px; +} + +.ProseMirror { + padding: 10px 8px 8px; + border: 1px solid var(--color-gray-4); + border-radius: 4px; +} + +.ProseMirror-focused { + border-color: var(--color-gray-4) !important; + outline: none; +} + +.ProseMirror ul, +.ProseMirror ol { + padding-left: 1.5em; + margin: 0; +} + +.ProseMirror li { + margin-left: 0; +} + +.inner-box { + border: 1px solid var(--color-gray-4); + padding-bottom: 10px; + padding-left: 12px; + padding-right: 12px; + border-radius: 4px; +} + +.button, +.button-save, +.button-remove, +.menu-button { + display: flex; + align-items: center; + justify-content: center; + height: 32px; + margin: 0; + padding: 0 8px; + border: 0; + background: transparent; + color: currentColor; +} + +.button:hover, +.button.is-active, +.menu-button:hover, +.menu-button.is-active { + background-color: var(--color-gray-5); +} + +.button:disabled, +.menu-button:disabled { + color: var(--color-gray-4); +} + +.button-save { + background-color: var(--color-green); + color: var(--color-white); +} + +.button-remove { + background-color: var(--color-red); + color: var(--color-white); +} + +.modal-close { + position: absolute; + top: 8px; + right: 8px; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 0; +} + +.text-style-dropdown::after { + content: "â–¾"; + font-size: 12px; + color: var(--color-gray-2); + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; +} + +.fixed-image { + width: 300px; + height: 200px; +} + +blockquote { + border-left: 4px solid #ddd; + margin-left: 0; + padding-left: 1em; + color: #555; + font-style: italic; +} + + + + diff --git a/src/components/dashboard/calender-component/CalendarEvent.tsx b/src/components/dashboard/calender-component/CalendarEvent.tsx index 9b448ee..cf2f6b7 100644 --- a/src/components/dashboard/calender-component/CalendarEvent.tsx +++ b/src/components/dashboard/calender-component/CalendarEvent.tsx @@ -12,6 +12,7 @@ export default function CalendarEvent({ handleDelete, handleAdd, handleAllDayUpdate, + deleteComponent, }) { const { eventName, @@ -474,41 +475,6 @@ export default function CalendarEvent({ Delete
- - -
- - {index == length - 1 && - - } -
); diff --git a/src/components/dashboard/calender-component/CalendarForm.tsx b/src/components/dashboard/calender-component/CalendarForm.tsx index 7a1ac23..5d4c167 100644 --- a/src/components/dashboard/calender-component/CalendarForm.tsx +++ b/src/components/dashboard/calender-component/CalendarForm.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; import CalendarEvent from "./CalendarEvent"; +import { Button } from "react-bootstrap"; export interface ICalendarEvent { eventName: string; @@ -35,8 +36,12 @@ export interface ICalendarData { export default function CalendarForm({ calendarData, + setCalendarData, + deleteComponent, }: { calendarData: ICalendarData; + setCalendarData: (newData: ICalendarData) => void; + deleteComponent: () => void; }) { const defaultEvent: ICalendarEvent = { eventName: "", @@ -57,11 +62,12 @@ export default function CalendarForm({ buttonText: "", }; - const [events, setEvents] = useState( - calendarData.events.length > 0 - ? calendarData.events - : [{ ...defaultEvent }] - ); + // const [events, setEvents] = useState( + // calendarData.events.length > 0 + // ? calendarData.events + // : [{ ...defaultEvent }] + // ); + const events = calendarData.events; const [displayNumber, setDisplayNumber] = useState( calendarData.displayNumber @@ -73,13 +79,21 @@ export default function CalendarForm({ value: string | number | boolean | string[], additionalUpdates: Partial = {} ) => { - setEvents((prevEvents) => { - return prevEvents.map((event, i) => + setCalendarData({ + ...calendarData, + events: events.map((event, i) => i === index ? { ...event, [field]: value, ...additionalUpdates } : event - ); - }); + ) + }) + // setEvents((prevEvents) => { + // return prevEvents.map((event, i) => + // i === index + // ? { ...event, [field]: value, ...additionalUpdates } + // : event + // ); + // }); }; const handleDisplayNumberChange = (value: number) => { @@ -87,8 +101,9 @@ export default function CalendarForm({ }; const handleAllDayUpdate = (index: number, isAllDay: boolean) => { - setEvents((prevEvents) => { - return prevEvents.map((event, i) => + setCalendarData({ + ...calendarData, + events: events.map((event, i) => i === index ? { ...event, @@ -97,18 +112,31 @@ export default function CalendarForm({ toTime: isAllDay ? "23:59" : "", } : event - ); - }); + ) + }) + // setEvents((prevEvents) => { + // return prevEvents.map((event, i) => + // i === index + // ? { + // ...event, + // isAllDay, + // fromTime: isAllDay ? "00:00" : "", + // toTime: isAllDay ? "23:59" : "", + // } + // : event + // ); + // }); }; const handleDelete = (index: number) => { - if (events.length > 1) { - setEvents((prevEvents) => prevEvents.filter((_, i) => i !== index)); - } + setCalendarData({ + ...calendarData, + events: events.filter((_, i) => i !== index) + }) }; const handleAdd = (index: number) => { - setEvents((prevEvents) => { + const newEventHandler = (prevEvents) => { const newEvents = [...prevEvents]; const newEvent = { ...defaultEvent }; @@ -118,7 +146,11 @@ export default function CalendarForm({ newEvents.splice(index + 1, 0, newEvent); } return newEvents; - }); + }; + setCalendarData({ + ...calendarData, + events: newEventHandler(events) + }) }; const renderEvents = () => { @@ -134,6 +166,7 @@ export default function CalendarForm({ handleAllDayUpdate={handleAllDayUpdate} handleDelete={handleDelete} handleAdd={handleAdd} + deleteComponent={deleteComponent} /> )); }; @@ -141,10 +174,41 @@ export default function CalendarForm({ return (
{renderEvents()} -
+
+ + +
+ {/*

Current Data:

{JSON.stringify({ displayNumber, events }, null, 2)}
-
+
*/}
); } diff --git a/src/components/dashboard/embed-component/EmbedComponent.tsx b/src/components/dashboard/embed-component/EmbedComponent.tsx index 7221680..5a02709 100644 --- a/src/components/dashboard/embed-component/EmbedComponent.tsx +++ b/src/components/dashboard/embed-component/EmbedComponent.tsx @@ -4,7 +4,7 @@ import ReactPlayer from 'react-player'; interface EventInfo { title: string, videoUrl: string; - thumbnail: string; + // thumbnail: string; } interface EmbedComponentProps { @@ -13,6 +13,7 @@ interface EmbedComponentProps { const EmbedComponent: React.FC = ({ eventInfo }) => { const [isPlaying, setIsPlaying] = useState(false); + const DEFAULT_THUMBNAIL = "https://joeferry.com/wp-content/plugins/video-thumbnails/default.jpg"; const handlePlayVideo = () => { setIsPlaying(true); @@ -22,7 +23,7 @@ const EmbedComponent: React.FC = ({ eventInfo }) => {
{!isPlaying ? (
- Video thumbnail + Video thumbnail
diff --git a/src/components/dashboard/embed-component/EmbedForm.tsx b/src/components/dashboard/embed-component/EmbedForm.tsx index a723e5d..b5d5ed1 100644 --- a/src/components/dashboard/embed-component/EmbedForm.tsx +++ b/src/components/dashboard/embed-component/EmbedForm.tsx @@ -1,99 +1,123 @@ -import React, { useState } from 'react'; -import InfoAlert from './InfoAlert'; +import React from "react"; +interface EmbedState { + embedLink: string; + title: string; +} -const EmbedForm: React.FC = () => { - const [embedLink, setEmbedLink] = useState(''); - const [title, setTitle] = useState(''); +const EmbedForm = ({ + embedState, + setEmbedState, + deleteComponent, +}: { + embedState: EmbedState; + setEmbedState: (newState: EmbedState) => void; + deleteComponent: () => void; +}) => { + const { embedLink, title } = embedState; - return ( -
-
- - setTitle(e.target.value)} - style={styles.input} - /> -
+ const setEmbedLink = (newEmbedLink) => { + setEmbedState({ ...embedState, embedLink: newEmbedLink }); + }; + const setTitle = (newTitle) => { + setEmbedState({ ...embedState, title: newTitle }); + }; -
- -