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.
115 2
Install
composer require eslam-reda-div/filament-timezone-detector
Latest Version:v1.1.0
PHP:^8.2
Maintainer: eslam-reda-div

Timezone Detector — Filament v5 Plugin

Latest Version on Packagist Total Downloads License

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.DateTimeFormat with 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 macrosTextColumn::toUserTimezone(), TextEntry::toUserTimezone(), DateTimePicker::fromUserTimezone()
  • Carbon macros$date->toUserTimezone(), ->toSystemTimezone(), ->formatInUserTimezone()
  • Facade & helpersTimezoneDetector::forDisplay(), to_user_timezone(), user_now(), etc.
  • Eloquent traitInteractsWithTimezone for 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