add template in hard
Some checks failed
Build Docker Image Front / run (pull_request) Failing after 52s
Build Docker Image Back / run (pull_request) Successful in 25s
JsDocs / coverage (pull_request) Successful in 22s
Test and coverage / coverage (pull_request) Successful in 1m26s

This commit is contained in:
2024-05-20 12:23:41 +02:00
parent 738381ee16
commit 99196c6dba
128 changed files with 15138 additions and 4873 deletions

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

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

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

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

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

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

View File

@ -0,0 +1,5 @@
---
import { SITE } from 'astrowind:config';
---
{SITE.googleSiteVerificationId && <meta name="google-site-verification" content={SITE.googleSiteVerificationId} />}

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

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

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

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