diff --git a/apps/web/e2e/__screenshots__/desktop-chromium/community-profile.png b/apps/web/e2e/__screenshots__/desktop-chromium/community-profile.png index 8fd7349..3f359a6 100644 Binary files a/apps/web/e2e/__screenshots__/desktop-chromium/community-profile.png and b/apps/web/e2e/__screenshots__/desktop-chromium/community-profile.png differ diff --git a/apps/web/e2e/__screenshots__/desktop-chromium/event-profile.png b/apps/web/e2e/__screenshots__/desktop-chromium/event-profile.png index 6c4c596..26010f3 100644 Binary files a/apps/web/e2e/__screenshots__/desktop-chromium/event-profile.png and b/apps/web/e2e/__screenshots__/desktop-chromium/event-profile.png differ diff --git a/apps/web/e2e/__screenshots__/desktop-chromium/event-watch-surface.png b/apps/web/e2e/__screenshots__/desktop-chromium/event-watch-surface.png index bcbb8c8..e789780 100644 Binary files a/apps/web/e2e/__screenshots__/desktop-chromium/event-watch-surface.png and b/apps/web/e2e/__screenshots__/desktop-chromium/event-watch-surface.png differ diff --git a/apps/web/e2e/__screenshots__/desktop-chromium/person-profile.png b/apps/web/e2e/__screenshots__/desktop-chromium/person-profile.png index 78ca299..c6eeccd 100644 Binary files a/apps/web/e2e/__screenshots__/desktop-chromium/person-profile.png and b/apps/web/e2e/__screenshots__/desktop-chromium/person-profile.png differ diff --git a/apps/web/e2e/__screenshots__/mobile-chromium/community-profile.png b/apps/web/e2e/__screenshots__/mobile-chromium/community-profile.png index 9d09bf0..9a67380 100644 Binary files a/apps/web/e2e/__screenshots__/mobile-chromium/community-profile.png and b/apps/web/e2e/__screenshots__/mobile-chromium/community-profile.png differ diff --git a/apps/web/e2e/__screenshots__/mobile-chromium/event-profile.png b/apps/web/e2e/__screenshots__/mobile-chromium/event-profile.png index 50715d7..816e6e9 100644 Binary files a/apps/web/e2e/__screenshots__/mobile-chromium/event-profile.png and b/apps/web/e2e/__screenshots__/mobile-chromium/event-profile.png differ diff --git a/apps/web/e2e/__screenshots__/mobile-chromium/event-watch-surface.png b/apps/web/e2e/__screenshots__/mobile-chromium/event-watch-surface.png index ad30b43..1ba490a 100644 Binary files a/apps/web/e2e/__screenshots__/mobile-chromium/event-watch-surface.png and b/apps/web/e2e/__screenshots__/mobile-chromium/event-watch-surface.png differ diff --git a/apps/web/e2e/__screenshots__/mobile-chromium/person-profile.png b/apps/web/e2e/__screenshots__/mobile-chromium/person-profile.png index f6b39c9..a161aa2 100644 Binary files a/apps/web/e2e/__screenshots__/mobile-chromium/person-profile.png and b/apps/web/e2e/__screenshots__/mobile-chromium/person-profile.png differ diff --git a/apps/web/src/app/_components/event-public-page.tsx b/apps/web/src/app/_components/event-public-page.tsx index 621cf76..3a0fcf6 100644 --- a/apps/web/src/app/_components/event-public-page.tsx +++ b/apps/web/src/app/_components/event-public-page.tsx @@ -56,6 +56,9 @@ export type PublicEventPreview = { communitySlug?: string; summary?: string; posterImageUrl?: string; + bannerImageUrl?: string; + thumbnailImageUrl?: string; + communityImageUrl?: string; source: { sourceType: EventSourceType; label: string; @@ -72,6 +75,9 @@ export type PublicEventPreview = { export type PublicEvent = Omit & { slug: string; notes?: string; + watchSurfaceEnabled: boolean; + authoredBannerImageUrl?: string; + authoredThumbnailImageUrl?: string; authoredMediaLinks: Array<{ type: EventMediaLinkType; label: string; @@ -101,6 +107,7 @@ export type PublicEvent = Omit & { displayName: string; roleLabel: string; trustLabel: ProfileTrustLabel; + imageUrl?: string; source: { sourceType: EventSourceType; label: string; @@ -118,6 +125,7 @@ export type PublicEvent = Omit & { slug: string; displayName: string; trustLabel: ProfileTrustLabel; + imageUrl?: string; }; source: { sourceType: EventSourceType; @@ -128,6 +136,7 @@ export type PublicEvent = Omit & { }; const eventPosterOverlay = "linear-gradient(135deg, rgba(25, 17, 31, 0.72), rgba(105, 56, 169, 0.2))"; +const eventEntityImageOverlay = "linear-gradient(135deg, rgba(27, 18, 37, 0.28), rgba(105, 56, 169, 0.14))"; function safeHttpsUrl(url: string | undefined): string | null { if (!url) { @@ -142,6 +151,34 @@ function safeHttpsUrl(url: string | undefined): string | null { } } +function initialsFor(name: string): string { + const initials = name + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join(""); + + return initials || "V"; +} + +function EntityImage({ imageUrl, label }: { imageUrl?: string; label: string }) { + const imageStyle = safeImageBackground(imageUrl, eventEntityImageOverlay); + + return ( + + ); +} + function EventTimeDefinition({ label, timestamp }: { label: string; timestamp: number }) { return (
@@ -155,14 +192,14 @@ function EventTimeDefinition({ label, timestamp }: { label: string; timestamp: n export function EventPreviewCard({ event }: { event: PublicEventPreview }) { const sourceUrl = safeHttpsUrl(event.source.url); - const posterStyle = safeImageBackground(event.posterImageUrl, eventPosterOverlay); + const thumbnailStyle = safeImageBackground(event.thumbnailImageUrl, eventPosterOverlay); const details = event.worlds.map((world) => world.displayName); return (
@@ -224,7 +261,7 @@ export function EventBackendNotice({ kind }: { kind: "missing-url" | "error" }) } export function EventPublicPage({ event, showEditLink = false }: { event: PublicEvent; showEditLink?: boolean }) { - const posterStyle = safeImageBackground(event.posterImageUrl, eventPosterOverlay); + const bannerStyle = safeImageBackground(event.bannerImageUrl, eventPosterOverlay); const sourceUrl = safeHttpsUrl(event.source.url); return ( @@ -247,7 +284,7 @@ export function EventPublicPage({ event, showEditLink = false }: { event: Public
@@ -282,6 +319,7 @@ export function EventPublicPage({ event, showEditLink = false }: { event: Public @@ -290,11 +328,14 @@ export function EventPublicPage({ event, showEditLink = false }: { event: Public Place
{event.communitySlug ? ( - - - {event.communityName ?? "Community profile"} + + + + + {event.communityName ?? "Community profile"} + + Host - Host ) : event.communityName ? (
@@ -304,12 +345,15 @@ export function EventPublicPage({ event, showEditLink = false }: { event: Public

No host listed.

)} {event.worlds.map((world) => ( - - - {world.displayName} + + + + + {world.displayName} + + {world.summary ? {world.summary} : null} + World - {world.summary ? {world.summary} : null} - World ))}
@@ -379,12 +423,15 @@ export function EventPublicPage({ event, showEditLink = false }: { event: Public

No lineup yet.

) : ( event.participants.map((participant) => ( - - - {participant.displayName} + + + + + {participant.displayName} + + {participant.roleLabel} + Profile - {participant.roleLabel} - Profile )) )} diff --git a/apps/web/src/app/_components/event-watch-surface.tsx b/apps/web/src/app/_components/event-watch-surface.tsx index 8f61f44..4058ff8 100644 --- a/apps/web/src/app/_components/event-watch-surface.tsx +++ b/apps/web/src/app/_components/event-watch-surface.tsx @@ -444,11 +444,13 @@ function WatchHlsVideo({ embed }: { embed: Extract export function EventWatchSurface({ doorsOpenAt, endAt, + enabled, mediaLinks, startAt, }: { doorsOpenAt?: number; endAt?: number; + enabled: boolean; mediaLinks: EventWatchMediaLink[]; startAt: number; }) { @@ -456,7 +458,7 @@ export function EventWatchSurface({ const currentTimestamp = useCurrentTimestamp(); const primaryWatchLink = selectPrimaryWatchLink(mediaLinks); - if (!primaryWatchLink || !isInScheduledWatchWindow({ doorsOpenAt, endAt, now: currentTimestamp, startAt })) { + if (!enabled || !primaryWatchLink || !isInScheduledWatchWindow({ doorsOpenAt, endAt, now: currentTimestamp, startAt })) { return null; } diff --git a/apps/web/src/app/_components/world-public-page.tsx b/apps/web/src/app/_components/world-public-page.tsx index 9b154c4..d8d5c34 100644 --- a/apps/web/src/app/_components/world-public-page.tsx +++ b/apps/web/src/app/_components/world-public-page.tsx @@ -42,6 +42,8 @@ type PublicWorldEventPreview = { communityName?: string; summary?: string; posterImageUrl?: string; + bannerImageUrl?: string; + thumbnailImageUrl?: string; mediaLinks: Array<{ type: "event_page" | "watch" | "stream" | "vrcdn" | "discord" | "ticket" | "other"; label: string; @@ -203,7 +205,7 @@ function EventList({
{events.map((event) => { const sourceUrl = event.source.url ? safeHttpsUrl(event.source.url) : null; - const posterStyle = safeImageBackground(event.posterImageUrl, worldHeroOverlay); + const posterStyle = safeImageBackground(event.thumbnailImageUrl, worldHeroOverlay); const posterTextClass = posterStyle ? "text-white/76" : "text-muted"; return ( diff --git a/apps/web/src/app/api/e2e/fixture-assets/[assetId]/route.ts b/apps/web/src/app/api/e2e/fixture-assets/[assetId]/route.ts index dcaec4f..82a5c86 100644 --- a/apps/web/src/app/api/e2e/fixture-assets/[assetId]/route.ts +++ b/apps/web/src/app/api/e2e/fixture-assets/[assetId]/route.ts @@ -17,6 +17,7 @@ type FixtureAsset = { from: string; to: string; accent: string; + showText?: boolean; }; const fixtureAssets: Record = { @@ -60,6 +61,28 @@ const fixtureAssets: Record = { to: "#0e7490", accent: "#fb7185", }, + "fixture-afterglow-event-banner": { + title: "Afterglow Harbor banner", + subtitle: "Hero artwork", + initials: "", + width: 1600, + height: 700, + from: "#121629", + to: "#134e67", + accent: "#fb7185", + showText: false, + }, + "fixture-afterglow-event-thumbnail": { + title: "Afterglow Harbor card", + subtitle: "Event thumbnail", + initials: "AG", + width: 960, + height: 960, + from: "#2b1721", + to: "#0e7490", + accent: "#fb7185", + showText: false, + }, }; function fixtureError(message: string, status = 403) { @@ -88,6 +111,13 @@ function renderSvg(asset: FixtureAsset) { const initials = escapeSvgText(asset.initials); const radius = Math.min(asset.width, asset.height) * 0.24; + const text = asset.showText === false + ? "" + : ` + ${initials} + ${title} + ${subtitle.toUpperCase()}`; + return ` @@ -103,9 +133,7 @@ function renderSvg(asset: FixtureAsset) { - ${initials} - ${title} - ${subtitle.toUpperCase()} + ${text} `; } diff --git a/apps/web/src/app/events/event-editor-form.tsx b/apps/web/src/app/events/event-editor-form.tsx index 1f637fd..449975e 100644 --- a/apps/web/src/app/events/event-editor-form.tsx +++ b/apps/web/src/app/events/event-editor-form.tsx @@ -63,7 +63,7 @@ const userSafeErrorPatterns = [ /Event end time must be after the start time\./, /Time zone must be a valid IANA time zone\./, /Time zone is required when event slots are provided\./, - /(?:Event source URL|Poster image URL|Media link URL|Participant source URL|Slot source URL) must (?:use https|be a valid URL)\./, + /(?:Event source URL|Poster image URL|Banner image URL|Thumbnail image URL|Media link URL|Participant source URL|Slot source URL) must (?:use https|be a valid URL)\./, /(?:Slot start time|Slot end time) must be a valid timestamp\./, /(?:Slot count|Slot offset minutes|Slot duration minutes|Break duration minutes) must be a whole number\./, /Slot end time must be after the start time\./, @@ -579,6 +579,9 @@ function ConnectedEventEditorForm({ event }: { event?: PublicEvent }) { sourceLabel: optionalString(stringField(formData.get("sourceLabel"))), sourceUrl: optionalString(stringField(formData.get("sourceUrl"))), posterImageUrl: optionalString(stringField(formData.get("posterImageUrl"))), + bannerImageUrl: optionalString(stringField(formData.get("bannerImageUrl"))), + thumbnailImageUrl: optionalString(stringField(formData.get("thumbnailImageUrl"))), + watchSurfaceEnabled: formData.get("watchSurfaceEnabled") === "on", mediaLinks: parseMediaLinks(mediaLinksText), participantLinks: parseParticipantLinks(stringField(formData.get("participantLinks"))), slotLinks: parseSlotLinks(slotText, startAt), @@ -682,18 +685,45 @@ function ConnectedEventEditorForm({ event }: { event?: PublicEvent }) {
+
+ + Banner image URL + + Wide event-page hero image. Falls back to the poster image. + + + Thumbnail image URL + + Compact event-card image. Falls back to the poster or banner image. + +
+ Media links