All Examples
Shopping Cart
E-commerce app with product listing, cart management, and checkout flow.
Routing
/
import { Effect, Match as M, Option, Schema as S } from 'effect'
import { Command, Runtime } from 'foldkit'
import { Document, Html, html } from 'foldkit/html'
import { m } from 'foldkit/message'
import { UrlRequest, load, pushUrl } from 'foldkit/navigation'
import { evo } from 'foldkit/struct'
import { Url, toString as urlToString } from 'foldkit/url'
import { products } from './data/products'
import { Cart } from './domain'
import { Cart as CartPage, Checkout, Products } from './page'
import {
AppRoute,
cartRouter,
checkoutRouter,
productsRouter,
urlToAppRoute,
} from './route'
// MODEL
export const Model = S.Struct({
route: AppRoute,
cart: Cart.Cart,
deliveryInstructions: S.String,
orderPlaced: S.Boolean,
productsPage: Products.Model,
})
export type Model = typeof Model.Type
// MESSAGE
export const CompletedNavigateInternal = m('CompletedNavigateInternal')
export const CompletedLoadExternal = m('CompletedLoadExternal')
export const ClickedLink = m('ClickedLink', {
request: UrlRequest,
})
export const ChangedUrl = m('ChangedUrl', { url: Url })
export const GotProductsMessage = m('GotProductsMessage', {
message: Products.Message,
})
export const ClickedQuantityChange = m('ClickedQuantityChange', {
itemId: S.String,
quantity: S.Number,
})
export const ClickedRemoveCartItem = m('ClickedRemoveCartItem', {
itemId: S.String,
})
export const ClickedClearCart = m('ClickedClearCart')
export const UpdatedDeliveryInstructions = m('UpdatedDeliveryInstructions', {
value: S.String,
})
export const ClickedPlaceOrder = m('ClickedPlaceOrder')
export const Message = S.Union([
CompletedNavigateInternal,
CompletedLoadExternal,
ClickedLink,
ChangedUrl,
GotProductsMessage,
ClickedQuantityChange,
ClickedRemoveCartItem,
ClickedClearCart,
UpdatedDeliveryInstructions,
ClickedPlaceOrder,
])
export type Message = typeof Message.Type
// INIT
export const init: Runtime.RoutingApplicationInit<Model, Message> = (
url: Url,
) => {
return [
{
route: urlToAppRoute(url),
cart: [],
deliveryInstructions: '',
orderPlaced: false,
productsPage: Products.init(products),
},
[],
]
}
// COMMAND
const NavigateInternal = Command.define(
'NavigateInternal',
{ url: S.String },
CompletedNavigateInternal,
)(({ url }) => pushUrl(url).pipe(Effect.as(CompletedNavigateInternal())))
const LoadExternal = Command.define(
'LoadExternal',
{ href: S.String },
CompletedLoadExternal,
)(({ href }) => load(href).pipe(Effect.as(CompletedLoadExternal())))
// UPDATE
type UpdateReturn = readonly [Model, ReadonlyArray<Command.Command<Message>>]
const withUpdateReturn = M.withReturnType<UpdateReturn>()
export const update = (model: Model, message: Message): UpdateReturn =>
M.value(message).pipe(
withUpdateReturn,
M.tagsExhaustive({
CompletedNavigateInternal: () => [model, []],
CompletedLoadExternal: () => [model, []],
ClickedLink: ({ request }) =>
M.value(request).pipe(
withUpdateReturn,
M.tagsExhaustive({
Internal: ({ url }) => [
model,
[NavigateInternal({ url: urlToString(url) })],
],
External: ({ href }) => [model, [LoadExternal({ href })]],
}),
),
ChangedUrl: ({ url }) => [
evo(model, {
route: () => urlToAppRoute(url),
}),
[],
],
GotProductsMessage: ({ message }) => {
const [nextProductsModel, commands, maybeOutMessage] = Products.update(
model.productsPage,
message,
)
const mappedCommands = Command.mapMessages(commands, message =>
GotProductsMessage({ message }),
)
return Option.match(maybeOutMessage, {
onNone: (): UpdateReturn => [
evo(model, { productsPage: () => nextProductsModel }),
mappedCommands,
],
onSome: M.type<Products.OutMessage>().pipe(
withUpdateReturn,
M.tagsExhaustive({
AddedToCart: ({ item }) => [
evo(model, {
productsPage: () => nextProductsModel,
cart: Cart.addItem(item),
}),
mappedCommands,
],
ChangedQuantity: ({ itemId, quantity }) => [
evo(model, {
productsPage: () => nextProductsModel,
cart: Cart.changeQuantity(itemId, quantity),
}),
mappedCommands,
],
}),
),
})
},
ClickedQuantityChange: ({ itemId, quantity }) => [
evo(model, {
cart: Cart.changeQuantity(itemId, quantity),
}),
[],
],
ClickedRemoveCartItem: ({ itemId }) => [
evo(model, {
cart: Cart.removeItem(itemId),
}),
[],
],
ClickedClearCart: () => [
evo(model, {
cart: () => [],
}),
[],
],
UpdatedDeliveryInstructions: ({ value }) => [
evo(model, {
deliveryInstructions: () => value,
}),
[],
],
ClickedPlaceOrder: () => [
evo(model, {
orderPlaced: () => true,
cart: () => [],
deliveryInstructions: () => '',
}),
[],
],
}),
)
// VIEW
const navigationView = (currentRoute: AppRoute, cartCount: number): Html => {
const h = html<Message>()
const navLinkClassName = (isActive: boolean) =>
`hover:bg-blue-600 font-medium px-3 py-1 rounded transition ${isActive ? 'bg-blue-700 bg-opacity-50' : ''}`
return h.nav(
[h.Class('bg-blue-500 text-white p-4 mb-6')],
[
h.ul(
[h.Class('max-w-6xl mx-auto flex gap-6 justify-center list-none')],
[
h.li(
[],
[
h.a(
[
h.Href(productsRouter({ searchText: Option.none() })),
h.Class(navLinkClassName(currentRoute._tag === 'Products')),
],
['Products'],
),
],
),
h.li(
[],
[
h.a(
[
h.Href(cartRouter()),
h.Class(navLinkClassName(currentRoute._tag === 'Cart')),
],
cartCount > 0 ? [`Cart (${cartCount})`] : ['Cart'],
),
],
),
h.li(
[],
[
h.a(
[
h.Href(checkoutRouter()),
h.Class(navLinkClassName(currentRoute._tag === 'Checkout')),
],
['Checkout'],
),
],
),
],
),
],
)
}
const productsView = (model: Model): Html => {
const h = html<Message>()
return h.submodel({
slotId: 'products',
model: model.productsPage,
view: Products.view,
viewInputs: { cart: model.cart },
toParentMessage: message => GotProductsMessage({ message }),
})
}
const cartView = (model: Model): Html => {
return CartPage.view(model.cart)
}
const checkoutView = (model: Model): Html => {
return Checkout.view(
model.cart,
model.deliveryInstructions,
model.orderPlaced,
)
}
const notFoundView = (path: string): Html => {
const h = html<Message>()
return h.div(
[h.Class('max-w-4xl mx-auto px-4 text-center')],
[
h.h1(
[h.Class('text-4xl font-bold text-red-600 mb-6')],
['404 - Page Not Found'],
),
h.p(
[h.Class('text-lg text-gray-600 mb-4')],
[`The path "${path}" was not found.`],
),
h.a(
[
h.Href(productsRouter({ searchText: Option.none() })),
h.Class('text-blue-500 hover:underline'),
],
['← Go to Products'],
),
],
)
}
const routeTitle = (route: Model['route']): string =>
M.value(route).pipe(
M.tag('Products', () => 'Shopping Cart'),
M.orElse(({ _tag }) => `${_tag} | Shopping Cart`),
)
export const view = (model: Model): Document => {
const h = html<Message>()
const routeContent = M.value(model.route).pipe(
M.tagsExhaustive({
Products: () => productsView(model),
Cart: () => cartView(model),
Checkout: () => checkoutView(model),
NotFound: ({ path }) => notFoundView(path),
}),
)
return {
title: routeTitle(model.route),
body: h.div(
[h.Class('min-h-screen bg-gray-100')],
[
h.header(
[],
[navigationView(model.route, Cart.totalItems(model.cart))],
),
h.main(
[h.Class('py-8')],
[h.keyed('div')(model.route._tag, [], [routeContent])],
),
],
),
}
}