Claude Code for Luxon: Modern Date and Time Handling — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Luxon: Modern Date and Time Handling
Frontend

Claude Code for Luxon: Modern Date and Time Handling

Published: June 24, 2027
Read time: 5 min read
By: Claude Skills 360

Luxon provides immutable, timezone-aware date handling — DateTime.now() returns the current moment. DateTime.fromISO("2024-01-15T10:30:00Z") parses ISO strings. DateTime.fromFormat("15/01/2024", "dd/MM/yyyy") parses custom formats. dt.plus({ days: 7, hours: 2 }) adds durations. dt.minus({ months: 1 }) subtracts. dt.startOf("week") snaps to start of period. dt.diff(other, ["days", "hours"]) computes differences. dt.setZone("America/New_York") converts timezone. dt.setLocale("fr").toLocaleString(DateTime.DATE_FULL) formats in locale. dt.toRelative() returns “3 hours ago”. dt.toISO() outputs ISO 8601. Interval.fromDateTimes(start, end) represents a range with .contains(), .count("days"). Duration.fromObject({ hours: 2, minutes: 30 }). Settings.defaultLocale = "en-US" sets global locale. Claude Code generates Luxon date utilities, timezone converters, duration formatters, and calendar helpers.

CLAUDE.md for Luxon

## Luxon Stack
- Version: luxon >= 3.5
- TypeScript: npm install @types/luxon
- Now: const now = DateTime.now() — timezone-aware, locale-aware
- Parse ISO: DateTime.fromISO("2024-01-15") — returns Invalid if wrong
- Parse custom: DateTime.fromFormat("01/15/2024", "MM/dd/yyyy")
- Math: dt.plus({ weeks: 2 }).minus({ hours: 3 })
- Format: dt.toFormat("yyyy-MM-dd") — custom tokens
- Locale: dt.setLocale("de").toLocaleString(DateTime.DATETIME_MED)
- Timezone: dt.setZone("Europe/Paris") or dt.toLocal() / dt.toUTC()
- Relative: dt.toRelative() → "5 minutes ago"
- Diff: end.diff(start, ["days", "hours"]).toObject()
- Interval: Interval.fromDateTimes(start, end).count("days")

Date Utility Library

// lib/dates/utils.ts — Luxon date helpers for a web app
import { DateTime, Duration, Interval, Settings } from "luxon"

// Global defaults — set once at app startup
Settings.defaultLocale = "en-US"
Settings.throwOnInvalid = true // Surface invalid dates early

// ── Parsing ────────────────────────────────────────────────────────────────

/** Parse an ISO string or epoch ms, return null on invalid */
export function parseDate(value: string | number | null | undefined): DateTime | null {
  if (!value) return null
  if (typeof value === "number") {
    const dt = DateTime.fromMillis(value)
    return dt.isValid ? dt : null
  }
  const dt = DateTime.fromISO(value)
  return dt.isValid ? dt : null
}

/** Parse user-input date in locale-appropriate format with fallback */
export function parseUserDate(input: string, locale = "en-US"): DateTime | null {
  const formats = ["MM/dd/yyyy", "dd/MM/yyyy", "yyyy-MM-dd", "d MMM yyyy", "MMMM d, yyyy"]
  for (const fmt of formats) {
    const dt = DateTime.fromFormat(input, fmt, { locale })
    if (dt.isValid) return dt
  }
  // Last resort: try ISO
  const iso = DateTime.fromISO(input)
  return iso.isValid ? iso : null
}

// ── Formatting ─────────────────────────────────────────────────────────────

export function formatDate(dt: DateTime | string | null, locale = "en-US"): string {
  const parsed = typeof dt === "string" ? parseDate(dt) : dt
  if (!parsed) return "—"
  return parsed.setLocale(locale).toLocaleString(DateTime.DATE_MED)
}

export function formatDateTime(dt: DateTime | string | null, locale = "en-US"): string {
  const parsed = typeof dt === "string" ? parseDate(dt) : dt
  if (!parsed) return "—"
  return parsed.setLocale(locale).toLocaleString(DateTime.DATETIME_MED)
}

export function formatRelative(dt: DateTime | string | null): string {
  const parsed = typeof dt === "string" ? parseDate(dt) : dt
  if (!parsed) return "—"
  return parsed.toRelative() ?? formatDate(parsed)
}

export function formatDuration(ms: number): string {
  if (ms < 60_000) return `${Math.round(ms / 1000)}s`
  if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`
  if (ms < 86_400_000) return `${(ms / 3_600_000).toFixed(1)}h`
  return `${Math.round(ms / 86_400_000)}d`
}

// ── Calendar math ──────────────────────────────────────────────────────────

export function getWeekDays(referenceDate?: DateTime): DateTime[] {
  const start = (referenceDate ?? DateTime.now()).startOf("week")
  return Array.from({ length: 7 }, (_, i) => start.plus({ days: i }))
}

export function getMonthGrid(year: number, month: number): (DateTime | null)[][] {
  const firstDay = DateTime.local(year, month, 1)
  const daysInMonth = firstDay.daysInMonth ?? 31
  const startPad = firstDay.weekday % 7 // 0 = Sunday

  const cells: (DateTime | null)[] = [
    ...Array<null>(startPad).fill(null),
    ...Array.from({ length: daysInMonth }, (_, i) => firstDay.plus({ days: i })),
  ]

  // Pad end to complete last week
  while (cells.length % 7 !== 0) cells.push(null)

  const rows: (DateTime | null)[][] = []
  for (let i = 0; i < cells.length; i += 7) rows.push(cells.slice(i, i + 7))
  return rows
}

export function getDateRange(start: DateTime, end: DateTime): DateTime[] {
  const interval = Interval.fromDateTimes(start.startOf("day"), end.endOf("day"))
  const count = Math.ceil(interval.count("days"))
  return Array.from({ length: count }, (_, i) => start.startOf("day").plus({ days: i }))
}

// ── Business logic ─────────────────────────────────────────────────────────

export function isWithinBusinessHours(dt: DateTime, timezone: string): boolean {
  const local = dt.setZone(timezone)
  const hour = local.hour
  const dow = local.weekday // 1=Mon, 7=Sun
  return dow <= 5 && hour >= 9 && hour < 17
}

export function nextBusinessDay(from?: DateTime): DateTime {
  let d = (from ?? DateTime.now()).plus({ days: 1 }).startOf("day")
  while (d.weekday > 5) d = d.plus({ days: 1 }) // Skip weekends
  return d
}

export function workdaysBetween(start: DateTime, end: DateTime): number {
  const days = getDateRange(start, end)
  return days.filter((d) => d.weekday <= 5).length
}

React Date Picker Component

// components/dates/DateRangePicker.tsx — calendar range selector using Luxon
"use client"
import { useState, useCallback } from "react"
import { DateTime } from "luxon"
import { getMonthGrid, getWeekDays } from "@/lib/dates/utils"

interface DateRangePickerProps {
  value?: { start: DateTime; end: DateTime }
  onChange?: (range: { start: DateTime; end: DateTime }) => void
  minDate?: DateTime
  maxDate?: DateTime
  locale?: string
}

const WEEKDAY_HEADERS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]

export function DateRangePicker({ value, onChange, minDate, maxDate, locale = "en-US" }: DateRangePickerProps) {
  const [viewMonth, setViewMonth] = useState(() => (value?.start ?? DateTime.now()).startOf("month"))
  const [hovered, setHovered] = useState<DateTime | null>(null)
  const [selecting, setSelecting] = useState<DateTime | null>(null)

  const grid = getMonthGrid(viewMonth.year, viewMonth.month)

  const isInRange = useCallback((day: DateTime): boolean => {
    const { start, end } = value ?? {}
    if (!start) return false
    if (selecting && !end) {
      const rangeEnd = hovered ?? selecting
      const lo = start < rangeEnd ? start : rangeEnd
      const hi = start < rangeEnd ? rangeEnd : start
      return day >= lo && day <= hi
    }
    return !!end && day >= start && day <= end
  }, [value, selecting, hovered])

  const handleDayClick = useCallback((day: DateTime) => {
    if (!selecting) {
      setSelecting(day)
      onChange?.({ start: day, end: day })
    } else {
      const start = selecting < day ? selecting : day
      const end = selecting < day ? day : selecting
      onChange?.({ start, end })
      setSelecting(null)
    }
  }, [selecting, onChange])

  const isDisabled = (day: DateTime) =>
    (minDate ? day < minDate.startOf("day") : false) ||
    (maxDate ? day > maxDate.endOf("day") : false)

  return (
    <div className="rounded-xl border bg-card p-4 w-72 shadow-md">
      {/* Month nav */}
      <div className="flex items-center justify-between mb-4">
        <button
          onClick={() => setViewMonth((m) => m.minus({ months: 1 }))}
          className="size-8 rounded-lg hover:bg-muted flex items-center justify-center text-sm"
        >‹</button>
        <span className="text-sm font-medium">
          {viewMonth.setLocale(locale).toLocaleString({ month: "long", year: "numeric" })}
        </span>
        <button
          onClick={() => setViewMonth((m) => m.plus({ months: 1 }))}
          className="size-8 rounded-lg hover:bg-muted flex items-center justify-center text-sm"
        >›</button>
      </div>

      {/* Weekday headers */}
      <div className="grid grid-cols-7 mb-1">
        {WEEKDAY_HEADERS.map((d) => (
          <div key={d} className="text-center text-xs text-muted-foreground py-1">{d}</div>
        ))}
      </div>

      {/* Days grid */}
      {grid.map((week, wi) => (
        <div key={wi} className="grid grid-cols-7">
          {week.map((day, di) => {
            if (!day) return <div key={di} />
            const isToday = day.hasSame(DateTime.now(), "day")
            const isStart = value?.start && day.hasSame(value.start, "day")
            const isEnd = value?.end && day.hasSame(value.end, "day")
            const inRange = isInRange(day)
            const disabled = isDisabled(day)

            return (
              <button
                key={di}
                disabled={disabled}
                onClick={() => handleDayClick(day)}
                onMouseEnter={() => selecting && setHovered(day)}
                onMouseLeave={() => setHovered(null)}
                className={[
                  "text-xs py-1.5 text-center transition-colors rounded-lg",
                  disabled ? "opacity-30 cursor-not-allowed" : "cursor-pointer",
                  isStart || isEnd ? "bg-primary text-primary-foreground font-semibold" : "",
                  inRange && !isStart && !isEnd ? "bg-primary/15 rounded-none" : "",
                  isToday && !isStart && !isEnd ? "ring-1 ring-primary" : "",
                  !disabled && !isStart && !isEnd ? "hover:bg-muted" : "",
                ].filter(Boolean).join(" ")}
              >
                {day.day}
              </button>
            )
          })}
        </div>
      ))}

      {/* Selection summary */}
      {value?.start && (
        <div className="mt-3 pt-3 border-t text-xs text-muted-foreground flex justify-between">
          <span>{value.start.setLocale(locale).toLocaleString(DateTime.DATE_SHORT)}</span>
          <span>→</span>
          <span>{value.end?.setLocale(locale).toLocaleString(DateTime.DATE_SHORT) ?? "…"}</span>
        </div>
      )}
    </div>
  )
}

For the date-fns alternative when tree-shakeable pure functions with no class wrapping, minimal bundle per-function, and a simpler functional style are preferred — date-fns has smaller per-function imports while Luxon provides a richer instance API with built-in timezone support without needing date-fns-tz, see the date-fns guide. For the Day.js alternative when a Moment.js-compatible minimal API (~2KB), optional plugin system, and broad familiarity from Moment.js migrations are needed — Day.js is even lighter than Luxon while Luxon has stronger timezone support via the Intl API and better TypeScript ergonomics, see the Day.js guide. The Claude Skills 360 bundle includes Luxon skill sets covering parsing, formatting, timezone handling, and calendar math. Start with the free tier to try date utility generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free