from fasthtml.common import *
# MonsterUI shadows fasthtml components with the same name
from monsterui.all import *
# If you don't want shadowing behavior, you use import monsterui.core as ... style instead
from fasthtml.components import Uk_input_tag
from fasthtml.svg import *
import calendar
from datetime import datetime
# Get frankenui and tailwind headers via CDN using Theme.blue.headers()
hdrs = Theme.blue.headers()
# fast_app is shadowed by MonsterUI to make it default to no Pico, and add body classes
# needed for frankenui theme styling
app, rt = fast_app(hdrs=hdrs, live=True)
products = [
{"name": "Kalpasi (Black Stone Flower)", "price": "$509.50", "img": "https://picsum.photos/400/100?random=1"},
{"name": "Radhuni (Wild Celery Seeds)", "price": "$505.25", "img": "https://picsum.photos/400/100?random=2"},
{"name": "Kokum", "price": "$503.00", "img": "https://picsum.photos/400/100?random=3"},
{"name": "Mace (Javitri)", "price": "$504.50", "img": "https://picsum.photos/400/100?random=4"},
{"name": "Anardana (Dried Pomegranate)", "price": "$506.25", "img": "https://picsum.photos/400/100?random=5"},
{"name": "Ajmod (Indian Lovage)", "price": "$507.00", "img": "https://picsum.photos/400/100?random=6"},
{"name": "Marathi Moggu", "price": "$508.50", "img": "https://picsum.photos/400/100?random=7"},
{"name": "Kalonji (Nigella Seeds)", "price": "$504.25", "img": "https://picsum.photos/400/100?random=8"}
]
CreateAccount = Card(
Grid(
Button(DivLAligned(UkIcon('github'), Div('Github')), cls="w-full"),
Button('Google', cls="w-full")
),
DividerSplit("OR CONTINUE WITH", text_cls=TextPresets.muted_sm),
LabelInput('Email', id='email', placeholder='m@example.com'),
LabelInput('Password', id='password',placeholder='Password', type='Password'),
header=(H3('Create an Account'), Subtitle('Enter your email below to create your account')),
footer=Button('Create Account', cls=(ButtonT.primary, 'w-full'))
)
PaypalSVG_data = "..." # unchanged
AppleSVG_data = "..." # unchanged
Card1Svg = Svg(viewBox="0 0 24 24", fill="none", stroke="currentColor", stroke_linecap="round", stroke_linejoin="round", stroke_width="2", cls="h-6 w-6 mr-1")(
Rect(width="20", height="14", x="2", y="5", rx="2"),
Path(d="M2 10h20")
)
PaypalSvg = Svg(role="img", viewBox="0 0 24 24", cls="h-6 w-6 mr-1")(Path(d=PaypalSVG_data, fill="currentColor"))
AppleSvg = Svg(role="img", viewBox="0 0 24 24", cls="h-6 w-6 mr-1")(Path(d=AppleSVG_data, fill="currentColor"))
PaymentMethod = Card(
Grid(
Button(DivCentered(Card1Svg, "Card"), cls='h-20 border-2 border-primary rounded-lg', type="button"),
Button(DivCentered(PaypalSvg, "PayPal"), cls='h-20 border rounded-lg', type="button"),
Button(DivCentered(AppleSvg, "Apple"), cls='h-20 border rounded-lg', type="button")
),
Form(
LabelInput('Name', id='name', placeholder='John Doe'),
LabelInput('Card Number', id='card_number', placeholder='1234 5678 9012 3456'),
Grid(
LabelSelect(*Options(*calendar.month_name[1:], selected_idx=0), label='Expires', id='expire_month'),
LabelSelect(*Options(*range(2024,2030), selected_idx=0), label='Year', id='expire_year'),
LabelInput('CVV', id='cvv', placeholder='CVV', cls='mt-0')
)
),
header=(H3('Payment Method'), Subtitle('Add a new payment method to your account.'))
)
def ProductCard(p):
# Card does lots of boilerplate classes so you can just pass in the content
return Card(
# width:100% makes the image take the full width so we are guarenteed that we won't
# have the image cut off or not large enough. Because all our images are a consistent
# size we do not need to worry about stretching or skewing the image, this is ideal.
# If you have images of different sizes, you will need to use object-fit:cover and/or
# height to either strech, shrink, or crop the image. It is much better to adjust your
# images to be a consistent size upfront so you don't have to handle edge cases of
# different images skeweing/stretching differently.
Img(src=p["img"], alt=p["name"], cls="w-full h-40 object-cover rounded-t-lg"),
# All components can take a cls argument to add additional styling - `mt-2` adds margin
# to the top (see spacing tutorial for details on spacing).
#
# Often adding space makes a site look more put together - usually the 2 - 5 range is a
# good choice
H4(p["name"], cls="mt-3 text-lg font-semibold text-gray-900"),
# There are helpful Enums, such as TextPresetsT, ButtonT, ContainerT, etc that allow for easy
# discoverability of class options.
# bold_sm is helpful for things that you want to look like regular text, but stand out
# visually for emphasis.
P(p["price"], cls=(TextPresets.bold_sm, "text-gray-700 mt-1")),
# ButtonT.primary is useful for actions you really want the user to take (like adding
# something to the cart) - these stand out visually. For dangerous actions (like
# deleting something) you generally would want to use ButtonT.destructive. For UX actions
# that aren't a goal of the page (like cancelling something that hasn't been submitted)
# you generally want the default styling.
Button("Add to cart", cls=(ButtonT.primary, "mt-3 w-full"),
hx_get=product_detail.to(product_name=p['name'], product_price=p['price']),
hx_push_url='true',
hx_target='body'),
cls="shadow-lg rounded-lg overflow-hidden bg-white hover:shadow-xl transition-shadow"
)
# Define this once, at module level (top of file is best)
example_product_description = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Praesent euismod, sapien nec facilisis tincidunt, nunc
nibh posuere justo, vitae luctus neque magna vel nulla.
Curabitur at felis ac nulla fermentum tincidunt.
Integer non risus nec nulla cursus porttitor.
Suspendisse potenti. Donec vel sapien nec erat
malesuada viverra sed a lorem.
- Proin facilisis ligula sed sapien tincidunt, at
fermentum magna volutpat.
- Curabitur vitae lectus nec justo cursus
sollicitudin non sed est.
- Sed ut perspiciatis unde omnis iste natus error
sit voluptatem accusantium doloremque laudantium.
"""
@rt
def index():
# Titled using a H1 title, sets the page title, and wraps contents in Main(Container(...)) using
# frankenui styles. Generally you will want to use Titled for all of your pages
return Titled("Store",
Grid(*[ProductCard(p) for p in products], cols_lg=3, gap=6, cls="p-6")
)
@rt
def product_detail(product_name: str, product_price: str):
return (
Title("Product Detail"),
Grid(
Div(
H3(product_name, cls="text-xl font-semibold mb-4"),
render_md(example_product_description.format(product_name=product_name))
),
Div(
H3("Order", cls="text-lg font-semibold mb-3"),
Form(
LabelInput("Product", id="name", value=product_name, readonly=True,
cls="bg-gray-100 cursor-not-allowed"),
LabelInput("Price", id="price", value=product_price, readonly=True,
cls="bg-gray-100 cursor-not-allowed"),
LabelInput("Quantity", id="quantity", placeholder="1"),
LabelInput("Email", id="email", placeholder="accountemail@example.com"),
Div(PaymentMethod, cls="space-y-4 mt-4"),
Button("Add to order", cls=(ButtonT.primary, "w-full mt-4")),
Button("Continue shopping",
cls=(ButtonT.secondary, "w-full mt-2"),
hx_get="/",
hx_push_url="true",
hx_target="body")
)
),
cols_lg=2,
cls="gap-8 p-6"
)
)
serve()