+
{message}
diff --git a/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss b/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss
deleted file mode 100644
index 1637015eb..000000000
--- a/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss
+++ /dev/null
@@ -1,1338 +0,0 @@
-@use "../../../styles/mixins.scss";
-
-// Design tokens
-$font-display: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-$font-body: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-
-$radius-xl: 28px;
-$radius-lg: 20px;
-$radius-md: 14px;
-$radius-sm: 10px;
-$radius-xs: 6px;
-
-$transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1);
-$transition-medium: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
-$transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
-
-// Background styles
-.background {
- position: fixed;
- width: 100%;
- height: 100%;
- background-size: cover;
- background-position: center;
- background-repeat: no-repeat;
- filter: blur(24px) saturate(1.3);
- z-index: -1;
- transform: scale(1.08);
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
-
- &::after {
- content: '';
- position: absolute;
- inset: 0;
- background: var(--background-overlay);
- }
-}
-
-.backgroundOverlay {
- position: fixed;
- inset: 0;
- z-index: -1;
-
- // Color overlay for MIRROR_COVER_IMAGE mode
- &::after {
- content: '';
- position: absolute;
- inset: 0;
- background-color: var(--overlay-color, transparent);
- opacity: 0.4;
- pointer-events: none;
- }
-
- // Noise texture
- &::before {
- content: '';
- position: absolute;
- inset: 0;
- opacity: 0.03;
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
- z-index: 1;
- }
-}
-
-// Main page wrapper
-.pageWrapper {
- min-height: 100vh;
- --background-overlay: #{rgba(white, 0.5)};
-
- &[data-mode="dark"] {
- --background-overlay: #{rgba(black, 0.6)};
- }
-}
-
-.container {
- min-height: 100vh;
- font-family: $font-body;
-
- // Theme variables from EventHomepage component
- --primary-color: var(--event-primary-color);
- --primary-text-color: var(--event-primary-text-color);
- --secondary-color: var(--event-secondary-color);
- --secondary-text-color: var(--event-secondary-text-color);
- --bg-color: var(--event-bg-color);
- --content-bg-color: var(--event-content-bg-color);
- --accent-contrast: var(--event-accent-contrast);
- --accent-soft: var(--event-accent-soft);
- --accent-muted: var(--event-accent-muted);
- --border-color: var(--event-border-color);
-}
-
-.wrapper {
- max-width: 920px;
- margin: 0 auto;
- padding: 24px 24px 16px;
-
- @include mixins.respond-below(md) {
- padding: 0;
- }
-}
-
-// Main unified card
-.mainCard {
- background-color: var(--content-bg-color);
- border-radius: $radius-xl;
- border: 1px solid var(--border-color);
- box-shadow:
- 0 4px 6px -1px rgba(0, 0, 0, 0.05),
- 0 10px 15px -3px rgba(0, 0, 0, 0.08),
- 0 20px 40px -5px rgba(0, 0, 0, 0.1);
- overflow: hidden;
- margin-bottom: 16px;
- animation: fadeInUp 0.5s ease-out backwards;
-
- @include mixins.respond-below(md) {
- border-radius: 0;
- border-left: none;
- border-right: none;
- box-shadow: none;
- margin-bottom: 0;
- }
-}
-
-// Hero section
-.heroSection {
- position: relative;
- overflow: hidden;
-}
-
-.coverWrapper {
- position: relative;
- width: 100%;
- max-height: 560px;
- overflow: hidden;
- background-color: var(--accent-soft);
-
- // When aspect ratio is provided via CSS custom property, use it
- // Otherwise fall back to auto sizing based on image
- &[style*="--cover-aspect-ratio"] {
- aspect-ratio: var(--cover-aspect-ratio);
- }
-
- @include mixins.respond-below(md) {
- max-height: 400px;
- }
-}
-
-.coverLqip {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- object-fit: cover;
- filter: blur(20px);
- transform: scale(1.1);
- z-index: 0;
-}
-
-.coverImage {
- width: 100%;
- display: block;
- transition: transform $transition-slow;
- z-index: 1;
-
- // When wrapper has aspect ratio, image should fill it
- .coverWrapper[style*="--cover-aspect-ratio"] & {
- position: relative;
- height: 100%;
- object-fit: cover;
- }
-
- // When no aspect ratio, let image determine height naturally
- .coverWrapper:not([style*="--cover-aspect-ratio"]) & {
- height: auto;
- }
-
- .mainCard:hover & {
- transform: scale(1.02);
- }
-}
-
-.heroGradient {
- position: absolute;
- inset: 0;
- background: linear-gradient(
- to top,
- rgba(0, 0, 0, 0.7) 0%,
- rgba(0, 0, 0, 0.3) 40%,
- transparent 100%
- );
- pointer-events: none;
-}
-
-.statusBadges {
- position: absolute;
- top: 20px;
- left: 20px;
- display: flex;
- gap: 8px;
- z-index: 2;
-}
-
-.statusBadge {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- background: rgba(0, 0, 0, 0.5);
- backdrop-filter: blur(20px);
- border-radius: $radius-sm;
- padding: 8px 14px;
- font-size: 0.8rem;
- font-weight: 600;
- color: white;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
-
- svg {
- width: 0.7rem;
- height: 0.7rem;
- filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.3));
- }
-}
-
-// Event header section
-.eventHeader {
- padding: 28px 32px 24px;
-
- @include mixins.respond-below(md) {
- padding: 24px 20px;
- }
-}
-
-.headerTopRow {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 16px;
-}
-
-.organizerPill {
- display: inline-flex;
- align-items: center;
- gap: 10px;
- background: var(--accent-soft);
- border-radius: 100px;
- padding: 6px 14px 6px 6px;
- text-decoration: none;
- transition: all $transition-fast;
- cursor: pointer;
-
- &:hover {
- transform: translateY(-1px) scale(1.02);
- }
-}
-
-.organizerPillAvatar {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- object-fit: cover;
- border: 2px solid var(--content-bg-color);
-}
-
-.organizerPillAvatarPlaceholder {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- background: var(--primary-color);
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--accent-contrast);
- font-size: 0.75rem;
- font-weight: 600;
- border: 2px solid var(--content-bg-color);
-}
-
-.organizerPillName {
- font-size: 0.9rem;
- font-weight: 500;
- color: var(--primary-text-color);
-}
-
-.actionButtons {
- display: flex;
- gap: 8px;
-}
-
-.actionButton {
- width: 42px;
- height: 42px;
- border-radius: 50%;
- background: var(--content-bg-color);
- border: 1px solid var(--border-color);
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- color: var(--secondary-color);
- transition: all $transition-fast;
-
- svg {
- width: 1rem;
- height: 1rem;
- }
-
- &:hover {
- background: var(--accent-soft);
- color: var(--primary-text-color);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
-
- &.favoriteButton:hover {
- color: #ef4444;
- background: rgba(239, 68, 68, 0.1);
- }
-}
-
-.eventTitle {
- font-family: $font-display;
- font-size: clamp(1.75rem, 5vw, 2.5rem);
- font-weight: 800;
- line-height: 1.15;
- letter-spacing: -0.02em;
- color: var(--primary-text-color);
- margin: 0 0 20px;
-}
-
-.eventMeta {
- display: flex;
- flex-direction: column;
- gap: 14px;
-}
-
-.metaItem {
- display: flex;
- align-items: center;
- gap: 14px;
-}
-
-.metaIconBox {
- width: 44px;
- height: 44px;
- border-radius: $radius-md;
- background: var(--accent-soft);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- color: var(--primary-color);
- transition: all $transition-fast;
-
- svg {
- width: 1.1rem;
- height: 1.1rem;
- }
-
- &:hover {
- background: var(--primary-color);
- color: var(--accent-contrast);
- transform: scale(1.05);
- }
-}
-
-.metaContent {
- flex: 1;
- min-width: 0;
-}
-
-.metaPrimary {
- font-weight: 600;
- color: var(--primary-text-color);
- font-size: 1rem;
- margin-bottom: 2px;
-}
-
-.metaSecondary {
- color: var(--secondary-color);
- font-size: 0.9rem;
-}
-
-.metaLink {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- color: var(--primary-color);
- font-weight: 500;
- font-size: 0.85rem;
- text-decoration: none;
- margin-top: 4px;
-
- svg {
- width: 0.7rem;
- height: 0.7rem;
- }
-
- &:hover {
- text-decoration: underline;
- }
-}
-
-.addToCalendarButton {
- margin-left: auto;
- padding: 8px 16px;
- background: transparent;
- border: 1px solid var(--border-color);
- border-radius: $radius-sm;
- font-size: 0.85rem;
- font-weight: 500;
- color: var(--secondary-color);
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 6px;
- transition: all $transition-fast;
-
- svg {
- width: 0.9rem;
- height: 0.9rem;
- }
-
- &:hover {
- background: var(--accent-soft);
- color: var(--primary-text-color);
- border-color: var(--primary-color);
- }
-
- @include mixins.respond-below(md) {
- display: none;
- }
-}
-
-// Section dividers
-.section {
- padding: 28px 32px;
- border-top: 1px solid var(--border-color);
-
- @include mixins.respond-below(md) {
- padding: 24px 20px;
- }
-}
-
-.sectionHeader {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 18px;
-}
-
-.sectionTitle {
- font-family: $font-display;
- font-size: 1.35rem;
- font-weight: 700;
- letter-spacing: -0.01em;
- color: var(--primary-text-color);
- margin: 0;
-}
-
-// Description / About section
-.description {
- color: var(--secondary-color);
- line-height: 1.7;
- font-size: 0.95rem;
-
- p {
- margin-bottom: 1em;
-
- &:last-child {
- margin-bottom: 0;
- }
- }
-
- a {
- color: var(--primary-color);
- text-decoration: none;
-
- &:hover {
- text-decoration: underline;
- }
- }
-
- h1, h2, h3, h4, h5, h6 {
- margin-top: 1.25em;
- margin-bottom: 0.5em;
- color: var(--primary-text-color);
- font-weight: 600;
- font-family: $font-display;
-
- &:first-child {
- margin-top: 0;
- }
- }
-
- h1 { font-size: 1.375rem; }
- h2 { font-size: 1.25rem; }
- h3 { font-size: 1.125rem; }
-
- ul, ol {
- padding-left: 1.5em;
- margin-bottom: 1em;
- }
-
- blockquote {
- border-left: 3px solid var(--primary-color);
- padding-left: 1em;
- margin: 1em 0;
- color: var(--secondary-color);
- font-style: italic;
- }
-
- img {
- max-width: 100%;
- height: auto;
- border-radius: $radius-md;
- margin: 1em 0;
- }
-}
-
-.readMoreToggle {
- color: var(--primary-color);
- font-weight: 500;
- font-size: 0.9rem;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- gap: 4px;
- margin-top: 8px;
- background: none;
- border: none;
- padding: 0;
-
- svg {
- transition: transform $transition-fast;
- }
-
- &[data-expanded="true"] svg {
- transform: rotate(180deg);
- }
-
- &:hover {
- text-decoration: underline;
- }
-}
-
-// Location section
-.locationContent {
- display: grid;
- grid-template-columns: 1fr 200px;
- gap: 20px;
- align-items: start;
-
- @include mixins.respond-below(md) {
- grid-template-columns: 1fr;
- }
-}
-
-.venueDetails {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.venueName {
- font-weight: 600;
- color: var(--primary-text-color);
- font-size: 1.05rem;
-}
-
-.venueAddress {
- color: var(--secondary-color);
- font-size: 0.9rem;
- line-height: 1.5;
-}
-
-.directionsLink {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- color: var(--primary-color);
- font-weight: 500;
- font-size: 0.9rem;
- text-decoration: none;
- margin-top: 8px;
-
- svg {
- width: 0.85rem;
- height: 0.85rem;
- }
-
- &:hover {
- text-decoration: underline;
- }
-}
-
-.mapContainer {
- border-radius: $radius-md;
- overflow: hidden;
- height: 120px;
- cursor: pointer;
- position: relative;
- transition: all $transition-fast;
- display: flex;
- align-items: center;
- justify-content: center;
-
- @include mixins.respond-below(md) {
- display: none;
- }
-
- &:hover {
- transform: scale(1.02);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- }
-
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
-}
-
-.mapPin {
- color: var(--primary-color);
- position: relative;
- z-index: 1;
- filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
- transition: opacity $transition-fast;
-
- .mapContainer:hover & {
- opacity: 0;
- }
-}
-
-.mapOverlay {
- position: absolute;
- inset: 0;
- background: rgba(0, 0, 0, 0.1);
- display: flex;
- align-items: center;
- justify-content: center;
- opacity: 0;
- transition: opacity $transition-fast;
-
- .mapContainer:hover & {
- opacity: 1;
- }
-}
-
-.mapOverlayLabel {
- background: var(--content-bg-color);
- padding: 8px 16px;
- border-radius: $radius-sm;
- font-size: 0.85rem;
- font-weight: 500;
- color: var(--primary-text-color);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- display: flex;
- align-items: center;
- gap: 6px;
-
- svg {
- width: 0.85rem;
- height: 0.85rem;
- }
-}
-
-// Tickets section - restyle .hi-product-widget-container
-.ticketsSection {
- // Override widget styles
- :global(.hi-product-widget-container) {
- background: transparent;
- padding: 0;
- font-family: $font-body;
- }
-
- :global(.hi-product-category-rows) {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- :global(.hi-product-category-row) {
- margin-bottom: 0;
- }
-
- :global(.hi-product-category-title) {
- font-family: $font-display;
- font-size: 1.35rem;
- font-weight: 700;
- letter-spacing: -0.01em;
- color: var(--primary-text-color);
- margin: 0 0 18px;
- }
-
- :global(.hi-product-category-description) {
- color: var(--secondary-color);
- font-size: 0.9rem;
- line-height: 1.6;
- margin-bottom: 16px;
- }
-
- :global(.hi-product-rows) {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-bottom: 0;
- }
-
- :global(.hi-product-row) {
- background: var(--content-bg-color);
- border-radius: $radius-lg;
- border: 2px solid var(--border-color);
- padding: 0;
- transition: all $transition-fast;
- cursor: pointer;
- position: relative;
- overflow: hidden;
-
- &:hover {
- border-color: color-mix(in srgb, var(--border-color) 150%, transparent);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
- }
- }
-
- :global(.hi-product-row.hi-product-highlighted) {
- background: var(--accent-soft);
- border-color: var(--primary-color);
-
- &:hover {
- background: color-mix(in srgb, var(--accent-soft) 80%, var(--content-bg-color));
- border-color: var(--primary-color);
- }
- }
-
- :global(.hi-product-highlight-message) {
- background: var(--primary-color);
- color: var(--accent-contrast);
- font-size: 0.75rem;
- font-weight: 600;
- padding: 6px 16px;
- text-align: center;
- letter-spacing: 0.02em;
- }
-
- :global(.hi-product-row.selected) {
- border-color: var(--primary-color);
- background: var(--accent-soft);
- }
-
- :global(.hi-title-row) {
- padding: 0;
- }
-
- :global(.hi-product-title) {
- padding: 20px 24px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- width: 100%;
-
- h3 {
- font-family: $font-display;
- font-weight: 700;
- font-size: 1.1rem;
- color: var(--primary-text-color);
- margin: 0;
- }
- }
-
- :global(.hi-product-title-metadata) {
- font-size: 0.8rem;
- color: var(--secondary-text-color);
- display: flex;
- align-items: center;
- gap: 8px;
- }
-
- :global(.hi-product-collapse-arrow) {
- color: var(--secondary-color);
-
- svg {
- transition: transform $transition-fast;
- }
- }
-
- :global(.hi-product-content) {
- padding: 0 24px 20px;
- border-top: 1px solid var(--border-color);
- margin-top: 0;
- }
-
- :global(.hi-price-tiers-rows) {
- padding-top: 16px;
- }
-
- :global(.hi-price-tier-row) {
- margin-bottom: 12px;
-
- &:last-child {
- margin-bottom: 0;
- }
- }
-
- :global(.hi-price-tier) {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 12px;
- }
-
- :global(.hi-price-tier-label) {
- font-weight: 600;
- color: var(--primary-text-color);
- font-size: 0.95rem;
- }
-
- :global(.hi-price-tier-price) {
- font-family: $font-display;
- font-weight: 700;
- font-size: 1.1rem;
- color: var(--primary-text-color);
- }
-
- :global(.hi-product-quantity-selector) {
- .button-input {
- display: flex;
- align-items: center;
- gap: 4px;
- background: var(--content-bg-color);
- border-radius: $radius-sm;
- padding: 4px;
- border: 1px solid var(--border-color);
-
- button {
- width: 36px;
- height: 36px;
- border-radius: $radius-xs;
- border: none;
- background: transparent;
- color: var(--secondary-color);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 0.9rem;
- cursor: pointer;
- transition: all $transition-fast;
-
- &:hover:not(:disabled) {
- background: var(--accent-soft);
- color: var(--primary-text-color);
- }
-
- &:disabled {
- opacity: 0.3;
- cursor: not-allowed;
- }
- }
-
- input {
- font-family: $font-display;
- font-weight: 700;
- min-width: 32px;
- text-align: center;
- font-size: 1rem;
- color: var(--primary-text-color);
- border: none;
- background: transparent;
- }
- }
- }
-
- :global(.hi-product-description-row) {
- color: var(--secondary-color);
- font-size: 0.875rem;
- line-height: 1.6;
- margin-top: 12px;
- padding-top: 12px;
- border-top: 1px solid var(--border-color);
- }
-
- :global(.hi-product-quantity-error) {
- background: rgba(239, 68, 68, 0.1);
- border: 1px solid rgba(239, 68, 68, 0.3);
- color: #dc2626;
- border-radius: $radius-sm;
- padding: 12px 16px;
- font-size: 0.875rem;
- margin: 12px 0;
- }
-
- :global(.hi-no-products) {
- text-align: center;
- padding: 40px 20px;
- color: var(--secondary-color);
- }
-
- :global(.hi-no-products-message) {
- font-size: 0.95rem;
- margin: 0;
- }
-
- :global(.hi-footer-row) {
- margin-top: 24px;
- padding-top: 24px;
- border-top: 1px solid var(--border-color);
-
- .hi-product-page-message {
- background: var(--accent-soft);
- border-radius: $radius-sm;
- padding: 12px 16px;
- margin-bottom: 16px;
- font-size: 0.9rem;
- color: var(--primary-text-color);
- }
- }
-
- :global(.hi-continue-button) {
- width: 100%;
- padding: 18px 24px !important;
- height: auto !important;
- background: var(--primary-color) !important;
- background-color: var(--primary-color) !important;
- color: var(--accent-contrast) !important;
- border: none !important;
- border-radius: $radius-md !important;
- font-family: $font-display;
- font-size: 1rem !important;
- font-weight: 700 !important;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- transition: all $transition-fast;
-
- // Override Mantine button internals
- :global(.mantine-Button-label) {
- color: var(--accent-contrast) !important;
- }
-
- &:hover:not(:disabled) {
- transform: translateY(-2px);
- box-shadow: 0 8px 20px var(--accent-soft);
- filter: brightness(1.1);
- background: var(--primary-color) !important;
- background-color: var(--primary-color) !important;
- }
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- transform: none;
- box-shadow: none;
- }
- }
-
- :global(.hi-promo-code-row) {
- margin-top: 14px;
- margin-bottom: 0;
- }
-
- :global(.hi-have-a-promo-code-link) {
- color: var(--secondary-color);
- font-size: 0.9rem;
- text-align: center;
- display: block;
- width: 100%;
-
- &:hover {
- color: var(--primary-color);
- }
- }
-
- :global(.hi-promo-code-input-wrapper) {
- gap: 12px;
- }
-
- :global(.hi-promo-code-input) {
- background: var(--content-bg-color);
- border: 1px solid var(--border-color);
- border-radius: $radius-sm;
- color: var(--primary-text-color);
- padding: 10px 14px;
- }
-
- :global(.hi-apply-promo-code-button) {
- background: var(--primary-color);
- color: var(--accent-contrast);
- border: none;
- border-radius: $radius-sm;
- font-weight: 600;
- padding: 10px 20px;
-
- &:hover {
- filter: brightness(1.1);
- }
- }
-
- :global(.hi-promo-code-applied) {
- color: var(--primary-text-color);
- font-size: 0.9rem;
-
- b {
- color: var(--primary-color);
- }
- }
-}
-
-// Organizer section
-.organizerCard {
- display: flex;
- gap: 20px;
- align-items: flex-start;
-
- @include mixins.respond-below(sm) {
- flex-direction: column;
- align-items: center;
- text-align: center;
- }
-}
-
-.organizerAvatar {
- width: 72px;
- height: 72px;
- border-radius: $radius-md;
- object-fit: cover;
- flex-shrink: 0;
-}
-
-.organizerAvatarPlaceholder {
- width: 72px;
- height: 72px;
- border-radius: $radius-md;
- background: var(--primary-color);
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--accent-contrast);
- font-size: 1.5rem;
- font-weight: 700;
- flex-shrink: 0;
-}
-
-.organizerContent {
- flex: 1;
- min-width: 0;
-}
-
-.organizerHeader {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 12px;
- margin-bottom: 8px;
-
- @include mixins.respond-below(sm) {
- flex-direction: column;
- align-items: center;
- }
-}
-
-.organizerName {
- font-family: $font-display;
- font-weight: 700;
- font-size: 1.15rem;
- color: var(--primary-text-color);
- margin: 0 0 4px;
-
- a {
- color: inherit;
- text-decoration: none;
-
- &:hover {
- color: var(--primary-color);
- }
- }
-}
-
-.organizerLocation {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.85rem;
- color: var(--secondary-color);
-
- svg {
- color: var(--primary-color);
- width: 0.75rem;
- height: 0.75rem;
- }
-
- a {
- color: inherit;
- text-decoration: none;
-
- &:hover {
- color: var(--primary-color);
- }
- }
-}
-
-.organizerBio {
- color: var(--secondary-color);
- font-size: 0.9rem;
- line-height: 1.6;
- margin-bottom: 16px;
-
- p {
- margin-bottom: 0.75em;
-
- &:last-child {
- margin-bottom: 0;
- }
- }
-
- a {
- color: var(--primary-color);
- text-decoration: none;
-
- &:hover {
- text-decoration: underline;
- }
- }
-}
-
-.organizerActions {
- display: flex;
- align-items: center;
- gap: 12px;
- flex-wrap: wrap;
-
- @include mixins.respond-below(sm) {
- justify-content: center;
- }
-}
-
-.socialLinks {
- display: flex;
- gap: 8px;
-}
-
-.socialLink {
- width: 38px;
- height: 38px;
- border-radius: 50%;
- background: var(--accent-soft);
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--secondary-color);
- text-decoration: none;
- font-size: 1rem;
- transition: all $transition-fast;
-
- &:hover {
- background: var(--primary-color);
- color: white;
- transform: translateY(-2px);
- }
-}
-
-.contactButton {
- padding: 10px 20px;
- border: 1px solid var(--border-color);
- background: var(--content-bg-color);
- color: var(--primary-text-color);
- border-radius: $radius-sm;
- font-weight: 600;
- font-size: 0.9rem;
- display: flex;
- align-items: center;
- gap: 8px;
- cursor: pointer;
- transition: all $transition-fast;
-
- svg {
- width: 0.9rem;
- height: 0.9rem;
- }
-
- &:hover {
- background: var(--primary-color);
- color: var(--accent-contrast);
- border-color: var(--primary-color);
- }
-}
-
-// Footer section
-.footerSection {
- text-align: center;
- padding: 40px 20px;
- padding-bottom: 100px;
- color: var(--secondary-text-color);
- font-size: 0.85rem;
-
- @include mixins.respond-below(md) {
- background-color: var(--content-bg-color);
- border-top: 1px solid var(--border-color);
- padding: 24px 20px;
- padding-bottom: 120px;
- margin: 0;
- }
-}
-
-.footerLinks {
- display: flex;
- justify-content: center;
- gap: 24px;
- margin-bottom: 16px;
-}
-
-.footerLink {
- color: var(--secondary-color);
- text-decoration: none;
- transition: color $transition-fast;
-
- &:hover {
- color: var(--primary-color);
- }
-}
-
-.poweredByFooter {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- font-size: 0.9rem;
- color: var(--secondary-color);
-
- strong {
- color: var(--primary-text-color);
- font-weight: 600;
- }
-
- a {
- color: inherit;
- text-decoration: none;
-
- &:hover {
- color: var(--primary-color);
- }
- }
-}
-
-// Floating scroll button
-.scrollToTicketsButton {
- position: fixed;
- bottom: 24px;
- right: 24px;
- z-index: 100;
- display: flex;
- align-items: center;
- gap: 8px;
- background: var(--primary-color);
- color: var(--accent-contrast);
- font-family: $font-display;
- font-weight: 600;
- font-size: 0.875rem;
- padding: 12px 20px;
- border-radius: 50px;
- border: none;
- cursor: pointer;
- box-shadow: 0 4px 20px var(--accent-soft);
- animation: fadeInUp 0.4s ease-out;
- transition: all $transition-fast;
-
- @include mixins.respond-below(sm) {
- bottom: 16px;
- right: 16px;
- padding: 10px 16px;
- font-size: 0.8125rem;
- }
-
- &:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 24px var(--accent-soft);
- }
-
- &:active {
- transform: translateY(0);
- }
-}
-
-// Animations
-@keyframes fadeInUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-// Animate sections
-.section {
- animation: fadeInUp 0.4s ease-out backwards;
-
- &:nth-child(2) { animation-delay: 0.1s; }
- &:nth-child(3) { animation-delay: 0.15s; }
- &:nth-child(4) { animation-delay: 0.2s; }
- &:nth-child(5) { animation-delay: 0.25s; }
-}
-
-// No cover image adjustments
-.heroSection:not(:has(.coverWrapper)) {
- .eventHeader {
- padding-top: 32px;
-
- @include mixins.respond-below(sm) {
- padding-top: 24px;
- }
- }
-}
-
-// Online event badge
-.onlineEventBadge {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- background: var(--accent-soft);
- color: var(--primary-color);
- padding: 6px 12px;
- border-radius: $radius-sm;
- font-size: 0.85rem;
- font-weight: 600;
-
- svg {
- width: 0.9rem;
- height: 0.9rem;
- }
-}
diff --git a/frontend/src/components/layouts/EventHomepage/index.tsx b/frontend/src/components/layouts/EventHomepage/index.tsx
index 42d8114e9..7d0c044b7 100644
--- a/frontend/src/components/layouts/EventHomepage/index.tsx
+++ b/frontend/src/components/layouts/EventHomepage/index.tsx
@@ -1,13 +1,9 @@
-import classes from "./EventHomepage.module.scss";
-import SelectProducts from "../../routes/product-widget/SelectProducts";
-import "../../../styles/widget/default.scss";
-import React, {useEffect, useRef, useState} from "react";
-import {EventDocumentHead} from "../../common/EventDocumentHead";
-import {eventCoverImage, eventHomepageUrl, imageUrl, organizerHomepageUrl} from "../../../utilites/urlHelper.ts";
-import {Event, OrganizerStatus} from "../../../types.ts";
-import {EventNotAvailable} from "./EventNotAvailable";
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { EventDocumentHead } from "../../common/EventDocumentHead";
+import { eventCoverImage, eventHomepageUrl, imageUrl, organizerHomepageUrl } from "../../../utilites/urlHelper.ts";
+import { Event, OrganizerStatus } from "../../../types.ts";
+import { EventNotAvailable } from "./EventNotAvailable";
import {
- IconArrowUpRight,
IconCalendar,
IconCalendarOff,
IconCalendarPlus,
@@ -19,25 +15,28 @@ import {
IconTicket,
IconWorld
} from "@tabler/icons-react";
-import {Anchor} from "@mantine/core";
-import {t} from "@lingui/macro";
-import {PoweredByFooter} from "../../common/PoweredByFooter";
-import {ContactOrganizerModal} from "../../common/ContactOrganizerModal";
-import {socialMediaConfig} from "../../../constants/socialMediaConfig";
+import { Anchor } from "@mantine/core";
+import { t } from "@lingui/macro";
+import { PoweredByFooter } from "../../common/PoweredByFooter";
+import { ContactOrganizerModal } from "../../common/ContactOrganizerModal";
+import { socialMediaConfig } from "../../../constants/socialMediaConfig";
import {
formatAddress,
getGoogleMapsUrl,
getShortLocationDisplay,
isAddressSet
} from "../../../utilites/addressUtilities.ts";
-import {StatusToggle} from "../../common/StatusToggle";
-import {getConfig} from "../../../utilites/config.ts";
-import {computeThemeVariables, validateThemeSettings} from "../../../utilites/themeUtils.ts";
-import {removeTransparency} from "../../../utilites/colorHelper.ts";
-import {ShareComponent} from "../../common/ShareIcon";
-import {EventDateRange} from "../../common/EventDateRange";
-import {CalendarOptionsPopover} from "../../common/CalendarOptionsPopover";
-import {isDateInPast} from "../../../utilites/dates.ts";
+import { StatusToggle } from "../../common/StatusToggle";
+import { getConfig } from "../../../utilites/config.ts";
+import { computeThemeVariables, validateThemeSettings, getContrastColor, generateMeshColors, detectMode } from "../../../utilites/themeUtils.ts";
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import { removeTransparency, isColorDark } from "../../../utilites/colorHelper.ts";
+import { ShareComponent } from "../../common/ShareIcon";
+import { EventDateRange } from "../../common/EventDateRange";
+import { CalendarOptionsPopover } from "../../common/CalendarOptionsPopover";
+import { isDateInPast } from "../../../utilites/dates.ts";
+import SelectProducts from "../../routes/product-widget/SelectProducts";
+import "../../../styles/widget/default.scss";
interface EventHomepageProps {
event?: Event;
@@ -45,75 +44,73 @@ interface EventHomepageProps {
promoCode?: string;
}
-const EventHomepage = ({...loaderData}: EventHomepageProps) => {
- const {event, promoCodeValid, promoCode} = loaderData;
- const [showScrollButton, setShowScrollButton] = useState(false);
+
+const EventHomepage = ({ ...loaderData }: EventHomepageProps) => {
+ const { event, promoCodeValid, promoCode } = loaderData;
const [contactModalOpen, setContactModalOpen] = useState(false);
const ticketsSectionRef = useRef
(null);
- useEffect(() => {
- let showTimer: NodeJS.Timeout;
-
- const checkTicketsPosition = () => {
- if (ticketsSectionRef.current) {
- const rect = ticketsSectionRef.current.getBoundingClientRect();
- const isBelowFold = rect.top > window.innerHeight;
- const isAboveView = rect.bottom < 0;
- const shouldShowButton = isBelowFold || isAboveView;
- setShowScrollButton(shouldShowButton);
- }
- };
-
- showTimer = setTimeout(() => {
- checkTicketsPosition();
- }, 500);
-
- const handleScroll = () => {
- checkTicketsPosition();
- };
-
- const handleResize = () => {
- checkTicketsPosition();
- };
-
- window.addEventListener('scroll', handleScroll);
- window.addEventListener('resize', handleResize);
-
- return () => {
- clearTimeout(showTimer);
- window.removeEventListener('scroll', handleScroll);
- window.removeEventListener('resize', handleResize);
- };
- }, []);
-
- const scrollToTickets = () => {
- ticketsSectionRef.current?.scrollIntoView({behavior: 'smooth', block: 'start'});
- };
-
- if (!event) {
- return ;
- }
+ if (!event) { return ; }
- const rawThemeSettings = event?.settings?.homepage_theme_settings;
+ // --- Dynamic Theming Logic ---
+ const rawThemeSettings = event?.settings?.homepage_theme_settings || event?.organizer?.settings?.homepage_theme_settings;
const themeSettings = validateThemeSettings(rawThemeSettings);
- const cssVars = computeThemeVariables(themeSettings);
- const backgroundType = themeSettings.background_type;
-
- const themeStyles = {
- '--event-bg-color': themeSettings.background,
- '--event-content-bg-color': cssVars['--theme-surface'],
- '--event-primary-color': themeSettings.accent,
- '--event-primary-text-color': cssVars['--theme-text-primary'],
- '--event-secondary-color': cssVars['--theme-text-secondary'],
- '--event-secondary-text-color': cssVars['--theme-text-tertiary'],
- '--event-accent-contrast': cssVars['--theme-accent-contrast'],
- '--event-accent-soft': cssVars['--theme-accent-soft'],
- '--event-accent-muted': cssVars['--theme-accent-muted'],
- '--event-border-color': cssVars['--theme-border'],
- } as React.CSSProperties;
+
+ const backgroundType = themeSettings.background_type || 'color';
+ const backgroundColor = themeSettings.background || (themeSettings.mode === 'dark' ? '#050505' : '#f9fafb');
+
+ const mode = themeSettings.mode === 'auto' ? detectMode(backgroundColor) : (themeSettings.mode || 'light');
+ const isCardDark = mode === 'dark';
+
+ const accentColor = themeSettings.accent || '#40296C';
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const meshColors = useMemo(() => generateMeshColors(backgroundColor), [backgroundColor]);
+
+ const isBgDark = backgroundType === 'MIRROR_COVER_IMAGE' || backgroundType === 'IMAGE'
+ ? isCardDark
+ : detectMode(backgroundColor) === 'dark';
+
+ // Text colors for elements directly on the background
+ const bgTextPrimary = isBgDark ? '!text-white' : '!text-gray-900';
+ const bgTextSecondary = isBgDark ? '!text-white/70' : '!text-gray-600';
+
+ // Heavy glassmorphism UI
+ const cardBg = isCardDark ? 'bg-black/20 backdrop-blur-2xl shadow-xl' : 'bg-white/40 backdrop-blur-2xl shadow-xl';
+ const ticketCardBg = isCardDark ? 'bg-black/20 backdrop-blur-2xl shadow-xl' : 'bg-white/40 backdrop-blur-2xl shadow-xl';
+
+ const cardTextPrimary = isCardDark ? 'text-white' : 'text-gray-900';
+ const cardTextSecondary = isCardDark ? 'text-white/70' : 'text-gray-500';
+ const cardProseClasses = isCardDark
+ // eslint-disable-next-line lingui/no-unlocalized-strings
+ ? 'prose-invert text-white/80 prose-headings:text-white prose-headings:font-bold prose-a:text-[var(--prose-accent)] hover:prose-a:brightness-110'
+ // eslint-disable-next-line lingui/no-unlocalized-strings
+ : 'text-gray-600 prose-headings:text-gray-900 prose-headings:font-bold prose-a:text-[var(--prose-accent)] hover:prose-a:brightness-90';
+
+ const textPrimary = cardTextPrimary;
+ const textSecondary = cardTextSecondary;
+ const proseClasses = cardProseClasses;
+
+ // Link specific text overrides to beat Mantine's global anchor styles
+ const linkTextPrimary = isCardDark ? '!text-white' : '!text-gray-900';
+ const linkTextSecondary = isCardDark ? '!text-white/70' : '!text-gray-500';
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const bgLinkTextPrimary = isBgDark ? '!text-white' : '!text-gray-900';
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const bgLinkTextSecondary = isBgDark ? '!text-white/70' : '!text-gray-600';
+
+ const borderStyle = isCardDark ? 'border-white/10' : 'border-white/50';
+ const cardHover = isCardDark ? 'hover:bg-black/30' : 'hover:bg-white/60';
+ // eslint-disable-next-line lingui/no-unlocalized-strings
+ const iconWrapperBg = isCardDark ? 'bg-white/10 text-white' : 'bg-gray-50 text-gray-600';
+ const subtleBtnBorder = isCardDark ? 'border-white/10' : 'border-gray-200';
+ // eslint-disable-next-line lingui/no-unlocalized-strings
+ const subtleBtnBg = isCardDark ? 'bg-black/40 hover:bg-black/60' : 'bg-white hover:bg-gray-50';
+ // -----------------------------
const coverImageData = eventCoverImage(event);
const coverImage = coverImageData?.url;
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const organizer = event.organizer!;
const organizerSocials = organizer?.settings?.social_media_handles;
const organizerLogo = imageUrl('ORGANIZER_LOGO', organizer?.images);
@@ -133,351 +130,284 @@ const EventHomepage = ({...loaderData}: EventHomepageProps) => {
const getStatusBadge = () => {
const products = event.products || event.product_categories?.flatMap(c => c.products || []) || [];
-
- if (products.length === 0) {
- return null;
- }
-
+ if (products.length === 0) return null;
const availableProducts = products.filter(p => p.is_available && !p.is_sold_out);
const allSoldOut = products.every(p => p.is_sold_out);
-
- if (allSoldOut) {
- return {text: t`Sold Out`, variant: 'danger'};
- }
-
- if (availableProducts.length === 0) {
- return null;
- }
-
- return {text: t`Tickets Available`, variant: 'success'};
+ if (allSoldOut) return { text: t`Sold Out`, variant: 'danger' };
+ if (availableProducts.length === 0) return null;
+ return { text: t`Tickets Available`, variant: 'success' };
};
-
const statusBadge = getStatusBadge();
-
const mapUrl = event.settings?.maps_url || (locationDetails ? getGoogleMapsUrl(locationDetails) : null);
- return (
- <>
- {event?.status && event?.id && (
-
- setTimeout(() => {
- window.location.reload();
- }, 1000)}
- />
- )}
+ const isImageBg = backgroundType === 'MIRROR_COVER_IMAGE';
+ const isGradientBg = backgroundType === 'GRADIENT';
-
-
-
- {event && }
-
- {/* Background */}
- {(coverImage && backgroundType === 'MIRROR_COVER_IMAGE') ? (
+ return (
+
+ {isImageBg && coverImage ? (
+
+ ) : isGradientBg ? (
+
+ ) : (
-
-
-
- {/* Main unified card */}
-
- {/* Hero Section */}
-
- {coverImage && (
-
- {coverImageData?.lqip_base64 && (
-

- )}
-

-
- {statusBadge && (
-
-
-
- {statusBadge.text}
-
-
- )}
-
- )}
-
- {/* Event Header */}
-
-
- {organizer && organizer.status === OrganizerStatus.LIVE ? (
-
- {organizerLogo ? (
-
- ) : (
-
- {organizer.name.charAt(0).toUpperCase()}
-
- )}
-
- {organizer.name}
-
-
- ) : (
-
- {organizerLogo ? (
-

- ) : (
-
- {organizer?.name?.charAt(0).toUpperCase() || '?'}
-
- )}
-
- {organizer?.name}
-
-
- )}
-
-
-
-
-
- {/* Future enhancement: Favorite/Heart button */}
- {/* */}
-
+ )}
+
+
+ {event &&
}
+
+
+
+ {/* Left Column - Fixed on Desktop */}
+
+ {/* Cover Image Card */}
+ {coverImage && (
+
+ {coverImageData?.lqip_base64 && (
+

+ )}
+

+
+
+ {/* Status Badge Over Image */}
+ {statusBadge && (
+
+
+
+ {statusBadge.text}
+
+ )}
-
{event.title}
-
-
- {/* Date/Time */}
-
-
-
-
-
-
-
-
+
+ {/* eslint-disable-next-line lingui/no-unlocalized-strings */}
+
+
+
+
+
+ )
+ }
+
+ {/* Organizer Card Desktop */}
+ {
+ organizer && organizer.status === OrganizerStatus.LIVE && (
+
+
{t`Presented By`}
+
+ {organizerLogo ? (
+

+ ) : (
+
+ {organizer.name.charAt(0).toUpperCase()}
-
- {/* Event Ended */}
- {event.end_date && isDateInPast(event.end_date) && (
-
-
-
-
-
-
{t`This event has ended`}
-
+ )}
+
+
+ {organizer.name}
+
+ {getShortLocationDisplay(organizerLocation) && (
+
)}
+
+
- {/* Online Event */}
- {isOnlineEvent && (
-
-
-
-
-
-
{t`Online Event`}
-
- {t`Join from anywhere`}
-
-
-
- )}
+ {/* Organizer Actions */}
+
+
- {/* Location */}
- {hasLocation && locationDetails && (
-
+
4 ? 'justify-between' : 'justify-end'}`}>
+ {websiteUrl && (
+
+
+
)}
+ {socialLinks.map(({ platform, handle, config }) => {
+ const IconComponent = config.icon;
+ return (
+
+
+
+ );
+ })}
+ )}
+
+ {event?.status && event?.id && (
+
setTimeout(() => { window.location.reload(); }, 1000)}
+ className={`${cardBg} rounded-2xl p-4 flex flex-col gap-4 border-none shadow-sm`}
+ contentClassName="flex flex-col gap-3"
+ textClassName={`font-semibold text-[15px] text-center ${cardTextPrimary}`}
+ buttonClassName="w-full py-2.5 px-4 rounded-xl text-sm font-medium transition flex justify-center items-center gap-2 shadow-sm hover:brightness-[1.15]"
+ buttonStyle={{ backgroundColor: accentColor, color: getContrastColor(accentColor) }}
+ />
+ )}
+
+
+ {/* Right Column - Main Content */}
+
- {/* About Section */}
- {event?.description && (
-
-
-
{t`About`}
+ {/* Header Details */}
+
+
+ {event.title}
+
+
+
+ {/* Date Card */}
+
+
+ {event.end_date && isDateInPast(event.end_date) ? : }
+
+
+
+ {event.end_date && isDateInPast(event.end_date) ? t`This event has ended` : }
-
+
+
+
- )}
+
- {/* Location Section (with map) */}
+ {/* Location Card */}
{hasLocation && locationDetails && (
-
-
-
{t`Location`}
+
+
+
-
)}
- {/* Tickets Section */}
-
+ {isOnlineEvent && (
+
+
+
+
+
+
{t`Online Event`}
+
{t`Join from anywhere`}
+
+
+ )}
+
+
+
+ {/* Registration / Tickets Section */}
+
+ {/* A very subtle top highlight line */}
+
+
+
+
+ {t`Registration`}
+
+
{
showPoweredBy={false}
/>
+
+
- {/* Organizer Section */}
- {organizer && organizer.status === OrganizerStatus.LIVE && (
-
-
-
{t`Organizer`}
-
-
- {organizerLogo ? (
-

- ) : (
-
- {organizer.name.charAt(0).toUpperCase()}
-
- )}
-
-
-
-
-
- {organizer.name}
-
-
- {getShortLocationDisplay(organizerLocation) && (
-
- )}
-
-
+ {/* About Section */}
+ {event?.description && (
+
+ )}
- {organizer.description && (
-
- )}
-
-
- {socialLinks.length > 0 && (
-
- {socialLinks.map(({platform, handle, config}) => {
- const IconComponent = config.icon;
- const url = config.baseUrl + handle;
- return (
-
-
-
- );
- })}
-
- )}
- {websiteUrl && (() => {
- try {
- const hostname = new URL(websiteUrl).hostname;
- return (
-
-
-
- );
- } catch {
- return null;
- }
- })()}
-
-
+ {/* Location Details block below description */}
+ {hasLocation && locationDetails && (
+
+ )}
+
+ {/* Organizer Card (Mobile View) */}
+ {organizer && organizer.status === OrganizerStatus.LIVE && (
+
+
{t`Presented By`}
+
+
+ {organizerLogo ? (
+

+ ) : (
+
+ {organizer.name.charAt(0).toUpperCase()}
+ )}
+
+
+ {organizer.name}
+
+ {getShortLocationDisplay(organizerLocation) && (
+
+ )}
- )}
-
- {/* Footer */}
-
-
-
- {t`Privacy Policy`}
-
-
- {t`Terms of Service`}
-
+ {organizer.description && (
+
+ )}
+
+
+
+
+ {websiteUrl && (
+
+
+
+ )}
+ {socialLinks.map(({ platform, handle, config }) => {
+ const IconComponent = config.icon;
+ return (
+
+
+
+ );
+ })}
+
+
-
-
-
- {/* Floating Scroll Button */}
- {showScrollButton && (
-
)}
+
+
+
+ {/* Footer */}
+
+
+
setContactModalOpen(false)} organizer={organizer} />
+
);
};
diff --git a/frontend/src/components/layouts/OrganizerHomepage/index.tsx b/frontend/src/components/layouts/OrganizerHomepage/index.tsx
index 8fde5ba47..c636e1205 100644
--- a/frontend/src/components/layouts/OrganizerHomepage/index.tsx
+++ b/frontend/src/components/layouts/OrganizerHomepage/index.tsx
@@ -1,22 +1,22 @@
-import {useLocation, useNavigate} from "react-router";
-import {ActionIcon, Anchor} from '@mantine/core';
-import {EventCard} from './EventCard';
+import { useLocation, useNavigate } from "react-router";
+import { ActionIcon, Anchor } from '@mantine/core';
+import { EventCard } from './EventCard';
import classes from './OrganizerHomepage.module.scss';
-import React, {useEffect, useState} from 'react';
-import {Event, GenericPaginatedResponse, Organizer} from "../../../types.ts";
-import {OrganizerDocumentHead} from "../../common/OrganizerDocumentHead";
-import {IconExternalLink, IconMail, IconMapPin, IconWorld} from '@tabler/icons-react';
-import {t} from "@lingui/macro";
-import {PoweredByFooter} from "../../common/PoweredByFooter";
-import {socialMediaConfig} from "../../../constants/socialMediaConfig";
-import {ContactOrganizerModal} from "../../common/ContactOrganizerModal";
-import {formatAddress, getShortLocationDisplay} from "../../../utilites/addressUtilities.ts";
-import {organizerHomepagePath} from "../../../utilites/urlHelper.ts";
-import {removeTransparency} from "../../../utilites/colorHelper.ts";
-import {StatusToggle} from "../../common/StatusToggle";
-import {getConfig} from "../../../utilites/config.ts";
-import {Pagination} from "../../common/Pagination";
-import {computeThemeVariables, validateThemeSettings} from "../../../utilites/themeUtils.ts";
+import React, { useEffect, useState } from 'react';
+import { Event, GenericPaginatedResponse, Organizer } from "../../../types.ts";
+import { OrganizerDocumentHead } from "../../common/OrganizerDocumentHead";
+import { IconExternalLink, IconMail, IconMapPin, IconWorld } from '@tabler/icons-react';
+import { t } from "@lingui/macro";
+import { PoweredByFooter } from "../../common/PoweredByFooter";
+import { socialMediaConfig } from "../../../constants/socialMediaConfig";
+import { ContactOrganizerModal } from "../../common/ContactOrganizerModal";
+import { formatAddress, getShortLocationDisplay } from "../../../utilites/addressUtilities.ts";
+import { organizerHomepagePath } from "../../../utilites/urlHelper.ts";
+import { removeTransparency } from "../../../utilites/colorHelper.ts";
+import { StatusToggle } from "../../common/StatusToggle";
+import { getConfig } from "../../../utilites/config.ts";
+import { Pagination } from "../../common/Pagination";
+import { computeThemeVariables, validateThemeSettings } from "../../../utilites/themeUtils.ts";
interface OrganizerHomepageProps {
organizer?: Organizer;
@@ -26,7 +26,7 @@ interface OrganizerHomepageProps {
}
const ScrollToTop = () => {
- const {pathname} = useLocation();
+ const { pathname } = useLocation();
useEffect(() => {
setTimeout(() => {
@@ -38,10 +38,10 @@ const ScrollToTop = () => {
}
export const OrganizerHomepage = ({
- organizer,
- eventsData,
- isPastEvents = false,
- }: OrganizerHomepageProps) => {
+ organizer,
+ eventsData,
+ isPastEvents = false,
+}: OrganizerHomepageProps) => {
const navigate = useNavigate();
const [contactModalOpen, setContactModalOpen] = useState(false);
@@ -100,7 +100,7 @@ export const OrganizerHomepage = ({
return (
<>
-
+
{organizer?.status && organizer?.id && (
)}
- {organizer &&
}
+ {organizer && }
{/* Background */}
- {(organizerCover && backgroundType === 'MIRROR_COVER_IMAGE') ? (
+ {(organizerCover && (backgroundType === 'MIRROR_COVER_IMAGE')) ? (
) : (
)}
@@ -188,7 +188,7 @@ export const OrganizerHomepage = ({
{getShortLocationDisplay(organizer?.settings?.location_details) && (
)}
{websiteUrl && (
-
+
{(socialLinks.length > 0) && (
- {socialLinks.map(({platform, handle, config}) => {
+ {socialLinks.map(({ platform, handle, config }) => {
const IconComponent = config.icon;
const url = config.baseUrl + handle;
return (
@@ -231,7 +231,7 @@ export const OrganizerHomepage = ({
variant="subtle"
size="md"
>
-
+
);
})}
@@ -241,7 +241,7 @@ export const OrganizerHomepage = ({
onClick={() => setContactModalOpen(true)}
className={classes.contactButton}
>
-
+
{t`Contact`}
@@ -251,7 +251,7 @@ export const OrganizerHomepage = ({
{organizer?.description && (
)}
@@ -335,7 +335,7 @@ export const OrganizerHomepage = ({
{t`Terms of Service`}
-
+
diff --git a/frontend/src/components/routes/event/HomepageDesigner/index.tsx b/frontend/src/components/routes/event/HomepageDesigner/index.tsx
index 6332e3e59..3423d063f 100644
--- a/frontend/src/components/routes/event/HomepageDesigner/index.tsx
+++ b/frontend/src/components/routes/event/HomepageDesigner/index.tsx
@@ -1,25 +1,25 @@
-import {useEffect, useRef, useState} from "react";
+import { useEffect, useRef, useState } from "react";
import classes from './HomepageDesigner.module.scss';
-import {useParams} from "react-router";
-import {useGetEventSettings} from "../../../../queries/useGetEventSettings.ts";
-import {useUpdateEventSettings} from "../../../../mutations/useUpdateEventSettings.ts";
-import {useFormErrorResponseHandler} from "../../../../hooks/useFormErrorResponseHandler.tsx";
-import {EventSettings, HomepageThemeSettings, IdParam} from "../../../../types.ts";
-import {showSuccess} from "../../../../utilites/notifications.tsx";
-import {t} from "@lingui/macro";
-import {useForm} from "@mantine/form";
-import {Button, Group, TextInput, Accordion, Stack, Text} from "@mantine/core";
-import {IconColorPicker, IconHelp, IconPhoto, IconPalette, IconTypography} from "@tabler/icons-react";
-import {Tooltip} from "../../../common/Tooltip";
-import {CustomSelect} from "../../../common/CustomSelect";
-import {GET_EVENT_IMAGES_QUERY_KEY, useGetEventImages} from "../../../../queries/useGetEventImages.ts";
-import {eventPreviewPath} from "../../../../utilites/urlHelper.ts";
-import {LoadingMask} from "../../../common/LoadingMask";
-import {ImageUploadDropzone} from "../../../common/ImageUploadDropzone";
-import {queryClient} from "../../../../utilites/queryClient.ts";
-import {GET_EVENT_PUBLIC_QUERY_KEY} from "../../../../queries/useGetEventPublic.ts";
-import {ThemeColorControls} from "../../../common/ThemeColorControls";
-import {validateThemeSettings} from "../../../../utilites/themeUtils.ts";
+import { useParams } from "react-router";
+import { useGetEventSettings } from "../../../../queries/useGetEventSettings.ts";
+import { useUpdateEventSettings } from "../../../../mutations/useUpdateEventSettings.ts";
+import { useFormErrorResponseHandler } from "../../../../hooks/useFormErrorResponseHandler.tsx";
+import { EventSettings, HomepageThemeSettings, IdParam } from "../../../../types.ts";
+import { showSuccess } from "../../../../utilites/notifications.tsx";
+import { t } from "@lingui/macro";
+import { useForm } from "@mantine/form";
+import { Button, Group, TextInput, Accordion, Stack, Text } from "@mantine/core";
+import { IconColorPicker, IconHelp, IconPhoto, IconPalette, IconTypography } from "@tabler/icons-react";
+import { Tooltip } from "../../../common/Tooltip";
+import { CustomSelect } from "../../../common/CustomSelect";
+import { GET_EVENT_IMAGES_QUERY_KEY, useGetEventImages } from "../../../../queries/useGetEventImages.ts";
+import { eventPreviewPath } from "../../../../utilites/urlHelper.ts";
+import { LoadingMask } from "../../../common/LoadingMask";
+import { ImageUploadDropzone } from "../../../common/ImageUploadDropzone";
+import { queryClient } from "../../../../utilites/queryClient.ts";
+import { GET_EVENT_PUBLIC_QUERY_KEY } from "../../../../queries/useGetEventPublic.ts";
+import { ThemeColorControls } from "../../../common/ThemeColorControls";
+import { validateThemeSettings } from "../../../../utilites/themeUtils.ts";
interface FormValues {
homepage_theme_settings: Partial
;
@@ -27,7 +27,7 @@ interface FormValues {
}
const HomepageDesigner = () => {
- const {eventId} = useParams();
+ const { eventId } = useParams();
const eventSettingsQuery = useGetEventSettings(eventId);
const eventImagesQuery = useGetEventImages(eventId);
const updateMutation = useUpdateEventSettings();
@@ -95,7 +95,7 @@ const HomepageDesigner = () => {
};
updateMutation.mutate(
- {eventSettings, eventId: eventId},
+ { eventSettings, eventId: eventId },
{
onSuccess: () => {
showSuccess(t`Successfully Updated Homepage Design`);
@@ -128,7 +128,7 @@ const HomepageDesigner = () => {
const settingsJson = JSON.stringify(settingsToSend);
if (settingsJson !== lastSentSettings.current) {
iframeRef.current.contentWindow.postMessage(
- {type: "UPDATE_SETTINGS", settings: settingsToSend},
+ { type: "UPDATE_SETTINGS", settings: settingsToSend },
"*"
);
lastSentSettings.current = settingsJson;
@@ -148,7 +148,7 @@ const HomepageDesigner = () => {
const value = Array.isArray(backgroundType) ? backgroundType[0] : backgroundType;
form.setFieldValue('homepage_theme_settings', {
...form.values.homepage_theme_settings,
- background_type: value as 'COLOR' | 'MIRROR_COVER_IMAGE',
+ background_type: value as 'COLOR' | 'GRADIENT' | 'MIRROR_COVER_IMAGE',
});
};
@@ -179,7 +179,7 @@ const HomepageDesigner = () => {
{t`Cover Image`}
-
+
{
,
- label: t`Color`,
+ icon: ,
+ label: t`Solid Color`,
value: 'COLOR',
- description: t`Choose a color for your background`,
+ description: t`Choose a solid color for your background`,
},
{
- icon: ,
- label: t`Use cover image`,
+ icon: ,
+ label: t`Animated Gradient`,
+ value: 'GRADIENT',
+ description: t`A fluid, animated mesh gradient`,
+ },
+ {
+ icon: ,
+ label: t`Cover Image`,
value: 'MIRROR_COVER_IMAGE',
description: t`Use a blurred version of the cover image as the background`,
disabled: !existingCover,
@@ -285,7 +291,7 @@ const HomepageDesigner = () => {
onLoad={() => setIframeLoaded(true)}
/>
) : (
-
+
)}
diff --git a/frontend/src/components/routes/organizer/OrganizerHomepageDesigner/index.tsx b/frontend/src/components/routes/organizer/OrganizerHomepageDesigner/index.tsx
index 7d79fdcd2..80e317310 100644
--- a/frontend/src/components/routes/organizer/OrganizerHomepageDesigner/index.tsx
+++ b/frontend/src/components/routes/organizer/OrganizerHomepageDesigner/index.tsx
@@ -1,32 +1,32 @@
-import {useEffect, useRef, useState} from "react";
+import { useEffect, useRef, useState } from "react";
import classes from './OrganizerHomepageDesigner.module.scss';
-import {useParams} from "react-router";
-import {useGetOrganizerSettings} from "../../../../queries/useGetOrganizerSettings.ts";
-import {useUpdateOrganizerSettings} from "../../../../mutations/useUpdateOrganizerSettings.ts";
-import {useFormErrorResponseHandler} from "../../../../hooks/useFormErrorResponseHandler.tsx";
-import {HomepageThemeSettings, IdParam, OrganizerSettings} from "../../../../types.ts";
-import {showSuccess} from "../../../../utilites/notifications.tsx";
-import {t} from "@lingui/macro";
-import {useForm} from "@mantine/form";
-import {Accordion, Button, Group, Stack, Text} from "@mantine/core";
-import {IconColorPicker, IconHelp, IconPalette, IconPhoto} from "@tabler/icons-react";
-import {Tooltip} from "../../../common/Tooltip";
-import {LoadingMask} from "../../../common/LoadingMask";
-import {CustomSelect} from "../../../common/CustomSelect";
-import {GET_ORGANIZER_QUERY_KEY, useGetOrganizer} from "../../../../queries/useGetOrganizer.ts";
-import {ImageUploadDropzone} from "../../../common/ImageUploadDropzone";
-import {organizerPreviewPath} from "../../../../utilites/urlHelper.ts";
-import {queryClient} from "../../../../utilites/queryClient.ts";
-import {GET_ORGANIZER_PUBLIC_QUERY_KEY} from "../../../../queries/useGetOrganizerPublic.ts";
-import {ThemeColorControls} from "../../../common/ThemeColorControls";
-import {computeThemeVariables, validateThemeSettings} from "../../../../utilites/themeUtils.ts";
+import { useParams } from "react-router";
+import { useGetOrganizerSettings } from "../../../../queries/useGetOrganizerSettings.ts";
+import { useUpdateOrganizerSettings } from "../../../../mutations/useUpdateOrganizerSettings.ts";
+import { useFormErrorResponseHandler } from "../../../../hooks/useFormErrorResponseHandler.tsx";
+import { HomepageThemeSettings, IdParam, OrganizerSettings } from "../../../../types.ts";
+import { showSuccess } from "../../../../utilites/notifications.tsx";
+import { t } from "@lingui/macro";
+import { useForm } from "@mantine/form";
+import { Accordion, Button, Group, Stack, Text } from "@mantine/core";
+import { IconColorPicker, IconHelp, IconPalette, IconPhoto } from "@tabler/icons-react";
+import { Tooltip } from "../../../common/Tooltip";
+import { LoadingMask } from "../../../common/LoadingMask";
+import { CustomSelect } from "../../../common/CustomSelect";
+import { GET_ORGANIZER_QUERY_KEY, useGetOrganizer } from "../../../../queries/useGetOrganizer.ts";
+import { ImageUploadDropzone } from "../../../common/ImageUploadDropzone";
+import { organizerPreviewPath } from "../../../../utilites/urlHelper.ts";
+import { queryClient } from "../../../../utilites/queryClient.ts";
+import { GET_ORGANIZER_PUBLIC_QUERY_KEY } from "../../../../queries/useGetOrganizerPublic.ts";
+import { ThemeColorControls } from "../../../common/ThemeColorControls";
+import { computeThemeVariables, validateThemeSettings } from "../../../../utilites/themeUtils.ts";
interface FormValues {
homepage_theme_settings: Partial
;
}
const OrganizerHomepageDesigner = () => {
- const {organizerId} = useParams();
+ const { organizerId } = useParams();
const organizerSettingsQuery = useGetOrganizerSettings(organizerId);
const organizerQuery = useGetOrganizer(organizerId);
const updateMutation = useUpdateOrganizerSettings();
@@ -120,7 +120,7 @@ const OrganizerHomepageDesigner = () => {
const settingsJson = JSON.stringify(settingsToSend);
if (settingsJson !== lastSentSettings.current) {
iframeRef.current.contentWindow.postMessage(
- {type: "UPDATE_ORGANIZER_SETTINGS", settings: settingsToSend},
+ { type: "UPDATE_ORGANIZER_SETTINGS", settings: settingsToSend },
"*"
);
lastSentSettings.current = settingsJson;
@@ -158,7 +158,7 @@ const OrganizerHomepageDesigner = () => {
const value = Array.isArray(backgroundType) ? backgroundType[0] : backgroundType;
form.setFieldValue('homepage_theme_settings', {
...form.values.homepage_theme_settings,
- background_type: value as 'COLOR' | 'MIRROR_COVER_IMAGE',
+ background_type: value as 'COLOR' | 'GRADIENT' | 'MIRROR_COVER_IMAGE',
});
};
@@ -179,7 +179,7 @@ const OrganizerHomepageDesigner = () => {
className={classes.accordion}
>
- }>
+ }>
{t`Images`}
@@ -189,7 +189,7 @@ const OrganizerHomepageDesigner = () => {
{t`Cover Image`}
-
+
{
{t`Logo`}
-
+
{
- }>
+ }>
{t`Theme & Colors`}
diff --git a/frontend/src/components/routes/product-widget/SelectProducts/Prices/Tiered/index.tsx b/frontend/src/components/routes/product-widget/SelectProducts/Prices/Tiered/index.tsx
index 5b40ecbf7..0bc00bb10 100644
--- a/frontend/src/components/routes/product-widget/SelectProducts/Prices/Tiered/index.tsx
+++ b/frontend/src/components/routes/product-widget/SelectProducts/Prices/Tiered/index.tsx
@@ -1,93 +1,272 @@
-import {Currency, ProductPriceDisplay} from "../../../../../common/Currency";
-import {Event, Product} from "../../../../../../types.ts";
-import {Group, TextInput} from "@mantine/core";
-import {NumberSelector} from "../../../../../common/NumberSelector";
-import {UseFormReturnType} from "@mantine/form";
-import {t} from "@lingui/macro";
-import {ProductPriceAvailability} from "../../../../../common/ProductPriceAvailability";
-import {getCurrencySymbol} from "../../../../../../utilites/currency.ts";
+/* eslint-disable lingui/no-unlocalized-strings */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { Currency, ProductPriceDisplay } from "../../../../../common/Currency";
+import { Event, Product } from "../../../../../../types.ts";
+import { Group, TextInput, Modal, Button } from "@mantine/core";
+import { NumberSelector } from "../../../../../common/NumberSelector";
+import { useState, useEffect, useRef } from "react";
+import { UseFormReturnType } from "@mantine/form";
+import { t } from "@lingui/macro";
+import { ProductPriceAvailability } from "../../../../../common/ProductPriceAvailability";
+import { getCurrencySymbol } from "../../../../../../utilites/currency.ts";
+import { getContrastColor, hexToRgb } from "../../../../../../utilites/themeUtils";
interface TieredPricingProps {
event: Event;
product: Product;
- form: UseFormReturnType
;
+ form: UseFormReturnType; // Need 'any' here due to Mantine form typing complexity with nested arrays, but we will ignore the warning
productIndex: number;
+ colors?: {
+ primary?: string;
+ primaryText?: string;
+ secondaryText?: string;
+ } | undefined;
}
-export const TieredPricing = ({product, event, form, productIndex}: TieredPricingProps) => {
+export const TieredPricing = ({ event, product, form, productIndex, colors }: TieredPricingProps) => {
+ const isDarkMode = colors?.primaryText === '#ffffff';
+ const textPrimaryClass = isDarkMode ? 'text-white' : 'text-gray-900';
+ const textSecondaryClass = isDarkMode ? 'text-gray-300' : 'text-gray-500';
+ const borderClass = isDarkMode ? 'border-white/10' : 'border-gray-100';
+ const mutedBgClass = isDarkMode ? 'bg-white/10' : 'bg-gray-100';
+
+ const [isDonationModalOpen, setIsDonationModalOpen] = useState(false);
+ const [donationModalIndex, setDonationModalIndex] = useState(null);
+ const [tempDonationAmount, setTempDonationAmount] = useState('');
+ const [showHintIndex, setShowHintIndex] = useState(null);
+
+ // To detect when a quantity increments, we can use a ref to store previous quantities
+ const prevQuantitiesRef = useRef([]);
+
+ useEffect(() => {
+ if (product.type !== 'DONATION') return;
+
+ const currentQuantities = product.prices?.map((_, index) => {
+ return form.values.products?.[productIndex]?.quantities?.[index]?.quantity || 0;
+ }) || [];
+
+ let shouldUpdateRef = false;
+ currentQuantities.forEach((qty, index) => {
+ const prevQty = prevQuantitiesRef.current[index] || 0;
+ if (qty > prevQty) {
+ // Quantity increased
+ setShowHintIndex(index);
+ // Hide after 4 seconds
+ setTimeout(() => {
+ setShowHintIndex(curr => curr === index ? null : curr);
+ }, 4000);
+ }
+ if (qty !== prevQty) {
+ shouldUpdateRef = true;
+ }
+ });
+
+ if (shouldUpdateRef || prevQuantitiesRef.current.length === 0) {
+ prevQuantitiesRef.current = currentQuantities;
+ }
+ }, [form.values.products?.[productIndex]?.quantities, product.prices, product.type, productIndex]);
+
+ const handleOpenDonationModal = (index: number) => {
+ const currentPrice = form.values.products?.[productIndex]?.quantities?.[index]?.price || product.price || 0;
+ setTempDonationAmount(currentPrice);
+ setDonationModalIndex(index);
+ setIsDonationModalOpen(true);
+ setShowHintIndex(null); // Hide hint if they clicked
+ };
+
+ const handleConfirmDonation = () => {
+ if (donationModalIndex !== null && tempDonationAmount !== '' && tempDonationAmount >= (product.price || 0)) {
+ form.setFieldValue(`products.${productIndex}.quantities.${donationModalIndex}.price`, tempDonationAmount);
+ setIsDonationModalOpen(false);
+ }
+ };
+
return (
<>
+ {/* Donation Dialog */}
+ setIsDonationModalOpen(false)}
+ centered
+ withCloseButton={false}
+ overlayProps={{
+ color: '#000000',
+ opacity: 0.85,
+ blur: 15,
+ }}
+ styles={{
+ content: {
+ backgroundColor: isDarkMode ? 'rgba(20, 20, 20, 0.65)' : 'rgba(255, 255, 255, 0.85)',
+ backdropFilter: 'blur(40px)',
+ WebkitBackdropFilter: 'blur(40px)',
+ border: `1px solid ${isDarkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)'}`,
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.7)',
+ borderRadius: '28px',
+ color: isDarkMode ? '#fff' : '#000',
+ },
+ body: {
+ padding: '40px 32px',
+ },
+ header: {
+ display: 'none',
+ }
+ }}
+ >
+
+
+ {t`Custom Donation Amount`}
+
+
+ {t`Minimum amount:`} {getCurrencySymbol(event?.currency)}{product.price}
+
+
+
+
+ {getCurrencySymbol(event?.currency)}
+ setTempDonationAmount(e.currentTarget.value ? parseFloat(e.currentTarget.value) : '')}
+ min={product.price}
+ step={0.01}
+ className="bg-transparent border-none outline-none w-32 text-center p-0 appearance-none m-0 shadow-none focus:ring-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
+ />
+
+ {tempDonationAmount !== '' && tempDonationAmount < (product.price || 0) && (
+
+ {t`Must be at least`} {getCurrencySymbol(event?.currency)}{product.price}
+
+ )}
+
+
+
+
+
+
+
+
{product?.prices?.map((price, index) => {
return (
-
-
-
-
{price.label}
-
- {product.type === 'DONATION' && (
+
+
+ {price.label &&
{price.label}
}
+
+ {product.type === 'DONATION' && (
+
-
+ className={`absolute left-1/2 -translate-x-1/2 bottom-[calc(100%+8px)] z-50 px-2.5 py-1.5 rounded-lg text-[11px] font-semibold whitespace-nowrap shadow-lg transition-all duration-300 ease-in-out border ${showHintIndex === index
+ ? 'opacity-100 translate-y-0 scale-100'
+ : 'opacity-0 translate-y-2 scale-95 pointer-events-none'
+ } ${isDarkMode ? 'bg-black/90 text-white border-white/10 backdrop-blur-md' : 'bg-white text-gray-900 border-gray-200 shadow-xl'}`}
+ >
+ {t`Click here to edit donation`}
+ {/* Tooltip Arrow */}
+
- )}
- {product.type !== 'DONATION' && (
-
handleOpenDonationModal(index)}
+ >
+
+ {getCurrencySymbol(event?.currency)}
+ {form.values.products?.[productIndex]?.quantities?.[index]?.price || product.price || 0}
+
+
+
+
+ )}
+ {product.type !== 'DONATION' && (
+
+ )}
+ {price.is_discounted && (
+
+
- )}
-
-
-
- {(product.is_available && price.is_available) && (
- <>
-
- {form.errors[`products.${productIndex}.quantities.${index}.quantity`] && (
-
- {form.errors[`products.${productIndex}.quantities.${index}.quantity`]}
-
- )}
- >
- )}
- {(!product.is_available || !price.is_available) && (
-
+
)}
-
+
+
+ {(product.is_available && price.is_available) && (
+
+
-
-
- )}
+ />
+ {form.errors[`products.${productIndex}.quantities.${index}.quantity`] && (
+
+ {form.errors[`products.${productIndex}.quantities.${index}.quantity`]}
+
+ )}
+
+ )}
+ {(!product.is_available || !price.is_available) && (
+
+
+
+ )}
+
);
})}
diff --git a/frontend/src/components/routes/product-widget/SelectProducts/index.tsx b/frontend/src/components/routes/product-widget/SelectProducts/index.tsx
index 2223f7918..fc5d013ce 100644
--- a/frontend/src/components/routes/product-widget/SelectProducts/index.tsx
+++ b/frontend/src/components/routes/product-widget/SelectProducts/index.tsx
@@ -1,4 +1,5 @@
-import {t, Trans} from "@lingui/macro";
+/* eslint-disable lingui/no-unlocalized-strings */
+import { t, Trans } from "@lingui/macro";
import {
ActionIcon,
Anchor,
@@ -11,31 +12,31 @@ import {
TextInput,
UnstyledButton
} from "@mantine/core";
-import {useNavigate, useParams} from "react-router";
-import {useMutation, useQueryClient} from "@tanstack/react-query";
-import {notifications} from "@mantine/notifications";
+import { useNavigate, useParams } from "react-router";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { notifications } from "@mantine/notifications";
import {
orderClientPublic,
ProductFormPayload,
ProductFormValue,
ProductPriceQuantityFormValue
} from "../../../../api/order.client.ts";
-import {useForm} from "@mantine/form";
-import {range, useInputState, useResizeObserver} from "@mantine/hooks";
-import React, {useEffect, useMemo, useRef, useState} from "react";
-import {showError, showInfo, showSuccess} from "../../../../utilites/notifications.tsx";
-import {addQueryStringToUrl, isObjectEmpty, removeQueryStringFromUrl} from "../../../../utilites/helpers.ts";
-import {TieredPricing} from "./Prices/Tiered";
+import { useForm } from "@mantine/form";
+import { range, useInputState, useResizeObserver } from "@mantine/hooks";
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { showError, showInfo, showSuccess } from "../../../../utilites/notifications.tsx";
+import { addQueryStringToUrl, isObjectEmpty, removeQueryStringFromUrl } from "../../../../utilites/helpers.ts";
+import { TieredPricing } from "./Prices/Tiered";
import classNames from 'classnames';
import '../../../../styles/widget/default.scss';
-import {ProductAvailabilityMessage} from "../../../common/ProductPriceAvailability";
-import {PoweredByFooter} from "../../../common/PoweredByFooter";
-import {Event, Product} from "../../../../types.ts";
-import {eventsClientPublic} from "../../../../api/event.client.ts";
-import {promoCodeClientPublic} from "../../../../api/promo-code.client.ts";
-import {IconChevronRight, IconX} from "@tabler/icons-react"
-import {getSessionIdentifier} from "../../../../utilites/sessionIdentifier.ts";
-import {Constants} from "../../../../constants.ts";
+import { ProductAvailabilityMessage } from "../../../common/ProductPriceAvailability";
+import { PoweredByFooter } from "../../../common/PoweredByFooter";
+import { Event, Product } from "../../../../types.ts";
+import { eventsClientPublic } from "../../../../api/event.client.ts";
+import { promoCodeClientPublic } from "../../../../api/promo-code.client.ts";
+import { IconChevronDown, IconChevronRight, IconX } from "@tabler/icons-react"
+import { getSessionIdentifier } from "../../../../utilites/sessionIdentifier.ts";
+import { Constants } from "../../../../constants.ts";
const AFFILIATE_EXPIRY_DAYS = 30;
@@ -62,7 +63,7 @@ interface SelectProductsProps {
event: Event;
promoCodeValid?: boolean;
promoCode?: string;
- backgroundType?: 'COLOR' | 'MIRROR_COVER_IMAGE';
+ backgroundType?: 'COLOR' | 'GRADIENT' | 'MIRROR_COVER_IMAGE';
colors?: {
primary?: string;
primaryText?: string;
@@ -77,8 +78,56 @@ interface SelectProductsProps {
showPoweredBy?: boolean;
}
+const TextSpoiler = ({ html, isDarkMode, lineClampClass = "line-clamp-3" }: { html: string, isDarkMode: boolean, lineClampClass?: string }) => {
+ const [expanded, setExpanded] = useState(false);
+ // Start as `true` so the button is present in the SSR HTML — preventing CLS.
+ // After hydration, the ResizeObserver will set this to false if text isn't actually truncated.
+ const [mayBeTruncated, setMayBeTruncated] = useState(true);
+ const contentRef = useRef(null);
+
+ useEffect(() => {
+ if (!contentRef.current) return;
+
+ const checkTruncation = () => {
+ if (contentRef.current && !expanded) {
+ setMayBeTruncated(contentRef.current.scrollHeight > contentRef.current.clientHeight + 2);
+ }
+ };
+
+ checkTruncation();
+
+ const resizeObserver = new ResizeObserver(() => {
+ checkTruncation();
+ });
+ resizeObserver.observe(contentRef.current);
+ return () => resizeObserver.disconnect();
+ }, [expanded, html]);
+
+ return (
+
+
+
+ {(mayBeTruncated || expanded) && (
+
+ )}
+
+
+ );
+};
+
const SelectProducts = (props: SelectProductsProps) => {
- const {eventId} = useParams();
+ const { eventId } = useParams();
const queryClient = useQueryClient();
const navigate = useNavigate();
@@ -99,7 +148,7 @@ const SelectProducts = (props: SelectProductsProps) => {
const affiliateCodeFromUrl = new URLSearchParams(window.location.search).get('aff');
if (affiliateCodeFromUrl) {
- const data = {code: affiliateCodeFromUrl, timestamp: now};
+ const data = { code: affiliateCodeFromUrl, timestamp: now };
localStorage.setItem(storageKey, JSON.stringify(data));
setAffiliateCode(affiliateCodeFromUrl);
return;
@@ -201,8 +250,8 @@ const SelectProducts = (props: SelectProductsProps) => {
const selectedProductQuantitySum = useMemo(() => {
let total = 0;
- form.values.products?.forEach(({quantities}) => {
- quantities?.forEach(({quantity}) => {
+ form.values.products?.forEach(({ quantities }) => {
+ quantities?.forEach(({ quantity }) => {
total += Number(quantity);
});
});
@@ -291,22 +340,30 @@ const SelectProducts = (props: SelectProductsProps) => {
|| props.widgetMode === 'preview'
|| products?.every(product => product.is_sold_out);
+ const isDarkMode = props.colors?.primaryText === '#ffffff';
+ const cardBgClass = isDarkMode ? 'bg-black/20 backdrop-blur-2xl border-white/10 hover:bg-black/30 hover:border-white/20 shadow-xl' : 'bg-white/40 backdrop-blur-2xl border-white/50 hover:border-white/70 shadow-xl';
+ const textPrimaryClass = isDarkMode ? 'text-white' : 'text-gray-900';
+ const textSecondaryClass = isDarkMode ? 'text-gray-300' : 'text-gray-500';
+ const mutedBgClass = isDarkMode ? 'bg-black/20 backdrop-blur-xl border-white/10' : 'bg-white/30 backdrop-blur-xl border-white/40';
+ const highlightCardClass = isDarkMode ? 'border-primary bg-primary/20 backdrop-blur-2xl shadow-[0_0_15px_rgba(var(--theme-accent-rgb),0.1)] ring-1 ring-primary/50' : 'border-primary bg-primary/10 backdrop-blur-2xl shadow-xl ring-1 ring-primary/50';
+
let productIndex = 0;
return (
-
+
{!productAreAvailable && (
-
+
{t`There are no products available for this event`}
@@ -334,7 +391,7 @@ const SelectProducts = (props: SelectProductsProps) => {
gap: '16px',
color: props.colors?.primaryText || 'inherit'
}}>
-
+
{
onClick={() => setOrderInProcessOverlayVisible(false)}
variant={'subtle'}
size={'sm'}
+ className={textPrimaryClass}
styles={{
root: {
- color: props.colors?.primaryText || 'var(--primary-color, #228be6)',
'&:hover': {
backgroundColor: 'transparent',
textDecoration: 'underline'
@@ -397,203 +454,257 @@ const SelectProducts = (props: SelectProductsProps) => {
)}
{(event && productAreAvailable) && (
-