from fasthtml.common import * from itertools import starmap icons = 'assets/icons' col = "flex flex-col" center = "flex items-center" between = "flex justify-between" gap2 = "flex gap-2" # inset = "shadow-[0_3px_2px_rgba(255,255,255,0.9),0_5px_5px_rgba(0,0,0,0.3)]" # bnset = "shadow-[0_3px_2px_rgba(255,255,255,0.1),0_4px_4px_rgba(0,0,0,0.7)]" inset = "shadow-[0_2px_2px_rgba(255,255,255,0.5),0_3px_3px_rgba(0,0,0,0.2)]" bnset = "shadow-[inset_0_2px_4px_rgba(255,255,255,0.1),0_4px_8px_rgba(0,0,0,0.5)]" section_base1= "pt-8 px-4 pb-24 gap-8 lg:gap-16 lg:pt-16 lg:px-16" section_base =f"{col} {section_base1}" def maxpx (px ): return f"w-full max-w-[{px}px]" def maxrem(rem): return f"w-full max-w-[{rem}rem]" def section_wrapper(content, bg_color, xtra="", flex=True): return Section(content, cls=f"bg-{bg_color} {section_base1} {col if flex else ''} -mt-8 lg:-mt-16 items-center rounded-t-3xl lg:rounded-t-[2.5rem] relative {xtra}") def section_header(mono_text, heading, subheading, max_width=32, center=True): pos = 'items-center text-center' if center else 'items-start text-start' return Div( P(mono_text, cls="mono-body text-opacity-60"), H2(heading, cls=f"text-black heading-2 {maxrem(max_width)}"), P(subheading, cls=f"l-body {maxrem(max_width)}"), cls=f"{maxrem(50)} mx-auto {col} {pos} gap-6") def arrow(d): return Button(Img(src=f"assets/icons/arrow-{d}.svg", alt="Arrow left"), cls="disabled:opacity-40 transition-opacity", id=f"slide{d.capitalize()}", aria_label=f"Slide {d}") def carousel(items, id="carousel-container", extra_classes=""): carousel_content = Div(*items, id=id, cls=f"hide-scrollbar {col} lg:flex-row gap-4 lg:gap-6 rounded-l-3xl xl:rounded-3xl w-full lg:overflow-hidden xl:overflow-hidden whitespace-nowrap {extra_classes}") arrows = Div( Div(arrow("left"), arrow("right"), cls=f"w-[4.5rem] {between} ml-auto"), cls=f"hidden lg:flex xl:flex justify-start {maxrem(41)} py-6 pl-6 pr-20") return Div(carousel_content, arrows, cls=f"max-h-fit {col} items-start lg:-mr-16 {maxpx(1440)} overflow-hidden") def testimonial_card(idx, comment, name, role, company, image_src): return Div( P(comment, cls="m-body text-black"), Div( Div(Img(src=image_src, alt=f"Picture of {name}", width="112", height="112"), cls="rounded-full w-11 h-11 lg:w-14 lg:h-14"), Div( P(name, cls=f"m-body text-black"), Div( P(role), Img(src=f"{icons}/dot.svg", alt="Dot separator", width="4", height="4"), P(company), cls=f"{gap2} xs-mono-body w-full"), cls="w-full"), cls=f"{center} justify-start gap-2"), id=f"testimonial-card-{idx+1}", cls=f"testimonial-card {col} flex-none whitespace-normal flex justify-between h-96 rounded-3xl items-start bg-soft-pink p-4 lg:p-8 {maxrem(36)} lg:w-96") def stack_item(name, icon_src, href): return A( Img(src=f"./assets/icons/stack/{icon_src}", alt=name, width="24", height="24"), P(name, cls="text-black/60"), href=href, target="_blank", rel="noopener noreferrer", cls=f"{gap2} items-center px-4 py-2 bg-white/60 rounded-full {inset}") def stacked_card(title, description, stacks, color): return Div( Div( H3(title, cls="heading-3 mb-4"), P(description, cls="mb-12"), Div(*starmap(stack_item, stacks), cls=f"{gap2} flex-wrap items-center"), cls=f"rounded-3xl {color} lg:p-12 p-6 {col} m-body") ) def accordion(id, question, answer, question_cls="", answer_cls="", container_cls=""): return Div( Input(id=f"collapsible-{id}", type="checkbox", cls=f"collapsible-checkbox peer/collapsible hidden"), Label( P(question, cls=f"flex-grow {question_cls}"), Img(src=f"{icons}/plus-icon.svg", alt="Expand", cls=f"plus-icon w-6 h-6"), Img(src=f"{icons}/minus-icon.svg", alt="Collapse", cls=f"minus-icon w-6 h-6"), _for=f"collapsible-{id}", cls="flex items-center cursor-pointer py-4 lg:py-6 pl-6 lg:pl-8 pr-4 lg:pr-6"), P(answer, cls=f"overflow-hidden max-h-0 pl-6 lg:pl-8 pr-4 lg:pr-6 peer-checked/collapsible:max-h-[30rem] peer-checked/collapsible:pb-4 peer-checked/collapsible:lg:pb-6 transition-all duration-300 ease-in-out {answer_cls}"), cls=container_cls) def video_player(txt): return ( # Video Popup container - TODO make pretty Div( Div( # 'Pastel green top bar', Div( H3(txt, cls='text-green-800 font-semibold'), # 'Close button', Button('X', id='closePopup', cls='text-green-800 hover:text-green-950'), cls='bg-green-200 p-2 flex justify-between items-center' ), # 'YouTube video iframe', Div( Iframe(id='youtubeVideo', width='560', height='315', src='', frameborder='0', allowfullscreen=''), cls='p-4' ), cls='bg-white rounded-lg shadow-lg overflow-hidden' ), id='videoPopup', cls='hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center' ) ) yt_frame = """""" def video_button(txt, poster_src, video_duration, video_id, poster_alt="Video poster", youtube_icon_src="/assets/icons/youtube.svg", max_width="350px"): return ( A( Img(src=poster_src, width='240', height='120', cls='rounded-full w-[7.5rem] h-auto', alt=poster_alt), Span(txt, Span(video_duration, cls='s-body text-black/60'), cls=f'text-black {col}'), P(Img(src=youtube_icon_src, width='41', height='30', alt='Youtube icon'), cls=f'flex-1 {center}'), Script(f""" me().on('click', (e) => {{ e.preventDefault(); me('#videoOverlay').classRemove('hidden').classAdd('flex'); me('#youtube-player').setAttribute('src', 'https://www.youtube.com/embed/{video_id}'); }});"""), id="openVideo", href='#', cls=f'{inset} p-2 rounded-full bg-white hover:bg-white/80 transition-colors duration-300 h-[76px] w-full max-w-[{max_width}] {center} gap-4' ), Div( Iframe(id="youtube-player", src="", cls="w-full aspect-video", allowfullscreen=True, allow="autoplay; encrypted-media"), Button("Close", Script(""" me().on('click', () => { me('#videoOverlay').classRemove('flex').classAdd('hidden'); me('#youtube-player').setAttribute('src', ''); });"""), id="closeVideo", cls="mt-4 bg-soft-pink text-black font-bold py-2 px-4 rounded" ), Script("document.addEventListener('keydown', (e) => { if (e.key === 'Escape') me('#closeVideo').send('click'); });"), id="videoOverlay", cls="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50" ) )