eslam-reda-div / filament-timezone-detector
A Filament v5 plugin that automatically detects user timezone from the browser and provides comprehensive helpers, facades, macros, and middleware for seamless timezone conversion across your entire application.
Package info
github.com/eslam-reda-div/filament-timezone-detector
pkg:composer/eslam-reda-div/filament-timezone-detector
Requires
- php: ^8.2
- filament/filament: ^5.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
README
Automatically detects the user's browser timezone and provides Facade, Helpers, Carbon macros, Filament table/form macros, and an Eloquent trait for seamless timezone conversion.
Store in UTC. Display in the user's local time. Automatically.
Features
- Auto browser detection — JavaScript detects IANA timezone via
Intl.DateTimeFormatwith 10+ fallback methods - Livewire v4 / SPA compatible — uses
Livewire.interceptRequest(),wire:navigate, fetch/XHR interceptors, cross-tab BroadcastChannel sync - Middleware — captures timezone from header, cookie, query param, or form input → stores in session
- One-liner macros —
TextColumn::toUserTimezone(),TextEntry::toUserTimezone(),DateTimePicker::fromUserTimezone() - Carbon macros —
$date->toUserTimezone(),->toSystemTimezone(),->formatInUserTimezone() - Facade & helpers —
TimezoneDetector::forDisplay(),to_user_timezone(),user_now(), etc. - Eloquent trait —
InteractsWithTimezonefor per-attribute conversion - Fully configurable — toggle every feature on/off, customize header/cookie/session names
Requirements
- PHP >= 8.2, Laravel >= 11.0, Filament >= 5.0
Installation
composer require eslam-reda-div/filament-timezone-detector
Register the plugin in your panel provider:
use EslamRedaDiv\TimezoneDetector\TimezoneDetectorPlugin; public function panel(Panel $panel): Panel { return $panel // ... ->plugin(TimezoneDetectorPlugin::make()); }
Optionally publish the config:
php artisan vendor:publish --tag="timezone-detector-config"
That's it. The plugin auto-loads JS detection, registers middleware, Carbon macros, and Filament macros.
How It Works
Browser (JS) Server (Middleware) Your Code
───────────── ────────────────── ─────────
Intl.DateTimeFormat() detects → Reads X-Timezone header → Facade / Helper / Carbon macro
timezone and sends it via: (or cookie / query param) converts any datetime between
• Livewire interceptRequest Stores in session('user_tz') user ↔ system timezone
• fetch / XHR / Axios interceptors
• Cookie + hidden form inputs
• BroadcastChannel (cross-tab)
Recommended: Store all datetimes in UTC (config/app.php → 'timezone' => 'UTC'). The plugin handles display conversion automatically.
Usage
Filament Table Columns
TextColumn::make('created_at')->dateTime()->toUserTimezone(), TextColumn::make('published_at')->dateTime('M d, Y h:i A')->toUserTimezone(), TextColumn::make('updated_at')->since()->toUserTimezone(),
Filament Infolist Entries
TextEntry::make('created_at')->dateTime()->toUserTimezone(), TextEntry::make('published_at')->dateTime('M d, Y h:i A')->toUserTimezone(), TextEntry::make('updated_at')->since()->toUserTimezone(),
Filament Form Fields
DateTimePicker::make('starts_at')->toUserTimezone(), // display + save in user TZ DateTimePicker::make('event_time')->fromUserTimezone(), // alias, clearer intent DatePicker::make('event_date')->toUserTimezone(),
Facade
use EslamRedaDiv\TimezoneDetector\Facades\TimezoneDetector; // Info TimezoneDetector::getUserTimezone(); // "America/New_York" TimezoneDetector::getSystemTimezone(); // "UTC" TimezoneDetector::getOffsetFromSystem(); // -4.0 TimezoneDetector::getUserUtcOffset(); // "-04:00" // Convert to user TZ (for display) TimezoneDetector::toUserTimezone($model->created_at); // Carbon TimezoneDetector::toUserTimezone('2025-06-15 14:00:00', 'M d, Y h:i A'); // "Jun 15, 2025 10:00 AM" TimezoneDetector::forDisplay($model->created_at); // formatted string TimezoneDetector::diffForHumans($model->created_at); // "2 hours ago" // Convert to system TZ (for storage) TimezoneDetector::toSystemTimezone($request->starts_at); // Carbon in UTC TimezoneDetector::forStorage($request->input('event_time')); // alias TimezoneDetector::fromUserInput('06/15/2025 10:00 AM', 'm/d/Y h:i A'); // parse + convert // Between any two TZs TimezoneDetector::convertTimezone($datetime, 'UTC', 'Asia/Tokyo', 'H:i'); // Current time TimezoneDetector::userNow(); // Carbon in user TZ TimezoneDetector::systemNow(); // Carbon in system TZ
Helper Functions
user_timezone(); // "America/New_York" system_timezone(); // "UTC" to_user_timezone($model->created_at, 'M d, Y h:i A'); // formatted in user TZ to_system_timezone($userInput); // Carbon in UTC convert_timezone($datetime, 'UTC', 'Asia/Tokyo'); // between any TZs user_now(); // Carbon in user TZ format_user_datetime($model->created_at); // display string diff_for_humans_user($model->created_at); // "2 hours ago"
Carbon Macros
$date->toUserTimezone(); // Carbon in user TZ $date->toSystemTimezone(); // Carbon in system TZ $date->formatInUserTimezone('M d, Y h:i A'); // formatted string $date->diffForHumansInUserTimezone(); // "2 hours ago" // Chaining with Eloquent $model->created_at->toUserTimezone()->format('H:i'); $model->deadline->toUserTimezone()->isPast();
Eloquent Model Trait
use EslamRedaDiv\TimezoneDetector\Concerns\InteractsWithTimezone; class Event extends Model { use InteractsWithTimezone; protected $casts = ['starts_at' => 'datetime', 'ends_at' => 'datetime']; } $event->toUserTimezone('starts_at'); // Carbon in user TZ $event->getDatetimeForUser('starts_at', 'd/m/Y H:i'); // formatted string $event->setDatetimeFromUser('starts_at', $input); // converts to UTC and sets $event->diffForHumansInUserTimezone('starts_at'); // "in 3 days" $event->convertAttributeTimezone('starts_at', 'Asia/Tokyo'); // any TZ
Blade Views
{{ to_user_timezone($post->created_at, 'M d, Y h:i A') }} {{ $post->created_at->formatInUserTimezone('M d, Y h:i A') }} {{ diff_for_humans_user($post->published_at) }} Your timezone: {{ user_timezone() }}
Controllers
// Store: convert user input → UTC $event->starts_at = TimezoneDetector::toSystemTimezone($request->starts_at); // Display: convert UTC → user TZ $display = TimezoneDetector::forDisplay($event->starts_at);
API Resources
'starts_at_local' => TimezoneDetector::toUserTimezone($this->starts_at)?->toISOString(), 'starts_at_display' => TimezoneDetector::forDisplay($this->starts_at),
Queued Jobs
The session is unavailable in jobs. Pass the timezone explicitly:
$userTz = user_timezone(); SendReminder::dispatch($event, $userTz); // In the job: $localTime = convert_timezone($this->event->starts_at, system_timezone(), $this->userTimezone, 'M d, Y h:i A');
Using Outside Filament Panels
Register the middleware manually and include the JS:
// bootstrap/app.php (Laravel 11+) use EslamRedaDiv\TimezoneDetector\Http\Middleware\DetectUserTimezone; ->withMiddleware(function (Middleware $middleware) { $middleware->web(append: [DetectUserTimezone::class]); })
{{-- Option 1: Use FilamentAsset (recommended, no publishing needed) --}} <script src="{{ \Filament\Support\Facades\FilamentAsset::getScriptSrc('timezone-detector', 'eslam-reda-div/filament-timezone-detector') }}"></script> {{-- Option 2: Publish and use from public/ --}} <script src="{{ asset('vendor/eslam-reda-div/filament-timezone-detector/timezone-detector.js') }}"></script>
For option 2, publish first: php artisan filament:assets
Configuration
// config/timezone-detector.php return [ 'system_timezone' => config('app.timezone', 'UTC'), 'fallback_timezone' => config('app.timezone', 'UTC'), 'header_name' => env('TIMEZONE_DETECTOR_HEADER', 'X-Timezone'), 'cookie_name' => env('TIMEZONE_DETECTOR_COOKIE', 'user_timezone'), 'session_key' => env('TIMEZONE_DETECTOR_SESSION_KEY', 'user_timezone'), 'query_param' => env('TIMEZONE_DETECTOR_QUERY_PARAM', 'timezone'), 'auto_middleware' => env('TIMEZONE_DETECTOR_AUTO_MIDDLEWARE', true), 'register_carbon_macros' => env('TIMEZONE_DETECTOR_CARBON_MACROS', true), 'register_column_macros' => env('TIMEZONE_DETECTOR_COLUMN_MACROS', true), 'register_field_macros' => env('TIMEZONE_DETECTOR_FIELD_MACROS', true), ];
Plugin Options (Per-Panel)
->plugin(TimezoneDetectorPlugin::make()) // default ->plugin(TimezoneDetectorPlugin::make()->withoutMiddleware()) // manual middleware ->plugin(TimezoneDetectorPlugin::make()->autoMiddleware(false)) // same as above
API Reference
Facade / Core Methods
| Method | Returns | Description |
|---|---|---|
getUserTimezone() |
string |
User's IANA timezone |
getSystemTimezone() |
string |
System/database timezone |
toUserTimezone($dt, $format?, $fromTz?) |
Carbon|string|null |
System → user TZ |
toSystemTimezone($dt, $format?, $toTz?) |
Carbon|string|null |
User → system TZ |
convertTimezone($dt, $from, $to, $format?) |
Carbon|string|null |
Any → any TZ |
userNow() / systemNow() |
Carbon |
Current time in user/system TZ |
forDisplay($dt, $format?) |
string|null |
Shorthand display format |
forStorage($dt, $format?) |
Carbon|string|null |
Shorthand for toSystemTimezone |
fromUserInput($dt, $format?) |
Carbon |
Parse user input → system TZ |
diffForHumans($dt) |
string|null |
"2 hours ago" in user TZ |
getOffsetFromSystem() |
float |
Offset in hours (e.g., -4.0) |
getUserUtcOffset() |
string |
UTC offset (e.g., "-04:00") |
isValidTimezone($tz) |
bool |
Validate IANA timezone |
getAvailableTimezones() |
array |
All IANA identifiers |
Carbon Macros
| Method | Returns | Description |
|---|---|---|
->toUserTimezone() |
Carbon |
Copy in user TZ |
->toSystemTimezone() |
Carbon |
Copy in system TZ |
->formatInUserTimezone($format?) |
string |
Format in user TZ |
->diffForHumansInUserTimezone() |
string |
Diff for humans in user TZ |
Filament Macros
| Target | Method |
|---|---|
TextColumn |
->toUserTimezone() |
TextEntry |
->toUserTimezone() |
DateTimePicker |
->toUserTimezone() / ->fromUserTimezone() |
DatePicker |
->toUserTimezone() |
Model Trait (InteractsWithTimezone)
| Method | Returns |
|---|---|
->toUserTimezone('attr') |
Carbon|null |
->toSystemTimezone('attr') |
Carbon|null |
->getDatetimeForUser('attr', $format?) |
string|null |
->setDatetimeFromUser('attr', $value, $format?) |
static |
->diffForHumansInUserTimezone('attr') |
string|null |
->convertAttributeTimezone('attr', $tz, $format?) |
Carbon|string|null |
Helper Functions
| Function | Returns |
|---|---|
user_timezone() |
string |
system_timezone() |
string |
to_user_timezone($dt, $format?, $fromTz?) |
Carbon|string|null |
to_system_timezone($dt, $format?, $toTz?) |
Carbon|string|null |
convert_timezone($dt, $from, $to, $format?) |
Carbon|string|null |
user_now() |
Carbon |
format_user_datetime($dt, $format?) |
string|null |
diff_for_humans_user($dt) |
string|null |
FAQ
Q: UTC shows on first page load?
Expected — JS hasn't run yet. After the first request, the real timezone is detected and used for all subsequent requests. The fallback_timezone config covers this initial load.
Q: Works with Filament SPA mode / wire:navigate?
Yes. The JS listens to livewire:navigate and livewire:navigated events and injects X-Timezone via Livewire.interceptRequest(), fetch/XHR interceptors, and cookies.
Q: Works outside Filament panels? Yes. See Using Outside Filament Panels.
Q: Timezone in queued jobs? The session is unavailable in jobs. Pass it explicitly when dispatching. See Queued Jobs.
Q: Does this change PHP's global timezone?
No. It never calls date_default_timezone_set(). Only converts individual values when you call the conversion methods.
Q: Livewire v4 compatible?
Yes. The JS uses Livewire.interceptRequest() (the new v4 API) with a fallback to Livewire.hook('request') for backward compatibility.
Testing
composer test
Credits
License
MIT — see LICENSE.md