Calendar widget v1

This commit is contained in:
2026-04-23 10:48:10 +02:00
parent 314fe4600f
commit 910be60ed5
7 changed files with 471 additions and 15 deletions
+235 -15
View File
@@ -22,10 +22,14 @@ func init() {
type diaryHandler struct{}
func (d *diaryHandler) handle(root, fsPath, urlPath string) *specialPage {
depth, ok := findDiaryContext(root, fsPath)
if !ok || depth == 0 {
depth, diaryRootFS, diaryRootURL, ok := findDiaryContext(root, fsPath, urlPath)
if !ok {
return nil
}
widget := computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath, depth)
if depth == 0 {
return &specialPage{Widget: widget}
}
var content template.HTML
switch depth {
case 1:
@@ -35,30 +39,246 @@ func (d *diaryHandler) handle(root, fsPath, urlPath string) *specialPage {
case 3:
content = renderDiaryDay(fsPath, urlPath)
}
return &specialPage{Content: content, SuppressListing: true}
return &specialPage{Content: content, SuppressListing: true, Widget: widget}
}
// findDiaryContext walks up from fsPath toward root looking for a
// .page-settings file with type=diary. Returns the depth of fsPath
// relative to the diary root, and whether one was found.
// depth=0 means fsPath itself is the diary root.
func findDiaryContext(root, fsPath string) (int, bool) {
current := fsPath
for depth := 0; ; depth++ {
s := readPageSettings(current)
// relative to the diary root, the diary root fs path, its URL, and
// whether a diary root was found. depth=0 means fsPath itself is the root.
func findDiaryContext(root, fsPath, urlPath string) (depth int, diaryRootFS, diaryRootURL string, ok bool) {
currentFS := fsPath
currentURL := urlPath
for d := 0; ; d++ {
s := readPageSettings(currentFS)
if s != nil && s.Type == "diary" {
return depth, true
return d, currentFS, currentURL, true
}
if current == root {
if currentFS == root {
break
}
parent := filepath.Dir(current)
if parent == current {
parent := filepath.Dir(currentFS)
if parent == currentFS {
break
}
current = parent
currentFS = parent
currentURL = parentURL(currentURL)
}
return 0, false
return 0, "", "", false
}
type calDay struct {
Num int
URL string
HasEntry bool
IsToday bool
IsCurrent bool
}
type calYear struct {
Num int
URL string
IsCurrent bool
}
type calMonth struct {
Num int
Name string
URL string
IsCurrent bool
}
type calendarData struct {
DisplayYear int
DisplayMonth int
MonthName string
DiaryURL string
YearURL string
MonthURL string
PrevMonURL string
NextMonURL string
Weeks [][]calDay
Years []calYear
AllMonths []calMonth
}
var diaryCalTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-calendar.html"))
func computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath string, depth int) template.HTML {
today := time.Now()
var displayYear, displayMonth, currentDay int
switch depth {
case 0:
displayYear = today.Year()
displayMonth = int(today.Month())
case 1:
y, err := strconv.Atoi(filepath.Base(fsPath))
if err != nil {
return ""
}
displayYear = y
if y == today.Year() {
displayMonth = int(today.Month())
} else {
displayMonth = 1
}
case 2:
m, err := strconv.Atoi(filepath.Base(fsPath))
if err != nil || m < 1 || m > 12 {
return ""
}
y, err := strconv.Atoi(filepath.Base(filepath.Dir(fsPath)))
if err != nil {
return ""
}
displayYear = y
displayMonth = m
case 3:
d, err := strconv.Atoi(filepath.Base(fsPath))
if err != nil || d < 1 || d > 31 {
return ""
}
monthFS := filepath.Dir(fsPath)
m, err := strconv.Atoi(filepath.Base(monthFS))
if err != nil || m < 1 || m > 12 {
return ""
}
y, err := strconv.Atoi(filepath.Base(filepath.Dir(monthFS)))
if err != nil {
return ""
}
displayYear = y
displayMonth = m
currentDay = d
default:
return ""
}
// Which days in the display month have diary subfolders?
monthFSPath := filepath.Join(diaryRootFS,
fmt.Sprintf("%d", displayYear),
fmt.Sprintf("%02d", displayMonth))
dayEntries, _ := os.ReadDir(monthFSPath)
hasDayEntry := map[int]bool{}
for _, e := range dayEntries {
if !e.IsDir() {
continue
}
d, err := strconv.Atoi(e.Name())
if err != nil || d < 1 || d > 31 {
continue
}
hasDayEntry[d] = true
}
// Build calendar grid with Monday as first column.
firstDay := time.Date(displayYear, time.Month(displayMonth), 1, 0, 0, 0, 0, time.UTC)
startOffset := int(firstDay.Weekday()+6) % 7
daysInMonth := time.Date(displayYear, time.Month(displayMonth)+1, 0, 0, 0, 0, 0, time.UTC).Day()
monthURLBase := path.Join(diaryRootURL,
fmt.Sprintf("%d", displayYear),
fmt.Sprintf("%02d", displayMonth)) + "/"
var weeks [][]calDay
week := make([]calDay, 7)
col := startOffset
for d := 1; d <= daysInMonth; d++ {
dayURL := path.Join(monthURLBase, fmt.Sprintf("%02d", d)) + "/"
cell := calDay{Num: d, HasEntry: hasDayEntry[d]}
if cell.HasEntry {
cell.URL = dayURL
} else {
cell.URL = dayURL + "?edit"
}
cell.IsCurrent = d == currentDay
cell.IsToday = d == today.Day() &&
time.Month(displayMonth) == today.Month() &&
displayYear == today.Year()
week[col] = cell
col++
if col == 7 {
weeks = append(weeks, week)
week = make([]calDay, 7)
col = 0
}
}
if col > 0 {
weeks = append(weeks, week)
}
prev := time.Date(displayYear, time.Month(displayMonth)-1, 1, 0, 0, 0, 0, time.UTC)
next := time.Date(displayYear, time.Month(displayMonth)+1, 1, 0, 0, 0, 0, time.UTC)
prevMonURL := path.Join(diaryRootURL,
fmt.Sprintf("%d", prev.Year()),
fmt.Sprintf("%02d", int(prev.Month()))) + "/"
nextMonURL := path.Join(diaryRootURL,
fmt.Sprintf("%d", next.Year()),
fmt.Sprintf("%02d", int(next.Month()))) + "/"
yearURL := path.Join(diaryRootURL, fmt.Sprintf("%d", displayYear)) + "/"
// Collect all year subdirectories in diary root (descending).
yearEntries, _ := os.ReadDir(diaryRootFS)
var years []calYear
yearSet := map[int]bool{}
for _, e := range yearEntries {
if !e.IsDir() {
continue
}
y, err := strconv.Atoi(e.Name())
if err != nil {
continue
}
yearSet[y] = true
years = append(years, calYear{
Num: y,
URL: path.Join(diaryRootURL, e.Name()) + "/",
IsCurrent: y == displayYear,
})
}
if !yearSet[displayYear] {
years = append(years, calYear{
Num: displayYear,
URL: yearURL,
IsCurrent: true,
})
}
sort.Slice(years, func(i, j int) bool { return years[i].Num > years[j].Num })
// All 12 months, ascending, linked to current display year.
allMonths := make([]calMonth, 12)
for m := 1; m <= 12; m++ {
allMonths[m-1] = calMonth{
Num: m,
Name: germanMonths[time.Month(m)],
URL: path.Join(diaryRootURL,
fmt.Sprintf("%d", displayYear),
fmt.Sprintf("%02d", m)) + "/",
IsCurrent: m == displayMonth,
}
}
data := calendarData{
DisplayYear: displayYear,
DisplayMonth: displayMonth,
MonthName: germanMonths[time.Month(displayMonth)],
DiaryURL: diaryRootURL,
YearURL: yearURL,
MonthURL: monthURLBase,
PrevMonURL: prevMonURL,
NextMonURL: nextMonURL,
Weeks: weeks,
Years: years,
AllMonths: allMonths,
}
var buf bytes.Buffer
if err := diaryCalTmpl.Execute(&buf, data); err != nil {
log.Printf("diary calendar template: %v", err)
return ""
}
return template.HTML(buf.String())
}
// diaryPhoto is a photo file whose name starts with a YYYY-MM-DD date prefix.