diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-22 15:08:37 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-22 15:08:37 +0300 |
| commit | fcc2f4704e39b0e69b377cc138f75027721dac22 (patch) | |
| tree | 732fc94b354a26c08fba9cc9059f9c6c900182be /src/components/widgets | |
Initial template
Diffstat (limited to 'src/components/widgets')
23 files changed, 1347 insertions, 0 deletions
diff --git a/src/components/widgets/Announcement.astro b/src/components/widgets/Announcement.astro new file mode 100644 index 0000000..8e4bf78 --- /dev/null +++ b/src/components/widgets/Announcement.astro @@ -0,0 +1,16 @@ +--- + +--- + +<div + class="dark text-muted text-sm bg-black dark:bg-transparent dark:border-b dark:border-slate-800 dark:text-slate-400 hidden md:flex gap-1 overflow-hidden px-3 py-2 relative text-ellipsis whitespace-nowrap" +> + <span + class="dark:bg-slate-700 bg-white/40 dark:text-slate-300 font-semibold px-1 py-0.5 text-xs mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-block" + >NEW</span + > + <a href="#contact" class="text-muted hover:underline dark:text-slate-400 font-medium" + >Skontaktuj się z nami - Darmowa wycena! »</a + > + +</div> diff --git a/src/components/widgets/BlogHighlightedPosts.astro b/src/components/widgets/BlogHighlightedPosts.astro new file mode 100644 index 0000000..75f35a9 --- /dev/null +++ b/src/components/widgets/BlogHighlightedPosts.astro @@ -0,0 +1,64 @@ +--- +import { APP_BLOG } from 'astrowind:config'; + +import Grid from '~/components/blog/Grid.astro'; + +import { getBlogPermalink } from '~/utils/permalinks'; +import { findPostsByIds } from '~/utils/blog'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import type { Widget } from '~/types'; + +export interface Props extends Widget { + title?: string; + linkText?: string; + linkUrl?: string | URL; + information?: string; + postIds: string[]; +} + +const { + title = await Astro.slots.render('title'), + linkText = 'View all posts', + linkUrl = getBlogPermalink(), + information = await Astro.slots.render('information'), + postIds = [], + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; + +const posts = APP_BLOG.isEnabled ? await findPostsByIds(postIds) : []; +--- + +{ + APP_BLOG.isEnabled ? ( + <WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container as string} bg={bg}> + <div class="flex flex-col lg:justify-between lg:flex-row mb-8"> + {title && ( + <div class="md:max-w-sm"> + <h2 + class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2" + set:html={title} + /> + {APP_BLOG.list.isEnabled && linkText && linkUrl && ( + <a + class="text-muted dark:text-slate-400 hover:text-primary transition ease-in duration-200 block mb-6 lg:mb-0" + href={linkUrl} + > + {linkText} » + </a> + )} + </div> + )} + + {information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />} + </div> + + <Grid posts={posts} /> + </WidgetWrapper> + ) : ( + <Fragment /> + ) +} diff --git a/src/components/widgets/BlogLatestPosts.astro b/src/components/widgets/BlogLatestPosts.astro new file mode 100644 index 0000000..28f66d4 --- /dev/null +++ b/src/components/widgets/BlogLatestPosts.astro @@ -0,0 +1,63 @@ +--- +import { APP_BLOG } from 'astrowind:config'; + +import Grid from '~/components/blog/Grid.astro'; + +import { getBlogPermalink } from '~/utils/permalinks'; +import { findLatestPosts } from '~/utils/blog'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import type { Widget } from '~/types'; +import Button from '~/components/ui/Button.astro'; + +export interface Props extends Widget { + title?: string; + linkText?: string; + linkUrl?: string | URL; + information?: string; + count?: number; +} + +const { + title = await Astro.slots.render('title'), + linkText = 'View all posts', + linkUrl = getBlogPermalink(), + information = await Astro.slots.render('information'), + count = 4, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; + +const posts = APP_BLOG.isEnabled ? await findLatestPosts({ count }) : []; +--- + +{ + APP_BLOG.isEnabled ? ( + <WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container as string} bg={bg}> + <div class="flex flex-col lg:justify-between lg:flex-row mb-8"> + {title && ( + <div class="md:max-w-sm"> + <h2 + class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2" + set:html={title} + /> + {APP_BLOG.list.isEnabled && linkText && linkUrl && ( + <Button variant="link" href={linkUrl}> + {' '} + {linkText} » + </Button> + )} + </div> + )} + + {information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />} + </div> + + <Grid posts={posts} /> + </WidgetWrapper> + ) : ( + <Fragment /> + ) +} diff --git a/src/components/widgets/Brands.astro b/src/components/widgets/Brands.astro new file mode 100644 index 0000000..7e42ae1 --- /dev/null +++ b/src/components/widgets/Brands.astro @@ -0,0 +1,38 @@ +--- +import { Icon } from 'astro-icon/components'; +import type { Brands as Props } from '~/types'; + +import Image from '~/components/common/Image.astro'; +import Headline from '~/components/ui/Headline.astro'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +const { + title = '', + subtitle = '', + tagline = '', + icons = [], + images = [], + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <Headline title={title} subtitle={subtitle} tagline={tagline} /> + + <div class="flex flex-wrap justify-center gap-x-6 sm:gap-x-12 lg:gap-x-24"> + {icons && icons.map((icon) => <Icon name={icon} class="py-3 lg:py-5 w-12 h-auto mx-auto sm:mx-0 text-gray-500" />)} + { + images && + images.map( + (image) => + image.src && ( + <div class="flex justify-center col-span-1 my-2 lg:my-4 py-1 px-3 rounded-md dark:bg-gray-200"> + <Image src={image.src} alt={image.alt || ''} class="max-h-12" width={120} height={48} layout="fixed" /> + </div> + ) + ) + } + </div> +</WidgetWrapper> diff --git a/src/components/widgets/CallToAction.astro b/src/components/widgets/CallToAction.astro new file mode 100644 index 0000000..f51aa91 --- /dev/null +++ b/src/components/widgets/CallToAction.astro @@ -0,0 +1,58 @@ +--- +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import type { CallToAction, Widget } from '~/types'; +import Headline from '~/components/ui/Headline.astro'; +import Button from '~/components/ui/Button.astro'; + +interface Props extends Widget { + title?: string; + subtitle?: string; + tagline?: string; + callToAction?: CallToAction; + actions?: string | CallToAction[]; +} + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline = await Astro.slots.render('tagline'), + actions = await Astro.slots.render('actions'), + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <div + class="max-w-3xl mx-auto text-center p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600" + > + <Headline + title={title} + subtitle={subtitle} + tagline={tagline} + classes={{ + container: 'mb-0 md:mb-0', + title: 'text-4xl md:text-4xl font-bold tracking-tighter mb-4 font-heading', + subtitle: 'text-xl text-muted dark:text-slate-400', + }} + /> + { + actions && ( + <div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 mt-6"> + {Array.isArray(actions) ? ( + actions.map((action) => ( + <div class="flex w-full sm:w-auto"> + <Button {...(action || {})} class="w-full sm:mb-0" /> + </div> + )) + ) : ( + <Fragment set:html={actions} /> + )} + </div> + ) + } + </div> +</WidgetWrapper> diff --git a/src/components/widgets/CallToActionImage.astro b/src/components/widgets/CallToActionImage.astro new file mode 100644 index 0000000..df13145 --- /dev/null +++ b/src/components/widgets/CallToActionImage.astro @@ -0,0 +1,73 @@ +--- +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import type { Widget } from '~/types'; +import Headline from '~/components/ui/Headline.astro'; +import Image from '~/components/common/Image.astro'; + +interface Props extends Widget { + title?: string; + subtitle?: string; + tagline?: string; + image?: { + src: string; + alt: string; + href?: string; + target?: string; + }; +} + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline = await Astro.slots.render('tagline'), + image, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <div + class="max-w-3xl mx-auto text-center p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600" + > + <Headline + title={title} + subtitle={subtitle} + tagline={tagline} + classes={{ + container: 'mb-0 md:mb-0', + title: 'text-4xl md:text-4xl font-bold tracking-tighter mb-4 font-heading', + subtitle: 'text-xl text-muted dark:text-slate-400', + }} + /> + { + image && ( + <div class="flex justify-center mt-6 px-4"> + {image.href ? ( + <a + href={image.href} + target={image.target || '_self'} + rel={image.target === '_blank' ? 'noopener noreferrer' : ''} + class="inline-block hover:opacity-80 transition-opacity duration-200" + > + <Image + src={image.src} + alt={image.alt} + class="max-h-20 h-auto w-auto max-w-full object-contain" + /> + </a> + ) : ( + <Image + src={image.src} + alt={image.alt} + class="max-h-20 h-auto w-auto max-w-full object-contain" + /> + )} + </div> + ) + } + </div> +</WidgetWrapper>
\ No newline at end of file diff --git a/src/components/widgets/Contact.astro b/src/components/widgets/Contact.astro new file mode 100644 index 0000000..122f4b0 --- /dev/null +++ b/src/components/widgets/Contact.astro @@ -0,0 +1,40 @@ +--- +import FormContainer from '~/components/ui/Form.astro'; +import Headline from '~/components/ui/Headline.astro'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import type { Contact as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline = await Astro.slots.render('tagline'), + inputs, + textarea, + disclaimer, + button, + description, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <Headline title={title} subtitle={subtitle} tagline={tagline} /> + + { + inputs && ( + <div class="flex flex-col max-w-xl mx-auto rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow p-4 sm:p-6 lg:p-8 w-full"> + <FormContainer + inputs={inputs} + textarea={textarea} + disclaimer={disclaimer} + button={button} + description={description} + /> + </div> + ) + } +</WidgetWrapper> diff --git a/src/components/widgets/Content.astro b/src/components/widgets/Content.astro new file mode 100644 index 0000000..694a198 --- /dev/null +++ b/src/components/widgets/Content.astro @@ -0,0 +1,94 @@ +--- +import type { Content as Props } from '~/types'; +import Headline from '~/components/ui/Headline.astro'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import Image from '~/components/common/Image.astro'; +import Button from '~/components/ui/Button.astro'; +import ItemGrid from '~/components/ui/ItemGrid.astro'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline, + content = await Astro.slots.render('content'), + callToAction, + items = [], + columns, + image = await Astro.slots.render('image'), + isReversed = false, + isAfterContent = false, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper + id={id} + isDark={isDark} + containerClass={`max-w-7xl mx-auto ${isAfterContent ? 'pt-0 md:pt-0 lg:pt-0' : ''} ${classes?.container ?? ''}`} + bg={bg} +> + <Headline + title={title} + subtitle={subtitle} + tagline={tagline} + classes={{ + container: 'max-w-xl sm:mx-auto lg:max-w-2xl', + title: 'text-4xl md:text-5xl font-bold tracking-tighter mb-4 font-heading', + subtitle: 'max-w-3xl mx-auto sm:text-center text-xl text-muted dark:text-slate-400', + }} + /> + <div class="mx-auto max-w-7xl p-4 md:px-8"> + <div class={`md:flex ${isReversed ? 'md:flex-row-reverse' : ''} md:gap-16`}> + <div class="md:basis-1/2 self-center"> + {content && <div class="mb-12 text-lg dark:text-slate-400" set:html={content} />} + + { + callToAction && ( + <div class="mt-[-40px] mb-8 text-primary"> + <Button variant="link" {...callToAction} /> + </div> + ) + } + + <ItemGrid + items={items} + columns={columns} + defaultIcon="tabler:check" + classes={{ + container: `gap-y-4 md:gap-y-8`, + panel: 'max-w-none', + title: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2', + description: 'text-muted dark:text-slate-400 ml-2 rtl:ml-0 rtl:mr-2', + icon: 'flex h-7 w-7 items-center justify-center rounded-full bg-green-600 dark:bg-green-700 text-gray-50 p-1', + action: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2', + }} + /> + </div> + <div aria-hidden="true" class="mt-10 md:mt-0 md:basis-1/2"> + { + image && ( + <div class="relative m-auto max-w-4xl"> + {typeof image === 'string' ? ( + <Fragment set:html={image} /> + ) : ( + <Image + class="mx-auto w-full rounded-lg bg-gray-500 shadow-lg" + width={500} + height={500} + widths={[400, 768]} + sizes="(max-width: 768px) 100vw, 432px" + layout="responsive" + {...image} + /> + )} + </div> + ) + } + </div> + </div> + </div> +</WidgetWrapper> diff --git a/src/components/widgets/FAQs.astro b/src/components/widgets/FAQs.astro new file mode 100644 index 0000000..cba9762 --- /dev/null +++ b/src/components/widgets/FAQs.astro @@ -0,0 +1,33 @@ +--- +import Headline from '~/components/ui/Headline.astro'; +import ItemGrid from '~/components/ui/ItemGrid.astro'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import type { Faqs as Props } from '~/types'; + +const { + title = '', + subtitle = '', + tagline = '', + items = [], + columns = 2, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <Headline title={title} subtitle={subtitle} tagline={tagline} /> + <ItemGrid + items={items} + columns={columns} + defaultIcon="tabler:chevron-right" + classes={{ + container: `${columns === 1 ? 'max-w-4xl' : ''} gap-y-8 md:gap-y-12`, + panel: 'max-w-none', + icon: 'flex-shrink-0 mt-1 w-6 h-6 text-primary', + }} + /> +</WidgetWrapper> diff --git a/src/components/widgets/Features.astro b/src/components/widgets/Features.astro new file mode 100644 index 0000000..8f42b62 --- /dev/null +++ b/src/components/widgets/Features.astro @@ -0,0 +1,36 @@ +--- +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import ItemGrid from '~/components/ui/ItemGrid.astro'; +import Headline from '~/components/ui/Headline.astro'; +import type { Features as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline = await Astro.slots.render('tagline'), + items = [], + columns = 2, + + defaultIcon, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-5xl ${classes?.container ?? ''}`} bg={bg}> + <Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} /> + <ItemGrid + items={items} + columns={columns} + defaultIcon={defaultIcon} + classes={{ + container: '', + title: 'md:text-[1.3rem]', + icon: 'text-white bg-primary rounded-full w-10 h-10 p-2 md:w-12 md:h-12 md:p-3 mr-4 rtl:ml-4 rtl:mr-0', + ...((classes?.items as Record<string, never>) ?? {}), + }} + /> +</WidgetWrapper> diff --git a/src/components/widgets/Features2.astro b/src/components/widgets/Features2.astro new file mode 100644 index 0000000..282337e --- /dev/null +++ b/src/components/widgets/Features2.astro @@ -0,0 +1,38 @@ +--- +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import Headline from '~/components/ui/Headline.astro'; +import ItemGrid2 from '~/components/ui/ItemGrid2.astro'; +import type { Features as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline = await Astro.slots.render('tagline'), + items = [], + columns = 3, + defaultIcon, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} /> + <ItemGrid2 + items={items} + columns={columns} + defaultIcon={defaultIcon} + classes={{ + container: 'gap-4 md:gap-6', + panel: + 'rounded-lg shadow-[0_4px_30px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_30px_rgba(0,0,0,0.1)] backdrop-blur border border-[#ffffff29] bg-white dark:bg-slate-900 p-6', + // panel: + // "text-center bg-page items-center md:text-left rtl:md:text-right md:items-start p-6 p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-800", + icon: 'w-12 h-12 mb-6 text-primary', + ...((classes?.items as Record<string, never>) ?? {}), + }} + /> +</WidgetWrapper> diff --git a/src/components/widgets/Features3.astro b/src/components/widgets/Features3.astro new file mode 100644 index 0000000..62ab475 --- /dev/null +++ b/src/components/widgets/Features3.astro @@ -0,0 +1,70 @@ +--- +import Headline from '~/components/ui/Headline.astro'; +import ItemGrid from '~/components/ui/ItemGrid.astro'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import Image from '~/components/common/Image.astro'; +import type { Features as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline = await Astro.slots.render('tagline'), + image, + items = [], + columns, + defaultIcon, + isBeforeContent, + isAfterContent, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper + id={id} + isDark={isDark} + containerClass={`${isBeforeContent ? 'md:pb-8 lg:pb-12' : ''} ${isAfterContent ? 'pt-0 md:pt-0 lg:pt-0' : ''} ${ + classes?.container ?? '' + }`} + bg={bg} +> + <Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} /> + + <div aria-hidden="true" class="aspect-w-16 aspect-h-7"> + { + image && ( + <div class="w-full h-80 object-cover rounded-xl mx-auto bg-gray-500 shadow-lg"> + {typeof image === 'string' ? ( + <Fragment set:html={image} /> + ) : ( + <Image + class="w-full h-80 object-cover rounded-xl mx-auto bg-gray-500 shadow-lg" + width="auto" + height={320} + widths={[400, 768]} + layout="fullWidth" + {...image} + /> + )} + </div> + ) + } + </div> + + <ItemGrid + items={items} + columns={columns} + defaultIcon={defaultIcon} + classes={{ + container: 'mt-12', + panel: 'max-w-full sm:max-w-md', + title: 'text-lg font-semibold', + description: 'mt-0.5', + icon: 'flex-shrink-0 mt-1 text-primary w-6 h-6', + ...((classes?.items as object) ?? {}), + }} + /> +</WidgetWrapper> diff --git a/src/components/widgets/Footer.astro b/src/components/widgets/Footer.astro new file mode 100644 index 0000000..70cac54 --- /dev/null +++ b/src/components/widgets/Footer.astro @@ -0,0 +1,61 @@ +--- +import { Icon } from 'astro-icon/components'; +import { SITE } from 'astrowind:config'; +import { getHomePermalink } from '~/utils/permalinks'; + +interface Link { + text?: string; + href?: string; + ariaLabel?: string; + icon?: string; +} + +interface Links { + title?: string; + links: Array<Link>; +} + +export interface Props { + links: Array<Links>; + secondaryLinks: Array<Link>; + socialLinks: Array<Link>; + footNote?: string; + theme?: string; +} + +const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme = 'light' } = Astro.props; +--- + +<footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose']}> + <div class="dark:bg-dark absolute inset-0 pointer-events-none" aria-hidden="true"></div> + <div + class="relative max-w-7xl mx-auto px-4 sm:px-6 dark:text-slate-300 intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade" + > + <div class="md:flex md:items-center md:justify-between py-6 md:py-8"> + { + socialLinks?.length ? ( + <ul class="flex mb-4 md:order-1 -ml-2 md:ml-4 md:mb-0 rtl:ml-0 rtl:-mr-2 rtl:md:ml-0 rtl:md:mr-4"> + {socialLinks.map(({ ariaLabel, href, text, icon }) => ( + <li> + <a + class="text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center" + aria-label={ariaLabel} + href={href} + > + {icon && <Icon name={icon} class="w-5 h-5" />} + <Fragment set:html={text} /> + </a> + </li> + ))} + </ul> + ) : ( + '' + ) + } + + <div class="text-sm mr-4 dark:text-muted"> + <Fragment set:html={footNote} /> + </div> + </div> + </div> +</footer> diff --git a/src/components/widgets/Header.astro b/src/components/widgets/Header.astro new file mode 100644 index 0000000..0064a30 --- /dev/null +++ b/src/components/widgets/Header.astro @@ -0,0 +1,14 @@ +--- +import { Icon } from 'astro-icon/components'; +--- + +<header class="bg-black text-white py-2 px-4"> + <div class="max-w-7xl mx-auto flex items-center justify-center"> + <div class="flex items-center space-x-2"> + <Icon name="tabler:phone" class="w-4 h-4" /> + <a href="tel:+48790209770" class="text-sm font-medium hover:text-gray-300 transition-colors"> + +48 790-209-770 + </a> + </div> + </div> +</header> diff --git a/src/components/widgets/Hero.astro b/src/components/widgets/Hero.astro new file mode 100644 index 0000000..cbd1154 --- /dev/null +++ b/src/components/widgets/Hero.astro @@ -0,0 +1,99 @@ +--- +import Image from '~/components/common/Image.astro'; +import Button from '~/components/ui/Button.astro'; + +import type { Hero as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline, + + content = await Astro.slots.render('content'), + actions = await Astro.slots.render('actions'), + image = await Astro.slots.render('image'), + + id, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}> + <div class="absolute inset-0 pointer-events-none" aria-hidden="true"> + <slot name="bg"> + {bg ? <Fragment set:html={bg} /> : null} + </slot> + </div> + <div class="relative max-w-7xl mx-auto px-4 sm:px-6"> + <div class="pt-0 md:pt-[76px] pointer-events-none"></div> + <div class="py-12 md:py-20"> + <div class="text-center pb-10 md:pb-16 max-w-5xl mx-auto"> + { + tagline && ( + <p + class="text-base text-secondary dark:text-primary font-bold tracking-wide uppercase intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade" + set:html={tagline} + /> + ) + } + { + title && ( + <h1 + class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade" + set:html={title} + /> + ) + } + <div class="max-w-3xl mx-auto"> + { + subtitle && ( + <p + class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade" + set:html={subtitle} + /> + ) + } + { + actions && ( + <div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"> + {Array.isArray(actions) ? ( + actions.map((action) => ( + <div class="flex w-full sm:w-auto"> + <Button {...(action || {})} class="w-full sm:mb-0" /> + </div> + )) + ) : ( + <Fragment set:html={actions} /> + )} + </div> + ) + } + </div> + {content && <Fragment set:html={content} />} + </div> + <div + class="intersect-once intercept-no-queue intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade" + > + { + image && ( + <div class="relative m-auto max-w-5xl"> + {typeof image === 'string' ? ( + <Fragment set:html={image} /> + ) : ( + <Image + class="mx-auto rounded-md w-full" + widths={[400, 768, 1024, 2040]} + sizes="(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px" + loading="eager" + width={1024} + height={576} + {...image} + /> + )} + </div> + ) + } + </div> + </div> + </div> +</section> diff --git a/src/components/widgets/Hero2.astro b/src/components/widgets/Hero2.astro new file mode 100644 index 0000000..e92870d --- /dev/null +++ b/src/components/widgets/Hero2.astro @@ -0,0 +1,99 @@ +--- +import Image from '~/components/common/Image.astro'; +import Button from '~/components/ui/Button.astro'; + +import type { Hero as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline, + + content = await Astro.slots.render('content'), + actions = await Astro.slots.render('actions'), + image = await Astro.slots.render('image'), + + id, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}> + <div class="absolute inset-0 pointer-events-none" aria-hidden="true"> + <slot name="bg"> + {bg ? <Fragment set:html={bg} /> : null} + </slot> + </div> + <div class="relative max-w-7xl mx-auto px-4 sm:px-6"> + <div class="pt-0 md:pt-[76px] pointer-events-none"></div> + <div class="py-12 md:py-20 lg:py-0 lg:flex lg:items-center lg:h-screen lg:gap-8"> + <div class="basis-1/2 text-center lg:text-left pb-10 md:pb-16 mx-auto"> + { + tagline && ( + <p + class="text-base text-secondary dark:text-primary font-bold tracking-wide uppercase intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter" + set:html={tagline} + /> + ) + } + { + title && ( + <h1 + class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter" + set:html={title} + /> + ) + } + <div class="max-w-3xl mx-auto lg:max-w-none"> + { + subtitle && ( + <p + class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter" + set:html={subtitle} + /> + ) + } + + { + actions && ( + <div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"> + {Array.isArray(actions) ? ( + actions.map((action) => ( + <div class="flex w-full sm:w-auto"> + <Button {...(action || {})} class="w-full sm:mb-0" /> + </div> + )) + ) : ( + <Fragment set:html={actions} /> + )} + </div> + ) + } + </div> + {content && <Fragment set:html={content} />} + </div> + <div class="basis-1/2"> + { + image && ( + <div class="relative m-auto max-w-5xl intersect-once intercept-no-queue motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"> + {typeof image === 'string' ? ( + <Fragment set:html={image} /> + ) : ( + <Image + class="mx-auto rounded-md w-full" + widths={[400, 768, 1024, 2040]} + sizes="(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px" + loading="eager" + width={1200} + height={800} + aspectRatio="3:2" + {...image} + /> + )} + </div> + ) + } + </div> + </div> + </div> +</section> diff --git a/src/components/widgets/HeroText.astro b/src/components/widgets/HeroText.astro new file mode 100644 index 0000000..be2a1b6 --- /dev/null +++ b/src/components/widgets/HeroText.astro @@ -0,0 +1,86 @@ +--- +import type { CallToAction } from '~/types'; +import Button from '~/components/ui/Button.astro'; + +export interface Props { + title?: string; + subtitle?: string; + tagline?: string; + content?: string; + callToAction?: string | CallToAction; + callToAction2?: string | CallToAction; +} + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline, + content = await Astro.slots.render('content'), + callToAction = await Astro.slots.render('callToAction'), + callToAction2 = await Astro.slots.render('callToAction2'), +} = Astro.props; +--- + +<section class="relative md:-mt-[76px] not-prose"> + <div class="absolute inset-0 pointer-events-none" aria-hidden="true"></div> + <div class="relative max-w-7xl mx-auto px-4 sm:px-6"> + <div class="pt-0 md:pt-[76px] pointer-events-none"></div> + <div class="py-12 md:py-20 pb-8 md:pb-8"> + <div class="text-center max-w-5xl mx-auto"> + { + tagline && ( + <p + class="text-base text-secondary dark:text-primary font-bold tracking-wide uppercase intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade" + set:html={tagline} + /> + ) + } + { + title && ( + <h1 + class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade" + set:html={title} + /> + ) + } + <div class="max-w-3xl mx-auto"> + { + subtitle && ( + <p + class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade" + set:html={subtitle} + /> + ) + } + <div + class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade" + > + { + callToAction && ( + <div class="flex w-full sm:w-auto"> + {typeof callToAction === 'string' ? ( + <Fragment set:html={callToAction} /> + ) : ( + <Button variant="primary" {...callToAction} /> + )} + </div> + ) + } + { + callToAction2 && ( + <div class="flex w-full sm:w-auto"> + {typeof callToAction2 === 'string' ? ( + <Fragment set:html={callToAction2} /> + ) : ( + <Button {...callToAction2} /> + )} + </div> + ) + } + </div> + </div> + {content && <Fragment set:html={content} />} + </div> + </div> + </div> +</section> diff --git a/src/components/widgets/Note.astro b/src/components/widgets/Note.astro new file mode 100644 index 0000000..3f43881 --- /dev/null +++ b/src/components/widgets/Note.astro @@ -0,0 +1,23 @@ +--- +import { Icon } from 'astro-icon/components'; + +export interface Props { + icon?: string; + title?: string; + description?: string; +} + +const { + icon = 'tabler:info-square', + title = await Astro.slots.render('title'), + description = await Astro.slots.render('description'), +} = Astro.props; +--- + +<section class="bg-section dark:bg-slate-800 not-prose"> + <div class="max-w-6xl mx-auto px-4 sm:px-6 py-4 text-md text-center font-medium"> + <Icon name={icon} class="w-5 h-5 inline-block align-text-bottom font-bold" /> + <span class="font-bold" set:html={title} /> + <Fragment set:html={description} /> + </div> +</section> diff --git a/src/components/widgets/Pricing.astro b/src/components/widgets/Pricing.astro new file mode 100644 index 0000000..3f20b74 --- /dev/null +++ b/src/components/widgets/Pricing.astro @@ -0,0 +1,83 @@ +--- +import { Icon } from 'astro-icon/components'; +import Button from '~/components/ui/Button.astro'; +import Headline from '~/components/ui/Headline.astro'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import type { Pricing as Props } from '~/types'; + +const { + title = '', + subtitle = '', + tagline = '', + prices = [], + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <Headline title={title} subtitle={subtitle} tagline={tagline} /> + <div class="flex items-stretch justify-center"> + <div class="grid grid-cols-3 gap-4 dark:text-white sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3"> + { + prices && + prices.map(({ title, subtitle, price, period, items, callToAction, hasRibbon = false, ribbonTitle }) => ( + <div class="col-span-3 mx-auto flex w-full sm:col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-1 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"> + {price && period && ( + <div class="rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow px-6 py-8 flex w-full max-w-sm flex-col justify-between text-center"> + {hasRibbon && ribbonTitle && ( + <div class="absolute right-[-5px] 2xl:right-[-8px] rtl:right-auto rtl:left-[-8px] rtl:2xl:left-[-10px] top-[-5px] 2xl:top-[-10px] z-[1] h-[100px] w-[100px] overflow-hidden text-right"> + <span class="absolute top-[19px] right-[-21px] rtl:right-auto rtl:left-[-21px] block w-full rotate-45 rtl:-rotate-45 bg-green-700 text-center text-[10px] font-bold uppercase leading-5 text-white shadow-[0_3px_10px_-5px_rgba(0,0,0,0.3)] before:absolute before:left-0 before:top-full before:z-[-1] before:border-[3px] before:border-r-transparent before:border-b-transparent before:border-l-green-800 before:border-t-green-800 before:content-[''] after:absolute after:right-0 after:top-full after:z-[-1] after:border-[3px] after:border-l-transparent after:border-b-transparent after:border-r-green-800 after:border-t-green-800 after:content-['']"> + {ribbonTitle} + </span> + </div> + )} + <div class="px-2 py-0"> + {title && ( + <h3 class="text-center text-xl font-semibold uppercase leading-6 tracking-wider mb-2">{title}</h3> + )} + {subtitle && <p class="font-light sm:text-lg text-gray-600 dark:text-slate-400">{subtitle}</p>} + <div class="my-8"> + <div class="flex items-center justify-center text-center mb-1"> + <span class="text-5xl">$</span> + <span class="text-6xl font-extrabold">{price}</span> + </div> + <span class="text-base leading-6 lowercase text-gray-600 dark:text-slate-400">{period}</span> + </div> + {items && ( + <ul class="my-8 md:my-10 space-y-2 text-left"> + {items.map( + ({ description, icon }) => + description && ( + <li class="mb-1.5 flex items-start space-x-3 leading-7"> + <div class="rounded-full bg-primary mt-1"> + <Icon name={icon ? icon : 'tabler:check'} class="w-5 h-5 font-bold p-1 text-white" /> + </div> + <span>{description}</span> + </li> + ) + )} + </ul> + )} + </div> + {callToAction && ( + <div class={`flex justify-center`}> + {typeof callToAction === 'string' ? ( + <Fragment set:html={callToAction} /> + ) : ( + callToAction && + callToAction.href && <Button {...(hasRibbon ? { variant: 'primary' } : {})} {...callToAction} /> + )} + </div> + )} + </div> + )} + </div> + )) + } + </div> + </div> +</WidgetWrapper> diff --git a/src/components/widgets/Stats.astro b/src/components/widgets/Stats.astro new file mode 100644 index 0000000..bf76ea0 --- /dev/null +++ b/src/components/widgets/Stats.astro @@ -0,0 +1,46 @@ +--- +import type { Stats as Props } from '~/types'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import Headline from '~/components/ui/Headline.astro'; +import { Icon } from 'astro-icon/components'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline, + stats = [], + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <Headline title={title} subtitle={subtitle} tagline={tagline} /> + <div class="flex flex-wrap justify-center -m-4 text-center"> + { + stats && + stats.map(({ amount, title, icon }) => ( + <div class="p-4 md:w-1/4 sm:w-1/2 w-full min-w-[220px] text-center md:border-r md:last:border-none dark:md:border-slate-500 intersect-once motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade intersect-quarter"> + {icon && ( + <div class="flex items-center justify-center mx-auto mb-4 text-primary"> + <Icon name={icon} class="w-10 h-10" /> + </div> + )} + {amount && ( + <div class="font-heading text-primary text-[2.6rem] font-bold dark:text-white lg:text-5xl xl:text-6xl"> + {amount} + </div> + )} + {title && ( + <div class="text-sm font-medium uppercase tracking-widest text-gray-800 dark:text-slate-400 lg:text-base"> + {title} + </div> + )} + </div> + )) + } + </div> +</WidgetWrapper> diff --git a/src/components/widgets/Steps.astro b/src/components/widgets/Steps.astro new file mode 100644 index 0000000..3c65bf6 --- /dev/null +++ b/src/components/widgets/Steps.astro @@ -0,0 +1,59 @@ +--- +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import Timeline from '~/components/ui/Timeline.astro'; +import Headline from '~/components/ui/Headline.astro'; +import Image from '~/components/common/Image.astro'; +import type { Steps as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline = await Astro.slots.render('tagline'), + items = [], + image = await Astro.slots.render('image'), + isReversed = false, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-5xl ${classes?.container ?? ''}`} bg={bg}> + <div class:list={['flex flex-col gap-8 md:gap-12', { 'md:flex-row-reverse': isReversed }, { 'md:flex-row': image }]}> + <div class:list={['md:py-4 md:self-center', { 'md:basis-1/2': image }, { 'w-full': !image }]}> + <Headline + title={title} + subtitle={subtitle} + tagline={tagline} + classes={{ + container: 'text-left rtl:text-right', + title: 'text-3xl lg:text-4xl', + ...((classes?.headline as object) ?? {}), + }} + /> + <Timeline items={items} classes={classes?.items as Record<string, never>} /> + </div> + { + image && ( + <div class="relative md:basis-1/2"> + {typeof image === 'string' ? ( + <Fragment set:html={image} /> + ) : ( + <Image + class="inset-0 object-cover object-top w-full rounded-md shadow-lg md:absolute md:h-full bg-gray-400 dark:bg-slate-700" + widths={[400, 768]} + sizes="(max-width: 768px) 100vw, 432px" + width={432} + height={768} + layout="cover" + src={image?.src} + alt={image?.alt || ''} + /> + )} + </div> + ) + } + </div> +</WidgetWrapper> diff --git a/src/components/widgets/Steps2.astro b/src/components/widgets/Steps2.astro new file mode 100644 index 0000000..0891663 --- /dev/null +++ b/src/components/widgets/Steps2.astro @@ -0,0 +1,79 @@ +--- +import { Icon } from 'astro-icon/components'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import Headline from '~/components/ui/Headline.astro'; +import Button from '~/components/ui/Button.astro'; +import type { Steps as Props } from '~/types'; + +const { + title = await Astro.slots.render('title'), + subtitle = await Astro.slots.render('subtitle'), + tagline, + callToAction = await Astro.slots.render('callToAction'), + items = [], + isReversed = false, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; + +// Function to make email addresses clickable +function makeEmailsClickable(text: string | undefined): string { + if (!text) return ''; + const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g; + return text.replace(emailRegex, '<a href="mailto:$1" class="text-primary hover:text-secondary transition-colors">$1</a>'); +} +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <div class={`flex flex-col gap-8 md:gap-12 md:flex-row ${isReversed ? 'md:flex-row-reverse' : ''}`}> + <div class={`w-full lg:w-1/2 gap-8 md:gap-12 ${isReversed ? 'lg:ml-16 md:ml-8 ml-0' : 'lg:mr-16 md:mr-8 mr-0'}`}> + <Headline + title={title} + subtitle={subtitle} + tagline={tagline} + classes={{ + container: 'text-center md:text-left rtl:md:text-right mb-4 md:mb-8', + title: 'mb-4 text-3xl lg:text-4xl font-bold font-heading', + subtitle: 'mb-8 text-xl text-muted dark:text-slate-400', + // ...((classes?.headline as {}) ?? {}), + }} + /> + + <div class="w-full text-center md:text-left rtl:md:text-right"> + { + typeof callToAction === 'string' ? ( + <Fragment set:html={callToAction} /> + ) : ( + callToAction && + callToAction.text && + callToAction.href && <Button variant="primary" {...callToAction} class="mb-12 w-auto" /> + ) + } + </div> + </div> + <div class="w-full lg:w-1/2 px-0"> + <ul class="space-y-10"> + { + items && items.length + ? items.map(({ title: title2, description, icon }, index) => ( + <li class="flex md:-mx-4"> + <div class="pr-4 sm:pl-4 rtl:pr-0 rtl:pl-4 rtl:sm:pl-0 rtl:sm:pr-4"> + <span class="flex w-16 h-16 mx-auto items-center justify-center text-2xl font-bold rounded-full bg-gray-100 text-primary"> + {icon ? <Icon name={icon} class="w-6 h-6 icon-bold" /> : index + 1} + </span> + </div> + <div class="pl-4 rtl:pl-0 rtl:pr-4"> + <h3 class="mb-4 text-xl font-semibold font-heading" set:html={title2} /> + <p class="text-muted dark:text-gray-400" set:html={description ? makeEmailsClickable(description) : ''} /> + </div> + </li> + )) + : '' + } + </ul> + </div> + </div> +</WidgetWrapper> diff --git a/src/components/widgets/Testimonials.astro b/src/components/widgets/Testimonials.astro new file mode 100644 index 0000000..11db7b5 --- /dev/null +++ b/src/components/widgets/Testimonials.astro @@ -0,0 +1,75 @@ +--- +import Headline from '~/components/ui/Headline.astro'; +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; +import Button from '~/components/ui/Button.astro'; +import Image from '~/components/common/Image.astro'; +import type { Testimonials as Props } from '~/types'; + +const { + title = '', + subtitle = '', + tagline = '', + testimonials = [], + callToAction, + + id, + isDark = false, + classes = {}, + bg = await Astro.slots.render('bg'), +} = Astro.props; +--- + +<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}> + <Headline title={title} subtitle={subtitle} tagline={tagline} /> + + <div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6"> + { + testimonials && + testimonials.map(({ title, testimonial, name, job, image }) => ( + <div class="flex h-auto intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"> + <div class="flex flex-col p-4 md:p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600"> + {title && <h2 class="text-lg font-medium leading-6 pb-4">{title}</h2>} + {testimonial && ( + <blockquote class="flex-auto"> + <p class="text-muted">" {testimonial} "</p> + </blockquote> + )} + + <hr class="border-slate-200 dark:border-slate-600 my-4" /> + + <div class="flex items-center"> + {image && ( + <div class="h-10 w-10 rounded-full border border-slate-200 dark:border-slate-600"> + {typeof image === 'string' ? ( + <Fragment set:html={image} /> + ) : ( + <Image + class="h-10 w-10 rounded-full border border-slate-200 dark:border-slate-600 min-w-full min-h-full" + width={40} + height={40} + widths={[400, 768]} + layout="fixed" + {...image} + /> + )} + </div> + )} + + <div class="grow ml-3 rtl:ml-0 rtl:mr-3"> + {name && <p class="text-base font-semibold">{name}</p>} + {job && <p class="text-xs text-muted">{job}</p>} + </div> + </div> + </div> + </div> + )) + } + </div> + { + callToAction && ( + <div class="flex justify-center mx-auto w-fit mt-8 md:mt-12 font-medium"> + <Button {...callToAction} /> + </div> + ) + } +</WidgetWrapper> |
