Scheduling workspace
https://fve.meiermade.com/components/page-examples/scheduling?state=error&view=month&range=-138 Workspace
Northwind Outdoor
This view could not be loaded
Your selection is preserved. Try loading it again.
open System
open FSharp.ViewEngine
open Acme.Components.Primitives
open Acme.Components.Application
open Acme.Components.Documentation
open type Html
open type Svg
open type Datastar
type ExamplePage =
| DependencyGraph
| ExecutionDetail
| FinancialReporting
| Messaging
| Operations
| Scheduling
| MediaManagement
type ReviewState =
| Setup
| Ready
| Loading
| Empty
| Failed
type ExampleQuery =
{ state: ReviewState
item: string
view: string
range: string }
let defaultQuery =
{ state = Ready
item = ""
view = ""
range = "" }
let slug =
function
| DependencyGraph -> "dependency-graph"
| ExecutionDetail -> "execution-detail"
| FinancialReporting -> "financial-reporting"
| Messaging -> "messaging"
| Operations -> "operations-dashboard"
| Scheduling -> "scheduling"
| MediaManagement -> "media-management"
let title =
function
| DependencyGraph -> "Dependency graph"
| ExecutionDetail -> "Execution detail"
| FinancialReporting -> "Financial reporting"
| Messaging -> "Messaging"
| Operations -> "Operations dashboard"
| Scheduling -> "Scheduling"
| MediaManagement -> "Media management"
let url page =
"/components/page-examples/" + slug page
let stateKey =
function
| Setup -> "setup"
| Ready -> "ready"
| Loading -> "loading"
| Empty -> "empty"
| Failed -> "error"
let queryUrl page query =
let pairs =
[ "state", stateKey query.state
"item", query.item
"view", query.view
"range", query.range ]
url page
+ "?"
+ (pairs
|> List.filter (snd >> String.IsNullOrEmpty >> not)
|> List.map (fun (key, value) -> key + "=" + Uri.EscapeDataString value)
|> String.concat "&")
let link (href: string) (label: string) : HtmlElement =
a {
_href href
_class
"font-medium text-[var(--fve-brand-text)] underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-[var(--fve-brand-ring)]"
label
}
let section (heading: string) (content: HtmlElement) =
Section.create (SectionHeader.create heading |> SectionHeader.withDivider) content
|> Section.render id
let details (fields: (string * string) list) =
DescriptionList.create [ for label, value in fields -> DetailField.text label value ]
|> DescriptionList.withColumns DescriptionListColumns.Three
|> DescriptionList.render
let reviewStates page =
(if page = Operations then [ Setup, "Setup" ] else [])
@ [ Ready, "Populated"; Loading, "Loading"; Empty, "Empty"; Failed, "Error" ]
let stateContent page query content =
match query.state with
| Setup
| Ready -> content
| Loading ->
div {
_class "p-8"
LoadingIndicator.create ("Loading " + (title page).ToLowerInvariant())
|> LoadingIndicator.withVisibleLabel
|> LoadingIndicator.render
}
| Empty ->
EmptyState.create "Nothing to show yet" "Change the current selection or return to the populated workspace."
|> EmptyState.withActions (link (queryUrl page { query with state = Ready }) "Return to workspace")
|> EmptyState.render
| Failed ->
Notice.create
"workspace-error"
"This view could not be loaded"
(p { "Your selection is preserved. Try loading it again." })
|> Notice.withTone Tone.Critical
|> Notice.withActions (link (queryUrl page { query with state = Ready }) "Try again")
|> Notice.render
/// Place durable workspace identity above the scrollable destination list.
let sideNavWorkspace (name: string) =
div {
p {
_class "text-xs font-semibold uppercase tracking-wide text-[var(--fve-muted-text)]"
"Workspace"
}
p {
_class "mt-1 truncate text-sm font-semibold"
name
}
}
let workspace page heading subtitle canvas actions content =
let product, workspaceName, destinations =
match page with
| DependencyGraph
| ExecutionDetail ->
"Relay", "Customer analytics", [ DependencyGraph, "Dependencies"; ExecutionDetail, "Executions" ]
| FinancialReporting -> "Ledger", "Northwind Studio", [ FinancialReporting, "Overview" ]
| Messaging -> "Gather", "Northwind Outdoor", [ Messaging, "Messages" ]
| _ ->
"Fieldwork", "Northwind Outdoor", [ Operations, "Overview"; Scheduling, "Schedule"; MediaManagement, "Photos" ]
let items =
[ for destination, label in destinations -> SideNavItem.create destination label ]
let navigation =
SideNav.create
(slug page + "-nav")
(product + " navigation")
(SideNavHeader.create product)
[ SideNavSection.ungrouped items ]
|> SideNav.withCurrent page
|> SideNav.withWidth SideNavWidth.Narrow
|> SideNav.withContext (sideNavWorkspace workspaceName)
|> SideNav.withMobileContext (sideNavWorkspace workspaceName)
|> SideNav.withFooter (
div {
_class "flex min-w-0 items-center gap-3"
Avatar.create "Andy Meier" "AM" |> Avatar.render
p {
_class "truncate text-sm font-medium"
"Andy Meier"
}
}
)
let header =
PageHeader.create heading
|> PageHeader.withSubtitle subtitle
|> PageHeader.withActions (ActionCluster.create (slug page + "-actions") actions)
let topBar =
PageTopBar.create ()
|> PageTopBar.withContent (
div {
_class "flex min-h-[var(--fve-shell-bar-min-height)] items-center px-4 sm:px-6 lg:px-8"
Breadcrumbs.create
(slug page + "-breadcrumbs")
(product + " breadcrumb")
[ BreadcrumbItem.create page product; BreadcrumbItem.create page heading ]
|> Breadcrumbs.render url
}
)
let body =
Page.create header content
|> Page.withTopBar topBar
|> Page.withWidth PageWidth.Full
|> Page.withBodyLayout (
if canvas then
PageBodyLayout.Canvas
else
PageBodyLayout.Padded
)
|> Page.render id
AppShell.create (slug page + "-shell") navigation body
|> AppShell.withTheme (
ComponentsTheme.sky
|> ComponentsTheme.withDensity Density.Compact
|> ComponentsTheme.withControlSize ControlSize.Small)
|> AppShell.withBoundary AppShellBoundary.Container
|> AppShell.asPreview (product + " workspace")
|> AppShell.render url
type ScheduledEvent =
{ id: string
name: string
date: DateOnly
time: string
start: TimeOnly
finish: TimeOnly
people: string
kind: string
photo: string }
let scheduledEvents =
[ { id = "lesson-201"
name = "Coastal trail lesson"
date = DateOnly(2026, 9, 17)
time = "10:00–11:30 AM"
start = TimeOnly(10, 0)
finish = TimeOnly(11, 30)
people = "Maya and Sam · Instructor: Andy Meier"
kind = "Lesson"
photo = "photo-303" }
{ id = "camp-017"
name = "Beginner riding camp"
date = DateOnly(2026, 9, 17)
time = "10:30 AM–12:00 PM"
start = TimeOnly(10, 30)
finish = TimeOnly(12, 0)
people = "3 riders · Instructor: Jordan"
kind = "Camp"
photo = "photo-301" }
{ id = "lesson-202"
name = "Cornering fundamentals"
date = DateOnly(2026, 9, 18)
time = "9:00–10:00 AM"
start = TimeOnly(9, 0)
finish = TimeOnly(10, 0)
people = "Riley · Instructor: Andy Meier"
kind = "Lesson"
photo = "photo-302" }
{ id = "ride-019"
name = "Open track practice"
date = DateOnly(2026, 9, 19)
time = "1:00–3:00 PM"
start = TimeOnly(13, 0)
finish = TimeOnly(15, 0)
people = "6 riders · Supervisor: Jordan"
kind = "Practice"
photo = "photo-304" }
{ id = "return-020"
name = "Equipment return"
date = DateOnly(2026, 9, 24)
time = "4:00–4:30 PM"
start = TimeOnly(16, 0)
finish = TimeOnly(16, 30)
people = "Equipment team · Andy Meier"
kind = "Equipment"
photo = "photo-304" } ]
let eventUrl event =
queryUrl Scheduling { defaultQuery with item = event.id }
let scheduling query =
let event = scheduledEvents |> List.tryFind (fun event -> event.id = query.item)
let content, heading =
match event with
| Some event ->
div {
_class "grid gap-8"
details
[ "Date", event.date.ToString("MMMM d, yyyy", Globalization.CultureInfo.InvariantCulture)
"Time", event.time
"Type", event.kind
"Participants", event.people
"Location", "Coastal training grounds"
"Status", "Confirmed" ]
section
"Session preparation"
(ul {
_class "list-disc space-y-2 pl-5 text-sm"
li { "Check helmets and protective equipment before riding." }
li { "Meet at the east entrance 15 minutes before the session." }
li { "Water and a shaded rest area are available beside the track." }
})
section
"Session photographs"
(link
(queryUrl MediaManagement { defaultQuery with item = event.photo })
"View matching photograph")
link (url Scheduling) "Back to schedule"
},
event.name
| None ->
let view =
match query.view with
| "day" -> CalendarView.Day
| "week" -> CalendarView.Week
| "year" -> CalendarView.Year
| _ -> CalendarView.Month
let today = DateOnly(2026, 9, 17)
let offset =
match Int32.TryParse query.range with
| true, value -> Math.Clamp(value, -366, 366)
| _ -> 0
let start = today.AddDays offset
let step direction =
let target =
match view with
| CalendarView.Day -> start.AddDays direction
| CalendarView.Week -> start.AddDays(direction * 7)
| CalendarView.Month -> start.AddMonths direction
| CalendarView.Year -> start.AddYears direction
target.DayNumber - today.DayNumber
Calendar.create
"Schedule"
view
start
[ for event in scheduledEvents ->
CalendarEvent.create
event.id
event.name
event.date
(eventUrl event)
|> CalendarEvent.withTime event.start event.finish
|> CalendarEvent.withDetail event.people ]
|> Calendar.withToday today (queryUrl Scheduling { query with range = "0" })
|> Calendar.withSelectedDate start
|> Calendar.withDateDestination (fun date -> queryUrl Scheduling { query with view = "day"; range = string (date.DayNumber - today.DayNumber) })
|> (if step -1 >= -366 then Calendar.withPrevious (queryUrl Scheduling { query with range = string (step -1) }) else id)
|> (if step 1 <= 366 then Calendar.withNext (queryUrl Scheduling { query with range = string (step 1) }) else id)
|> Calendar.withViewDestinations
[ for value, label in
[ CalendarView.Day, "day"
CalendarView.Week, "week"
CalendarView.Month, "month"
CalendarView.Year, "year" ] ->
value, queryUrl Scheduling { query with view = label } ]
|> Calendar.render id,
"Schedule"
workspace
Scheduling
heading
"Northwind Outdoor · Times in America/New_York"
false
[ ApplicationAction.link (url Operations) "Overview" ]
(stateContent Scheduling query content)
div { _class "h-[44rem]"; scheduling defaultQuery }
|> Browser.create
|> Browser.withAddress "https://fve.meiermade.com/components/page-examples/scheduling?state=ready"
|> Fixture.browser "page-workspace" "Workspace" "/components/page-examples/scheduling?state=ready"
|> Fixture.render