feat: astro template for front and fix login(#13)
Reviewed-on: #13 Co-authored-by: Clement <c.boesmier@aptatio.com> Co-committed-by: Clement <c.boesmier@aptatio.com>
This commit is contained in:
63
front/src/components/CustomStyles.astro
Normal file
63
front/src/components/CustomStyles.astro
Normal file
@ -0,0 +1,63 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
<style is:inline>
|
||||
:root {
|
||||
--aw-font-sans: 'InterVariable';
|
||||
--aw-font-serif: 'InterVariable';
|
||||
--aw-font-heading: 'InterVariable';
|
||||
|
||||
--aw-color-primary: rgb(1 97 239);
|
||||
--aw-color-secondary: rgb(1 84 207);
|
||||
--aw-color-accent: rgb(109 40 217);
|
||||
|
||||
--aw-color-text-heading: rgb(0 0 0);
|
||||
--aw-color-text-default: rgb(16 16 16);
|
||||
--aw-color-text-muted: rgb(16 16 16 / 66%);
|
||||
--aw-color-bg-page: rgb(255 255 255);
|
||||
|
||||
--aw-color-bg-page-dark: rgb(3 6 32);
|
||||
|
||||
::selection {
|
||||
background-color: lavender;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
--aw-font-sans: 'InterVariable';
|
||||
--aw-font-serif: 'InterVariable';
|
||||
--aw-font-heading: 'InterVariable';
|
||||
|
||||
--aw-color-primary: rgb(1 97 239);
|
||||
--aw-color-secondary: rgb(1 84 207);
|
||||
--aw-color-accent: rgb(109 40 217);
|
||||
|
||||
--aw-color-text-heading: rgb(247, 248, 248);
|
||||
--aw-color-text-default: rgb(229 236 246);
|
||||
--aw-color-text-muted: rgb(229 236 246 / 66%);
|
||||
--aw-color-bg-page: rgb(3 6 32);
|
||||
|
||||
::selection {
|
||||
background-color: black;
|
||||
color: snow;
|
||||
}
|
||||
}
|
||||
</style>
|
10
front/src/components/Favicons.astro
Normal file
10
front/src/components/Favicons.astro
Normal file
@ -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';
|
||||
---
|
||||
|
||||
<link rel="shortcut icon" href={favIcon} />
|
||||
<link rel="icon" type="image/svg+xml" href={favIconSvg.src} />
|
||||
<link rel="mask-icon" href={favIconSvg.src} color="#8D46E7" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={appleTouchIcon.src} />
|
9
front/src/components/Logo.astro
Normal file
9
front/src/components/Logo.astro
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
import { SITE } from 'astrowind:config';
|
||||
---
|
||||
|
||||
<span
|
||||
class="self-center ml-2 rtl:ml-0 rtl:mr-2 text-2xl md:text-xl font-bold text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
🚀 {SITE?.name}
|
||||
</span>
|
53
front/src/components/Oauth.astro
Normal file
53
front/src/components/Oauth.astro
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
import ContactUs from 'components/widgets/Contact.astro';
|
||||
import CallToAction from 'components/widgets/CallToAction.astro';
|
||||
|
||||
const pb = Astro.locals.pb
|
||||
const oauths = (await pb.collection('users').listAuthMethods()).authProviders;
|
||||
const discordProvider = oauths.find(item => item.name === 'discord');
|
||||
const googleProvider = oauths.find(item => item.name === 'google');
|
||||
|
||||
---
|
||||
|
||||
<>
|
||||
<CallToAction
|
||||
actions={[
|
||||
{
|
||||
variant: 'primary',
|
||||
text: 'Discord',
|
||||
href: discordProvider!.authUrl + Astro.url.protocol + "//" + Astro.url.host + '/account/oauth',
|
||||
icon: 'tabler:brand-discord',
|
||||
class: "oauth-btn",
|
||||
"data-cookie": encodeURIComponent(JSON.stringify(discordProvider))
|
||||
},
|
||||
{
|
||||
variant: 'primary',
|
||||
text: 'Google',
|
||||
href: googleProvider!.authUrl + Astro.url.protocol + "//" + Astro.url.host + '/account/oauth',
|
||||
icon: 'tabler:brand-google',
|
||||
class: "oauth-btn",
|
||||
"data-cookie": encodeURIComponent(JSON.stringify(googleProvider))
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Fragment slot="title">
|
||||
Oauth
|
||||
</Fragment>
|
||||
|
||||
<Fragment slot="subtitle">
|
||||
Connecter Vous aussi avec
|
||||
</Fragment>
|
||||
</CallToAction>
|
||||
</>
|
||||
|
||||
<script>
|
||||
// import { date } from "astro/zod";
|
||||
|
||||
document.cookie = "provider" + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||
// console.log(date.toString)
|
||||
|
||||
const btn = document.querySelectorAll('.oauth-btn')
|
||||
btn.forEach((item: Element) =>(item.addEventListener('click', (ev) =>{
|
||||
document.cookie = "provider" + "=" + item.getAttribute('data-cookie') + "; path=/;"// expires=" + ;
|
||||
})))
|
||||
</script>
|
14
front/src/components/blog/Grid.astro
Normal file
14
front/src/components/blog/Grid.astro
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
import Item from 'components/blog/GridItem.astro';
|
||||
import type { Post } from 'types';
|
||||
|
||||
export interface Props {
|
||||
posts: Array<Post>;
|
||||
}
|
||||
|
||||
const { posts } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="grid gap-6 row-gap-5 md:grid-cols-2 lg:grid-cols-4 -mb-6">
|
||||
{posts.map((post) => <Item post={post} />)}
|
||||
</div>
|
69
front/src/components/blog/GridItem.astro
Normal file
69
front/src/components/blog/GridItem.astro
Normal file
@ -0,0 +1,69 @@
|
||||
---
|
||||
import { APP_BLOG } from 'astrowind: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);
|
||||
|
||||
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
|
||||
---
|
||||
|
||||
<article class="mb-6 transition">
|
||||
<div class="relative md:h-64 bg-gray-400 dark:bg-slate-700 rounded shadow-lg mb-6">
|
||||
{
|
||||
image &&
|
||||
(link ? (
|
||||
<a href={link}>
|
||||
<Image
|
||||
src={image}
|
||||
class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||
widths={[400, 900]}
|
||||
width={400}
|
||||
sizes="(max-width: 900px) 400px, 900px"
|
||||
alt={post.title}
|
||||
aspectRatio="16:9"
|
||||
layout="cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<Image
|
||||
src={image}
|
||||
class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||
widths={[400, 900]}
|
||||
width={400}
|
||||
sizes="(max-width: 900px) 400px, 900px"
|
||||
alt={post.title}
|
||||
aspectRatio="16:9"
|
||||
layout="cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
|
||||
{
|
||||
link ? (
|
||||
<a class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200" href={link}>
|
||||
{post.title}
|
||||
</a>
|
||||
) : (
|
||||
post.title
|
||||
)
|
||||
}
|
||||
</h3>
|
||||
|
||||
<p class="text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>
|
||||
</article>
|
12
front/src/components/blog/Headline.astro
Normal file
12
front/src/components/blog/Headline.astro
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
const { title = await Astro.slots.render('default'), subtitle = await Astro.slots.render('subtitle') } = Astro.props;
|
||||
---
|
||||
|
||||
<header class="mb-8 md:mb-16 text-center max-w-3xl mx-auto">
|
||||
<h1 class="text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading" set:html={title} />
|
||||
{
|
||||
subtitle && (
|
||||
<div class="mt-2 md:mt-3 mx-auto text-xl text-gray-500 dark:text-slate-400 font-medium" set:html={subtitle} />
|
||||
)
|
||||
}
|
||||
</header>
|
20
front/src/components/blog/List.astro
Normal file
20
front/src/components/blog/List.astro
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
import Item from 'components/blog/ListItem.astro';
|
||||
import type { Post } from 'types';
|
||||
|
||||
export interface Props {
|
||||
posts: Array<Post>;
|
||||
}
|
||||
|
||||
const { posts } = Astro.props;
|
||||
---
|
||||
|
||||
<ul>
|
||||
{
|
||||
posts.map((post) => (
|
||||
<li class="mb-12 md:mb-20">
|
||||
<Item post={post} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
118
front/src/components/blog/ListItem.astro
Normal file
118
front/src/components/blog/ListItem.astro
Normal file
@ -0,0 +1,118 @@
|
||||
---
|
||||
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 'astrowind: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') : '';
|
||||
---
|
||||
|
||||
<article class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 ${image ? 'md:grid-cols-2' : ''}`}>
|
||||
{
|
||||
image &&
|
||||
(link ? (
|
||||
<a class="relative block group" href={link ?? 'javascript:void(0)'}>
|
||||
<div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
|
||||
{image && (
|
||||
<Image
|
||||
src={image}
|
||||
class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||
widths={[400, 900]}
|
||||
width={900}
|
||||
sizes="(max-width: 900px) 400px, 900px"
|
||||
alt={post.title}
|
||||
aspectRatio="16:9"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
|
||||
{image && (
|
||||
<Image
|
||||
src={image}
|
||||
class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||
widths={[400, 900]}
|
||||
width={900}
|
||||
sizes="(max-width: 900px) 400px, 900px"
|
||||
alt={post.title}
|
||||
aspectRatio="16:9"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<div class="mt-2">
|
||||
<header>
|
||||
<div class="mb-1">
|
||||
<span class="text-sm">
|
||||
<Icon name="tabler:clock" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
|
||||
<time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
|
||||
{
|
||||
post.author && (
|
||||
<>
|
||||
{' '}
|
||||
· <Icon name="tabler:user" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
|
||||
<span>{post.author.replaceAll('-', ' ')}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
post.category && (
|
||||
<>
|
||||
{' '}
|
||||
·{' '}
|
||||
<a class="hover:underline" href={getPermalink(post.category.slug, 'category')}>
|
||||
{post.category.title}
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<h2 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
|
||||
{
|
||||
link ? (
|
||||
<a
|
||||
class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200"
|
||||
href={link}
|
||||
>
|
||||
{post.title}
|
||||
</a>
|
||||
) : (
|
||||
post.title
|
||||
)
|
||||
}
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
{post.excerpt && <p class="flex-grow text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>}
|
||||
{
|
||||
post.tags && Array.isArray(post.tags) ? (
|
||||
<footer class="mt-5">
|
||||
<PostTags tags={post.tags} />
|
||||
</footer>
|
||||
) : (
|
||||
<Fragment />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</article>
|
36
front/src/components/blog/Pagination.astro
Normal file
36
front/src/components/blog/Pagination.astro
Normal file
@ -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) && (
|
||||
<div class="container flex">
|
||||
<div class="flex flex-row mx-auto container justify-between">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
class={`md:px-3 px-3 mr-2 ${!prevUrl ? 'invisible' : ''}`}
|
||||
href={getPermalink(prevUrl)}
|
||||
>
|
||||
<Icon name="tabler:chevron-left" class="w-6 h-6" />
|
||||
<p class="ml-2">{prevText}</p>
|
||||
</Button>
|
||||
|
||||
<Button variant="tertiary" class={`md:px-3 px-3 ${!nextUrl ? 'invisible' : ''}`} href={getPermalink(nextUrl)}>
|
||||
<span class="mr-2">{nextText}</span>
|
||||
<Icon name="tabler:chevron-right" class="w-6 h-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
28
front/src/components/blog/RelatedPosts.astro
Normal file
28
front/src/components/blog/RelatedPosts.astro
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
import { APP_BLOG } from 'astrowind:config';
|
||||
|
||||
import { getRelatedPosts } from 'utils/blog';
|
||||
import BlogHighlightedPosts from '../widgets/BlogHighlightedPosts.astro';
|
||||
import type { Post } from 'types';
|
||||
import { getBlogPermalink } from 'utils/permalinks';
|
||||
|
||||
export interface Props {
|
||||
post: Post;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
|
||||
---
|
||||
|
||||
{
|
||||
APP_BLOG.isRelatedPostsEnabled ? (
|
||||
<BlogHighlightedPosts
|
||||
classes={{ container: 'pt-0 lg:pt-0 md:pt-0' }}
|
||||
title="Related Posts"
|
||||
linkText="View All Posts"
|
||||
linkUrl={getBlogPermalink()}
|
||||
postIds={relatedPosts.map((post) => post.id)}
|
||||
/>
|
||||
) : null
|
||||
}
|
99
front/src/components/blog/SinglePost.astro
Normal file
99
front/src/components/blog/SinglePost.astro
Normal file
@ -0,0 +1,99 @@
|
||||
---
|
||||
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;
|
||||
---
|
||||
|
||||
<section class="py-8 sm:py-16 lg:py-20 mx-auto">
|
||||
<article>
|
||||
<header class={post.image ? '' : ''}>
|
||||
<div class="flex justify-between flex-col sm:flex-row max-w-3xl mx-auto mt-0 mb-2 px-4 sm:px-6 sm:items-center">
|
||||
<p>
|
||||
<Icon name="tabler:clock" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
|
||||
<time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
|
||||
{
|
||||
post.author && (
|
||||
<>
|
||||
{' '}
|
||||
· <Icon name="tabler:user" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
|
||||
<span class="inline-block">{post.author}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
post.category && (
|
||||
<>
|
||||
{' '}
|
||||
·{' '}
|
||||
<a class="hover:underline inline-block" href={getPermalink(post.category.slug, 'category')}>
|
||||
{post.category.title}
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
post.readingTime && (
|
||||
<>
|
||||
· <span>{post.readingTime}</span> min read
|
||||
</>
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h1
|
||||
class="px-4 sm:px-6 max-w-3xl mx-auto text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading"
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
<p
|
||||
class="max-w-3xl mx-auto mt-4 mb-8 px-4 sm:px-6 text-xl md:text-2xl text-muted dark:text-slate-400 text-justify"
|
||||
>
|
||||
{post.excerpt}
|
||||
</p>
|
||||
|
||||
{
|
||||
post.image ? (
|
||||
<Image
|
||||
src={post.image}
|
||||
class="max-w-full lg:max-w-[900px] mx-auto mb-6 sm:rounded-md bg-gray-400 dark:bg-slate-700"
|
||||
widths={[400, 900]}
|
||||
sizes="(max-width: 900px) 400px, 900px"
|
||||
alt={post?.excerpt || ''}
|
||||
width={900}
|
||||
height={506}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6">
|
||||
<div class="border-t dark:border-slate-700" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
<div
|
||||
class="mx-auto px-6 sm:px-6 max-w-3xl prose prose-md lg:prose-xl dark:prose-invert dark:prose-headings:text-slate-300 prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-primary dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg mt-8 prose-headings:scroll-mt-[80px] prose-li:my-0"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="mx-auto px-6 sm:px-6 max-w-3xl mt-8 flex justify-between flex-col sm:flex-row">
|
||||
<PostTags tags={post.tags} class="mr-5 rtl:mr-0 rtl:ml-5" />
|
||||
<SocialShare url={url} text={post.title} class="mt-5 sm:mt-1 align-middle text-gray-500 dark:text-slate-600" />
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
45
front/src/components/blog/Tags.astro
Normal file
45
front/src/components/blog/Tags.astro
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
import { getPermalink } from 'utils/permalinks';
|
||||
|
||||
import { APP_BLOG } from 'astrowind: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 && (
|
||||
<span class="align-super font-normal underline underline-offset-4 decoration-2 dark:text-slate-400">
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
<ul class={className}>
|
||||
{tags.map((tag) => (
|
||||
<li class="bg-gray-100 dark:bg-slate-700 inline-block mr-2 rtl:mr-0 rtl:ml-2 mb-2 py-0.5 px-2 lowercase font-medium">
|
||||
{!APP_BLOG?.tag?.isEnabled ? (
|
||||
tag.title
|
||||
) : (
|
||||
<a
|
||||
href={getPermalink(tag.slug, isCategory ? 'category' : 'tag')}
|
||||
class="text-muted dark:text-slate-300 hover:text-primary dark:hover:text-gray-200"
|
||||
>
|
||||
{tag.title}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
20
front/src/components/blog/ToBlogLink.astro
Normal file
20
front/src/components/blog/ToBlogLink.astro
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { getBlogPermalink } from 'utils/permalinks';
|
||||
import { I18N } from 'astrowind:config';
|
||||
import Button from 'components/ui/Button.astro';
|
||||
|
||||
const { textDirection } = I18N;
|
||||
---
|
||||
|
||||
<div class="mx-auto px-6 sm:px-6 max-w-3xl pt-8 md:pt-4 pb-12 md:pb-20">
|
||||
<Button variant="tertiary" class="px-3 md:px-3" href={getBlogPermalink()}>
|
||||
{
|
||||
textDirection === 'rtl' ? (
|
||||
<Icon name="tabler:chevron-right" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
|
||||
) : (
|
||||
<Icon name="tabler:chevron-left" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
|
||||
)
|
||||
} Back to Blog
|
||||
</Button>
|
||||
</div>
|
13
front/src/components/common/Analytics.astro
Normal file
13
front/src/components/common/Analytics.astro
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
import { GoogleAnalytics } from '@astrolib/analytics';
|
||||
import { ANALYTICS } from 'astrowind:config';
|
||||
---
|
||||
|
||||
{
|
||||
ANALYTICS?.vendors?.googleAnalytics?.id ? (
|
||||
<GoogleAnalytics
|
||||
id={String(ANALYTICS.vendors.googleAnalytics.id)}
|
||||
partytown={ANALYTICS?.vendors?.googleAnalytics?.partytown}
|
||||
/>
|
||||
) : null
|
||||
}
|
33
front/src/components/common/ApplyColorMode.astro
Normal file
33
front/src/components/common/ApplyColorMode.astro
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
import { UI } from 'astrowind:config';
|
||||
|
||||
// TODO: This code is temporary
|
||||
---
|
||||
|
||||
<script is:inline define:vars={{ defaultTheme: UI.theme || 'system' }}>
|
||||
function applyTheme(theme) {
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
const matches = document.querySelectorAll('[data-aw-toggle-color-scheme] > input');
|
||||
|
||||
if (matches && matches.length) {
|
||||
matches.forEach((elem) => {
|
||||
elem.checked = theme !== 'dark';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ((defaultTheme && defaultTheme.endsWith(':only')) || (!localStorage.theme && defaultTheme !== 'system')) {
|
||||
applyTheme(defaultTheme.replace(':only', ''));
|
||||
} else if (
|
||||
localStorage.theme === 'dark' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
) {
|
||||
applyTheme('dark');
|
||||
} else {
|
||||
applyTheme('light');
|
||||
}
|
||||
</script>
|
162
front/src/components/common/BasicScripts.astro
Normal file
162
front/src/components/common/BasicScripts.astro
Normal file
@ -0,0 +1,162 @@
|
||||
---
|
||||
import { UI } from 'astrowind:config';
|
||||
---
|
||||
|
||||
<script is:inline define:vars={{ defaultTheme: UI.theme }}>
|
||||
if (window.basic_script) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.basic_script = true;
|
||||
|
||||
function applyTheme(theme) {
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
const initTheme = function () {
|
||||
if ((defaultTheme && defaultTheme.endsWith(':only')) || (!localStorage.theme && defaultTheme !== 'system')) {
|
||||
applyTheme(defaultTheme.replace(':only', ''));
|
||||
} else if (
|
||||
localStorage.theme === 'dark' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
) {
|
||||
applyTheme('dark');
|
||||
} else {
|
||||
applyTheme('light');
|
||||
}
|
||||
};
|
||||
initTheme();
|
||||
|
||||
function attachEvent(selector, event, fn) {
|
||||
const matches = typeof selector === 'string' ? document.querySelectorAll(selector) : selector;
|
||||
if (matches && matches.length) {
|
||||
matches.forEach((elem) => {
|
||||
elem.addEventListener(event, (e) => fn(e, elem), false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const onLoad = function () {
|
||||
let lastKnownScrollPosition = window.scrollY;
|
||||
let ticking = true;
|
||||
|
||||
attachEvent('#header nav', 'click', function () {
|
||||
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
document.getElementById('header')?.classList.remove('h-screen');
|
||||
document.getElementById('header')?.classList.remove('expanded');
|
||||
document.getElementById('header')?.classList.remove('bg-page');
|
||||
document.querySelector('#header nav')?.classList.add('hidden');
|
||||
document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
|
||||
});
|
||||
|
||||
attachEvent('[data-aw-toggle-menu]', 'click', function (_, elem) {
|
||||
elem.classList.toggle('expanded');
|
||||
document.body.classList.toggle('overflow-hidden');
|
||||
document.getElementById('header')?.classList.toggle('h-screen');
|
||||
document.getElementById('header')?.classList.toggle('expanded');
|
||||
document.getElementById('header')?.classList.toggle('bg-page');
|
||||
document.querySelector('#header nav')?.classList.toggle('hidden');
|
||||
document.querySelector('#header > div > div:last-child')?.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
attachEvent('[data-aw-toggle-color-scheme]', 'click', function () {
|
||||
if (defaultTheme.endsWith(':only')) {
|
||||
return;
|
||||
}
|
||||
document.documentElement.classList.toggle('dark');
|
||||
localStorage.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
attachEvent('[data-aw-social-share]', 'click', function (_, elem) {
|
||||
const network = elem.getAttribute('data-aw-social-share');
|
||||
const url = encodeURIComponent(elem.getAttribute('data-aw-url'));
|
||||
const text = encodeURIComponent(elem.getAttribute('data-aw-text'));
|
||||
|
||||
let href;
|
||||
switch (network) {
|
||||
case 'facebook':
|
||||
href = `https://www.facebook.com/sharer.php?u=${url}`;
|
||||
break;
|
||||
case 'twitter':
|
||||
href = `https://twitter.com/intent/tweet?url=${url}&text=${text}`;
|
||||
break;
|
||||
case 'linkedin':
|
||||
href = `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${text}`;
|
||||
break;
|
||||
case 'whatsapp':
|
||||
href = `https://wa.me/?text=${text}%20${url}`;
|
||||
break;
|
||||
case 'mail':
|
||||
href = `mailto:?subject=%22${text}%22&body=${text}%20${url}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const newlink = document.createElement('a');
|
||||
newlink.target = '_blank';
|
||||
newlink.href = href;
|
||||
newlink.click();
|
||||
});
|
||||
|
||||
const screenSize = window.matchMedia('(max-width: 767px)');
|
||||
screenSize.addEventListener('change', function () {
|
||||
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
document.getElementById('header')?.classList.remove('h-screen');
|
||||
document.getElementById('header')?.classList.remove('expanded');
|
||||
document.getElementById('header')?.classList.remove('bg-page');
|
||||
document.querySelector('#header nav')?.classList.add('hidden');
|
||||
document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
|
||||
});
|
||||
|
||||
function applyHeaderStylesOnScroll() {
|
||||
const header = document.querySelector('#header[data-aw-sticky-header]');
|
||||
if (!header) return;
|
||||
if (lastKnownScrollPosition > 60 && !header.classList.contains('scroll')) {
|
||||
header.classList.add('scroll');
|
||||
} else if (lastKnownScrollPosition <= 60 && header.classList.contains('scroll')) {
|
||||
header.classList.remove('scroll');
|
||||
}
|
||||
ticking = false;
|
||||
}
|
||||
applyHeaderStylesOnScroll();
|
||||
|
||||
attachEvent([document], 'scroll', function () {
|
||||
lastKnownScrollPosition = window.scrollY;
|
||||
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
applyHeaderStylesOnScroll();
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
const onPageShow = function () {
|
||||
document.documentElement.classList.add('motion-safe:scroll-smooth');
|
||||
const elem = document.querySelector('[data-aw-toggle-menu]');
|
||||
if (elem) {
|
||||
elem.classList.remove('expanded');
|
||||
}
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
document.getElementById('header')?.classList.remove('h-screen');
|
||||
document.getElementById('header')?.classList.remove('expanded');
|
||||
document.querySelector('#header nav')?.classList.add('hidden');
|
||||
};
|
||||
|
||||
window.onload = onLoad;
|
||||
window.onpageshow = onPageShow;
|
||||
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
initTheme();
|
||||
onLoad();
|
||||
onPageShow();
|
||||
});
|
||||
</script>
|
8
front/src/components/common/CommonMeta.astro
Normal file
8
front/src/components/common/CommonMeta.astro
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
import { getAsset } from 'utils/permalinks';
|
||||
---
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<link rel="sitemap" href={getAsset('/sitemap-index.xml')} />
|
61
front/src/components/common/Image.astro
Normal file
61
front/src/components/common/Image.astro
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
import { findImage } from 'utils/images';
|
||||
import {
|
||||
getImagesOptimized,
|
||||
astroAsseetsOptimizer,
|
||||
unpicOptimizer,
|
||||
isUnpicCompatible,
|
||||
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 (
|
||||
typeof _image === 'string' &&
|
||||
(_image.startsWith('http://') || _image.startsWith('https://')) &&
|
||||
isUnpicCompatible(_image)
|
||||
) {
|
||||
image = await getImagesOptimized(_image, props, unpicOptimizer);
|
||||
} else if (_image) {
|
||||
image = await getImagesOptimized(_image, props, astroAsseetsOptimizer);
|
||||
}
|
||||
---
|
||||
|
||||
{
|
||||
!image ? (
|
||||
<Fragment />
|
||||
) : (
|
||||
<img src={image.src} crossorigin="anonymous" referrerpolicy="no-referrer" {...image.attributes} />
|
||||
)
|
||||
}
|
68
front/src/components/common/Metadata.astro
Normal file
68
front/src/components/common/Metadata.astro
Normal file
@ -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 'astrowind: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,
|
||||
}
|
||||
);
|
||||
---
|
||||
|
||||
<AstroSeo {...{ ...seoProps, openGraph: await adaptOpenGraphImages(seoProps?.openGraph, Astro.site) }} />
|
5
front/src/components/common/SiteVerification.astro
Normal file
5
front/src/components/common/SiteVerification.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import { SITE } from 'astrowind:config';
|
||||
---
|
||||
|
||||
{SITE.googleSiteVerificationId && <meta name="google-site-verification" content={SITE.googleSiteVerificationId} />}
|
65
front/src/components/common/SocialShare.astro
Normal file
65
front/src/components/common/SocialShare.astro
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
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;
|
||||
---
|
||||
|
||||
<div class={className}>
|
||||
<span class="align-super font-bold text-slate-500 dark:text-slate-400">Share:</span>
|
||||
<button
|
||||
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||
title="Twitter Share"
|
||||
data-aw-social-share="twitter"
|
||||
data-aw-url={url}
|
||||
data-aw-text={text}
|
||||
><Icon
|
||||
name="tabler:brand-x"
|
||||
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||
/>
|
||||
</button>
|
||||
<button class="ml-2 rtl:ml-0 rtl:mr-2" title="Facebook Share" data-aw-social-share="facebook" data-aw-url={url}
|
||||
><Icon
|
||||
name="tabler:brand-facebook"
|
||||
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||
title="Linkedin Share"
|
||||
data-aw-social-share="linkedin"
|
||||
data-aw-url={url}
|
||||
data-aw-text={text}
|
||||
><Icon
|
||||
name="tabler:brand-linkedin"
|
||||
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||
title="Whatsapp Share"
|
||||
data-aw-social-share="whatsapp"
|
||||
data-aw-url={url}
|
||||
data-aw-text={text}
|
||||
><Icon
|
||||
name="tabler:brand-whatsapp"
|
||||
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||
title="Email Share"
|
||||
data-aw-social-share="mail"
|
||||
data-aw-url={url}
|
||||
data-aw-text={text}
|
||||
><Icon
|
||||
name="tabler:mail"
|
||||
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
6
front/src/components/common/SplitbeeAnalytics.astro
Normal file
6
front/src/components/common/SplitbeeAnalytics.astro
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
const { doNotTrack = true, noCookieMode = false, url = 'https://cdn.splitbee.io/sb.js' } = Astro.props;
|
||||
---
|
||||
|
||||
<!-- Splitbee Analytics -->
|
||||
<script is:inline data-respect-dnt={doNotTrack} data-no-cookie={noCookieMode} async src={url}></script>
|
29
front/src/components/common/ToggleMenu.astro
Normal file
29
front/src/components/common/ToggleMenu.astro
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
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;
|
||||
---
|
||||
|
||||
<button type="button" class={className} aria-label={label} data-aw-toggle-menu>
|
||||
<span class="sr-only">{label}</span>
|
||||
<slot>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:rotate-45 group-[.expanded]:translate-y-2.5"
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:opacity-0"
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:-rotate-45 group-[.expanded]:-translate-y-2.5"
|
||||
></span>
|
||||
</slot>
|
||||
</button>
|
28
front/src/components/common/ToggleTheme.astro
Normal file
28
front/src/components/common/ToggleTheme.astro
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
import { UI } from 'astrowind: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')) && (
|
||||
<button type="button" class={className} aria-label={label} data-aw-toggle-color-scheme>
|
||||
<Icon name={iconName} class={iconClass} />
|
||||
</button>
|
||||
)
|
||||
}
|
11
front/src/components/ui/Background.astro
Normal file
11
front/src/components/ui/Background.astro
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
export interface Props {
|
||||
isDark?: boolean;
|
||||
}
|
||||
|
||||
const { isDark = false } = Astro.props;
|
||||
---
|
||||
|
||||
<div class:list={['absolute inset-0', { 'bg-dark dark:bg-transparent': isDark }]}>
|
||||
<slot />
|
||||
</div>
|
40
front/src/components/ui/Button.astro
Normal file
40
front/src/components/ui/Button.astro
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { CallToAction as Props } from 'types';
|
||||
|
||||
const {
|
||||
variant = 'secondary',
|
||||
target,
|
||||
text = Astro.slots.render('default'),
|
||||
icon = '',
|
||||
class: className = '',
|
||||
type,
|
||||
...rest
|
||||
} = Astro.props;
|
||||
|
||||
const variants = {
|
||||
primary: 'btn-primary',
|
||||
secondary: 'btn-secondary',
|
||||
tertiary: 'btn btn-tertiary',
|
||||
link: 'cursor-pointer hover:text-primary',
|
||||
};
|
||||
---
|
||||
|
||||
{
|
||||
type === 'button' || type === 'submit' || type === 'reset' ? (
|
||||
<button type={type} class={twMerge(variants[variant] || '', className)} {...rest}>
|
||||
<Fragment set:html={text} />
|
||||
{icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
class={twMerge(variants[variant] || '', className)}
|
||||
{...(target ? { target: target, rel: 'noopener noreferrer' } : {})}
|
||||
{...rest}
|
||||
>
|
||||
<Fragment set:html={text} />
|
||||
{icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
|
||||
</a>
|
||||
)
|
||||
}
|
22
front/src/components/ui/DListItem.astro
Normal file
22
front/src/components/ui/DListItem.astro
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
// 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');
|
||||
---
|
||||
|
||||
<h6 set:html={dt} />
|
||||
<div class="dd ml-8" set:html={content} />
|
87
front/src/components/ui/Form.astro
Normal file
87
front/src/components/ui/Form.astro
Normal file
@ -0,0 +1,87 @@
|
||||
---
|
||||
import type { Form as Props } from 'types';
|
||||
import Button from 'components/ui/Button.astro';
|
||||
|
||||
const { inputs, textarea, disclaimer, button = 'Contact us', description = '', id, method, enctype } = Astro.props;
|
||||
---
|
||||
|
||||
<form id={id} method={method} enctype={enctype}>
|
||||
{
|
||||
inputs &&
|
||||
inputs.map(
|
||||
({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '' }) =>
|
||||
name && (
|
||||
<div class="mb-6">
|
||||
{label && (
|
||||
<label for={name} class="block text-sm font-medium">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
type={type}
|
||||
name={name}
|
||||
id={name}
|
||||
autocomplete={autocomplete}
|
||||
placeholder={placeholder}
|
||||
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
textarea && (
|
||||
<div>
|
||||
<label for="textarea" class="block text-sm font-medium">
|
||||
{textarea.label}
|
||||
</label>
|
||||
<textarea
|
||||
id="textarea"
|
||||
name={textarea.name ? textarea.name : 'message'}
|
||||
rows={textarea.rows ? textarea.rows : 4}
|
||||
placeholder={textarea.placeholder}
|
||||
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
disclaimer && (
|
||||
<div class="mt-3 flex items-start">
|
||||
<div class="flex mt-0.5">
|
||||
<input
|
||||
id="disclaimer"
|
||||
name="disclaimer"
|
||||
type="checkbox"
|
||||
class="cursor-pointer mt-1 py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label for="disclaimer" class="cursor-pointer select-none text-sm text-gray-600 dark:text-gray-400">
|
||||
{disclaimer.label}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
button && (
|
||||
<div class="mt-10 grid">
|
||||
<Button variant="primary" type="submit">
|
||||
{button}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
description && (
|
||||
<div class="mt-3 text-center">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</form>
|
35
front/src/components/ui/Headline.astro
Normal file
35
front/src/components/ui/Headline.astro
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
import type { Headline as Props } from 'types';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const {
|
||||
title = await Astro.slots.render('title'),
|
||||
subtitle = await Astro.slots.render('subtitle'),
|
||||
tagline,
|
||||
classes = {},
|
||||
} = Astro.props;
|
||||
|
||||
const {
|
||||
container: containerClass = 'max-w-3xl',
|
||||
title: titleClass = 'text-3xl md:text-4xl ',
|
||||
subtitle: subtitleClass = 'text-xl',
|
||||
} = classes;
|
||||
---
|
||||
|
||||
{
|
||||
(title || subtitle || tagline) && (
|
||||
<div class={twMerge('mb-8 md:mx-auto md:mb-12 text-center', containerClass)}>
|
||||
{tagline && (
|
||||
<p class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase" set:html={tagline} />
|
||||
)}
|
||||
{title && (
|
||||
<h2
|
||||
class={twMerge('font-bold leading-tighter tracking-tighter font-heading text-heading text-3xl', titleClass)}
|
||||
set:html={title}
|
||||
/>
|
||||
)}
|
||||
|
||||
{subtitle && <p class={twMerge('mt-4 text-muted', subtitleClass)} set:html={subtitle} />}
|
||||
</div>
|
||||
)
|
||||
}
|
65
front/src/components/ui/ItemGrid.astro
Normal file
65
front/src/components/ui/ItemGrid.astro
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
import type { ItemGrid as Props } from 'types';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import Button from './Button.astro';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
|
||||
|
||||
const {
|
||||
container: containerClass = '',
|
||||
panel: panelClass = '',
|
||||
title: titleClass = '',
|
||||
description: descriptionClass = '',
|
||||
icon: defaultIconClass = 'text-primary',
|
||||
action: actionClass = '',
|
||||
} = classes;
|
||||
---
|
||||
|
||||
{
|
||||
items && (
|
||||
<div
|
||||
class={twMerge(
|
||||
`grid mx-auto gap-8 md:gap-y-12 ${
|
||||
columns === 4
|
||||
? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
|
||||
: columns === 3
|
||||
? 'lg:grid-cols-3 sm:grid-cols-2'
|
||||
: columns === 2
|
||||
? 'sm:grid-cols-2 '
|
||||
: ''
|
||||
}`,
|
||||
containerClass
|
||||
)}
|
||||
>
|
||||
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||
<div>
|
||||
<div class={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
|
||||
<div class="flex justify-center">
|
||||
{(icon || defaultIcon) && (
|
||||
<Icon
|
||||
name={icon || defaultIcon}
|
||||
class={twMerge('w-7 h-7 mr-2 rtl:mr-0 rtl:ml-2', defaultIconClass, itemClasses?.icon)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div class="mt-0.5">
|
||||
{title && <h3 class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</h3>}
|
||||
{description && (
|
||||
<p
|
||||
class={twMerge(`${title ? 'mt-3' : ''} text-muted`, descriptionClass, itemClasses?.description)}
|
||||
set:html={description}
|
||||
/>
|
||||
)}
|
||||
{callToAction && (
|
||||
<div class={twMerge(`${title || description ? 'mt-3' : ''}`, actionClass, itemClasses?.actionClass)}>
|
||||
<Button variant="link" {...callToAction} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
53
front/src/components/ui/ItemGrid2.astro
Normal file
53
front/src/components/ui/ItemGrid2.astro
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
import type { ItemGrid as Props } from 'types';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import Button from './Button.astro';
|
||||
|
||||
const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
|
||||
|
||||
const {
|
||||
container: containerClass = '',
|
||||
// container: containerClass = "sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
||||
panel: panelClass = '',
|
||||
title: titleClass = '',
|
||||
description: descriptionClass = '',
|
||||
icon: defaultIconClass = 'text-primary',
|
||||
} = classes;
|
||||
---
|
||||
|
||||
{
|
||||
items && (
|
||||
<div
|
||||
class={twMerge(
|
||||
`grid gap-8 gap-x-12 sm:gap-y-8 ${
|
||||
columns === 4
|
||||
? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
|
||||
: columns === 3
|
||||
? 'lg:grid-cols-3 sm:grid-cols-2'
|
||||
: columns === 2
|
||||
? 'sm:grid-cols-2 '
|
||||
: ''
|
||||
}`,
|
||||
containerClass
|
||||
)}
|
||||
>
|
||||
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||
<div class={twMerge('relative flex flex-col', panelClass, itemClasses?.panel)}>
|
||||
{(icon || defaultIcon) && (
|
||||
<Icon name={icon || defaultIcon} class={twMerge('mb-2 w-10 h-10', defaultIconClass, itemClasses?.icon)} />
|
||||
)}
|
||||
<div class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</div>
|
||||
{description && (
|
||||
<p class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)} set:html={description} />
|
||||
)}
|
||||
{callToAction && (
|
||||
<div class="mt-2">
|
||||
<Button {...callToAction} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
54
front/src/components/ui/Timeline.astro
Normal file
54
front/src/components/ui/Timeline.astro
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { Item } from 'types';
|
||||
|
||||
export interface Props {
|
||||
items?: Array<Item>;
|
||||
defaultIcon?: string;
|
||||
classes?: Record<string, string>;
|
||||
}
|
||||
|
||||
const { items = [], classes = {}, defaultIcon } = Astro.props as Props;
|
||||
|
||||
const {
|
||||
container: containerClass = '',
|
||||
panel: panelClass = '',
|
||||
title: titleClass = '',
|
||||
description: descriptionClass = '',
|
||||
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
|
||||
} = classes;
|
||||
---
|
||||
|
||||
{
|
||||
items && items.length && (
|
||||
<div class={containerClass}>
|
||||
{items.map(({ title, description, icon, classes: itemClasses = {} }, index = 0) => (
|
||||
<div class={twMerge('flex', panelClass, itemClasses?.panel)}>
|
||||
<div class="flex flex-col items-center mr-4 rtl:mr-0 rtl:ml-4">
|
||||
<div>
|
||||
<div class="flex items-center justify-center">
|
||||
{(icon || defaultIcon) && (
|
||||
<Icon
|
||||
name={icon || defaultIcon}
|
||||
class={twMerge('w-10 h-10 p-2 rounded-full border-2', defaultIconClass, itemClasses?.icon)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{index !== items.length - 1 && <div class="w-px h-full bg-black/10 dark:bg-slate-400/50" />}
|
||||
</div>
|
||||
<div class={`pt-1 ${index !== items.length - 1 ? 'pb-8' : ''}`}>
|
||||
{title && <p class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)} set:html={title} />}
|
||||
{description && (
|
||||
<p
|
||||
class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)}
|
||||
set:html={description}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
31
front/src/components/ui/WidgetWrapper.astro
Normal file
31
front/src/components/ui/WidgetWrapper.astro
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
import type { HTMLTag } from 'astro/types';
|
||||
import type { Widget } from 'types';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import Background from './Background.astro';
|
||||
|
||||
export interface Props extends Widget {
|
||||
containerClass?: string;
|
||||
['as']?: HTMLTag;
|
||||
}
|
||||
|
||||
const { id, isDark = false, containerClass = '', bg, as = 'section' } = Astro.props;
|
||||
|
||||
const WrapperTag = as;
|
||||
---
|
||||
|
||||
<WrapperTag class="relative not-prose scroll-mt-[72px]" {...id ? { id } : {}}>
|
||||
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
|
||||
<slot name="bg">
|
||||
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} />}
|
||||
</slot>
|
||||
</div>
|
||||
<div
|
||||
class:list={[
|
||||
twMerge('relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20 text-default', containerClass),
|
||||
{ dark: isDark },
|
||||
]}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</WrapperTag>
|
23
front/src/components/widgets/Announcement.astro
Normal file
23
front/src/components/widgets/Announcement.astro
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<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="https://astro.build/blog/astro-480/" class="text-muted hover:underline dark:text-slate-400 font-medium"
|
||||
>Astro 4.8 is now available! »</a
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="ltr:ml-auto rtl:mr-auto w-[5.6rem] h-[1.25rem] ml-auto bg-contain inline-block bg-[url(https://img.shields.io/github/stars/onwidget/astrowind.svg?style=social&label=Stars&maxAge=86400)]"
|
||||
title="If you like AstroWind, give us a star."
|
||||
href="https://github.com/onwidget/astrowind"
|
||||
>
|
||||
</a>
|
||||
</div>
|
64
front/src/components/widgets/BlogHighlightedPosts.astro
Normal file
64
front/src/components/widgets/BlogHighlightedPosts.astro
Normal file
@ -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 />
|
||||
)
|
||||
}
|
63
front/src/components/widgets/BlogLatestPosts.astro
Normal file
63
front/src/components/widgets/BlogLatestPosts.astro
Normal file
@ -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 '../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 />
|
||||
)
|
||||
}
|
38
front/src/components/widgets/Brands.astro
Normal file
38
front/src/components/widgets/Brands.astro
Normal file
@ -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>
|
58
front/src/components/widgets/CallToAction.astro
Normal file
58
front/src/components/widgets/CallToAction.astro
Normal file
@ -0,0 +1,58 @@
|
||||
---
|
||||
import WidgetWrapper from '../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:list={["w-full", "sm:mb-0", action.class]} />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<Fragment set:html={actions} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</WidgetWrapper>
|
46
front/src/components/widgets/Contact.astro
Normal file
46
front/src/components/widgets/Contact.astro
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
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,
|
||||
formid,
|
||||
method,
|
||||
enctype,
|
||||
|
||||
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}
|
||||
id={formid}
|
||||
method={method}
|
||||
enctype={enctype}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</WidgetWrapper>
|
94
front/src/components/widgets/Content.astro
Normal file
94
front/src/components/widgets/Content.astro
Normal file
@ -0,0 +1,94 @@
|
||||
---
|
||||
import type { Content as Props } from 'types';
|
||||
import Headline from '../ui/Headline.astro';
|
||||
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||
import Image from 'components/common/Image.astro';
|
||||
import Button from 'components/ui/Button.astro';
|
||||
import ItemGrid from '../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>
|
33
front/src/components/widgets/FAQs.astro
Normal file
33
front/src/components/widgets/FAQs.astro
Normal file
@ -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>
|
36
front/src/components/widgets/Features.astro
Normal file
36
front/src/components/widgets/Features.astro
Normal file
@ -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>
|
38
front/src/components/widgets/Features2.astro
Normal file
38
front/src/components/widgets/Features2.astro
Normal file
@ -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>
|
70
front/src/components/widgets/Features3.astro
Normal file
70
front/src/components/widgets/Features3.astro
Normal file
@ -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>
|
102
front/src/components/widgets/Footer.astro
Normal file
102
front/src/components/widgets/Footer.astro
Normal file
@ -0,0 +1,102 @@
|
||||
---
|
||||
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, mt-auto']}>
|
||||
<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">
|
||||
<div class="grid grid-cols-12 gap-4 gap-y-8 sm:gap-8 py-8 md:py-12">
|
||||
<div class="col-span-12 lg:col-span-4">
|
||||
<div class="mb-2">
|
||||
<a class="inline-block font-bold text-xl" href={getHomePermalink()}>{SITE?.name}</a>
|
||||
</div>
|
||||
<div class="text-sm text-muted flex gap-1">
|
||||
{
|
||||
secondaryLinks.map(({ text, href }, index) => (
|
||||
<>
|
||||
{index !== 0 ? ' · ' : ''}
|
||||
<a
|
||||
class="text-muted hover:text-gray-700 dark:text-gray-400 hover:underline transition duration-150 ease-in-out"
|
||||
href={href}
|
||||
set:html={text}
|
||||
/>
|
||||
</>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
links.map(({ title, links }) => (
|
||||
<div class="col-span-6 md:col-span-3 lg:col-span-2">
|
||||
<div class="dark:text-gray-300 font-medium mb-2">{title}</div>
|
||||
{links && Array.isArray(links) && links.length > 0 && (
|
||||
<ul class="text-sm">
|
||||
{links.map(({ text, href, ariaLabel }) => (
|
||||
<li class="mb-2">
|
||||
<a
|
||||
class="text-muted hover:text-gray-700 hover:underline dark:text-gray-400 transition duration-150 ease-in-out"
|
||||
href={href}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<Fragment set:html={text} />
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<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>
|
173
front/src/components/widgets/Header.astro
Normal file
173
front/src/components/widgets/Header.astro
Normal file
@ -0,0 +1,173 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import Logo from 'components/Logo.astro'
|
||||
import ToggleTheme from 'components/common/ToggleTheme.astro'
|
||||
import ToggleMenu from 'components/common/ToggleMenu.astro'
|
||||
import Button from 'components/ui/Button.astro'
|
||||
|
||||
import { getHomePermalink } from 'utils/permalinks'
|
||||
import { trimSlash, getAsset } from 'utils/permalinks'
|
||||
import type { CallToAction } from 'types'
|
||||
|
||||
const pb = Astro.locals.pb
|
||||
|
||||
const connected = pb.authStore.isValid
|
||||
interface Link {
|
||||
text?: string
|
||||
href?: string
|
||||
ariaLabel?: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
interface ActionLink extends CallToAction {}
|
||||
|
||||
interface MenuLink extends Link {
|
||||
links?: Array<MenuLink>
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
id?: string
|
||||
links?: Array<MenuLink>
|
||||
actions?: Array<ActionLink>
|
||||
isSticky?: boolean
|
||||
isDark?: boolean
|
||||
isFullWidth?: boolean
|
||||
showToggleTheme?: boolean
|
||||
showRssFeed?: boolean
|
||||
position?: string
|
||||
}
|
||||
|
||||
const {
|
||||
id = 'header',
|
||||
links = [],
|
||||
actions = [],
|
||||
isSticky = false,
|
||||
isDark = false,
|
||||
isFullWidth = false,
|
||||
showToggleTheme = false,
|
||||
showRssFeed = false,
|
||||
position = 'center',
|
||||
} = Astro.props
|
||||
|
||||
const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`
|
||||
---
|
||||
|
||||
<header
|
||||
class:list={[
|
||||
{ sticky: isSticky, relative: !isSticky, dark: isDark },
|
||||
'top-0 z-40 flex-none mx-auto w-full border-b border-gray-50/0 transition-[opacity] ease-in-out',
|
||||
]}
|
||||
{...isSticky ? { 'data-aw-sticky-header': true } : {}}
|
||||
{...id ? { id } : {}}
|
||||
>
|
||||
<div class="absolute inset-0"></div>
|
||||
<div
|
||||
class:list={[
|
||||
'relative text-default py-3 px-3 md:px-6 mx-auto w-full',
|
||||
{
|
||||
'md:flex md:justify-between': position !== 'center',
|
||||
},
|
||||
{
|
||||
'md:grid md:grid-cols-3 md:items-center': position === 'center',
|
||||
},
|
||||
{
|
||||
'max-w-7xl': !isFullWidth,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div class:list={[{ 'mr-auto rtl:mr-0 rtl:ml-auto': position === 'right' }, 'flex justify-between']}>
|
||||
<a class="flex items-center" href={getHomePermalink()}>
|
||||
<Logo />
|
||||
</a>
|
||||
<div class="flex items-center md:hidden">
|
||||
<ToggleMenu />
|
||||
</div>
|
||||
</div>
|
||||
<nav
|
||||
class="items-center w-full md:w-auto hidden md:flex md:mx-5 text-default overflow-y-auto overflow-x-hidden md:overflow-y-visible md:overflow-x-auto md:justify-self-center"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<ul
|
||||
class="flex flex-col md:flex-row md:self-center w-full md:w-auto text-xl md:text-[0.9375rem] tracking-[0.01rem] font-medium md:justify-center"
|
||||
>
|
||||
{
|
||||
links.map(({ text, href, links }) => (
|
||||
<li class={links?.length ? 'dropdown' : ''}>
|
||||
{links?.length ? (
|
||||
<>
|
||||
<button type="button" class="hover:text-link dark:hover:text-white px-4 py-3 flex items-center">
|
||||
{text}{' '}
|
||||
<Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden md:inline" />
|
||||
</button>
|
||||
<ul class="dropdown-menu md:backdrop-blur-md dark:md:bg-dark rounded md:absolute pl-4 md:pl-0 md:hidden font-medium md:bg-white/90 md:min-w-[200px] drop-shadow-xl">
|
||||
{links.map(({ text: text2, href: href2 }) => (
|
||||
<li>
|
||||
<a
|
||||
class:list={[
|
||||
'first:rounded-t last:rounded-b md:hover:bg-gray-100 hover:text-link dark:hover:text-white dark:hover:bg-gray-700 py-2 px-5 block whitespace-no-wrap',
|
||||
{ 'aw-link-active': href2 === currentPath },
|
||||
]}
|
||||
href={href2}
|
||||
>
|
||||
{text2}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<a
|
||||
class:list={[
|
||||
'hover:text-link dark:hover:text-white px-4 py-3 flex items-center',
|
||||
{ 'aw-link-active': href === currentPath },
|
||||
]}
|
||||
href={href}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<div
|
||||
class:list={[
|
||||
{ 'ml-auto rtl:ml-0 rtl:mr-auto': position === 'left' },
|
||||
'hidden md:self-center md:flex items-center md:mb-0 fixed w-full md:w-auto md:static justify-end left-0 rtl:left-auto rtl:right-0 bottom-0 p-3 md:p-0 md:justify-self-end',
|
||||
]}
|
||||
>
|
||||
<div class="items-center flex justify-between w-full md:w-auto">
|
||||
<div class="flex">
|
||||
{showToggleTheme && <ToggleTheme iconClass="w-6 h-6 md:w-5 md:h-5 md:inline-block" />}
|
||||
{
|
||||
showRssFeed && (
|
||||
<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="RSS Feed"
|
||||
href={getAsset('/rss.xml')}
|
||||
>
|
||||
<Icon name="tabler:rss" class="w-5 h-5" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{ !connected && (
|
||||
<span class="ml-4 rtl:ml-0 rtl:mr-4">
|
||||
<Button href='/account/login' variant='primary' class="ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto">
|
||||
Connexion
|
||||
</Button>
|
||||
<Button href='/account/register' variant='secondary' class="ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto">
|
||||
Inscription
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
{ connected && (
|
||||
<Button href='/account/logout' variant='primary' class="ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto">
|
||||
Déconnexion
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
92
front/src/components/widgets/Hero.astro
Normal file
92
front/src/components/widgets/Hero.astro
Normal file
@ -0,0 +1,92 @@
|
||||
---
|
||||
import Image from 'components/common/Image.astro';
|
||||
import Button from 'components/ui/Button.astro';
|
||||
import type { CallToAction } from 'types';
|
||||
|
||||
export interface Props {
|
||||
id?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
tagline?: string;
|
||||
content?: string;
|
||||
actions?: string | CallToAction[];
|
||||
image?: string | unknown; // TODO: find HTMLElementProps
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
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'),
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
|
||||
<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">
|
||||
<div class="text-center pb-10 md:pb-16 max-w-5xl mx-auto">
|
||||
{
|
||||
tagline && (
|
||||
<p
|
||||
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||
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"
|
||||
set:html={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" 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">
|
||||
{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>
|
||||
{
|
||||
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>
|
91
front/src/components/widgets/Hero2.astro
Normal file
91
front/src/components/widgets/Hero2.astro
Normal file
@ -0,0 +1,91 @@
|
||||
---
|
||||
import Image from 'components/common/Image.astro';
|
||||
import type { CallToAction } from 'types';
|
||||
import Button from 'components/ui/Button.astro';
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
tagline?: string;
|
||||
content?: string;
|
||||
actions?: string | CallToAction[];
|
||||
image?: string | unknown; // TODO: find HTMLElementProps
|
||||
}
|
||||
|
||||
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'),
|
||||
} = 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 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-blue-200 font-bold tracking-wide uppercase"
|
||||
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"
|
||||
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" 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">
|
||||
{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">
|
||||
{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={600}
|
||||
height={600}
|
||||
{...image}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
77
front/src/components/widgets/HeroText.astro
Normal file
77
front/src/components/widgets/HeroText.astro
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
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-blue-200 font-bold tracking-wide uppercase"
|
||||
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"
|
||||
set:html={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" 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">
|
||||
{
|
||||
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>
|
11
front/src/components/widgets/Note.astro
Normal file
11
front/src/components/widgets/Note.astro
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
---
|
||||
|
||||
<section class="bg-blue-50 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">
|
||||
<span class="font-bold">
|
||||
<Icon name="tabler:info-square" class="w-5 h-5 inline-block align-text-bottom" /> Philosophy:</span
|
||||
> Simplicity, Best Practices and High Performance
|
||||
</div>
|
||||
</section>
|
83
front/src/components/widgets/Pricing.astro
Normal file
83
front/src/components/widgets/Pricing.astro
Normal file
@ -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">
|
||||
{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>
|
46
front/src/components/widgets/Stats.astro
Normal file
46
front/src/components/widgets/Stats.astro
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
import type { Stats as Props } from 'types';
|
||||
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||
import Headline from '../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">
|
||||
{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>
|
59
front/src/components/widgets/Steps.astro
Normal file
59
front/src/components/widgets/Steps.astro
Normal file
@ -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>
|
72
front/src/components/widgets/Steps2.astro
Normal file
72
front/src/components/widgets/Steps2.astro
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
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;
|
||||
---
|
||||
|
||||
<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-blue-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} />
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
: ''
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</WidgetWrapper>
|
75
front/src/components/widgets/Testimonials.astro
Normal file
75
front/src/components/widgets/Testimonials.astro
Normal file
@ -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">
|
||||
<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>
|
Reference in New Issue
Block a user