summaryrefslogtreecommitdiff
path: root/packages/web-util/src/forms/Calendar.tsx
blob: a0df639f3cc5cd035b5a478f311f4f911d0c21b3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { AbsoluteTime } from "@gnu-taler/taler-util"
import { add as dateAdd, sub as dateSub, eachDayOfInterval, endOfMonth, endOfWeek, format, getMonth, getYear, isSameDay, isSameMonth, startOfDay, startOfMonth, startOfWeek } from "date-fns"
import { VNode, h } from "preact"
import { useState } from "preact/hooks"
import { useTranslationContext } from "../index.browser.js"

export function Calendar({ value, onChange }: { value: AbsoluteTime | undefined, onChange: (v: AbsoluteTime) => void }): VNode {
  const today = startOfDay(new Date())
  const selected = !value ? today : new Date(AbsoluteTime.toStampMs(value))
  const [showingDate, setShowingDate] = useState(selected)
  const month = getMonth(showingDate)
  const year = getYear(showingDate)

  const start = startOfWeek(startOfMonth(showingDate));
  const end = endOfWeek(endOfMonth(showingDate));
  const daysInMonth = eachDayOfInterval({ start, end });
  const { i18n } = useTranslationContext()
  const monthNames = [
    i18n.str`January`,
    i18n.str`February`,
    i18n.str`March`,
    i18n.str`April`,
    i18n.str`May`,
    i18n.str`June`,
    i18n.str`July`,
    i18n.str`August`,
    i18n.str`September`,
    i18n.str`October`,
    i18n.str`November`,
    i18n.str`December`,
  ]
  return <div class="text-center p-2">
    <div class="flex items-center text-gray-900">
      <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
        onClick={() => {
          setShowingDate(dateSub(showingDate, { years: 1 }))
        }}>
        <span class="sr-only">
          {i18n.str`Previous year`}
        </span>
        <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
        </svg>
      </button>
      <div class="flex-auto text-sm font-semibold">{year}</div>
      <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
        onClick={() => {
          setShowingDate(dateAdd(showingDate, { years: 1 }))
        }}>
        <span class="sr-only">
          {i18n.str`Next year`}
        </span>
        <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
        </svg>
      </button>
    </div>
    <div class="mt-4 flex items-center text-gray-900">
      <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
        onClick={() => {
          setShowingDate(dateSub(showingDate, { months: 1 }))
        }}>
        <span class="sr-only">
          {i18n.str`Previous month`}
        </span>
        <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
        </svg>
      </button>
      <div class="flex-auto text-sm font-semibold">{monthNames[month]}</div>
      <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 rounded-sm "
        onClick={() => {
          setShowingDate(dateAdd(showingDate, { months: 1 }))
        }}>
        <span class="sr-only">
          {i18n.str`Next month`}
        </span>
        <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
        </svg>
      </button>
    </div>
    <div class="mt-6 grid grid-cols-7 text-xs leading-6 text-gray-500">
      <div>M</div>
      <div>T</div>
      <div>W</div>
      <div>T</div>
      <div>F</div>
      <div>S</div>
      <div>S</div>
    </div>
    <div class="isolate mt-2">
      <div class="grid grid-cols-7 gap-px rounded-lg bg-gray-200 text-sm shadow ring-1 ring-gray-200">
        {daysInMonth.map(current => (
          <button type="button"
            data-month={isSameMonth(current, showingDate)}
            data-today={isSameDay(current, today)}
            data-selected={isSameDay(current, selected)}
            onClick={() => {
              onChange(AbsoluteTime.fromStampMs(current.getTime()))
            }}
            class="text-gray-400 hover:bg-gray-700 focus:z-10 py-1.5 
          data-[month=false]:bg-gray-100 data-[month=true]:bg-white 
            data-[today=true]:font-semibold  
          data-[month=true]:text-gray-900
          data-[today=true]:bg-red-300 data-[today=true]:hover:bg-red-200
          data-[month=true]:hover:bg-gray-200
          data-[selected=true]:!bg-blue-400 data-[selected=true]:hover:!bg-blue-300 ">
            <time dateTime={format(current, "yyyy-MM-dd")}
              class="mx-auto flex h-7 w-7 py-4 px-5 sm:px-8 items-center justify-center rounded-full">
              {format(current, "dd")}
            </time>
          </button>
        ))}
      </div>
      {daysInMonth.length < 40 ? <div class="w-7 h-7 m-1.5" /> : undefined}
    </div>
  </div>
}