Claude-skill-registry datetime-timezone
Handle datetime and timezone conversions correctly across frontend, API, and database. Use this skill when working with datetime-local inputs, scheduling features, due dates, reminders, or any time-sensitive functionality. Covers the critical pitfall of browser datetime-local inputs lacking timezone info.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/datetime-timezone" ~/.claude/skills/majiayu000-claude-skill-registry-datetime-timezone && rm -rf "$T"
manifest:
skills/data/datetime-timezone/SKILL.mdsource content
DateTime and Timezone Handling
Correctly handle datetime values across the full stack: browser → API → database → back to browser.
When to Use
- Implementing due dates, reminders, or scheduling features
- Working with
HTML inputsdatetime-local - Storing timestamps in PostgreSQL/databases
- Displaying times in user's local timezone
- Debugging "wrong time" issues
The Golden Rule
Store UTC, Display Local
Browser (local) → Convert to UTC → API → Store UTC → Database Database → Return UTC → API → Browser → Display in local
Critical Pitfall: datetime-local Input
The HTML
<input type="datetime-local"> returns a string WITHOUT timezone info:
<input type="datetime-local" value="2025-12-15T23:13">
This gives you
"2025-12-15T23:13" - but is this UTC? Local time? The browser doesn't tell you.
The Bug
// WRONG - Backend interprets as UTC, but user entered local time! const dueDate = "2025-12-15T23:13" // User in PKT (UTC+5) await api.createTask({ due_date: dueDate }) // Backend stores as 23:13 UTC // But user meant 23:13 PKT = 18:13 UTC // Task is now 5 hours late!
The Fix
// CORRECT - Convert local datetime to UTC ISO string function handleSubmit() { let dueDateUTC: string | undefined = undefined if (dueDate) { // datetime-local gives "2025-12-15T23:13" (local time, no TZ) // new Date() interprets it in browser's local timezone // toISOString() converts to UTC: "2025-12-15T18:13:00.000Z" const localDate = new Date(dueDate) dueDateUTC = localDate.toISOString() } await api.createTask({ due_date: dueDateUTC }) }
Frontend Patterns (TypeScript/React)
Converting datetime-local to UTC
// Input: "2025-12-15T23:13" (from datetime-local) // Output: "2025-12-15T18:13:00.000Z" (UTC ISO string) function localToUTC(localDatetime: string): string { const date = new Date(localDatetime) return date.toISOString() }
Converting UTC to datetime-local (for editing)
// Input: "2025-12-15T18:13:00Z" (UTC from API) // Output: "2025-12-15T23:13" (local, for datetime-local input) function utcToLocal(utcDatetime: string): string { const date = new Date(utcDatetime) // Format: YYYY-MM-DDTHH:MM (no seconds, no Z) const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') const hours = String(date.getHours()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, '0') return `${year}-${month}-${day}T${hours}:${minutes}` }
Full Form Example (React)
"use client" import { useState } from "react" export function TaskForm() { const [dueDate, setDueDate] = useState("") // Local datetime string const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() // Convert local to UTC for API let dueDateUTC: string | undefined if (dueDate) { dueDateUTC = new Date(dueDate).toISOString() } await api.createTask({ title: "My Task", due_date: dueDateUTC, // UTC ISO string }) } return ( <form onSubmit={handleSubmit}> <input type="datetime-local" value={dueDate} onChange={(e) => setDueDate(e.target.value)} /> <button type="submit">Create</button> </form> ) }
Editing Existing Task
"use client" import { useState, useEffect } from "react" export function EditTaskForm({ task }: { task: Task }) { const [dueDate, setDueDate] = useState("") useEffect(() => { // Convert UTC from API to local for input if (task.due_date) { setDueDate(utcToLocal(task.due_date)) } }, [task]) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() // Convert back to UTC for API const dueDateUTC = dueDate ? new Date(dueDate).toISOString() : undefined await api.updateTask(task.id, { due_date: dueDateUTC }) } return ( <input type="datetime-local" value={dueDate} onChange={(e) => setDueDate(e.target.value)} /> ) }
Backend Patterns (Python/FastAPI)
Pydantic Model with Timezone Normalization
from datetime import UTC, datetime from pydantic import field_validator from sqlmodel import SQLModel class TaskCreate(SQLModel): title: str due_date: datetime | None = None @field_validator("due_date", mode="after") @classmethod def normalize_datetime(cls, v: datetime | None) -> datetime | None: """Convert timezone-aware to naive UTC for storage.""" if v is None: return None # If timezone-aware, convert to UTC and strip timezone if v.tzinfo is not None: v = v.astimezone(UTC).replace(tzinfo=None) return v
API Endpoint
@router.post("/tasks", response_model=TaskRead) async def create_task( task_in: TaskCreate, session: AsyncSession = Depends(get_session), ): # due_date is already normalized to naive UTC by validator task = Task(**task_in.model_dump()) session.add(task) await session.commit() await session.refresh(task) return task
Response Serialization
from datetime import datetime class TaskRead(SQLModel): id: int title: str due_date: datetime | None = None # Returned as ISO string with Z suffix
FastAPI automatically serializes
datetime as ISO 8601 with Z suffix (UTC).
Database Storage
Always store as naive UTC (no timezone info):
-- PostgreSQL due_date TIMESTAMP -- NOT TIMESTAMP WITH TIME ZONE
# SQLModel from datetime import datetime from sqlmodel import Field class Task(SQLModel, table=True): due_date: datetime | None = Field(default=None)
Displaying Times to Users
Relative Time (e.g., "in 2 hours")
import { formatDistanceToNow } from 'date-fns' function formatRelative(utcDatetime: string): string { const date = new Date(utcDatetime) return formatDistanceToNow(date, { addSuffix: true }) // "in 2 hours", "3 days ago" }
Absolute Local Time
function formatLocal(utcDatetime: string): string { const date = new Date(utcDatetime) return date.toLocaleString() // "12/15/2025, 11:13:00 PM" (in user's locale) }
With Specific Format
import { format } from 'date-fns' function formatDateTime(utcDatetime: string): string { const date = new Date(utcDatetime) return format(date, 'MMM d, yyyy h:mm a') // "Dec 15, 2025 11:13 PM" }
Debugging Timezone Issues
1. Log at Every Step
// Frontend console.log("datetime-local value:", dueDate) // "2025-12-15T23:13" console.log("Converted to UTC:", new Date(dueDate).toISOString()) // "2025-12-15T18:13:00.000Z"
# Backend logger.info("Received due_date: %s", task_in.due_date) # 2025-12-15T18:13:00+00:00 logger.info("Stored due_date: %s", task.due_date) # 2025-12-15 18:13:00
2. Check Browser Timezone
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone) // "Asia/Karachi" (PKT = UTC+5)
3. Verify UTC Offset
const date = new Date("2025-12-15T23:13") console.log("UTC offset (minutes):", date.getTimezoneOffset()) // -300 for UTC+5 (PKT)
Common Pitfalls
1. Sending datetime-local Directly
// WRONG const dueDate = e.target.value // "2025-12-15T23:13" await api.post({ due_date: dueDate }) // No timezone! // CORRECT const dueDate = new Date(e.target.value).toISOString() await api.post({ due_date: dueDate }) // "2025-12-15T18:13:00.000Z"
2. Using Date.now() Without toISOString()
// WRONG - sends milliseconds timestamp await api.post({ timestamp: Date.now() }) // 1734285180000 // CORRECT await api.post({ timestamp: new Date().toISOString() }) // "2025-12-15T18:13:00.000Z"
3. Comparing Dates Without Normalization
# WRONG - comparing aware and naive if task.due_date < datetime.now(UTC): # Error or wrong result # CORRECT - both naive UTC if task.due_date < datetime.utcnow():
4. Displaying UTC Directly
// WRONG - shows UTC time to user <span>{task.due_date}</span> // "2025-12-15T18:13:00Z" (confusing!) // CORRECT - convert to local <span>{new Date(task.due_date).toLocaleString()}</span> // "Dec 15, 2025, 11:13 PM"
Testing
Test Timezone Conversion
describe('datetime conversion', () => { it('converts local to UTC correctly', () => { // Mock timezone to PKT (UTC+5) const localDatetime = "2025-12-15T23:13" const utc = new Date(localDatetime).toISOString() // In PKT, 23:13 local = 18:13 UTC expect(utc).toBe("2025-12-15T18:13:00.000Z") }) })
Test Backend Normalization
def test_datetime_normalization(): # With timezone task = TaskCreate( title="Test", due_date=datetime(2025, 12, 15, 18, 13, tzinfo=UTC) ) assert task.due_date.tzinfo is None # Normalized to naive assert task.due_date == datetime(2025, 12, 15, 18, 13)
Summary
| Location | Format | Example |
|---|---|---|
input | Local, no TZ | |
| API request/response | UTC ISO 8601 | |
| Database | Naive UTC | |
| Display to user | Local formatted | |