feat: astro template for front and fix login(#13)
All checks were successful
Build Docker Image Front / run (push) Successful in 25s
Build Docker Image Back / run (push) Successful in 25s

Reviewed-on: #13
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
This commit is contained in:
2024-05-21 10:07:56 +02:00
committed by Clement
parent 57a57c63ff
commit 1593fa3493
145 changed files with 14966 additions and 2182 deletions

View 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>

View 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 />
)
}

View 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 />
)
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>