Skip to content

Add banner expiration#690

Open
RisingOrange wants to merge 3 commits intoPauseAI:mainfrom
RisingOrange:banner-expiration
Open

Add banner expiration#690
RisingOrange wants to merge 3 commits intoPauseAI:mainfrom
RisingOrange:banner-expiration

Conversation

@RisingOrange
Copy link
Contributor

@RisingOrange RisingOrange commented Mar 18, 2026

Closes #676

Campaign banners had to be manually disabled after they expired, which was easy to forget. This PR introduces the pattern of using isDateRangeActive() date checks so banners auto-expire without code changes. Banners also now self-hide on their target page without needing {#if} wrappers in the layout. Also cleans up debug leftovers in Banner and extracts shared localStorage helpers.

Changes

  • Replace && false gates on expired banners with isDateRangeActive() checks in layout conditions
  • Add isDateRangeActive() helper (src/lib/dateRange.ts) that parses date-only strings as local calendar days (avoids UTC midnight issues with new Date('YYYY-MM-DD'))
  • Extract safe getItem/setItem localStorage helpers into src/lib/localStorage.ts (browser guard + try-catch)
  • Clean up Banner component: remove leftover console.logs and redundant DOM click handler, fix target path comparison to use deLocalizeHref
  • Add eager path check at component init for both Banner and CampaignBanner. This hides the banner on its target page during SSR (no flash), removing the need for {#if pathname !== href} wrappers in the layout

Design decision

I initially added expiresAt/startsAt props directly on the Banner components, but discovered this doesn't work with the layout's if/else chain: an expired banner still consumes its branch, blocking fallback banners (e.g. GB users would get neither the expired protest banner nor NearbyEvent). Moving the date check to the layout condition fixes this since the branch itself evaluates to false.

Adding a new campaign banner

{#if isDateRangeActive({ endsOn: '2026-04-15' })}
    <CampaignBanner href="/my-event" id="my-event">
        ...
    </CampaignBanner>
{/if}

endsOn is inclusive (end of day, local time). startsOn is also supported for future-dated campaigns.

Open question

When a user visits the campaign page directly (e.g. /brussels-ep-protest-2026), CampaignBanner permanently hides itself (even on future page loads). Banner only hides while the user is on that page and comes back on other pages. This inconsistency is pre-existing. Should we align them?

Test plan

  • Verify no expired banners render on homepage or other pages
  • Temporarily set an endsOn date in the future (on a banner in +layout.svelte), confirm banner appears
  • Confirm banner is hidden on its target page (no flash)
  • Dismiss a banner, refresh, confirm it stays dismissed
  • Test with a startsOn in the future, confirm banner doesn't appear

@netlify
Copy link

netlify bot commented Mar 18, 2026

👷 Deploy request for pauseai pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 397bb90

// Show the hero on the homepage, but nowhere else
$: hero = deLocalizeHref($page.url.pathname) === '/'

$: if (browser && deLocalizeHref($page.url.pathname) === '/india-summit-2026') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer needed: CampaignBanner already auto-dismisses when the user navigates to its href page (via the reactive $: block in the component).

{:else if false}
<Banner contrast={hero} target="/littlehelpers">
{:else if isDateRangeActive({ endsOn: '2024-12-31' })}
<Banner contrast={hero} id="holiday-littlehelpers" target="/littlehelpers">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously had no id, so dismissals didn't persist across page loads. Added one so the close button actually remembers the user's choice.

@RisingOrange RisingOrange marked this pull request as ready for review March 18, 2026 17:37
@Wituareard
Copy link
Collaborator

Wituareard commented Mar 18, 2026

This probably has the same flickering issue #663 fixed but for all users right? I think I didn't really like the manual inclusion in the HTML and procrastinated reviewing it. Maybe we can get Vite to properly bundle a script and link it in the document head? SvelteKit doesn't seem to have a way to add blocking scripts from my research

@Wituareard
Copy link
Collaborator

I moved the blocking logic to a separate file and merged the PR. Can you merge it in here and build on that @RisingOrange ?

Banners dismissed via localStorage previously stayed hidden forever, and
old campaigns were manually disabled with `&& false`. Now banners accept
expiresAt/startsAt date props to auto-expire, and dismissals are scoped
per campaign id. Extracts shared localStorage logic into bannerStorage.ts,
removes debug console.logs and hacky DOM click handler, and fixes Banner
target path comparison to use deLocalizeHref.
Campaign banners dismissed via localStorage stayed hidden forever, and
old campaigns were manually disabled with `&& false` — error-prone and
cluttered.

Add isDateRangeActive() helper that parses date-only strings as local
calendar days, and use it in layout conditions to replace `&& false`
gates. This keeps the active/inactive decision in the layout where the
if/else branching happens, avoiding a bug where expired component-level
props would still consume the branch and block fallback banners.

Other improvements:
- Extract safe localStorage helpers (getItem/setItem) into localStorage.ts
- Remove debug console.logs and hacky DOM click handler from Banner
- Add eager path check at init to prevent flash without {#if} wrappers
- Fix Banner target path comparison to use deLocalizeHref
@Wituareard
Copy link
Collaborator

Oops sorry didn't see your reaction. Hope I didn't cause a conflict on your end

@RisingOrange
Copy link
Contributor Author

RisingOrange commented Mar 19, 2026

@Wituareard No problem! I'm still thinking about how best to solve the flash issue.

Are you doing scheduled rebuilds (e.g. via GitHub Actions cron + Netlify build hook) for the site? It would help with banner flashes due to the server-side build having evaluated the date conditions on a previous day.

It could also help keep prerendered data (like some of the Airtable endpoints) fresh. The GHA could also be used to trigger rebuilds, instead of commits used purely to trigger rebuilds (like Trigger Netlify build to test missing API key behavior).

A single workflow could handle both scheduled and manual triggers:

# .github/workflows/rebuild.yml
name: Rebuild site
on:
  schedule:
    - cron: '0 */6 * * *' # every 6 hours
  workflow_dispatch:
jobs:
  rebuild:
    runs-on: ubuntu-latest
    steps:
      - run: curl -X POST "${{ secrets.NETLIFY_BUILD_HOOK }}"

Just needs a Netlify build hook URL added as a repo secret.

@RisingOrange RisingOrange changed the title Add client-side banner expiration Add banner expiration Mar 19, 2026
@Wituareard
Copy link
Collaborator

Wituareard commented Mar 19, 2026

Maybe banners could add a svelte:head element to register themselves and we somehow make sure the hiding script is placed after them.

Are you suggesting to entirely move the banner expiration to the server? Could work since we probably don't need more accuracy than 24h.

Daily builds could be a good idea regardless, didn't know of that feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Outdated campaign banners are easy to miss

2 participants