diff --git a/e2e/tests/organizers/organizer-public-page.spec.ts b/e2e/tests/organizers/organizer-public-page.spec.ts index 0428f96c39..fb38137f3f 100644 --- a/e2e/tests/organizers/organizer-public-page.spec.ts +++ b/e2e/tests/organizers/organizer-public-page.spec.ts @@ -33,13 +33,15 @@ test.describe('organizer public page', () => { }); const [next] = [...event.occurrences].sort((a, b) => a.start_date.localeCompare(b.start_date)); const nextDate = new Date(next.start_date); - const nextLabel = `${nextDate.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' })} ${nextDate.getUTCDate()}`; + const nextMonth = nextDate.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' }); + const nextWeekday = nextDate.toLocaleString('en-US', { weekday: 'short', timeZone: 'UTC' }); const publicPage = new OrganizerPublicPage(page); await publicPage.goto(organizer.id, organizer.slug); const card = publicPage.eventLink(event.title); - await expect(card.getByTestId('organizer-event-card-date')).toContainText(nextLabel); + await expect(card).toContainText(new RegExp(`${nextMonth}\\s*${nextDate.getUTCDate()}(?!\\d)`)); + await expect(card.getByTestId('organizer-event-card-date')).toContainText(nextWeekday); await expect(card.getByTestId('organizer-event-card-date')).toContainText(/7:00\s?PM/); await expect(card.getByText('+2 more dates')).toBeVisible(); await expect(card.getByText('Happening now')).toHaveCount(0); diff --git a/frontend/src/components/common/ContactOrganizerModal/ContactOrganizerModal.module.scss b/frontend/src/components/common/ContactOrganizerModal/ContactOrganizerModal.module.scss index a8eed79087..1542f22459 100644 --- a/frontend/src/components/common/ContactOrganizerModal/ContactOrganizerModal.module.scss +++ b/frontend/src/components/common/ContactOrganizerModal/ContactOrganizerModal.module.scss @@ -1,14 +1,56 @@ -.contactModal { +.content { + --mantine-color-body: var(--event-content-bg-color, #ffffff); + --mantine-color-text: var(--event-primary-text-color, #1a1a1a); + --mantine-color-dimmed: var(--event-secondary-color, #525252); + --mantine-color-placeholder: var(--event-secondary-text-color, #737373); + --mantine-color-default-border: var(--event-border-color, rgba(0, 0, 0, 0.1)); + --mantine-primary-color-filled: var(--event-primary-color, #7c3aed); + background: var(--mantine-color-body); + color: var(--mantine-color-text); + font-family: var(--theme-font-family, inherit); + + :global(.mantine-InputWrapper-root .mantine-Input-input) { + --input-bg: var(--mantine-color-body); + + &:not([data-error]) { + --input-color: var(--mantine-color-text); + } + } +} + +.header { + background: var(--mantine-color-body); +} + +.title { + font-size: 1.125rem; + font-weight: 700; + letter-spacing: -0.015em; +} + +.close { + color: var(--mantine-color-dimmed); + + &:hover { + background: color-mix(in srgb, var(--mantine-color-text) 8%, transparent); + } } .cancelButton { + color: var(--mantine-color-text); + + &:hover { + background: color-mix(in srgb, var(--mantine-color-text) 8%, transparent); + } } .submitButton { border-radius: 10px; + background: var(--event-primary-color, var(--mantine-primary-color-filled)); + color: var(--event-accent-contrast, #ffffff); &:hover { - opacity: 0.9; - transform: translateY(-1px); + background: var(--event-primary-color, var(--mantine-primary-color-filled)); + filter: brightness(0.94); } } diff --git a/frontend/src/components/common/ContactOrganizerModal/index.tsx b/frontend/src/components/common/ContactOrganizerModal/index.tsx index 33f992ea91..c812160e54 100644 --- a/frontend/src/components/common/ContactOrganizerModal/index.tsx +++ b/frontend/src/components/common/ContactOrganizerModal/index.tsx @@ -65,7 +65,10 @@ export const ContactOrganizerModal: React.FC = ({ onClose={onClose} title={t`Contact ${organizer?.name || 'Organizer'}`} size="md" - className={classes.contactModal} + radius={18} + withinPortal={false} + overlayProps={{backgroundOpacity: 0.6, blur: 6}} + classNames={{content: classes.content, header: classes.header, title: classes.title, close: classes.close}} >
diff --git a/frontend/src/components/layouts/HomepageFrame/HomepageFrame.module.scss b/frontend/src/components/layouts/HomepageFrame/HomepageFrame.module.scss index d81c1466a7..44b22a9b59 100644 --- a/frontend/src/components/layouts/HomepageFrame/HomepageFrame.module.scss +++ b/frontend/src/components/layouts/HomepageFrame/HomepageFrame.module.scss @@ -144,10 +144,14 @@ } &[data-variant="banner"][data-sized] { - max-height: min(36svh, 280px); + max-height: min(40svh, 300px); @include hp.tablet-up { - max-height: min(320px, 40vh); + max-height: min(420px, 50vh); + } + + .coverImage { + object-fit: cover; } } @@ -280,6 +284,7 @@ } .footer { + isolation: isolate; display: flex; align-items: center; justify-content: space-between; diff --git a/frontend/src/components/layouts/OrganizerHomepage/EventList/EventList.module.scss b/frontend/src/components/layouts/OrganizerHomepage/EventList/EventList.module.scss index acf6ee5cd8..74c4cf8ae0 100644 --- a/frontend/src/components/layouts/OrganizerHomepage/EventList/EventList.module.scss +++ b/frontend/src/components/layouts/OrganizerHomepage/EventList/EventList.module.scss @@ -167,7 +167,7 @@ .thumb { position: relative; display: block; - width: 176px; + width: 160px; aspect-ratio: 16 / 10; overflow: hidden; border-radius: 14px; @@ -180,56 +180,6 @@ } } -.spotlight { - display: grid; - grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); - gap: 8px; - margin-bottom: 36px; - padding: 10px; - border: 1px solid var(--hp-border); - border-radius: 28px; - background: var(--hp-fill); - color: inherit; - text-decoration: none; - transition: border-color 0.2s hp.$ease, box-shadow 0.2s hp.$ease; - @include hp.focus-ring; - - &:hover { - border-color: var(--hp-border-strong); - box-shadow: var(--hp-shadow-md); - - .artwork { - transform: scale(1.03); - } - - .cta svg { - transform: translateX(3px); - } - } - - @include hp.below-wide { - grid-template-columns: minmax(0, 1fr); - } - - @include hp.mobile { - margin-bottom: 24px; - border-radius: 24px; - } -} - -.spotlightMedia { - position: relative; - display: block; - aspect-ratio: 16 / 10; - overflow: hidden; - border-radius: 20px; - background: var(--hp-fill-strong); - - @include hp.mobile { - border-radius: 16px; - } -} - .artworkFill { position: absolute; inset: 0; @@ -249,124 +199,15 @@ transition: transform 0.6s hp.$ease; } -.spotlightArt { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: - radial-gradient(90% 80% at 15% 10%, color-mix(in srgb, var(--hp-accent) 45%, #fff), transparent 60%), - radial-gradient(70% 70% at 90% 100%, color-mix(in srgb, var(--hp-accent) 70%, #000), transparent 70%), - var(--hp-accent); - color: var(--hp-on-accent); - - span { - font-size: 1rem; - font-weight: 700; - letter-spacing: 0.16em; - text-transform: uppercase; - opacity: 0.85; - } - - strong { - font-size: clamp(4rem, 9vw, 6.5rem); - font-weight: 700; - line-height: 1; - letter-spacing: -0.05em; - } -} - -.spotlightBody { - display: flex; - flex-direction: column; - justify-content: center; - gap: 8px; - min-width: 0; - padding: 24px 28px; - - @include hp.mobile { - padding: 16px 12px 12px; - } -} - -.spotlightTop { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.spotlightLabel { - display: inline-flex; - align-items: center; - height: 26px; - padding: 0 10px; - border-radius: 999px; - background: var(--hp-surface); - color: var(--hp-text); - font-size: 0.75rem; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - box-shadow: var(--hp-shadow-sm); -} - -.spotlightTitle { - margin: 0; - color: var(--hp-text); - font-size: clamp(1.5rem, 1.1rem + 1.3vw, 2.125rem); - font-weight: 700; - line-height: 1.1; - letter-spacing: -0.03em; - text-wrap: balance; - overflow-wrap: anywhere; -} - .description { - display: -webkit-box; - margin: 0; - overflow: hidden; + margin: 1px 0 0; color: var(--hp-muted); - font-size: 0.9375rem; - line-height: 1.6; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; -} - -.spotlightFooter { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-top: 14px; -} - -.cta { - display: inline-flex; - align-items: center; - gap: 8px; - height: 44px; - margin-left: auto; - padding: 0 20px; - border-radius: 999px; - background: var(--hp-accent); - color: var(--hp-on-accent); - font-size: 0.9375rem; - font-weight: 700; - white-space: nowrap; - - svg { - width: 18px; - height: 18px; - transition: transform 0.2s hp.$ease; - } + font-size: 0.875rem; + line-height: 1.5; + @include mixins.ellipsis; - @include hp.mobile { - width: 100%; - justify-content: center; + @include hp.narrow { + display: none; } } + diff --git a/frontend/src/components/layouts/OrganizerHomepage/EventList/index.tsx b/frontend/src/components/layouts/OrganizerHomepage/EventList/index.tsx index 6e6ca19bc7..387fdfdbeb 100644 --- a/frontend/src/components/layouts/OrganizerHomepage/EventList/index.tsx +++ b/frontend/src/components/layouts/OrganizerHomepage/EventList/index.tsx @@ -1,14 +1,14 @@ import {useId} from "react"; import {Link} from "react-router"; import {plural, t} from "@lingui/macro"; -import {IconArrowRight, IconCalendarRepeat, IconMapPin, IconWorld} from "@tabler/icons-react"; +import {IconCalendarRepeat, IconMapPin, IconWorld} from "@tabler/icons-react"; import {Event, Image, LocationType} from "../../../../types.ts"; import {eventHomepagePath} from "../../../../utilites/urlHelper.ts"; import {formatDate, formatDateWithLocale, formatMonthHeading} from "../../../../utilites/dates.ts"; import {formatAddress} from "../../../../utilites/addressUtilities.ts"; import {summariseEventLocations} from "../../../../utilites/effectiveLocation.ts"; import {summariseEventPrice, summariseEventSchedule} from "../../../../utilites/eventListing.ts"; -import {eventPriceLabel, eventScheduleLabel} from "../../../../utilites/eventListingLabels.ts"; +import {eventPriceLabel, eventScheduleLabel, eventTimeLabel} from "../../../../utilites/eventListingLabels.ts"; import {decodeHtmlEntities} from "../../../../utilites/htmlEntities.ts"; import {groupConsecutive} from "../../../../utilites/groupConsecutive.ts"; import {responsiveImage} from "../../../../utilites/imageTransform.ts"; @@ -50,6 +50,8 @@ const listingDetails = (event: Event, past: boolean) => { anchorDate: anchorDate(event, past, schedule), cover: event.images?.find(image => image.type === 'EVENT_COVER'), dateLine: eventScheduleLabel(schedule, event.timezone), + timeLine: eventTimeLabel(schedule, event.timezone), + description: decodeHtmlEntities(event.description_preview || ''), timezone: schedule.start && !schedule.isSeriesRange ? formatDateWithLocale(schedule.start, 'timezone', event.timezone) : null, }; }; @@ -63,9 +65,9 @@ const Artwork = ({image, sizes, lazy}: { image: Image, sizes: string, lazy?: boo ); -const DateLine = ({event, details, id}: { event: Event, details: ListingDetails, id: string }) => ( +const DateLine = ({event, details, id, compact}: { event: Event, details: ListingDetails, id: string, compact?: boolean }) => (
- {details.dateLine} + {compact && details.timeLine ? details.timeLine : details.dateLine} {details.timezone && ( {` (${details.timezone})`} )} @@ -127,7 +129,8 @@ const EventRow = ({event, past, titleAs: Title}: { event: Event, past: boolean, :
); }; - -export const EventSpotlight = ({event}: { event: Event }) => { - const details = listingDetails(event, false); - const start = details.schedule.start; - const description = decodeHtmlEntities(event.description_preview || ''); - const id = useId(); - - return ( - -
- {details.cover - ? - : start && ( - - )} -
-
-
- {t`Next up`} - {details.schedule.isHappeningNow && } -
- -

{event.title}

- {description &&

{description}

} - -
- - -
-
- - ); -}; diff --git a/frontend/src/components/layouts/OrganizerHomepage/OrganizerHomepage.module.scss b/frontend/src/components/layouts/OrganizerHomepage/OrganizerHomepage.module.scss index 6c120896c3..fc34eb7674 100644 --- a/frontend/src/components/layouts/OrganizerHomepage/OrganizerHomepage.module.scss +++ b/frontend/src/components/layouts/OrganizerHomepage/OrganizerHomepage.module.scss @@ -175,7 +175,7 @@ .bioText { &[data-clamped] { - max-height: 5.1em; + max-height: 10em; overflow: hidden; &[data-overflows] { diff --git a/frontend/src/components/layouts/OrganizerHomepage/index.tsx b/frontend/src/components/layouts/OrganizerHomepage/index.tsx index 4ec6cd4a19..e39e5a9010 100644 --- a/frontend/src/components/layouts/OrganizerHomepage/index.tsx +++ b/frontend/src/components/layouts/OrganizerHomepage/index.tsx @@ -1,5 +1,5 @@ import {useLocation, useNavigate} from "react-router"; -import {EventList, EventSpotlight} from './EventList'; +import {EventList} from './EventList'; import classes from './OrganizerHomepage.module.scss'; import {Fragment, useEffect, useRef, useState} from 'react'; import {Event, GenericPaginatedResponse, HomepageSection, Organizer} from "../../../types.ts"; @@ -135,9 +135,6 @@ export const OrganizerHomepage = ({ const renderEventsBlock = (heading: string | null | undefined, includesPastEvents: boolean) => { const showPastToggle = includesPastEvents || isPastEvents; - const isFirstPage = (eventsData?.meta.current_page ?? 1) === 1; - const spotlight = !isPastEvents && isFirstPage ? events[0] : undefined; - const listEvents = spotlight ? events.slice(1) : events; return (
@@ -178,10 +175,7 @@ export const OrganizerHomepage = ({ )} ) : ( - <> - {spotlight && } - {listEvents.length > 0 && } - + )} {eventsData && eventsData.meta.total > eventsData.meta.per_page && ( @@ -262,6 +256,7 @@ export const OrganizerHomepage = ({ themeSettings={themeSettings} mirrorImageUrl={cover?.url} footerSource="app-organizer-footer" + width="medium" outside={( { }).format(dayjs.utc(date).toDate()); }; -export const formatWeekday = (date: string, tz: string): string => new Intl.DateTimeFormat( +export const formatWeekday = (date: string, tz: string, weekday: 'long' | 'short' = 'long'): string => new Intl.DateTimeFormat( toIntlLocale(getSafeLocale(getClientLocale())), - {weekday: 'long', timeZone: tz}, + {weekday, timeZone: tz}, ).format(dayjs.utc(date).toDate()); export const formatMonthHeading = (date: string, tz: string): string => { diff --git a/frontend/src/utilites/eventListingLabels.ts b/frontend/src/utilites/eventListingLabels.ts index ff07365846..5186e79b9b 100644 --- a/frontend/src/utilites/eventListingLabels.ts +++ b/frontend/src/utilites/eventListingLabels.ts @@ -2,7 +2,7 @@ import {t} from "@lingui/macro"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; -import {formatDateWithLocale, isSameDayInTimezone} from "./dates.ts"; +import {formatDateWithLocale, formatWeekday, isSameDayInTimezone} from "./dates.ts"; import {formatCurrency} from "./currency.ts"; import {EventPriceSummary, EventScheduleSummary} from "./eventListing.ts"; @@ -53,3 +53,13 @@ export const eventScheduleLabel = (schedule: EventScheduleSummary, tz: string): ? `${startLabel} โ€“ ${endTime}` : `${startLabel} โ€“ ${formatDay(end, tz, withYear)}, ${endTime}`; }; + +export const eventTimeLabel = (schedule: EventScheduleSummary, tz: string): string | null => { + const {start, end} = schedule; + if (!start || schedule.isSeriesRange || schedule.hasEnded || (end && !isSameDayInTimezone(start, end, tz))) { + return null; + } + const startTime = formatDateWithLocale(start, "timeOnly", tz); + + return `${formatWeekday(start, tz, 'short')} ยท ${end ? `${startTime} โ€“ ${formatDateWithLocale(end, "timeOnly", tz)}` : startTime}`; +};