go-htmx-gsd #1
14 changed files with 239 additions and 16 deletions
|
|
@ -149,9 +149,10 @@ func main() {
|
|||
fileDeps := web.FilesDeps{Queries: q, Files: filesStore, MaxUploadMB: maxUploadMB}
|
||||
etapeDeps := web.EtapesDeps{Queries: q}
|
||||
eventDeps := web.EventsDeps{Queries: q}
|
||||
planningDeps := web.PlanningDeps{Queries: q}
|
||||
|
||||
// D-09: pass the embedded static FS — binary has zero runtime file dependencies.
|
||||
router, err := web.NewRouter(pool, assets.Static, deps, tabloDeps, taskDeps, etapeDeps, eventDeps, fileDeps, csrfKey, env)
|
||||
router, err := web.NewRouter(pool, assets.Static, deps, tabloDeps, taskDeps, etapeDeps, eventDeps, planningDeps, fileDeps, csrfKey, env)
|
||||
if err != nil {
|
||||
slog.Error("router init failed", "err", err)
|
||||
os.Exit(1)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func newTestRouterWithCSRF(q *sqlc.Queries, store *auth.Store) http.Handler {
|
|||
csrfKey[i] = byte(i + 1)
|
||||
}
|
||||
deps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, FilesDeps{Queries: q}, csrfKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, csrfKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
panic("newTestRouterWithCSRF: " + err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ var testCSRFKey = func() []byte {
|
|||
// Referer header are accepted.
|
||||
func newTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||
deps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
panic("newTestRouter: " + err.Error())
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ func newTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
|||
// enabling rate-limit tests to use a fake clock.
|
||||
func newTestRouterWithLimiter(q *sqlc.Queries, store *auth.Store, rl *auth.LimiterStore) http.Handler {
|
||||
deps := AuthDeps{Queries: q, Store: store, Secure: false, Limiter: rl}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
panic("newTestRouterWithLimiter: " + err.Error())
|
||||
}
|
||||
|
|
@ -56,7 +56,7 @@ func newTestRouterWithLimiter(q *sqlc.Queries, store *auth.Store, rl *auth.Limit
|
|||
|
||||
func newAuthPageRouter(t *testing.T, deps AuthDeps) http.Handler {
|
||||
t.Helper()
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, PlanningDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("NewRouter: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ func newEtapeTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
|||
tabloDeps := TablosDeps{Queries: q}
|
||||
taskDeps := TasksDeps{Queries: q}
|
||||
etapeDeps := EtapesDeps{Queries: q}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, etapeDeps, EventsDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, etapeDeps, EventsDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
panic("newEtapeTestRouter: " + err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func newEventTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
|||
taskDeps := TasksDeps{Queries: q}
|
||||
etapeDeps := EtapesDeps{Queries: q}
|
||||
eventDeps := EventsDeps{Queries: q}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, etapeDeps, eventDeps, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, etapeDeps, eventDeps, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
panic("newEventTestRouter: " + err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ func newFileTestRouter(q *sqlc.Queries, store *auth.Store, fileStore files.FileS
|
|||
tabloDeps := TablosDeps{Queries: q}
|
||||
taskDeps := TasksDeps{Queries: q}
|
||||
fileDeps := FilesDeps{Queries: q, Files: fileStore, MaxUploadMB: 25}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, fileDeps, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, PlanningDeps{Queries: q}, fileDeps, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
panic("newFileTestRouter: " + err.Error())
|
||||
}
|
||||
|
|
@ -170,7 +170,7 @@ func TestFileUploadTooLarge(t *testing.T) {
|
|||
tabloDeps := TablosDeps{Queries: q}
|
||||
taskDeps := TasksDeps{Queries: q}
|
||||
fileDeps := FilesDeps{Queries: q, Files: stub, MaxUploadMB: 1}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, fileDeps, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, PlanningDeps{Queries: q}, fileDeps, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("NewRouter: %v", err)
|
||||
}
|
||||
|
|
|
|||
59
backend/internal/web/handlers_planning.go
Normal file
59
backend/internal/web/handlers_planning.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backend/internal/auth"
|
||||
"backend/internal/db/sqlc"
|
||||
"backend/templates"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
)
|
||||
|
||||
type PlanningDeps struct {
|
||||
Queries *sqlc.Queries
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
func parsePlanningStart(raw string, now time.Time) time.Time {
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return today
|
||||
}
|
||||
parsed, err := time.Parse("2006-01-02", raw)
|
||||
if err != nil {
|
||||
return today
|
||||
}
|
||||
return time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, time.Local)
|
||||
}
|
||||
|
||||
func PlanningPageHandler(deps PlanningDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, user, _ := auth.Authed(r.Context())
|
||||
now := time.Now
|
||||
if deps.Now != nil {
|
||||
now = deps.Now
|
||||
}
|
||||
start := parsePlanningStart(r.URL.Query().Get("start"), now())
|
||||
end := start.AddDate(0, 0, 13)
|
||||
|
||||
rows, err := deps.Queries.ListUserEventsRange(r.Context(), sqlc.ListUserEventsRangeParams{
|
||||
UserID: user.ID,
|
||||
StartDate: pgDateFromTime(start),
|
||||
EndDate: pgDateFromTime(end),
|
||||
})
|
||||
if err != nil {
|
||||
slog.Default().Error("planning: ListUserEventsRange failed", "user_id", user.ID, "err", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
agenda := templates.NewPlanningAgenda(start, end, parsePlanningStart("", now()), rows)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.PlanningPage(user, csrf.Token(r), agenda).Render(r.Context(), w)
|
||||
}
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ func withGoogleClaims(deps AuthDeps, claims auth.ProviderClaims) AuthDeps {
|
|||
|
||||
func newSocialRouter(t *testing.T, deps AuthDeps) http.Handler {
|
||||
t.Helper()
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, PlanningDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("NewRouter: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import (
|
|||
func newTabloTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||
tabloDeps := TablosDeps{Queries: q}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
panic("newTabloTestRouter: " + err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func newTaskTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
|||
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||
tabloDeps := TablosDeps{Queries: q}
|
||||
taskDeps := TasksDeps{Queries: q}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
panic("newTaskTestRouter: " + err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ func TestReadyz_Down(t *testing.T) {
|
|||
// was public. The HTMX demo content is tested by
|
||||
// TestProtected_HomeAuthRendersUserEmail in handlers_auth_test.go.
|
||||
func TestIndex_UnauthRedirects(t *testing.T) {
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, PlanningDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("NewRouter: %v", err)
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@ func TestIndex_UnauthRedirects(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDemoTime_Fragment(t *testing.T) {
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, PlanningDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("NewRouter: %v", err)
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ func TestDemoTime_Fragment(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRequestID_HeaderSet(t *testing.T) {
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, EtapesDeps{}, EventsDeps{}, PlanningDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("NewRouter: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ type Pinger interface {
|
|||
// trustedOrigins is an optional list of additional origins for the CSRF
|
||||
// referer check (used in integration tests to allow localhost requests without
|
||||
// a Referer header). In production, pass no extra args — leave empty.
|
||||
func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDeps, taskDeps TasksDeps, etapeDeps EtapesDeps, eventDeps EventsDeps, fileDeps FilesDeps, csrfKey []byte, env string, trustedOrigins ...string) (http.Handler, error) {
|
||||
func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDeps, taskDeps TasksDeps, etapeDeps EtapesDeps, eventDeps EventsDeps, planningDeps PlanningDeps, fileDeps FilesDeps, csrfKey []byte, env string, trustedOrigins ...string) (http.Handler, error) {
|
||||
r := chi.NewRouter()
|
||||
r.Use(RequestIDMiddleware)
|
||||
r.Use(chimw.RealIP)
|
||||
|
|
@ -84,6 +84,7 @@ func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDep
|
|||
r.Get("/", TablosListHandler(tabloDeps))
|
||||
r.Post("/logout", LogoutHandler(deps))
|
||||
r.Get("/account/providers", AccountProvidersHandler(deps))
|
||||
r.Get("/planning", PlanningPageHandler(planningDeps))
|
||||
// Static segments BEFORE parametric (Pitfall 1 — chi v5 route resolution).
|
||||
r.Get("/tablos/new", TablosNewHandler(tabloDeps))
|
||||
r.Post("/tablos", TablosCreateHandler(tabloDeps))
|
||||
|
|
|
|||
67
backend/templates/planning.templ
Normal file
67
backend/templates/planning.templ
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"backend/internal/auth"
|
||||
)
|
||||
|
||||
templ PlanningPage(user *auth.User, csrfToken string, agenda PlanningAgenda) {
|
||||
@Layout("Planning - Xtablo", user, csrfToken) {
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-[28px] font-semibold leading-tight">Planning</h1>
|
||||
<p class="mt-1 text-sm text-slate-600">{ agenda.RangeLabel }</p>
|
||||
</div>
|
||||
<nav class="flex flex-wrap items-center gap-2" aria-label="Planning navigation">
|
||||
<a href={ templ.SafeURL(agenda.PrevURL) } class="ui-button ui-button-soft-neutral-md">Previous 14 days</a>
|
||||
<a
|
||||
href={ templ.SafeURL(agenda.TodayURL) }
|
||||
if agenda.ShowingToday {
|
||||
class="ui-button ui-button-soft-neutral-md"
|
||||
} else {
|
||||
class="ui-button ui-button-solid-default-md"
|
||||
}
|
||||
>Today</a>
|
||||
<a href={ templ.SafeURL(agenda.NextURL) } class="ui-button ui-button-soft-neutral-md">Next 14 days</a>
|
||||
</nav>
|
||||
</div>
|
||||
if len(agenda.Events) == 0 {
|
||||
<div class="border border-slate-200 bg-slate-50 px-4 py-12">
|
||||
<h2 class="text-xl font-semibold leading-snug text-slate-800">No events in this range</h2>
|
||||
<p class="mt-2 text-base text-slate-600">Use the navigation controls to browse another 14-day window.</p>
|
||||
</div>
|
||||
} else {
|
||||
<ul class="divide-y divide-slate-200 border-y border-slate-200" aria-label="Planning agenda">
|
||||
for _, event := range agenda.Events {
|
||||
@PlanningEventListItem(event)
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ PlanningEventListItem(event PlanningEventRow) {
|
||||
<li class="bg-white px-4 py-3 hover:bg-slate-50">
|
||||
<a href={ templ.SafeURL(event.URL) } class="block focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2" aria-label={ "Open event in tablo: " + event.Title }>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:gap-3">
|
||||
<span class="text-sm font-medium text-slate-700">{ event.TimeRange }</span>
|
||||
<span class="text-base font-semibold text-slate-900">{ event.Title }</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm text-slate-600">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
if event.HasColor {
|
||||
<span class="inline-block h-2.5 w-2.5 rounded-full" style={ "background-color: " + event.TabloColor }></span>
|
||||
}
|
||||
<span>{ event.TabloTitle }</span>
|
||||
</span>
|
||||
if event.HasLocation {
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{ event.Location }</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
95
backend/templates/planning_forms.go
Normal file
95
backend/templates/planning_forms.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"backend/internal/db/sqlc"
|
||||
)
|
||||
|
||||
type PlanningAgenda struct {
|
||||
Start time.Time
|
||||
End time.Time
|
||||
Today time.Time
|
||||
RangeLabel string
|
||||
PrevURL string
|
||||
TodayURL string
|
||||
NextURL string
|
||||
ShowingToday bool
|
||||
Events []PlanningEventRow
|
||||
}
|
||||
|
||||
type PlanningEventRow struct {
|
||||
Title string
|
||||
TimeRange string
|
||||
TabloTitle string
|
||||
TabloColor string
|
||||
HasColor bool
|
||||
Location string
|
||||
HasLocation bool
|
||||
URL string
|
||||
}
|
||||
|
||||
func NewPlanningAgenda(start time.Time, end time.Time, today time.Time, rows []sqlc.ListUserEventsRangeRow) PlanningAgenda {
|
||||
events := make([]PlanningEventRow, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
location := ""
|
||||
if row.Location.Valid {
|
||||
location = row.Location.String
|
||||
}
|
||||
color := ""
|
||||
if row.TabloColor.Valid {
|
||||
color = row.TabloColor.String
|
||||
}
|
||||
events = append(events, PlanningEventRow{
|
||||
Title: row.Title,
|
||||
TimeRange: PlanningEventTimeRange(row),
|
||||
TabloTitle: row.TabloTitle,
|
||||
TabloColor: color,
|
||||
HasColor: color != "",
|
||||
Location: location,
|
||||
HasLocation: location != "",
|
||||
URL: PlanningEventURL(row),
|
||||
})
|
||||
}
|
||||
return PlanningAgenda{
|
||||
Start: start,
|
||||
End: end,
|
||||
Today: today,
|
||||
RangeLabel: PlanningRangeLabel(start, end),
|
||||
PrevURL: PlanningURL(start.AddDate(0, 0, -14)),
|
||||
TodayURL: "/planning",
|
||||
NextURL: PlanningURL(start.AddDate(0, 0, 14)),
|
||||
ShowingToday: samePlanningDay(start, today),
|
||||
Events: events,
|
||||
}
|
||||
}
|
||||
|
||||
func PlanningURL(start time.Time) string {
|
||||
return "/planning?start=" + start.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func PlanningEventURL(row sqlc.ListUserEventsRangeRow) string {
|
||||
return "/tablos/" + row.TabloID.String() + "/events?month=" + row.EventDate.Time.Format("2006-01")
|
||||
}
|
||||
|
||||
func PlanningEventTimeRange(row sqlc.ListUserEventsRangeRow) string {
|
||||
start := FormatEventTime(row.StartTime)
|
||||
if start == "" {
|
||||
return ""
|
||||
}
|
||||
if !row.EndTime.Valid {
|
||||
return start
|
||||
}
|
||||
return start + "-" + FormatEventTime(row.EndTime)
|
||||
}
|
||||
|
||||
func PlanningRangeLabel(start time.Time, end time.Time) string {
|
||||
if start.Year() == end.Year() {
|
||||
return start.Format("January 2") + " - " + end.Format("January 2, 2006")
|
||||
}
|
||||
return start.Format("January 2, 2006") + " - " + end.Format("January 2, 2006")
|
||||
}
|
||||
|
||||
func samePlanningDay(a time.Time, b time.Time) bool {
|
||||
return a.Year() == b.Year() && a.Month() == b.Month() && a.Day() == b.Day()
|
||||
}
|
||||
Loading…
Reference in a new issue