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"
|
import "./index.less"
|
||||||
|
|
||||||
export default (props) => {
|
const FeaturedEventAnnouncement = (props) => {
|
||||||
const { backgroundImg, backgroundStyle, logoImg, title, description } = props.data?.announcement ?? {}
|
const { announcement } = props.data
|
||||||
|
|
||||||
const onClickEvent = () => {
|
const onClickEvent = () => {
|
||||||
if (!props.data?._id) {
|
if (!props.data?._id) {
|
||||||
console.error("No event ID provided")
|
console.error("No event ID provided")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
app.location.push(`/featured-event/${props.data?._id}`)
|
app.location.push(`/event/${props.data?._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
if (!announcement) {
|
||||||
key={props.index}
|
return null
|
||||||
className="featuredEvent"
|
}
|
||||||
style={{
|
|
||||||
backgroundImage: `url(${backgroundImg})`,
|
|
||||||
...backgroundStyle
|
|
||||||
}}
|
|
||||||
onClick={onClickEvent}
|
|
||||||
>
|
|
||||||
<div className="featuredEvent_wrapper">
|
|
||||||
<div className="logo">
|
|
||||||
<img
|
|
||||||
src={logoImg}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="content">
|
return (
|
||||||
<h1>{title}</h1>
|
<div
|
||||||
<h3>{description}</h3>
|
key={props.index}
|
||||||
</div>
|
className="featured_event"
|
||||||
</div>
|
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">
|
<div className="featured_event-content">
|
||||||
<Icons.FiTarget /> <span>Featured event</span>
|
<h1>{announcement.title}</h1>
|
||||||
</div>
|
<h3>{announcement.description}</h3>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
<div className="featured_event-indicator">
|
||||||
|
<Icons.FiTarget /> <span>Featured event</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FeaturedEventAnnouncement
|
||||||
|
@ -1,103 +1,88 @@
|
|||||||
.featuredEvent {
|
.featured_event {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
align-items: center;
|
width: 100%;
|
||||||
//justify-content: flex-start;
|
min-height: 120px;
|
||||||
|
|
||||||
min-width: 300px;
|
border-radius: 12px;
|
||||||
max-width: 350px;
|
|
||||||
|
|
||||||
border-radius: 12px;
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
padding: 10px 20px;
|
overflow: hidden;
|
||||||
|
|
||||||
background-repeat: no-repeat;
|
cursor: pointer;
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
|
|
||||||
cursor: pointer;
|
.featured_event-logo {
|
||||||
|
height: 100%;
|
||||||
|
width: 155px;
|
||||||
|
|
||||||
.featuredEvent_wrapper {
|
padding: 20px;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
width: 100%;
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
align-items: center;
|
.featured_event-content {
|
||||||
justify-content: space-between;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.logo {
|
align-items: flex-start;
|
||||||
height: 100%;
|
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 {
|
color: inherit;
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
overflow: hidden;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-self: flex-end;
|
|
||||||
|
|
||||||
width: 100%;
|
text-overflow: ellipsis;
|
||||||
max-width: calc(90% - 70px);
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
overflow: hidden;
|
h3 {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
margin-left: 20px;
|
color: inherit;
|
||||||
margin-bottom: 10px;
|
overflow: hidden;
|
||||||
|
|
||||||
h1 {
|
text-overflow: ellipsis;
|
||||||
font-size: 1.5rem;
|
white-space: nowrap;
|
||||||
font-weight: 700;
|
word-break: break-all;
|
||||||
font-family: "Space Grotesk", sans-serif;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
color: inherit;
|
.featured_event-indicator {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 55;
|
||||||
|
|
||||||
overflow: hidden;
|
font-size: 0.6rem;
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
bottom: 0;
|
||||||
white-space: nowrap;
|
right: 0;
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
padding: 10px;
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color: inherit;
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 12px 0 0 0;
|
||||||
|
|
||||||
overflow: hidden;
|
svg,
|
||||||
|
span {
|
||||||
text-overflow: ellipsis;
|
color: inherit;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,47 +1,29 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
import Announcement from "../FeaturedEventAnnouncement"
|
import Announcement from "../FeaturedEventAnnouncement"
|
||||||
|
import EventsModel from "@models/events"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
export default React.memo((props) => {
|
const FeaturedEventsAnnouncements = React.memo((props) => {
|
||||||
const [featuredEvents, setFeaturedEvents] = React.useState([])
|
const [
|
||||||
|
L_FeaturedEvents,
|
||||||
|
R_FeaturedEvents,
|
||||||
|
E_FeaturedEvents,
|
||||||
|
M_FeaturedEvents,
|
||||||
|
] = app.cores.api.useRequest(EventsModel.getFeatured)
|
||||||
|
|
||||||
const fetchFeaturedEvents = React.useCallback(async () => {
|
if (!Array.isArray(R_FeaturedEvents)) {
|
||||||
let { data } = await app.cores.api.customRequest({
|
return null
|
||||||
url: "/featured_events",
|
}
|
||||||
method: "GET"
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
app.message.error(`Failed to fetch featured events`)
|
|
||||||
|
|
||||||
return {
|
return (
|
||||||
data: null
|
<div className="featuredEvents">
|
||||||
}
|
{R_FeaturedEvents.map((event, index) => (
|
||||||
})
|
<Announcement index={index} data={event} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
if (data) {
|
export default FeaturedEventsAnnouncements
|
||||||
// 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>
|
|
||||||
})
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
.featuredEvents {
|
.featuredEvents {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.featuredEvent {
|
width: 100%;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
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