mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
Upgrade featured event system
This commit is contained in:
parent
4a4e5d2bd5
commit
e596f7f8bb
@ -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 <div
|
||||
key={props.index}
|
||||
className="featuredEvent"
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImg})`,
|
||||
...backgroundStyle
|
||||
}}
|
||||
onClick={onClickEvent}
|
||||
>
|
||||
<div className="featuredEvent_wrapper">
|
||||
<div className="logo">
|
||||
<img
|
||||
src={logoImg}
|
||||
/>
|
||||
</div>
|
||||
if (!announcement) {
|
||||
return null
|
||||
}
|
||||
|
||||
<div className="content">
|
||||
<h1>{title}</h1>
|
||||
<h3>{description}</h3>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key={props.index}
|
||||
className="featured_event"
|
||||
style={{
|
||||
backgroundImage: `url(${announcement.backgroundImg})`,
|
||||
...announcement.backgroundStyle,
|
||||
}}
|
||||
onClick={onClickEvent}
|
||||
>
|
||||
<div className="featured_event-logo">
|
||||
<img
|
||||
src={announcement.logoImg}
|
||||
style={announcement.logoStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="indicator">
|
||||
<Icons.FiTarget /> <span>Featured event</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="featured_event-content">
|
||||
<h1>{announcement.title}</h1>
|
||||
<h3>{announcement.description}</h3>
|
||||
</div>
|
||||
|
||||
<div className="featured_event-indicator">
|
||||
<Icons.FiTarget /> <span>Featured event</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeaturedEventAnnouncement
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
svg,
|
||||
span {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<div className="featuredEvents">
|
||||
{R_FeaturedEvents.map((event, index) => (
|
||||
<Announcement index={index} data={event} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
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 <div className="featuredEvents">
|
||||
{featuredEvents.map((event, index) => <Announcement index={index} data={event} />)}
|
||||
</div>
|
||||
})
|
||||
export default FeaturedEventsAnnouncements
|
||||
|
@ -1,8 +1,6 @@
|
||||
.featuredEvents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.featuredEvent {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
width: 100%;
|
||||
}
|
||||
|
277
packages/app/src/pages/event/[id].jsx
Executable file
277
packages/app/src/pages/event/[id].jsx
Executable file
@ -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) => (
|
||||
<a
|
||||
key={key}
|
||||
href={result[1]}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{result[1]}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
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 (
|
||||
<div className="field">
|
||||
<div className="field-label">
|
||||
<Icons.FiClock />
|
||||
<p>
|
||||
{prefix} {label}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const EventStartDate = ({ startDate, started }) => (
|
||||
<Popover
|
||||
content={
|
||||
<EventCountdown
|
||||
date={startDate}
|
||||
prefix={started ? "Started" : "Starts"}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="field">
|
||||
<div className="field-label">
|
||||
<Icons.FiCalendar />
|
||||
{startDate.toLocaleString(DateTime.DATE_FULL)}
|
||||
</div>
|
||||
<div className="field-label">
|
||||
<Icons.FiClock />
|
||||
{startDate.toLocaleString(DateTime.TIME_SIMPLE)}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
|
||||
const EventHeader = ({ pageConfig, event, contrastColor }) => {
|
||||
if (pageConfig.header) {
|
||||
return (
|
||||
<div
|
||||
id="eventHeader"
|
||||
className="header custom"
|
||||
style={{
|
||||
...(pageConfig.header.style ?? {}),
|
||||
color: `var(--text-color-${contrastColor})`,
|
||||
}}
|
||||
>
|
||||
{pageConfig.header.displayLogo && (
|
||||
<div className="logo">
|
||||
<img src={pageConfig.header.logoImg} alt="Event Logo" />
|
||||
</div>
|
||||
)}
|
||||
{pageConfig.header.displayTitle && (
|
||||
<div className="title">
|
||||
<h1>{pageConfig.header.title}</h1>
|
||||
<h2>{pageConfig.header.description}</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="header"
|
||||
style={event.announcement.backgroundStyle}
|
||||
id="eventHeader"
|
||||
>
|
||||
{event.announcement.logoImg && (
|
||||
<div className="logo">
|
||||
<img src={event.announcement.logoImg} alt="Event Logo" />
|
||||
</div>
|
||||
)}
|
||||
<div className="title">
|
||||
<h1>{event.name}</h1>
|
||||
<h2>{event.announcement.description}</h2>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 <Skeleton active />
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="event">
|
||||
<EventHeader
|
||||
pageConfig={pageConfig}
|
||||
event={R_Event}
|
||||
contrastColor={contrastColor}
|
||||
/>
|
||||
|
||||
<div className="content">
|
||||
<div className="panel">
|
||||
<div className="card">
|
||||
{started && !ended && (
|
||||
<div className="field">
|
||||
<div className="field-label">
|
||||
<div className="pulse_circle" />
|
||||
<p>Started</p>
|
||||
</div>
|
||||
<div className="field-value">
|
||||
<EventCountdown
|
||||
date={endDate}
|
||||
prefix="Ends"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!started && (
|
||||
<EventStartDate
|
||||
startDate={startDate}
|
||||
started={started}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="field">
|
||||
<div className="field-label">
|
||||
<Icons.FiMapPin />
|
||||
{ProcessString(LocationProcessRegexs)(
|
||||
R_Event.location,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!eventStartedOrEnded && (
|
||||
<div className="card">
|
||||
<Button onClick={handleClickAddToCalendar}>
|
||||
<Icons.FiCalendar /> Add to Calendar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{started && pageConfig.livestreamId && (
|
||||
<div className="card">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleClickWatchLiveStream}
|
||||
>
|
||||
<Icons.FiPlay /> Watch Live
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="card">
|
||||
<div className="page-render">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
children={R_Event.page}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventPage
|
158
packages/app/src/pages/event/index.less
Executable file
158
packages/app/src/pages/event/index.less
Executable file
@ -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);
|
||||
}
|
||||
}
|
@ -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 <a key={key} href={result[1]} target="_blank" rel="noopener noreferrer">{result[1]}</a>
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
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 <div className="dates">
|
||||
<div className="startsAt">
|
||||
{
|
||||
dates[0]
|
||||
}
|
||||
</div>
|
||||
<span className="separator">
|
||||
to
|
||||
</span>
|
||||
<div className="endsAt">
|
||||
{
|
||||
dates[1]
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
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 <Skeleton active />
|
||||
}
|
||||
|
||||
return <div className="event">
|
||||
{
|
||||
eventData.customHeader &&
|
||||
<div className="header custom" style={eventData.customHeader.style} id="eventHeader">
|
||||
{
|
||||
eventData.customHeader.displayLogo && <div className="logo">
|
||||
<img src={eventData.announcement.logoImg} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
eventData.customHeader.displayTitle && <div className="title">
|
||||
<h1>{eventData.name}</h1>
|
||||
<h2>{eventData.announcement.description}</h2>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!eventData.customHeader && <div className="header" style={eventData.announcement.backgroundStyle} id="eventHeader">
|
||||
{eventData.announcement.logoImg &&
|
||||
<div className="logo">
|
||||
<img src={eventData.announcement.logoImg} />
|
||||
</div>
|
||||
}
|
||||
<div className="title">
|
||||
<h1>{eventData.name}</h1>
|
||||
<h2>{eventData.announcement.description}</h2>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="content">
|
||||
<div className="panel">
|
||||
<div className="card">
|
||||
<div className="dates">
|
||||
<Icons.FiCalendar /> {Array.isArray(eventData.dates) && renderDates(eventData.dates)}
|
||||
</div>
|
||||
|
||||
<div className="location">
|
||||
<Icons.FiMapPin /> {ProcessString(LocationProcessRegexs)(eventData.location)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<Button>
|
||||
<Icons.FiCalendar /> Add to Calendar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="card">
|
||||
<div className="description">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{eventData.description}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
41
packages/app/src/utils/createGoogleCalendarEvent/index.js
Normal file
41
packages/app/src/utils/createGoogleCalendarEvent/index.js
Normal file
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user