|
| 1 | +from dataclasses import asdict |
| 2 | +from typing import Literal |
| 3 | + |
| 4 | +import mesop as me |
| 5 | + |
| 6 | +ROW_GAP = 15 |
| 7 | +BOX_PADDING = 20 |
| 8 | + |
| 9 | + |
| 10 | +@me.stateclass |
| 11 | +class State: |
| 12 | + first_name: str |
| 13 | + last_name: str |
| 14 | + username: str |
| 15 | + email: str |
| 16 | + address: str |
| 17 | + address_2: str |
| 18 | + country: str |
| 19 | + state: str |
| 20 | + zip: str |
| 21 | + payment_type: str |
| 22 | + name_on_card: str |
| 23 | + credit_card: str |
| 24 | + expiration: str |
| 25 | + cvv: str |
| 26 | + errors: dict[str, str] |
| 27 | + |
| 28 | + |
| 29 | +def is_mobile(): |
| 30 | + return me.viewport_size().width < 620 |
| 31 | + |
| 32 | + |
| 33 | +def calc_input_size(items: int): |
| 34 | + return int( |
| 35 | + (me.viewport_size().width - (ROW_GAP * items) - (BOX_PADDING * 2)) / items |
| 36 | + ) |
| 37 | + |
| 38 | + |
| 39 | +def load(e: me.LoadEvent): |
| 40 | + me.set_theme_density(-3) |
| 41 | + me.set_theme_mode("system") |
| 42 | + |
| 43 | + |
| 44 | +@me.page( |
| 45 | + security_policy=me.SecurityPolicy( |
| 46 | + allowed_iframe_parents=["https://google.github.io"] |
| 47 | + ), |
| 48 | + path="/form_billing", |
| 49 | + on_load=load, |
| 50 | +) |
| 51 | +def page(): |
| 52 | + state = me.state(State) |
| 53 | + |
| 54 | + with me.box( |
| 55 | + style=me.Style( |
| 56 | + padding=me.Padding.all(BOX_PADDING), |
| 57 | + max_width=800, |
| 58 | + margin=me.Margin.symmetric(horizontal="auto"), |
| 59 | + ) |
| 60 | + ): |
| 61 | + me.text( |
| 62 | + "Billing form", |
| 63 | + type="headline-4", |
| 64 | + style=me.Style(margin=me.Margin(bottom=10)), |
| 65 | + ) |
| 66 | + |
| 67 | + with form_group(): |
| 68 | + name_width = calc_input_size(2) if is_mobile() else "100%" |
| 69 | + input_field(label="First name", width=name_width) |
| 70 | + input_field(label="Last name", width=name_width) |
| 71 | + |
| 72 | + with form_group(): |
| 73 | + input_field(label="Username") |
| 74 | + |
| 75 | + with me.box(style=me.Style(display="flex", gap=ROW_GAP)): |
| 76 | + input_field(label="Email", input_type="email") |
| 77 | + |
| 78 | + with me.box(style=me.Style(display="flex", gap=ROW_GAP)): |
| 79 | + input_field(label="Address") |
| 80 | + |
| 81 | + with form_group(): |
| 82 | + input_field(label="Address 2") |
| 83 | + |
| 84 | + with form_group(): |
| 85 | + country_state_zip_width = calc_input_size(3) if is_mobile() else "100%" |
| 86 | + input_field(label="Country", width=country_state_zip_width) |
| 87 | + input_field(label="State", width=country_state_zip_width) |
| 88 | + input_field(label="Zip", width=country_state_zip_width) |
| 89 | + |
| 90 | + divider() |
| 91 | + |
| 92 | + me.text( |
| 93 | + "Payment", |
| 94 | + type="headline-4", |
| 95 | + style=me.Style(margin=me.Margin(bottom=10)), |
| 96 | + ) |
| 97 | + |
| 98 | + with form_group(flex_direction="column"): |
| 99 | + me.radio( |
| 100 | + key="payment_type", |
| 101 | + on_change=on_change, |
| 102 | + options=[ |
| 103 | + me.RadioOption(label="Credit card", value="credit_card"), |
| 104 | + me.RadioOption(label="Debit card", value="debit_card"), |
| 105 | + me.RadioOption(label="Paypal", value="paypal"), |
| 106 | + ], |
| 107 | + style=me.Style( |
| 108 | + display="flex", flex_direction="column", margin=me.Margin(bottom=20) |
| 109 | + ), |
| 110 | + ) |
| 111 | + if "payment_type" in state.errors: |
| 112 | + me.text( |
| 113 | + state.errors["payment_type"], |
| 114 | + style=me.Style( |
| 115 | + margin=me.Margin(top=-30, left=5, bottom=15), |
| 116 | + color=me.theme_var("error"), |
| 117 | + font_size=13, |
| 118 | + ), |
| 119 | + ) |
| 120 | + |
| 121 | + with form_group(): |
| 122 | + payments_width = calc_input_size(2) if is_mobile() else "100%" |
| 123 | + input_field(label="Name on card", width=payments_width) |
| 124 | + input_field(label="Credit card", width=payments_width) |
| 125 | + |
| 126 | + with form_group(): |
| 127 | + input_field(label="Expiration", width=payments_width) |
| 128 | + input_field(label="CVV", width=payments_width, input_type="number") |
| 129 | + |
| 130 | + divider() |
| 131 | + |
| 132 | + me.button( |
| 133 | + "Continue to checkout", |
| 134 | + type="flat", |
| 135 | + style=me.Style(width="100%", padding=me.Padding.all(25), font_size=20), |
| 136 | + on_click=on_click, |
| 137 | + ) |
| 138 | + |
| 139 | + |
| 140 | +def divider(): |
| 141 | + with me.box(style=me.Style(margin=me.Margin.symmetric(vertical=20))): |
| 142 | + me.divider() |
| 143 | + |
| 144 | + |
| 145 | +@me.content_component |
| 146 | +def form_group(flex_direction: Literal["row", "column"] = "row"): |
| 147 | + with me.box( |
| 148 | + style=me.Style( |
| 149 | + display="flex", flex_direction=flex_direction, gap=ROW_GAP, width="100%" |
| 150 | + ) |
| 151 | + ): |
| 152 | + me.slot() |
| 153 | + |
| 154 | + |
| 155 | +def input_field( |
| 156 | + *, |
| 157 | + key: str = "", |
| 158 | + label: str, |
| 159 | + value: str = "", |
| 160 | + width: str | int = "100%", |
| 161 | + input_type: Literal[ |
| 162 | + "color", |
| 163 | + "date", |
| 164 | + "datetime-local", |
| 165 | + "email", |
| 166 | + "month", |
| 167 | + "number", |
| 168 | + "password", |
| 169 | + "search", |
| 170 | + "tel", |
| 171 | + "text", |
| 172 | + "time", |
| 173 | + "url", |
| 174 | + "week", |
| 175 | + ] = "text", |
| 176 | +): |
| 177 | + state = me.state(State) |
| 178 | + key = key if key else label.lower().replace(" ", "_") |
| 179 | + with me.box(style=me.Style(flex_grow=1, width=width)): |
| 180 | + me.input( |
| 181 | + key=key, |
| 182 | + label=label, |
| 183 | + value=value, |
| 184 | + appearance="outline", |
| 185 | + style=me.Style(width=width), |
| 186 | + type=input_type, |
| 187 | + on_blur=on_blur, |
| 188 | + ) |
| 189 | + if key in state.errors: |
| 190 | + me.text( |
| 191 | + state.errors[key], |
| 192 | + style=me.Style( |
| 193 | + margin=me.Margin(top=-13, left=15, bottom=15), |
| 194 | + color=me.theme_var("error"), |
| 195 | + font_size=13, |
| 196 | + ), |
| 197 | + ) |
| 198 | + |
| 199 | + |
| 200 | +def on_change(e: me.RadioChangeEvent): |
| 201 | + state = me.state(State) |
| 202 | + setattr(state, e.key, e.value) |
| 203 | + |
| 204 | + |
| 205 | +def on_blur(e: me.InputBlurEvent): |
| 206 | + state = me.state(State) |
| 207 | + setattr(state, e.key, e.value) |
| 208 | + |
| 209 | + |
| 210 | +def on_click(e: me.ClickEvent): |
| 211 | + state = me.state(State) |
| 212 | + |
| 213 | + # Replace with real validation logic. |
| 214 | + errors = {} |
| 215 | + for key, value in asdict(state).items(): # type: ignore |
| 216 | + if key == "error": |
| 217 | + continue |
| 218 | + if not value: |
| 219 | + errors[key] = f"{key.replace('_', ' ').capitalize()} is required" |
| 220 | + state.errors = errors |
| 221 | + |
| 222 | + # Replace with form processing logic. |
| 223 | + if not state.errors: |
| 224 | + pass |
0 commit comments