Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions buzz/api/events/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ class EventExternalLink(APIResponse):
url: str


class EventCountdown(APIResponse):
starts_at: str
ends_at: str


class EventDetail(APIResponse):
"""One event, with everything the manage page edits or shows."""

Expand All @@ -82,6 +87,9 @@ class EventDetail(APIResponse):
# The organiser's own link, or the one Zoom issued when the meeting was booked.
meeting_link: str | None = None
is_published: bool
show_countdown: bool = False
# None once the event is over.
countdown: EventCountdown | None = None
primary_host: EventHostRef | None = None
co_hosts: list[EventHostRef] = Field(default_factory=list)
external_links: list[EventExternalLink] = Field(default_factory=list)
Expand Down
3 changes: 3 additions & 0 deletions buzz/api/events/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
)
from buzz.api.filters.conditions import ListConditions, event_questions, filter_field, question_fields
from buzz.api.filters.schemas import FilterField
from buzz.events.countdown import event_countdown
from buzz.events.doctype.buzz_event.buzz_event import RESERVED_EVENT_ROUTES, BuzzEvent
from buzz.permissions import has_team_access, my_teams
from buzz.utils import is_app_installed
Expand Down Expand Up @@ -145,6 +146,7 @@ def split_by_date(events: list[MyEvent]) -> tuple[list[MyEvent], list[MyEvent]]:
"venue",
"meeting_link",
"is_published",
"show_countdown",
)


Expand All @@ -167,6 +169,7 @@ def event_detail(event: str) -> EventDetail:
"primary_host": primary_host_of(row.team),
"co_hosts": co_hosts_of(event),
"external_links": external_links_of(event),
"countdown": event_countdown(row),
}
)

Expand Down
20 changes: 20 additions & 0 deletions buzz/events/countdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from datetime import datetime
from datetime import time as clock_time

from frappe.utils import get_time, getdate

from buzz.www.event.meta import event_time_zone


def event_countdown(event) -> dict | None:
"""Start and end of the banner countdown, or None once the event is over."""
# An event with no closing time runs to the end of its last day.
# Whole seconds: JavaScript only promises to parse milliseconds.
zone = event_time_zone(event)
start_time = get_time(event.start_time) if event.start_time else clock_time.min
end_time = get_time(event.end_time) if event.end_time else clock_time(23, 59, 59)
starts_at = datetime.combine(getdate(event.start_date), start_time, zone)
ends_at = datetime.combine(getdate(event.end_date or event.start_date), end_time, zone)
if ends_at <= datetime.now(zone):
return None
return {"starts_at": starts_at.isoformat(), "ends_at": ends_at.isoformat()}
10 changes: 9 additions & 1 deletion buzz/events/doctype/buzz_event/buzz_event.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"website_tab",
"is_published",
"is_featured",
"show_countdown",
"route",
"default_ticket_type",
"column_break_ndtl",
Expand Down Expand Up @@ -247,6 +248,13 @@
"label": "Featured",
"permlevel": 1
},
{
"default": "0",
"description": "Shows the time until the event starts on the event page banner.",
"fieldname": "show_countdown",
"fieldtype": "Check",
"label": "Show Event Countdown"
},
{
"fieldname": "tab_2_tab",
"fieldtype": "Tab Break",
Expand Down Expand Up @@ -681,7 +689,7 @@
"link_fieldname": "event"
}
],
"modified": "2026-10-02 12:00:00.000000",
"modified": "2026-10-12 12:00:00.000000",
"modified_by": "Administrator",
"module": "Events",
"name": "Buzz Event",
Expand Down
1 change: 1 addition & 0 deletions buzz/events/doctype/buzz_event/buzz_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class BuzzEvent(Document):
send_booking_confirmation_email: DF.Check
send_ticket_email: DF.Check
short_description: DF.SmallText | None
show_countdown: DF.Check
show_sponsorship_section: DF.Check
sponsor_deck_attachments: DF.Table[SponsorshipDeckItem]
sponsor_deck_cc: DF.SmallText | None
Expand Down
111 changes: 1 addition & 110 deletions buzz/public/themes/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -261,118 +261,12 @@ body:has(.login-frame:not([data-ready])) {
border-radius: var(--radius-banner);
}

/* Countdown pill, straddling the banner's bottom edge like the dashboard's */
/* Holds the countdown pill (countdown.css) on the banner's bottom edge */

.event-banner-frame {
position: relative;
}

.countdown-pill {
position: absolute;
bottom: 0;
left: 50%;
display: inline-flex;
align-items: center;
gap: 6px;
height: 26px;
padding: 0 10px;
border-radius: 999px;
background: var(--surface-raised);
box-shadow:
0 0 0 1px var(--border),
0 4px 12px rgb(0 0 0 / 0.12);
color: var(--ink-muted);
font-size: 12px;
font-weight: 500;
letter-spacing: 0;
white-space: nowrap;
transform: translate(-50%, 50%);
animation: countdown-in 250ms var(--ease-out) both;
}

.countdown-pill[hidden] {
display: none;
}

.countdown-pill:not([data-live]) :is(.live-dot, .countdown-live),
.countdown-pill[data-live] .countdown-upcoming {
display: none;
}

.countdown-value {
display: inline-flex;
color: var(--ink-strong);
font-weight: 600;
font-variant-numeric: tabular-nums;
}

.roll-slot {
display: inline-grid;
overflow: hidden;
}

.roll-slot > span {
grid-area: 1 / 1;
}

.roll-in {
animation: roll-in 300ms var(--ease-out) both;
}

.roll-out {
animation: roll-out 300ms var(--ease-out) both;
}

.live-dot {
position: relative;
display: inline-flex;
width: 8px;
height: 8px;
}

.live-dot::before,
.live-dot::after {
content: "";
position: absolute;
inset: 0;
border-radius: 999px;
background: #16a34a;
}

.live-dot::before {
opacity: 0.75;
animation: live-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}

@keyframes countdown-in {
from {
opacity: 0;
transform: translate(-50%, 50%) scale(0.9);
}
}

@keyframes roll-in {
from {
opacity: 0;
transform: translateY(-70%);
}
}

@keyframes roll-out {
to {
opacity: 0;
transform: translateY(70%);
}
}

@keyframes live-ping {
75%,
100% {
opacity: 0;
transform: scale(2);
}
}

.event-intro {
display: flex;
align-items: center;
Expand Down Expand Up @@ -1272,9 +1166,6 @@ summary:focus-visible {
}

@media (prefers-reduced-motion: reduce) {
.countdown-pill,
.roll-in,
.roll-out,
.live-dot::before {
animation: none;
}
Expand Down
117 changes: 117 additions & 0 deletions buzz/public/themes/countdown.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* Countdown pill on an event banner, shared by the event page and the dashboard.
The banner's frame positions it; the host page supplies the colour tokens. */

.countdown-pill {
position: absolute;
bottom: 0;
left: 50%;
display: inline-flex;
align-items: center;
gap: 6px;
height: 26px;
padding: 0 10px;
border-radius: 999px;
background: var(--surface-raised);
box-shadow:
0 0 0 1px var(--border),
0 4px 12px rgb(0 0 0 / 0.12);
color: var(--ink-muted);
font-size: 12px;
font-weight: 500;
letter-spacing: 0;
white-space: nowrap;
transform: translate(-50%, 50%);
animation: countdown-in 250ms var(--ease-out) both;
}

.countdown-pill[hidden] {
display: none;
}

.countdown-pill:not([data-live]) :is(.live-dot, .countdown-live),
.countdown-pill[data-live] .countdown-upcoming {
display: none;
}

.countdown-value {
display: inline-flex;
color: var(--ink-strong);
font-weight: 600;
font-variant-numeric: tabular-nums;
}

.roll-slot {
display: inline-grid;
overflow: hidden;
}

.roll-slot > span {
grid-area: 1 / 1;
}

.roll-in {
animation: roll-in 300ms var(--ease-out) both;
}

.roll-out {
animation: roll-out 300ms var(--ease-out) both;
}

.live-dot {
position: relative;
display: inline-flex;
width: 8px;
height: 8px;
}

.live-dot::before,
.live-dot::after {
content: "";
position: absolute;
inset: 0;
border-radius: 999px;
background: #16a34a;
}

.live-dot::before {
opacity: 0.75;
animation: live-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}

@keyframes countdown-in {
from {
opacity: 0;
transform: translate(-50%, 50%) scale(0.9);
}
}

@keyframes roll-in {
from {
opacity: 0;
transform: translateY(-70%);
}
}

@keyframes roll-out {
to {
opacity: 0;
transform: translateY(70%);
}
}

@keyframes live-ping {
75%,
100% {
opacity: 0;
transform: scale(2);
}
}

@media (prefers-reduced-motion: reduce) {
.countdown-pill,
.roll-in,
.roll-out,
.live-dot::before {
animation: none;
}
}
1 change: 1 addition & 0 deletions buzz/templates/event_page/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=Newsreader:wght@400&display=swap">
<link rel="stylesheet" href="{{ bundled_asset('espresso.bundle.css') }}">
<link rel="stylesheet" href="/assets/buzz/themes/countdown.css?v={{ build_version }}">
<link rel="stylesheet" href="/assets/buzz/themes/base.css?v={{ build_version }}">
<link rel="stylesheet" href="/assets/buzz/islands/icons.css">
<style>{{ theme_css }}</style>
Expand Down
Loading
Loading