Forms
Subscription Calendar code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/subscription-calendar.blade.php
@props([
'value' => '2026-01-16',
'monthlyTotal' => '$156.23',
'events' => [
['date' => '2026-01-02', 'mark' => 'N', 'tone' => 'text-red-600 dark:text-red-400', 'billing' => 'monthly'],
['date' => '2026-01-04', 'mark' => '✳', 'tone' => 'text-accent-500 dark:text-accent-400', 'billing' => 'monthly'],
['date' => '2026-01-07', 'mark' => 'A', 'tone' => 'text-red-500 dark:text-red-400', 'billing' => 'monthly'],
['date' => '2026-01-10', 'mark' => '◉', 'tone' => 'text-brand-700 dark:text-brand-300', 'billing' => 'yearly'],
['date' => '2026-01-12', 'mark' => '●', 'tone' => 'text-gray-500', 'billing' => 'monthly'],
['date' => '2026-01-15', 'mark' => '◈', 'tone' => 'text-gray-600 dark:text-gray-400', 'billing' => 'monthly'],
['date' => '2026-01-20', 'mark' => '◒', 'tone' => 'text-brand-500 dark:text-brand-400', 'billing' => 'monthly'],
['date' => '2026-01-25', 'mark' => '✣', 'tone' => 'text-amber-500 dark:text-amber-400', 'billing' => 'yearly'],
['date' => '2026-01-28', 'mark' => '●', 'tone' => 'text-accent-600 dark:text-accent-300', 'billing' => 'monthly'],
],
])
@php
/*
| A compact billing calendar that visualises scheduled subscriptions by
| renewal date, cadence and total monthly spend.
*/
@endphp
<section
x-data="{
selected: @js($value),
view: new Date(@js($value.'T00:00:00')),
events: @js($events),
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
weekdays: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
get label() { return `${this.months[this.view.getMonth()]}, ${this.view.getFullYear()}`; },
format(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
},
get days() {
const year = this.view.getFullYear();
const month = this.view.getMonth();
const first = new Date(year, month, 1);
const offset = (first.getDay() + 6) % 7;
return Array.from({ length: 35 }, (_, index) => {
const date = new Date(year, month, index - offset + 1);
return {
date: this.format(date),
day: date.getDate(),
current: date.getMonth() === month,
};
});
},
eventFor(date) { return this.events.find((event) => event.date === date); },
move(months) { this.view = new Date(this.view.getFullYear(), this.view.getMonth() + months, 1); },
choose(day) {
this.selected = day.date;
this.$dispatch('subscription-calendar-selected', { date: day.date });
},
today() {
const date = new Date();
this.view = new Date(date.getFullYear(), date.getMonth(), 1);
this.selected = this.format(date);
},
}"
{{ $attributes->merge(['class' => 'w-full max-w-xl overflow-hidden rounded-[1.75rem] border border-gray-200/80 bg-gray-50 text-gray-950 shadow-[0_22px_55px_-35px_rgba(15,23,42,0.45)] dark:border-gray-800 dark:bg-gray-900 dark:text-white']) }}
aria-label="Subscription calendar"
>
<div class="flex items-center gap-2 px-5 pb-4 pt-5 sm:px-6">
<h2 class="text-lg font-semibold tracking-tight" x-text="label"></h2>
<button type="button" @click="today()" class="rounded-full border border-gray-200 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700">Today</button>
<div class="ms-auto flex items-center gap-1">
<button type="button" @click="move(-1)" class="rounded-full p-2 text-gray-400 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-white" aria-label="Previous month"><x-ui.icon name="caret-left" weight="bold" size="sm" /></button>
<button type="button" @click="move(1)" class="rounded-full p-2 text-gray-400 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-white" aria-label="Next month"><x-ui.icon name="caret-right" weight="bold" size="sm" /></button>
</div>
<button type="button" @click="$dispatch('subscription-calendar-add')" class="ms-1 grid size-10 place-items-center rounded-full bg-brand-600 text-xl font-light text-white shadow-lg shadow-brand-600/25 transition hover:bg-brand-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-900" aria-label="Add subscription">+</button>
</div>
<div class="border-y border-gray-200/80 bg-white px-4 py-4 dark:border-gray-800 dark:bg-gray-950/40 sm:px-5">
<div class="grid grid-cols-7 gap-1.5 text-center">
<template x-for="weekday in weekdays" :key="weekday"><span class="rounded-xl bg-gray-50 py-2 text-[10px] font-semibold uppercase tracking-wide text-gray-400 dark:bg-gray-800/70 dark:text-gray-500" x-text="weekday"></span></template>
<template x-for="day in days" :key="day.date">
<button
type="button"
@click="choose(day)"
:aria-pressed="selected === day.date"
:class="{
'border-brand-300 bg-brand-50 text-brand-700 shadow-sm dark:border-brand-700 dark:bg-brand-950/30 dark:text-brand-300': selected === day.date,
'border-transparent bg-gray-50/80 text-gray-500 hover:border-gray-200 hover:bg-white dark:bg-gray-800/50 dark:text-gray-400 dark:hover:border-gray-700 dark:hover:bg-gray-800': selected !== day.date && day.current,
'border-transparent bg-[repeating-linear-gradient(45deg,transparent,transparent_4px,rgba(148,163,184,.08)_4px,rgba(148,163,184,.08)_5px)] text-gray-300 dark:text-gray-700': ! day.current,
}"
class="relative flex aspect-square min-h-13 flex-col items-center justify-between rounded-xl border px-1 py-1.5 text-sm transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
>
<span class="font-mono text-xs" x-text="day.day"></span>
<span class="grid h-5 place-items-center text-lg font-bold leading-none" :class="eventFor(day.date)?.tone" x-text="eventFor(day.date)?.mark"></span>
<span x-show="eventFor(day.date)" class="absolute end-1.5 top-1.5 size-1.5 rounded-full" :class="eventFor(day.date)?.billing === 'yearly' ? 'bg-amber-400' : 'bg-accent-500'"></span>
</button>
</template>
</div>
<div class="mt-4 flex flex-wrap items-center gap-x-5 gap-y-2 text-[10px] font-semibold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">
<span class="inline-flex items-center gap-2"><i class="size-2 rounded-full bg-accent-500"></i>Monthly</span>
<span class="inline-flex items-center gap-2"><i class="size-2 rounded-full bg-amber-400"></i>Yearly</span>
<span class="ms-auto font-mono tracking-normal text-gray-400"><strong class="text-gray-900 dark:text-white" x-text="events.length"></strong> subscriptions / <strong class="text-gray-900 dark:text-white">3</strong> new</span>
</div>
</div>
<footer class="flex items-center justify-between gap-4 px-5 py-4 sm:px-6">
<div class="flex items-center gap-3 text-gray-400" aria-label="Calendar tools">
<button type="button" class="rounded-lg p-1 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-white" aria-label="More calendar actions">⋮</button>
<span class="h-5 w-px bg-gray-200 dark:bg-gray-700"></span>
<x-ui.icon name="magnifying-glass" size="sm" />
<x-ui.icon name="download-simple" size="sm" />
</div>
<p class="text-right font-mono text-[11px] uppercase tracking-[0.1em] text-gray-400">Monthly total: <strong class="ms-2 text-sm tracking-normal text-gray-900 dark:text-white">{{ $monthlyTotal }}</strong></p>
</footer>
</section>