commit 4b4ef61261e0693322ef33a93e8b2ce1c1ccda0b Author: Andre Rossouw Date: Sat Dec 23 18:38:21 2023 +0200 first commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8927e2c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a7af2a5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +dist +node_modules +.github +types.generated.d.ts \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..296d99a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,53 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + env: { + node: true, + es2022: true, + browser: true, + }, + extends: ['eslint:recommended', 'plugin:astro/recommended'], + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: __dirname, + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: {}, + overrides: [ + { + files: ['*.js'], + rules: { + 'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], + }, + }, + { + files: ['*.astro'], + parser: 'astro-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + extraFileExtensions: ['.astro'], + }, + rules: { + 'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], + }, + }, + { + files: ['*.ts'], + parser: '@typescript-eslint/parser', + extends: ['plugin:@typescript-eslint/recommended'], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' }, + ], + '@typescript-eslint/no-non-null-assertion': 'off', + }, + }, + { + // Define the configuration for ` + + + + + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..6f27bb6 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: \ No newline at end of file diff --git a/sandbox.config.json b/sandbox.config.json new file mode 100644 index 0000000..1860764 --- /dev/null +++ b/sandbox.config.json @@ -0,0 +1,11 @@ +{ + "infiniteLoopProtection": true, + "hardReloadOnChange": false, + "view": "browser", + "template": "node", + "container": { + "port": 3000, + "startScript": "start", + "node": "18" + } +} diff --git a/src/assets/favicons/apple-touch-icon.png b/src/assets/favicons/apple-touch-icon.png new file mode 100644 index 0000000..6262d06 Binary files /dev/null and b/src/assets/favicons/apple-touch-icon.png differ diff --git a/src/assets/favicons/favicon.ico b/src/assets/favicons/favicon.ico new file mode 100644 index 0000000..9f9502a Binary files /dev/null and b/src/assets/favicons/favicon.ico differ diff --git a/src/assets/favicons/favicon.svg b/src/assets/favicons/favicon.svg new file mode 100644 index 0000000..f157bd1 --- /dev/null +++ b/src/assets/favicons/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/src/assets/images/app-store.png b/src/assets/images/app-store.png new file mode 100644 index 0000000..8d634c0 Binary files /dev/null and b/src/assets/images/app-store.png differ diff --git a/src/assets/images/default.png b/src/assets/images/default.png new file mode 100644 index 0000000..c543a36 Binary files /dev/null and b/src/assets/images/default.png differ diff --git a/src/assets/images/google-play.png b/src/assets/images/google-play.png new file mode 100644 index 0000000..179f1ff Binary files /dev/null and b/src/assets/images/google-play.png differ diff --git a/src/assets/images/hero.png b/src/assets/images/hero.png new file mode 100644 index 0000000..3675e8d Binary files /dev/null and b/src/assets/images/hero.png differ diff --git a/src/assets/styles/tailwind.css b/src/assets/styles/tailwind.css new file mode 100644 index 0000000..38014f2 --- /dev/null +++ b/src/assets/styles/tailwind.css @@ -0,0 +1,90 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + .bg-page { + background-color: var(--aw-color-bg-page); + } + .bg-dark { + background-color: var(--aw-color-bg-page-dark); + } + .bg-light { + background-color: var(--aw-color-bg-page); + } + .text-page { + color: var(--aw-color-text-page); + } + .text-muted { + color: var(--aw-color-text-muted); + } +} + +@layer components { + .btn { + @apply inline-flex items-center justify-center rounded-full border-gray-400 border bg-transparent font-medium text-center text-base text-page leading-snug transition py-3.5 px-6 md:px-8 ease-in duration-200 focus:ring-blue-500 focus:ring-offset-blue-200 focus:ring-2 focus:ring-offset-2 hover:bg-gray-100 hover:border-gray-600 dark:text-slate-300 dark:border-slate-500 dark:hover:bg-slate-800 dark:hover:border-slate-800 cursor-pointer; + } + + .btn-primary { + @apply btn font-semibold bg-primary text-white border-primary hover:bg-secondary hover:border-secondary hover:text-white dark:text-white dark:bg-primary dark:border-primary dark:hover:border-secondary dark:hover:bg-secondary; + } + + .btn-secondary { + @apply btn; + } + + .btn-tertiary { + @apply btn border-none shadow-none text-muted hover:text-gray-900 dark:text-gray-400 dark:hover:text-white; + } +} + +#header.scroll > div:first-child { + @apply bg-page md:bg-white/90 md:backdrop-blur-md; + box-shadow: 0 0.375rem 1.5rem 0 rgb(140 152 164 / 13%); +} +.dark #header.scroll > div:first-child, +#header.scroll.dark > div:first-child { + @apply bg-page md:bg-[#030621e6] border-b border-gray-500/20; + box-shadow: none; +} +/* #header.scroll > div:last-child { + @apply py-3; +} */ + +#header.expanded nav { + position: fixed; + top: 70px; + left: 0; + right: 0; + bottom: 70px !important; + padding: 0 5px; +} + +.dropdown:hover .dropdown-menu { + display: block; +} + +[astro-icon].icon-light > * { + stroke-width: 1.2; +} + +[astro-icon].icon-bold > * { + stroke-width: 2.4; +} + +[data-aw-toggle-menu] path { + @apply transition; +} +[data-aw-toggle-menu].expanded g > path:first-child { + @apply -rotate-45 translate-y-[15px] translate-x-[-3px]; +} + +[data-aw-toggle-menu].expanded g > path:last-child { + @apply rotate-45 translate-y-[-8px] translate-x-[14px]; +} + +/* To deprecated */ + +.dd *:first-child { + margin-top: 0; +} diff --git a/src/components/CustomStyles.astro b/src/components/CustomStyles.astro new file mode 100644 index 0000000..04a2d13 --- /dev/null +++ b/src/components/CustomStyles.astro @@ -0,0 +1,60 @@ +--- +import '@fontsource-variable/inter'; + +// 'DM Sans' +// Nunito +// Dosis +// Outfit +// Roboto +// Literata +// 'IBM Plex Sans' +// Karla +// Poppins +// 'Fira Sans' +// 'Libre Franklin' +// Inconsolata +// Raleway +// Oswald +// 'Space Grotesk' +// Urbanist +--- + + diff --git a/src/components/Favicons.astro b/src/components/Favicons.astro new file mode 100644 index 0000000..fed6696 --- /dev/null +++ b/src/components/Favicons.astro @@ -0,0 +1,10 @@ +--- +import favIcon from '~/assets/favicons/favicon.ico'; +import favIconSvg from '~/assets/favicons/favicon.svg'; +import appleTouchIcon from '~/assets/favicons/apple-touch-icon.png'; +--- + + + + + diff --git a/src/components/Logo.astro b/src/components/Logo.astro new file mode 100644 index 0000000..f196575 --- /dev/null +++ b/src/components/Logo.astro @@ -0,0 +1,7 @@ +--- +import { SITE } from '~/utils/config'; +--- + + + πŸš€ {SITE?.name} + diff --git a/src/components/blog/Grid.astro b/src/components/blog/Grid.astro new file mode 100644 index 0000000..1b62be4 --- /dev/null +++ b/src/components/blog/Grid.astro @@ -0,0 +1,14 @@ +--- +import Item from '~/components/blog/GridItem.astro'; +import type { Post } from '~/types'; + +export interface Props { + posts: Array; +} + +const { posts } = Astro.props; +--- + +
+ {posts.map((post) => )} +
diff --git a/src/components/blog/GridItem.astro b/src/components/blog/GridItem.astro new file mode 100644 index 0000000..360de3d --- /dev/null +++ b/src/components/blog/GridItem.astro @@ -0,0 +1,55 @@ +--- +import { APP_BLOG } from '~/utils/config'; +import type { Post } from '~/types'; + +import Image from '~/components/common/Image.astro'; + +import { findImage } from '~/utils/images'; +import { getPermalink } from '~/utils/permalinks'; + + +export interface Props { + post: Post; +} + +const { post } = Astro.props; +const image = (await findImage(post.image)); +--- + +
+
+ { + image && ( + + {post.title} + + ) + } +
+

+ { + !APP_BLOG?.post?.isEnabled ? ( + post.title + ) : ( + + {post.title} + + ) + } +

+

{post.excerpt}

+
diff --git a/src/components/blog/Headline.astro b/src/components/blog/Headline.astro new file mode 100644 index 0000000..5d3ccc6 --- /dev/null +++ b/src/components/blog/Headline.astro @@ -0,0 +1,12 @@ +--- +const { title = await Astro.slots.render('default'), subtitle = await Astro.slots.render('subtitle') } = Astro.props; +--- + +
+

+ { + subtitle && ( +
+ ) + } +

diff --git a/src/components/blog/List.astro b/src/components/blog/List.astro new file mode 100644 index 0000000..6a80ae3 --- /dev/null +++ b/src/components/blog/List.astro @@ -0,0 +1,20 @@ +--- +import Item from '~/components/blog/ListItem.astro'; +import type { Post } from '~/types'; + +export interface Props { + posts: Array; +} + +const { posts } = Astro.props; +--- + +
    + { + posts.map((post) => ( +
  • + +
  • + )) + } +
diff --git a/src/components/blog/ListItem.astro b/src/components/blog/ListItem.astro new file mode 100644 index 0000000..18c2b1c --- /dev/null +++ b/src/components/blog/ListItem.astro @@ -0,0 +1,83 @@ +--- +import type { ImageMetadata } from 'astro'; +import { Icon } from 'astro-icon/components'; +import Image from '~/components/common/Image.astro'; +import PostTags from '~/components/blog/Tags.astro'; + +import { APP_BLOG } from '~/utils/config'; +import type { Post } from '~/types'; + +import { getPermalink } from '~/utils/permalinks'; +import { findImage } from '~/utils/images'; +import { getFormattedDate } from '~/utils/utils'; + +export interface Props { + post: Post; +} + +const { post } = Astro.props; +const image = (await findImage(post.image)) as ImageMetadata | undefined; + +const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : ''; +--- + + diff --git a/src/components/blog/Pagination.astro b/src/components/blog/Pagination.astro new file mode 100644 index 0000000..051587c --- /dev/null +++ b/src/components/blog/Pagination.astro @@ -0,0 +1,36 @@ +--- +import { Icon } from 'astro-icon/components'; +import { getPermalink } from '~/utils/permalinks'; +import Button from '~/components/ui/Button.astro'; + +export interface Props { + prevUrl?: string; + nextUrl?: string; + prevText?: string; + nextText?: string; +} + +const { prevUrl, nextUrl, prevText = 'Newer posts', nextText = 'Older posts' } = Astro.props; +--- + +{ + (prevUrl || nextUrl) && ( +
+
+ + + +
+
+ ) +} diff --git a/src/components/blog/SinglePost.astro b/src/components/blog/SinglePost.astro new file mode 100644 index 0000000..6744720 --- /dev/null +++ b/src/components/blog/SinglePost.astro @@ -0,0 +1,90 @@ +--- +import { Icon } from 'astro-icon/components'; + +import Image from '~/components/common/Image.astro'; +import PostTags from '~/components/blog/Tags.astro'; +import SocialShare from '~/components/common/SocialShare.astro'; + +import { getPermalink } from '~/utils/permalinks'; +import { getFormattedDate } from '~/utils/utils'; + +import type { Post } from '~/types'; + +export interface Props { + post: Post; + url: string | URL; +} + +const { post, url } = Astro.props; +const { Content } = post; +--- + +
+
+
+
+

+ + + { + post.category && ( + <> + {' '} + Β·{' '} + + {post.category.replaceAll('-', ' ')} + + + ) + } + {post.readingTime && <> Β· {post.readingTime} min read} +

+
+

+ {post.title} +

+

+ {post.excerpt} +

+ + { + post.image ? ( + {post?.excerpt + ) : ( +
+
+
+ ) + } +
+
+ { + Content ? ( + + ) : ( + + ) + } +
+
+ + +
+
+
diff --git a/src/components/blog/Tags.astro b/src/components/blog/Tags.astro new file mode 100644 index 0000000..04e2a01 --- /dev/null +++ b/src/components/blog/Tags.astro @@ -0,0 +1,41 @@ +--- +import { getPermalink } from '~/utils/permalinks'; + +import { APP_BLOG } from '~/utils/config'; +import type { Post } from '~/types'; + +export interface Props { + tags: Post['tags']; + class?: string; + title?: string | undefined; + isCategory?: boolean; +} + +const { tags, class: className = 'text-sm', title = undefined, isCategory = false } = Astro.props; +--- + +{ + tags && Array.isArray(tags) && ( + <> + <> + {title !== undefined && {title}} + +
    + {tags.map((tag) => ( +
  • + {!APP_BLOG?.tag?.isEnabled ? ( + tag + ) : ( + + {tag} + + )} +
  • + ))} +
+ + ) +} diff --git a/src/components/blog/ToBlogLink.astro b/src/components/blog/ToBlogLink.astro new file mode 100644 index 0000000..e3e28ef --- /dev/null +++ b/src/components/blog/ToBlogLink.astro @@ -0,0 +1,20 @@ +--- +import { Icon } from 'astro-icon/components'; +import { getBlogPermalink } from '~/utils/permalinks'; +import { I18N } from '~/utils/config'; +import Button from '~/components/ui/Button.astro'; + +const { textDirection } = I18N; +--- + +
+ +
diff --git a/src/components/common/Analytics.astro b/src/components/common/Analytics.astro new file mode 100644 index 0000000..8bf883a --- /dev/null +++ b/src/components/common/Analytics.astro @@ -0,0 +1,10 @@ +--- +import { GoogleAnalytics } from '@astrolib/analytics'; +import { ANALYTICS } from '~/utils/config'; +--- + +{ + ANALYTICS?.vendors?.googleAnalytics?.id ? ( + + ) : null +} diff --git a/src/components/common/ApplyColorMode.astro b/src/components/common/ApplyColorMode.astro new file mode 100644 index 0000000..f7e2b54 --- /dev/null +++ b/src/components/common/ApplyColorMode.astro @@ -0,0 +1,33 @@ +--- +import { UI } from "~/utils/config"; + +// TODO: This code is temporary +--- + + diff --git a/src/components/common/BasicScripts.astro b/src/components/common/BasicScripts.astro new file mode 100644 index 0000000..d9c5ad5 --- /dev/null +++ b/src/components/common/BasicScripts.astro @@ -0,0 +1,150 @@ +--- +import { UI } from '~/utils/config'; +--- + + diff --git a/src/components/common/CommonMeta.astro b/src/components/common/CommonMeta.astro new file mode 100644 index 0000000..96be816 --- /dev/null +++ b/src/components/common/CommonMeta.astro @@ -0,0 +1,8 @@ +--- +import { getAsset } from '~/utils/permalinks'; +--- + + + + + \ No newline at end of file diff --git a/src/components/common/Image.astro b/src/components/common/Image.astro new file mode 100644 index 0000000..019d6a5 --- /dev/null +++ b/src/components/common/Image.astro @@ -0,0 +1,52 @@ +--- +import { findImage } from '~/utils/images'; +import { + getImagesOptimized, + astroAsseetsOptimizer, + unpicOptimizer, + type ImageProps, + type AttributesProps +} from '~/utils/images-optimization'; + +type Props = ImageProps; +type ImageType = { + src: string; + attributes: AttributesProps; +} + +const props = Astro.props; + +if (props.alt === undefined || props.alt === null) { + throw new Error(); +} + +if (typeof props.width === 'string') { + props.width = parseInt(props.width); +} + +if (typeof props.height === 'string') { + props.height = parseInt(props.height); +} + +if (!props.loading) { + props.loading = 'lazy'; +} + +if (!props.decoding) { + props.decoding = 'async'; +} + +const _image = await findImage(props.src); + +let image: ImageType | undefined = undefined; + +if (_image !== null && typeof _image === 'object') { + image = await getImagesOptimized(_image, props, astroAsseetsOptimizer); +} else if (typeof _image === 'string' && (_image.startsWith('http://') || _image.startsWith('https://'))) { + image = await getImagesOptimized(_image, props, unpicOptimizer); +} else if (_image) { + image = await getImagesOptimized(_image, props); +} +--- + +{!image ? : } diff --git a/src/components/common/Metadata.astro b/src/components/common/Metadata.astro new file mode 100644 index 0000000..0def884 --- /dev/null +++ b/src/components/common/Metadata.astro @@ -0,0 +1,68 @@ +--- +import merge from 'lodash.merge'; +import { AstroSeo } from '@astrolib/seo'; + +import type { Props as AstroSeoProps } from '@astrolib/seo'; + +import { SITE, METADATA, I18N } from '~/utils/config'; +import type { MetaData } from '~/types'; +import { getCanonical } from '~/utils/permalinks'; + +import { adaptOpenGraphImages } from '~/utils/images'; + +export interface Props extends MetaData { + dontUseTitleTemplate?: boolean; +} + +const { + title, + ignoreTitleTemplate = false, + canonical = String(getCanonical(String(Astro.url.pathname))), + robots = {}, + description, + openGraph = {}, + twitter = {}, +} = Astro.props; + +const seoProps: AstroSeoProps = merge( + { + title: '', + titleTemplate: '%s', + canonical: canonical, + noindex: true, + nofollow: true, + description: undefined, + openGraph: { + url: canonical, + site_name: SITE?.name, + images: [], + locale: I18N?.language || 'en', + type: 'website', + }, + twitter: { + cardType: openGraph?.images?.length ? 'summary_large_image' : 'summary', + }, + }, + { + title: METADATA?.title?.default, + titleTemplate: METADATA?.title?.template, + noindex: typeof METADATA?.robots?.index !== 'undefined' ? !METADATA.robots.index : undefined, + nofollow: typeof METADATA?.robots?.follow !== 'undefined' ? !METADATA.robots.follow : undefined, + description: METADATA?.description, + openGraph: METADATA?.openGraph, + twitter: METADATA?.twitter, + }, + { + title: title, + titleTemplate: ignoreTitleTemplate ? '%s' : undefined, + canonical: canonical, + noindex: typeof robots?.index !== 'undefined' ? !robots.index : undefined, + nofollow: typeof robots?.follow !== 'undefined' ? !robots.follow : undefined, + description: description, + openGraph: { url: canonical, ...openGraph }, + twitter: twitter, + } +); +--- + + diff --git a/src/components/common/SiteVerification.astro b/src/components/common/SiteVerification.astro new file mode 100644 index 0000000..d67b76b --- /dev/null +++ b/src/components/common/SiteVerification.astro @@ -0,0 +1,5 @@ +--- +import { SITE } from "~/utils/config"; +--- + +{SITE.googleSiteVerificationId && } \ No newline at end of file diff --git a/src/components/common/SocialShare.astro b/src/components/common/SocialShare.astro new file mode 100644 index 0000000..852587b --- /dev/null +++ b/src/components/common/SocialShare.astro @@ -0,0 +1,45 @@ +--- +import { Icon } from 'astro-icon/components'; + +export interface Props { + text: string; + url: string | URL; + class?: string; +} + +const { text, url, class: className = 'inline-block' } = Astro.props; +--- + +
+ Share: + + + + + +
diff --git a/src/components/common/SplitbeeAnalytics.astro b/src/components/common/SplitbeeAnalytics.astro new file mode 100644 index 0000000..b3c349a --- /dev/null +++ b/src/components/common/SplitbeeAnalytics.astro @@ -0,0 +1,6 @@ +--- +const { doNotTrack = true, noCookieMode = false, url = 'https://cdn.splitbee.io/sb.js' } = Astro.props; +--- + + + diff --git a/src/components/common/ToggleMenu.astro b/src/components/common/ToggleMenu.astro new file mode 100644 index 0000000..79a1bf6 --- /dev/null +++ b/src/components/common/ToggleMenu.astro @@ -0,0 +1,34 @@ +--- +export interface Props { + label?: string; + class?: string; +} + +const { + label = 'Toggle Menu', + class: + className = "flex flex-col h-12 w-12 rounded justify-center items-center cursor-pointer group", +} = Astro.props; +--- + + diff --git a/src/components/common/ToggleTheme.astro b/src/components/common/ToggleTheme.astro new file mode 100644 index 0000000..4f4b695 --- /dev/null +++ b/src/components/common/ToggleTheme.astro @@ -0,0 +1,28 @@ +--- +import { Icon } from 'astro-icon/components'; + +import { UI } from '~/utils/config'; + +export interface Props { + label?: string; + class?: string; + iconClass?: string; + iconName?: string; +} + +const { + label = 'Toggle between Dark and Light mode', + class: + className = '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', + iconClass = 'w-6 h-6', + iconName = 'tabler:sun', +} = Astro.props; +--- + +{ + !(UI.theme && UI.theme.endsWith(':only')) && ( + + ) +} diff --git a/src/components/ui/Background.astro b/src/components/ui/Background.astro new file mode 100644 index 0000000..791cef9 --- /dev/null +++ b/src/components/ui/Background.astro @@ -0,0 +1,7 @@ +--- +const { isDark = false } = Astro.props; +--- + +
+ +
diff --git a/src/components/ui/Button.astro b/src/components/ui/Button.astro new file mode 100644 index 0000000..e68e0fb --- /dev/null +++ b/src/components/ui/Button.astro @@ -0,0 +1,40 @@ +--- +import { Icon } from 'astro-icon/components'; +import { twMerge } from 'tailwind-merge'; +import type { CallToAction } from '~/types'; + +const { + variant = 'secondary', + target, + text = Astro.slots.render('default'), + icon = '', + class: className = '', + type, + ...rest +} = Astro.props as CallToAction; + +const variants = { + primary: 'btn-primary', + secondary: 'btn-secondary', + tertiary: 'btn btn-tertiary', + link: 'cursor-pointer hover:text-primary', +}; +--- + +{ + type === 'button' || type === 'submit' || type === 'reset' ? ( + + ) : ( + + + {icon && } + + ) +} diff --git a/src/components/ui/DListItem.astro b/src/components/ui/DListItem.astro new file mode 100644 index 0000000..95fd649 --- /dev/null +++ b/src/components/ui/DListItem.astro @@ -0,0 +1,23 @@ +--- +// component: DListItem +// +// Mimics the html 'dl' (description list) +// +// The 'dt' item is the item 'term' and is inserted into an 'h6' tag. +// Caller needs to style the 'h6' tag appropriately. +// +// You can put pretty much any content you want between the open and +// closing tags - it's simply contained in an enclosing div with a +// margin left. No need for 'dd' items. +// +const { + dt +} = Astro.props; +interface Props { + dt:string +} + +const content:string = await Astro.slots.render('default'); +--- +
+
\ No newline at end of file diff --git a/src/components/ui/Form.astro b/src/components/ui/Form.astro new file mode 100644 index 0000000..64f3f36 --- /dev/null +++ b/src/components/ui/Form.astro @@ -0,0 +1,85 @@ +--- +import type { Form } from '~/types'; +import Button from '~/components/ui/Button.astro'; + +const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props as Form; +--- + +
+ { + inputs && + inputs.map( + ({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '' }) => + name && ( +
+ {label && ( + + )} + +
+ ) + ) + } + + { + textarea && ( +
+ +