Tutorial App
This is a heavily commented example of a product gallery app built with fh-FrankenUI for those that like to learn by example. This tutorial app assumes you have some familiarity with fasthtml apps already, so focuses on what fh-frankenui adds on top of fasthtml. To make the most out of this tutorial, you should follow these steps:
- Briefly read through this to get an overview of what is happening, without focusing on any details
- Install fasthtml and fh-frankenui
- Copy the code into your own project locally and run it using `python app.py`
- Go through the code in detail to understand how it works by experimenting with changing things
Tip: Try adding
import fasthtml.common as fh
, so you can replace things with the base fasthtml components to see what happens! For example, try replacingH4
withfh.H4
orButton
withfh.Button
.
from fasthtml.common import *
# fh_frankenui shadows fasthtml components with the same name
from fh_frankenui.core import *
# If you don't want shadowing behavior, you use import frankenui.core as ... style instead
# Get frankenui and tailwind headers via CDN using Theme.blue.headers()
hdrs = Theme.blue.headers()
# fast_app is shadowed by fh_frankenui to make it default to no Pico, and add body classes
# needed for frankenui theme styling
app, rt = fast_app(hdrs=hdrs)
# Examples Product Data to render in the gallery and detail pages
products = [
{"name": "Laptop", "price": "$999", "img": "https://picsum.photos/400/100?random=1"},
{"name": "Smartphone", "price": "$599", "img": "https://picsum.photos/400/100?random=2"},
{"name": "Headphones", "price": "$199", "img": "https://picsum.photos/400/100?random=3"},
{"name": "Smartwatch", "price": "$299", "img": "https://picsum.photos/400/100?random=4"},
{"name": "Tablet", "price": "$449", "img": "https://picsum.photos/400/100?random=5"},
{"name": "Camera", "price": "$799", "img": "https://picsum.photos/400/100?random=6"},]
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"], style="width:100%"),
# 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-2"),
# There are helpful Enums, such as TextFont, 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=TextFont.bold_sm),
# ButtonT.primary is useful for actions you really want the user to take (like adding
# something to the card) - these stand out visually. For dangerous actions (like
# deleting something) you generally would want to use ButtonT.danger. 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("Click me!", cls=(ButtonT.primary, "mt-2"),
# HTMX can be used as normal on any component
hx_get=product_detail.to(product_name=p['name']),
hx_push_url='true',
hx_target='body'))
@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("Example Store Front!",
Grid(*[ProductCard(p) for p in products], cols_lg=3))
example_product_description = """\n
This is a sample detailed description of the {product_name}. You can see when clicking on the card
from the gallery you can:
+ Have a detailed description of the product on the page
+ Have an order form to fill out and submit
+ Anything else you want!
"""
@rt
def product_detail(product_name:str):
return Titled("Product Detail",
# Grid lays out its children in a responsive grid
Grid(
Div(
H1(product_name),
# render_md is a helper that renders markdown into HTML using frankenui styles.
render_md(example_product_description.format(product_name=product_name))),
Div(
H3("Order Form"),
# Form automatically has a class of 'space-y-3' for a margin between each child.
Form(
# LabelInput is a convience wrapper for a label and input that links them.
LabelInput("Name", id='name'),
LabelInput("Email", id='email'),
LabelInput("Quantity", id='quantity'),
# ButtonT.primary because this is the primary action of the page!
Button("Submit", cls=ButtonT.primary))
),
# Grid has defaults and args for cols at different breakpoints, but you can pass in
# your own to customize responsiveness.
cols_lg=2))
serve()