diff --git a/packages/app/src/components/FeaturedEventAnnouncement/index.jsx b/packages/app/src/components/FeaturedEventAnnouncement/index.jsx
index 32c766fc..005bc3ed 100755
--- a/packages/app/src/components/FeaturedEventAnnouncement/index.jsx
+++ b/packages/app/src/components/FeaturedEventAnnouncement/index.jsx
@@ -3,42 +3,49 @@ import { Icons } from "@components/Icons"
import "./index.less"
-export default (props) => {
- const { backgroundImg, backgroundStyle, logoImg, title, description } = props.data?.announcement ?? {}
+const FeaturedEventAnnouncement = (props) => {
+ const { announcement } = props.data
- const onClickEvent = () => {
- if (!props.data?._id) {
- console.error("No event ID provided")
- return false
- }
+ const onClickEvent = () => {
+ if (!props.data?._id) {
+ console.error("No event ID provided")
+ return false
+ }
- app.location.push(`/featured-event/${props.data?._id}`)
- }
+ app.location.push(`/event/${props.data?._id}`)
+ }
- return
-
-
-

-
+ if (!announcement) {
+ return null
+ }
-
-
{title}
- {description}
-
-
+ return (
+
+
+

+
-
- Featured event
-
-
-}
\ No newline at end of file
+
+
{announcement.title}
+ {announcement.description}
+
+
+
+ Featured event
+
+
+ )
+}
+
+export default FeaturedEventAnnouncement
diff --git a/packages/app/src/components/FeaturedEventAnnouncement/index.less b/packages/app/src/components/FeaturedEventAnnouncement/index.less
index 73d67efd..de59b445 100755
--- a/packages/app/src/components/FeaturedEventAnnouncement/index.less
+++ b/packages/app/src/components/FeaturedEventAnnouncement/index.less
@@ -1,103 +1,88 @@
-.featuredEvent {
- position: relative;
- z-index: 50;
+.featured_event {
+ position: relative;
+ z-index: 50;
- display: flex;
- flex-direction: row;
+ display: flex;
+ flex-direction: row;
- align-items: center;
- //justify-content: flex-start;
+ width: 100%;
+ min-height: 120px;
- min-width: 300px;
- max-width: 350px;
+ border-radius: 12px;
- border-radius: 12px;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
- padding: 10px 20px;
+ overflow: hidden;
- background-repeat: no-repeat;
- background-size: cover;
- background-position: center;
+ cursor: pointer;
- cursor: pointer;
+ .featured_event-logo {
+ height: 100%;
+ width: 155px;
- .featuredEvent_wrapper {
- display: flex;
- flex-direction: row;
+ padding: 20px;
- width: 100%;
- height: 100%;
+ img {
+ height: 100%;
+ width: 100%;
+ }
+ }
- align-items: center;
- justify-content: space-between;
+ .featured_event-content {
+ display: flex;
+ flex-direction: column;
- .logo {
- height: 100%;
+ align-items: flex-start;
+ justify-content: center;
- width: fit-content;
+ width: 100%;
+ height: 100%;
- max-width: 70px;
+ h1 {
+ font-size: 1.5rem;
+ font-weight: 700;
+ font-family: "Space Grotesk", sans-serif;
- img {
- height: 100%;
- width: 100%;
- }
- }
+ color: inherit;
- .content {
- display: flex;
- flex-direction: column;
- align-self: flex-end;
+ overflow: hidden;
- width: 100%;
- max-width: calc(90% - 70px);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
- overflow: hidden;
+ h3 {
+ font-size: 0.8rem;
+ font-weight: 400;
- margin-left: 20px;
- margin-bottom: 10px;
+ color: inherit;
+ overflow: hidden;
- h1 {
- font-size: 1.5rem;
- font-weight: 700;
- font-family: "Space Grotesk", sans-serif;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ word-break: break-all;
+ }
+ }
- color: inherit;
+ .featured_event-indicator {
+ position: absolute;
+ z-index: 55;
- overflow: hidden;
+ font-size: 0.6rem;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
+ bottom: 0;
+ right: 0;
- h3 {
- font-size: 0.8rem;
- font-weight: 400;
+ padding: 10px;
- color: inherit;
+ background-color: rgba(0, 0, 0, 0.5);
+ border-radius: 12px 0 0 0;
- overflow: hidden;
-
- text-overflow: ellipsis;
- white-space: nowrap;
- word-break: break-all;
- }
- }
- }
-
- .indicator {
- position: absolute;
- z-index: 55;
-
- font-size: 0.6rem;
-
- bottom: 0;
- right: 0;
-
- padding: 10px;
-
- svg, span {
- color: inherit;
- }
- }
-}
\ No newline at end of file
+ svg,
+ span {
+ color: inherit;
+ }
+ }
+}
diff --git a/packages/app/src/components/FeaturedEventsAnnouncements/index.jsx b/packages/app/src/components/FeaturedEventsAnnouncements/index.jsx
index dfcd3843..0b6bcd36 100755
--- a/packages/app/src/components/FeaturedEventsAnnouncements/index.jsx
+++ b/packages/app/src/components/FeaturedEventsAnnouncements/index.jsx
@@ -1,47 +1,29 @@
import React from "react"
import Announcement from "../FeaturedEventAnnouncement"
+import EventsModel from "@models/events"
import "./index.less"
-export default React.memo((props) => {
- const [featuredEvents, setFeaturedEvents] = React.useState([])
+const FeaturedEventsAnnouncements = React.memo((props) => {
+ const [
+ L_FeaturedEvents,
+ R_FeaturedEvents,
+ E_FeaturedEvents,
+ M_FeaturedEvents,
+ ] = app.cores.api.useRequest(EventsModel.getFeatured)
- const fetchFeaturedEvents = React.useCallback(async () => {
- let { data } = await app.cores.api.customRequest({
- url: "/featured_events",
- method: "GET"
- }).catch((err) => {
- console.error(err)
- app.message.error(`Failed to fetch featured events`)
+ if (!Array.isArray(R_FeaturedEvents)) {
+ return null
+ }
- return {
- data: null
- }
- })
+ return (
+
+ {R_FeaturedEvents.map((event, index) => (
+
+ ))}
+
+ )
+})
- if (data) {
- // parse announcement data
- data = data.map((item) => {
- try {
- item.announcement = JSON.parse(item.announcement)
- } catch (error) {
- console.error(error)
- app.message.error(`Failed to parse announcement data`)
- }
- return item
- })
-
- setFeaturedEvents(data)
- }
-
- }, [])
-
- React.useEffect(() => {
- fetchFeaturedEvents()
- }, [])
-
- return
- {featuredEvents.map((event, index) =>
)}
-
-})
\ No newline at end of file
+export default FeaturedEventsAnnouncements
diff --git a/packages/app/src/components/FeaturedEventsAnnouncements/index.less b/packages/app/src/components/FeaturedEventsAnnouncements/index.less
index 16273c9a..cad11bd1 100755
--- a/packages/app/src/components/FeaturedEventsAnnouncements/index.less
+++ b/packages/app/src/components/FeaturedEventsAnnouncements/index.less
@@ -1,8 +1,6 @@
.featuredEvents {
- display: flex;
- flex-direction: column;
+ display: flex;
+ flex-direction: column;
- .featuredEvent {
- margin-bottom: 20px;
- }
-}
\ No newline at end of file
+ width: 100%;
+}
diff --git a/packages/app/src/pages/event/[id].jsx b/packages/app/src/pages/event/[id].jsx
new file mode 100755
index 00000000..1bca629a
--- /dev/null
+++ b/packages/app/src/pages/event/[id].jsx
@@ -0,0 +1,277 @@
+import React, { useState, useEffect } from "react"
+import { Skeleton, Button, Tooltip, Popover, Tag } from "antd"
+import ReactMarkdown from "react-markdown"
+import remarkGfm from "remark-gfm"
+import rehypeRaw from "rehype-raw"
+import { DateTime } from "luxon"
+import createGoogleCalendarEvent from "@utils/createGoogleCalendarEvent"
+
+import EventsModel from "@models/events"
+import useCenteredContainer from "@hooks/useCenteredContainer"
+
+import { Icons } from "@components/Icons"
+
+import ContrastYIQ from "@utils/contrastYIQ"
+import ProcessString from "@utils/processString"
+
+import "./index.less"
+
+const LocationProcessRegexs = [
+ {
+ regex: /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi,
+ fn: (key, result) => (
+
+ {result[1]}
+
+ ),
+ },
+]
+
+const EventCountdown = ({ date, prefix }) => {
+ const [label, setLabel] = useState(null)
+
+ useEffect(() => {
+ function updateCountdown() {
+ const nowDate = DateTime.local()
+ const fromDate = DateTime.fromISO(date)
+ const diff = fromDate.diff(nowDate, "minutes").values
+ setLabel(nowDate.plus(diff).toRelative())
+ }
+
+ updateCountdown()
+ const interval = setInterval(updateCountdown, 1000)
+ return () => clearInterval(interval)
+ }, [date])
+
+ return (
+
+
+
+
+ {prefix} {label}
+
+
+
+ )
+}
+
+const EventStartDate = ({ startDate, started }) => (
+
+ }
+ >
+
+
+
+ {startDate.toLocaleString(DateTime.DATE_FULL)}
+
+
+
+ {startDate.toLocaleString(DateTime.TIME_SIMPLE)}
+
+
+
+)
+
+const EventHeader = ({ pageConfig, event, contrastColor }) => {
+ if (pageConfig.header) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+const EventPage = (props) => {
+ useCenteredContainer(false)
+
+ const [L_Event, R_Event, E_Event, M_Event] = app.cores.api.useRequest(
+ EventsModel.data,
+ props.params["id"],
+ )
+ const [contrastColor, setContrastColor] = useState(null)
+ const [started, setStarted] = useState(false)
+ const [ended, setEnded] = useState(false)
+
+ useEffect(() => {
+ if (!R_Event) return
+
+ // Calculate event status
+ const now = DateTime.local()
+ const eventStart = DateTime.fromISO(R_Event.startDate)
+ const eventEnd = DateTime.fromISO(R_Event.endDate)
+
+ const startDiff = eventStart.diff(now, "minutes").values
+ const endDiff = eventEnd.diff(now, "minutes").values
+
+ setStarted(startDiff.minutes < 0)
+ setEnded(endDiff.minutes < 0)
+
+ // Calculate contrast color for header
+ if (R_Event.pageConfig?.header?.style?.backgroundImage) {
+ const url = R_Event.pageConfig.header.style.backgroundImage
+ .replace("url(", "")
+ .replace(")", "")
+ .replace(/['"]/gi, "")
+
+ ContrastYIQ.fromUrl(url).then(setContrastColor)
+ }
+ }, [R_Event])
+
+ const handleClickWatchLiveStream = () => {
+ if (!R_Event?.pageConfig?.livestreamId) return
+ app.location.push(`/tv/live/${R_Event.pageConfig.livestreamId}`)
+ }
+
+ const handleClickAddToCalendar = () => {
+ createGoogleCalendarEvent({
+ title: R_Event.name,
+ startDate: new Date(R_Event.startDate),
+ endDate: new Date(R_Event.endDate),
+ description: `${R_Event.shortDescription} - See details at ${location.href}`,
+ location: R_Event.location,
+ })
+ }
+
+ if (E_Event) {
+ return null
+ }
+
+ if (L_Event) {
+ return
+ }
+
+ if (!R_Event) {
+ return null
+ }
+
+ const eventStartedOrEnded = started || ended
+ const startDate = DateTime.fromISO(R_Event.startDate)
+ const endDate = DateTime.fromISO(R_Event.endDate)
+ const { pageConfig } = R_Event
+
+ return (
+
+
+
+
+
+
+ {started && !ended && (
+
+ )}
+
+ {!started && (
+
+ )}
+
+
+
+
+ {ProcessString(LocationProcessRegexs)(
+ R_Event.location,
+ )}
+
+
+
+
+ {!eventStartedOrEnded && (
+
+
+
+ )}
+
+ {started && pageConfig.livestreamId && (
+
+
+
+ )}
+
+
+
+
+
+ )
+}
+
+export default EventPage
diff --git a/packages/app/src/pages/event/index.less b/packages/app/src/pages/event/index.less
new file mode 100755
index 00000000..91c53c00
--- /dev/null
+++ b/packages/app/src/pages/event/index.less
@@ -0,0 +1,158 @@
+.event {
+ display: flex;
+ flex-direction: column;
+
+ align-items: center;
+ justify-content: flex-end;
+
+ width: 100%;
+ gap: 20px;
+
+ .header {
+ display: flex;
+ flex-direction: row;
+
+ width: 100%;
+ height: 330px;
+
+ padding: 20px;
+
+ border-radius: 12px;
+
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+
+ .logo {
+ height: 100%;
+
+ width: fit-content;
+
+ max-width: 200px;
+
+ img {
+ height: 100%;
+ width: 100%;
+ }
+ }
+
+ .title {
+ display: flex;
+ flex-direction: column;
+
+ margin-left: 50px;
+
+ padding: 20px 0;
+
+ font-family: "Space Grotesk", sans-serif;
+
+ h1 {
+ font-size: 2rem;
+ font-weight: 700;
+ }
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ p,
+ span {
+ color: inherit;
+ }
+ }
+
+ .content {
+ display: grid;
+
+ width: 100%;
+
+ grid-template-columns: 30% 1fr;
+ grid-template-rows: 1fr;
+ grid-column-gap: 20px;
+ grid-row-gap: 20px;
+
+ .panel {
+ display: flex;
+ flex-direction: column;
+
+ height: fit-content;
+
+ gap: 20px;
+
+ .card {
+ display: flex;
+ flex-direction: column;
+
+ height: fit-content;
+
+ padding: 20px;
+
+ background-color: var(--background-color-accent);
+
+ border-radius: 12px;
+ }
+ }
+ }
+
+ .page-render {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ ul {
+ margin: 0 30px;
+ padding: 0;
+ }
+ }
+}
+
+.field {
+ display: inline-flex;
+ flex-direction: row;
+
+ align-items: center;
+ justify-content: space-between;
+
+ width: 100%;
+ gap: 10px;
+
+ .field-label,
+ .field-value {
+ display: flex;
+ flex-direction: row;
+
+ gap: 4px;
+
+ align-items: center;
+ }
+}
+
+.started_tag {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 6px;
+}
+
+.pulse_circle {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background-color: green;
+ animation: pulse 1s infinite;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.2);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
diff --git a/packages/app/src/pages/featured-event/[id].jsx b/packages/app/src/pages/featured-event/[id].jsx
deleted file mode 100755
index 283bc627..00000000
--- a/packages/app/src/pages/featured-event/[id].jsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import React from "react"
-import { Skeleton, Button } from "antd"
-import ReactMarkdown from "react-markdown"
-import remarkGfm from "remark-gfm"
-
-import { Icons } from "@components/Icons"
-
-import ContrastYIQ from "@utils/contrastYIQ"
-import ProcessString from "@utils/processString"
-
-import "./index.less"
-
-const LocationProcessRegexs = [
- {
- regex: /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi,
- fn: (key, result) => {
- return {result[1]}
- }
- }
-]
-
-export default (props) => {
- const eventId = props.params["id"]
-
- const [eventData, setEventData] = React.useState(null)
-
- const fetchEventData = async () => {
- const { data } = await app.cores.api.customRequest({
- method: "GET",
- url: `/featured_event/${eventId}`
- }).catch((err) => {
- console.error(err)
- app.message.error("Failed to fetch event data")
-
- return {
- data: null
- }
- })
-
- if (data) {
- try {
- if (data.announcement) {
- data.announcement = JSON.parse(data.announcement)
- }
-
- if (data.customHeader) {
- data.customHeader = JSON.parse(data.customHeader)
- }
-
- setEventData(data)
- } catch (error) {
- console.error(error)
- app.message.error("Failed to parse event data")
- }
- }
- }
-
- const renderDates = (dates) => {
- return
-
- {
- dates[0]
- }
-
-
- to
-
-
- {
- dates[1]
- }
-
-
- }
-
- console.log(eventData)
-
- React.useEffect(() => {
- fetchEventData()
- }, [])
-
- React.useEffect(() => {
- // get average color of custom background image
- if (eventData?.customHeader.style) {
- let backgroundImage = eventData.customHeader.style.backgroundImage
-
- if (backgroundImage) {
- backgroundImage = backgroundImage.replace("url(", "").replace(")", "").replace(/\"/gi, "").replace(/\"/gi, "").replace(/\'/gi, "")
-
- console.log(backgroundImage)
-
- ContrastYIQ.fromUrl(backgroundImage).then((contrastColor) => {
- console.log(`YIQ returns [${contrastColor}] as contrast color`)
- document.getElementById("eventHeader").style.color = `var(--text-color-${contrastColor})`
- })
- }
- }
- }, [eventData])
-
- if (!eventData) {
- return
- }
-
- return
- {
- eventData.customHeader &&
-
- }
-
- {
- !eventData.customHeader &&
- }
-
-
-
-
-
- {Array.isArray(eventData.dates) && renderDates(eventData.dates)}
-
-
-
- {ProcessString(LocationProcessRegexs)(eventData.location)}
-
-
-
-
-
-
-
-
-
-
-
-
- {eventData.description}
-
-
-
-
-
-
-}
\ No newline at end of file
diff --git a/packages/app/src/pages/featured-event/index.less b/packages/app/src/pages/featured-event/index.less
deleted file mode 100755
index 8146ada6..00000000
--- a/packages/app/src/pages/featured-event/index.less
+++ /dev/null
@@ -1,117 +0,0 @@
-.event {
- display: flex;
- flex-direction: column;
-
- align-items: center;
- justify-content: flex-end;
-
- color: var(--text-color);
-
- .header {
- display: flex;
- flex-direction: row;
-
- width: 100%;
-
- padding: 20px;
-
- margin-bottom: 20px;
-
- border-radius: 12px;
-
- &.custom {
- background-position: center;
- background-size: cover;
- background-repeat: no-repeat;
- }
-
- .logo {
- height: 100%;
-
- width: fit-content;
-
- max-width: 200px;
-
- img {
- height: 100%;
- width: 100%;
- }
- }
-
- .title {
- display: flex;
- flex-direction: column;
-
- margin-left: 50px;
-
- padding: 20px 0;
-
- font-family: "Space Grotesk", sans-serif;
-
- h1 {
- font-size: 2rem;
- font-weight: 700;
- }
- }
-
- h1,
- h2,
- h3,
- h4,
- h5,
- h6,
- p,
- span {
- color: inherit;
- }
- }
-
- .content {
- display: grid;
- grid-template-columns: 30% 1fr;
- grid-template-rows: 1fr;
- grid-column-gap: 20px;
- grid-row-gap: 0px;
-
- .panel {
- display: flex;
- flex-direction: column;
-
- height: fit-content;
-
- .card {
- display: flex;
- flex-direction: column;
-
- height: fit-content;
-
- padding: 20px;
-
- background-color: var(--background-color-accent);
-
- border-radius: 12px;
-
- margin-bottom: 20px;
- }
- }
- }
-
- .dates {
- display: flex;
- flex-direction: row;
-
- margin-bottom: 10px;
-
- .startsAt {
- display: flex;
-
- margin-right: 10px;
- }
-
- .endsAt {
- display: flex;
-
- margin-left: 10px;
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/src/utils/createGoogleCalendarEvent/index.js b/packages/app/src/utils/createGoogleCalendarEvent/index.js
new file mode 100644
index 00000000..e364b375
--- /dev/null
+++ b/packages/app/src/utils/createGoogleCalendarEvent/index.js
@@ -0,0 +1,41 @@
+export default (eventDetails) => {
+ // validate required parameters
+ if (
+ !eventDetails.title ||
+ !eventDetails.startDate ||
+ !eventDetails.endDate
+ ) {
+ throw new Error(
+ "Title, start date, and end date are required parameters",
+ )
+ }
+
+ // format dates for calendar URL
+ const formatDate = (date) => {
+ return date.toISOString().replace(/-|:|\.\d+/g, "")
+ }
+
+ const startTime = formatDate(eventDetails.startDate)
+ const endTime = formatDate(eventDetails.endDate)
+
+ // create calendar URL (Google Calendar format)
+ let calendarUrl =
+ "https://calendar.google.com/calendar/render?action=TEMPLATE"
+
+ // add event details to URL
+ calendarUrl += `&text=${encodeURIComponent(eventDetails.title)}`
+ calendarUrl += `&dates=${startTime}/${endTime}`
+
+ if (eventDetails.description) {
+ calendarUrl += `&details=${encodeURIComponent(eventDetails.description)}`
+ }
+
+ if (eventDetails.location) {
+ calendarUrl += `&location=${encodeURIComponent(eventDetails.location)}`
+ }
+
+ // open the calendar URL in a new tab
+ window.open(calendarUrl, "_blank")
+
+ return calendarUrl
+}