change le favicon
This commit is contained in:
parent
47eef4ca6f
commit
5d67ab803a
7 changed files with 9 additions and 815 deletions
|
|
@ -1,8 +1,7 @@
|
||||||
---
|
---
|
||||||
import { getImage } from "astro:assets";
|
import { getImage } from "astro:assets";
|
||||||
import { OG, SEO, SITE } from "@data/constants";
|
import { OG, SEO, SITE } from "@data/constants";
|
||||||
import faviconSvgSrc from "@images/icon.svg";
|
import faviconSrc from "@images/favicon.png";
|
||||||
import faviconSrc from "@images/icon.png";
|
|
||||||
|
|
||||||
// Default properties for the Meta component. These values are used if props are not provided.
|
// Default properties for the Meta component. These values are used if props are not provided.
|
||||||
// 'meta' sets a default description meta tag to describe the page content.
|
// 'meta' sets a default description meta tag to describe the page content.
|
||||||
|
|
@ -60,11 +59,6 @@ const alternateLanguageLinks: string = Object.entries(languages)
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
// Generate and optimize the favicon images
|
|
||||||
const faviconSvg = await getImage({
|
|
||||||
src: faviconSvgSrc,
|
|
||||||
format: "svg",
|
|
||||||
});
|
|
||||||
|
|
||||||
const appleTouchIcon = await getImage({
|
const appleTouchIcon = await getImage({
|
||||||
src: faviconSrc,
|
src: faviconSrc,
|
||||||
|
|
@ -122,7 +116,6 @@ const appleTouchIcon = await getImage({
|
||||||
|
|
||||||
<!-- Links for favicons -->
|
<!-- Links for favicons -->
|
||||||
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
|
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
|
||||||
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<link href={appleTouchIcon.src} rel="apple-touch-icon" />
|
<link href={appleTouchIcon.src} rel="apple-touch-icon" />
|
||||||
<link href={appleTouchIcon.src} rel="shortcut icon" />
|
<link href={appleTouchIcon.src} rel="shortcut icon" />
|
||||||
|
|
|
||||||
BIN
src/images/favicon.png
Normal file
BIN
src/images/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
|
|
@ -3,20 +3,16 @@ import sharp from "sharp";
|
||||||
import ico from "sharp-ico";
|
import ico from "sharp-ico";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
const faviconSrc = path.resolve("src/images/icon.png");
|
const faviconSrc = path.resolve("src/images/favicon.png");
|
||||||
|
|
||||||
export const GET: APIRoute = async () => {
|
export const GET: APIRoute = async () => {
|
||||||
|
|
||||||
// Resize the image to multiple sizes
|
// Resize the image to multiple sizes
|
||||||
const sizes = [16, 32];
|
const sizes = [16, 32];
|
||||||
|
|
||||||
const buffers = await Promise.all(
|
const buffers = await Promise.all(
|
||||||
sizes.map(async (size) => {
|
sizes.map(async (size) => {
|
||||||
return await sharp(faviconSrc)
|
return await sharp(faviconSrc).resize(size).toFormat("png").toBuffer();
|
||||||
.resize(size)
|
}),
|
||||||
.toFormat("png")
|
|
||||||
.toBuffer();
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert the image to an ICO file
|
// Convert the image to an ICO file
|
||||||
|
|
|
||||||
|
|
@ -1,268 +0,0 @@
|
||||||
---
|
|
||||||
// Import section components
|
|
||||||
import { SITE } from "@data/constants";
|
|
||||||
import MainLayout from "@/layouts/MainLayout.astro";
|
|
||||||
import { Image } from "astro:assets";
|
|
||||||
import { getCollection, render } from "astro:content";
|
|
||||||
|
|
||||||
// Use `getStaticPaths` to generate static routes for generated pages on build
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const insightPosts = await getCollection("insights", ({ id }) =>
|
|
||||||
id.startsWith("en/")
|
|
||||||
);
|
|
||||||
return insightPosts.map((post) => {
|
|
||||||
const idWithoutLang = post.id.replace(/^en\//, ""); // Remove the "fr/" prefix
|
|
||||||
return {
|
|
||||||
params: { id: idWithoutLang },
|
|
||||||
props: { post },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the props for this page that define a specific insight post
|
|
||||||
const { post } = Astro.props;
|
|
||||||
|
|
||||||
const { Content } = await render(post);
|
|
||||||
|
|
||||||
const pageTitle: string = `${post.data.title} | ${SITE.title}`;
|
|
||||||
---
|
|
||||||
|
|
||||||
<MainLayout title={pageTitle}>
|
|
||||||
<section class="py-6 sm:py-8 lg:py-12">
|
|
||||||
<div class="mx-auto max-w-screen-xl px-4 md:px-8">
|
|
||||||
<div class="grid gap-8 md:grid-cols-2 lg:gap-12">
|
|
||||||
<div>
|
|
||||||
<div class="h-64 overflow-hidden rounded-lg shadow-lg md:h-auto">
|
|
||||||
<Image
|
|
||||||
class="h-full w-full object-cover object-center"
|
|
||||||
src={post.data.cardImage}
|
|
||||||
alt={post.data.cardImageAlt}
|
|
||||||
draggable={"false"}
|
|
||||||
format={"avif"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="progress-mobile"
|
|
||||||
class="fixed left-0 top-0 h-2 w-full bg-gradient-to-r from-orange-400/30 to-orange-400 md:hidden"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div id="pin" class="mt-10 hidden space-y-4 md:block">
|
|
||||||
<div
|
|
||||||
class="h-px w-full overflow-hidden bg-neutral-300 dark:bg-neutral-700"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
id="progress"
|
|
||||||
class="h-px w-full bg-gradient-to-r from-orange-400/30 to-orange-400"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-pretty text-sm text-neutral-500">
|
|
||||||
Table of Contents:
|
|
||||||
</p>
|
|
||||||
<div id="toc" class="">
|
|
||||||
<ul
|
|
||||||
class="space-y-2 text-pretty text-base text-neutral-700 transition duration-300 dark:text-neutral-400"
|
|
||||||
>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:pt-8">
|
|
||||||
<h1
|
|
||||||
class="mb-4 text-balance text-center text-2xl font-bold text-neutral-800 dark:text-neutral-200 sm:text-3xl md:mb-6 md:text-left"
|
|
||||||
>
|
|
||||||
{post.data.title}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<article
|
|
||||||
class="text-pretty text-lg text-neutral-700 dark:text-neutral-300"
|
|
||||||
>
|
|
||||||
<Content />
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</MainLayout>
|
|
||||||
|
|
||||||
<style is:global>
|
|
||||||
:root {
|
|
||||||
--transition-cubic: cubic-bezier(0.165, 0.84, 0.44, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
article h2,
|
|
||||||
article h3,
|
|
||||||
article h4,
|
|
||||||
article h5,
|
|
||||||
article h6 {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
scroll-margin-top: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toc li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: all 300ms var(--transition-cubic);
|
|
||||||
}
|
|
||||||
|
|
||||||
#toc li.selected {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toc li svg {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
transition:
|
|
||||||
height 400ms var(--transition-cubic),
|
|
||||||
width 400ms var(--transition-cubic);
|
|
||||||
}
|
|
||||||
|
|
||||||
#toc li.selected svg {
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
margin-right: 0.3rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const onScroll = (): void => {
|
|
||||||
const article = document.querySelector("article");
|
|
||||||
if (!article) return;
|
|
||||||
|
|
||||||
const articleHeight = article.offsetHeight;
|
|
||||||
const articleOffsetTop = article.offsetTop;
|
|
||||||
|
|
||||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
||||||
|
|
||||||
if (articleHeight && articleOffsetTop && scrollTop) {
|
|
||||||
const progress =
|
|
||||||
((scrollTop - articleOffsetTop) /
|
|
||||||
(articleHeight - window.innerHeight)) *
|
|
||||||
100;
|
|
||||||
|
|
||||||
const progressBar = document.getElementById("progress");
|
|
||||||
const progressBarMobile = document.getElementById("progress-mobile");
|
|
||||||
|
|
||||||
if (progressBar && progressBarMobile) {
|
|
||||||
progressBar.style.width = `${progress}%`;
|
|
||||||
progressBarMobile.style.width = `${progress}%`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (event) => {
|
|
||||||
window.onscroll = onScroll;
|
|
||||||
|
|
||||||
// Set initial width of progress bar
|
|
||||||
const progressBar = document.getElementById("progress");
|
|
||||||
const progressBarMobile = document.getElementById("progress-mobile");
|
|
||||||
|
|
||||||
if (progressBar && progressBarMobile) {
|
|
||||||
progressBar.style.width = "0%";
|
|
||||||
progressBarMobile.style.width = "0%";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
import { gsap } from "gsap";
|
|
||||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
||||||
|
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
|
||||||
gsap.timeline({
|
|
||||||
scrollTrigger: {
|
|
||||||
scrub: 1,
|
|
||||||
pin: true,
|
|
||||||
trigger: "#pin",
|
|
||||||
start: "top 20%",
|
|
||||||
endTrigger: "footer",
|
|
||||||
end: "top bottom",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const SVG_HTML_STRING =
|
|
||||||
'<svg class="w-0 h-0 flex-none" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#fa5a15"><path stroke-linecap="round" stroke-linejoin="round" d="m12.75 15 3-3m0 0-3-3m3 3h-7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"></svg>';
|
|
||||||
|
|
||||||
function setActiveLinkById(id: string | null) {
|
|
||||||
const listItems = document.querySelectorAll("#toc li");
|
|
||||||
listItems.forEach((item) => item.classList.remove("selected"));
|
|
||||||
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
const activeLink = document.querySelector(`#toc a[href="#${id}"]`);
|
|
||||||
|
|
||||||
if (!activeLink) return;
|
|
||||||
|
|
||||||
const listItem = activeLink.parentElement;
|
|
||||||
listItem?.classList.add("selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
// The article element that contains the Markdown content
|
|
||||||
const article: HTMLElement | null = document.querySelector("article");
|
|
||||||
// The ToC container <ul> element
|
|
||||||
const tocList: HTMLElement | null = document.querySelector("#toc ul");
|
|
||||||
|
|
||||||
const headings: NodeListOf<HTMLElement> | [] = article
|
|
||||||
? article.querySelectorAll("h1, h2, h3, h4, h5, h6")
|
|
||||||
: [];
|
|
||||||
|
|
||||||
headings.forEach((heading, i) => {
|
|
||||||
if (heading instanceof HTMLElement) {
|
|
||||||
const listItem = document.createElement("li");
|
|
||||||
listItem.className = "toc-level-" + heading.tagName.toLowerCase();
|
|
||||||
|
|
||||||
const tempDiv = document.createElement("div");
|
|
||||||
tempDiv.innerHTML = SVG_HTML_STRING;
|
|
||||||
|
|
||||||
const svg = tempDiv.firstChild;
|
|
||||||
listItem.appendChild(svg as Node);
|
|
||||||
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = "#" + heading.id;
|
|
||||||
link.textContent = heading.textContent;
|
|
||||||
listItem.appendChild(link);
|
|
||||||
|
|
||||||
tocList?.appendChild(listItem);
|
|
||||||
|
|
||||||
gsap.timeline({
|
|
||||||
scrollTrigger: {
|
|
||||||
trigger: heading,
|
|
||||||
start: "top 20%",
|
|
||||||
end: () =>
|
|
||||||
`bottom top+=${i === headings.length - 1 ? 0 : (headings[i + 1] as HTMLElement).getBoundingClientRect().height}`,
|
|
||||||
onEnter: () => setActiveLinkById(heading.id),
|
|
||||||
onLeaveBack: () =>
|
|
||||||
setActiveLinkById((headings[i - 1] as HTMLElement)?.id),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type { APIRoute, ImageMetadata } from "astro";
|
import type { APIRoute, ImageMetadata } from "astro";
|
||||||
import { getImage } from "astro:assets";
|
import { getImage } from "astro:assets";
|
||||||
import icon from "@images/icon.png";
|
import icon from "@images/favicon.png";
|
||||||
import maskableIcon from "@images/icon-maskable.png";
|
import maskableIcon from "@images/icon-maskable.png";
|
||||||
|
|
||||||
interface Favicon {
|
interface Favicon {
|
||||||
purpose: 'any' | 'maskable' | 'monochrome';
|
purpose: "any" | "maskable" | "monochrome";
|
||||||
src: ImageMetadata;
|
src: ImageMetadata;
|
||||||
sizes: number[];
|
sizes: number[];
|
||||||
}
|
}
|
||||||
|
|
@ -12,12 +12,12 @@ interface Favicon {
|
||||||
const sizes = [192, 512];
|
const sizes = [192, 512];
|
||||||
const favicons: Favicon[] = [
|
const favicons: Favicon[] = [
|
||||||
{
|
{
|
||||||
purpose: 'any',
|
purpose: "any",
|
||||||
src: icon,
|
src: icon,
|
||||||
sizes,
|
sizes,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
purpose: 'maskable',
|
purpose: "maskable",
|
||||||
src: maskableIcon,
|
src: maskableIcon,
|
||||||
sizes,
|
sizes,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,392 +0,0 @@
|
||||||
---
|
|
||||||
// Import section components
|
|
||||||
import MainLayout from "@/layouts/MainLayout.astro";
|
|
||||||
import ProductTabBtn from "@components/ui/buttons/ProductTabBtn.astro";
|
|
||||||
import PrimaryCTA from "@components/ui/buttons/PrimaryCTA.astro";
|
|
||||||
import { Image } from "astro:assets";
|
|
||||||
import { getCollection } from "astro:content";
|
|
||||||
import { SITE } from "@data/constants";
|
|
||||||
|
|
||||||
// Global declaration for gsap animation library
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
gsap: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This gets the static paths for all the unique products
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const productEntries = await getCollection("products", ({ id }) =>
|
|
||||||
id.startsWith("en/")
|
|
||||||
);
|
|
||||||
return productEntries.map((product) => {
|
|
||||||
const idWithoutLang = product.id.replace(/^en\//, ""); // Remove the "en/" prefix
|
|
||||||
return {
|
|
||||||
params: { id: idWithoutLang },
|
|
||||||
props: { product },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { product } = Astro.props;
|
|
||||||
|
|
||||||
const pageTitle: string = `${product.data.title} | ${SITE.title}`;
|
|
||||||
---
|
|
||||||
|
|
||||||
<MainLayout title={pageTitle}>
|
|
||||||
<div id="overlay" class="fixed inset-0 bg-neutral-200 dark:bg-neutral-800">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section
|
|
||||||
class="mx-auto flex max-w-[85rem] flex-col px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
id="fadeText"
|
|
||||||
class="mb-8 max-w-prose text-pretty font-light text-neutral-700 dark:text-neutral-300 sm:text-xl"
|
|
||||||
>
|
|
||||||
{product.data.main.content}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-center justify-between space-y-4 sm:flex-row sm:space-y-0"
|
|
||||||
>
|
|
||||||
<div id="fadeInUp">
|
|
||||||
<h1
|
|
||||||
class="block text-4xl font-bold tracking-tighter text-neutral-800 dark:text-neutral-200 sm:text-5xl md:text-6xl lg:text-7xl"
|
|
||||||
>
|
|
||||||
{product.data.title}
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg text-neutral-600 dark:text-neutral-400">
|
|
||||||
{product.data.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Image
|
|
||||||
id="fadeInMoveRight"
|
|
||||||
src={product.data.main.imgMain}
|
|
||||||
class="w-[600px]"
|
|
||||||
alt={product.data.main.imgAlt}
|
|
||||||
format={"avif"}
|
|
||||||
loading={"eager"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="mx-auto max-w-[85rem] px-4 pt-10 sm:px-6 lg:px-8 lg:pt-14">
|
|
||||||
<nav
|
|
||||||
class="mx-auto grid max-w-6xl gap-y-px sm:flex sm:gap-x-4 sm:gap-y-0"
|
|
||||||
aria-label="Tabs"
|
|
||||||
role="tablist"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
product.data.tabs.map((tab, index) => (
|
|
||||||
<ProductTabBtn
|
|
||||||
title={tab.title}
|
|
||||||
id={tab.id}
|
|
||||||
dataTab={tab.dataTab}
|
|
||||||
first={index === 0}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="mt-12 md:mt-16">
|
|
||||||
<div id="tabs-with-card-1" role="tabpanel">
|
|
||||||
<div class="mx-auto max-w-[85rem] px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14">
|
|
||||||
<div class="grid gap-12 md:grid-cols-2">
|
|
||||||
<div class="lg:w-3/4">
|
|
||||||
<h2
|
|
||||||
class="text-balance text-3xl font-bold tracking-tight text-neutral-800 dark:text-neutral-200 md:leading-tight lg:text-4xl"
|
|
||||||
>
|
|
||||||
{product.data.longDescription.title}
|
|
||||||
</h2>
|
|
||||||
<p
|
|
||||||
class="mt-3 text-pretty text-neutral-600 dark:text-neutral-400"
|
|
||||||
>
|
|
||||||
{product.data.longDescription.subTitle}
|
|
||||||
</p>
|
|
||||||
<p class="mt-5">
|
|
||||||
<PrimaryCTA
|
|
||||||
title={product.data.longDescription.btnTitle}
|
|
||||||
url={product.data.longDescription.btnURL}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-6 lg:space-y-10">
|
|
||||||
{
|
|
||||||
product.data.descriptionList.map((list) => (
|
|
||||||
<div class="flex">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-base font-bold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
|
||||||
{list.title}
|
|
||||||
</h3>
|
|
||||||
<p class="mt-1 text-neutral-600 dark:text-neutral-400">
|
|
||||||
{list.subTitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tabs-with-card-2" class="hidden" role="tabpanel">
|
|
||||||
<div class="mx-auto max-w-[85rem] px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14">
|
|
||||||
<div class="grid w-full grid-cols-1 gap-x-16 md:grid-cols-2">
|
|
||||||
<div class="max-w-md space-y-6">
|
|
||||||
{
|
|
||||||
product.data.specificationsLeft.map((spec) => (
|
|
||||||
<div>
|
|
||||||
<h3 class="block font-bold text-neutral-800 dark:text-neutral-200">
|
|
||||||
{spec.title}
|
|
||||||
</h3>
|
|
||||||
<p class="text-neutral-600 dark:text-neutral-400">
|
|
||||||
{spec.subTitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
product.data.specificationsRight ? (
|
|
||||||
<div class="mt-6 max-w-md space-y-6 md:ml-auto md:mt-0">
|
|
||||||
{product.data.specificationsRight?.map((spec) => (
|
|
||||||
<div>
|
|
||||||
<h3 class="block font-bold text-neutral-800 dark:text-neutral-200">
|
|
||||||
{spec.title}
|
|
||||||
</h3>
|
|
||||||
<p class="text-neutral-600 dark:text-neutral-400">
|
|
||||||
{spec.subTitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : product.data.tableData ? (
|
|
||||||
<div class="mt-6 space-y-6 md:ml-auto md:mt-0">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="-m-1.5 overflow-x-auto">
|
|
||||||
<div class="inline-block min-w-full p-1.5 align-middle">
|
|
||||||
<div class="overflow-hidden">
|
|
||||||
<table class="min-w-full divide-y divide-neutral-300 dark:divide-neutral-700">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{product.data.tableData?.[0].feature?.map(
|
|
||||||
(header) => (
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-neutral-500 dark:text-neutral-500"
|
|
||||||
>
|
|
||||||
{header}
|
|
||||||
</th>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-neutral-300 dark:divide-neutral-700">
|
|
||||||
{product.data.tableData?.map((row) =>
|
|
||||||
// Wrap each row's content in a separate <tr> element
|
|
||||||
row.description.map((rowData) => (
|
|
||||||
<tr>
|
|
||||||
{/* Iterate through each cell value in the row's description array */}
|
|
||||||
{rowData.map((cellValue) => (
|
|
||||||
// Render each cell value in its own <td> element
|
|
||||||
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
|
||||||
{cellValue}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tabs-with-card-3" class="hidden" role="tabpanel">
|
|
||||||
<div class="mx-auto mb-20 flex w-full md:mb-28 2xl:w-4/5">
|
|
||||||
<div
|
|
||||||
class="relative left-12 top-12 z-10 overflow-hidden rounded-xl shadow-lg md:left-12 md:top-16 md:-ml-12 lg:ml-0"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
product.data.blueprints.first && (
|
|
||||||
<Image
|
|
||||||
src={product.data.blueprints.first}
|
|
||||||
class="h-full w-full object-cover object-center"
|
|
||||||
alt="Blueprint Illustration"
|
|
||||||
format={"avif"}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative right-12 overflow-hidden rounded-xl shadow-xl">
|
|
||||||
{
|
|
||||||
product.data.blueprints.second && (
|
|
||||||
<Image
|
|
||||||
src={product.data.blueprints.second}
|
|
||||||
class="h-full w-full object-cover object-center"
|
|
||||||
alt="Blueprint Illustration"
|
|
||||||
format={"avif"}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MainLayout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { gsap } from "gsap";
|
|
||||||
|
|
||||||
type AnimationSettings = {
|
|
||||||
autoAlpha?: number;
|
|
||||||
y?: number;
|
|
||||||
x?: number;
|
|
||||||
willChange?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function setElementAnimationDefaults(
|
|
||||||
id: string,
|
|
||||||
settings: AnimationSettings
|
|
||||||
) {
|
|
||||||
gsap.set(id, settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
setElementAnimationDefaults("#fadeText", {
|
|
||||||
autoAlpha: 0,
|
|
||||||
y: 50,
|
|
||||||
willChange: "transform, opacity",
|
|
||||||
});
|
|
||||||
|
|
||||||
setElementAnimationDefaults("#fadeInUp", {
|
|
||||||
autoAlpha: 0,
|
|
||||||
y: 50,
|
|
||||||
willChange: "transform, opacity",
|
|
||||||
});
|
|
||||||
|
|
||||||
setElementAnimationDefaults("#fadeInMoveRight", {
|
|
||||||
autoAlpha: 0,
|
|
||||||
x: 300,
|
|
||||||
willChange: "transform, opacity",
|
|
||||||
});
|
|
||||||
|
|
||||||
let timeline = gsap.timeline({ defaults: { overwrite: "auto" } });
|
|
||||||
|
|
||||||
timeline.to("#fadeText", {
|
|
||||||
duration: 1.5,
|
|
||||||
autoAlpha: 1,
|
|
||||||
y: 0,
|
|
||||||
delay: 1,
|
|
||||||
ease: "power2.out",
|
|
||||||
});
|
|
||||||
|
|
||||||
timeline.to(
|
|
||||||
"#fadeInUp",
|
|
||||||
{ duration: 1.5, autoAlpha: 1, y: 0, ease: "power2.out" },
|
|
||||||
"-=1.2"
|
|
||||||
);
|
|
||||||
|
|
||||||
timeline.to(
|
|
||||||
"#fadeInMoveRight",
|
|
||||||
{ duration: 1.5, autoAlpha: 1, x: 0, ease: "power2.inOut" },
|
|
||||||
"-=1.4"
|
|
||||||
);
|
|
||||||
|
|
||||||
timeline.to("#overlay", { duration: 1, autoAlpha: 0, delay: 0.2 });
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
function setButtonInactive(btn: any, activeButton: any) {
|
|
||||||
if (btn !== activeButton) {
|
|
||||||
btn.classList.remove(
|
|
||||||
"active",
|
|
||||||
"bg-neutral-100",
|
|
||||||
"hover:border-transparent",
|
|
||||||
"dark:bg-white/[.05]"
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabId = btn.getAttribute("data-target");
|
|
||||||
if (tabId) {
|
|
||||||
const contentElement = document.querySelector(tabId);
|
|
||||||
if (contentElement) {
|
|
||||||
contentElement.classList.add("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changeHeadingStyle(
|
|
||||||
btn,
|
|
||||||
["text-neutral-800", "dark:text-neutral-200"],
|
|
||||||
["text-orange-400", "dark:text-orange-300"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function activateButton(button: any) {
|
|
||||||
button.classList.add(
|
|
||||||
"active",
|
|
||||||
"bg-neutral-100",
|
|
||||||
",hover:border-transparent",
|
|
||||||
"dark:bg-white/[.05]"
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabId = button.getAttribute("data-target");
|
|
||||||
if (tabId) {
|
|
||||||
const contentElementToShow = document.querySelector(tabId);
|
|
||||||
if (contentElementToShow) {
|
|
||||||
contentElementToShow.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changeHeadingStyle(
|
|
||||||
button,
|
|
||||||
["text-orange-400", "dark:text-orange-300"],
|
|
||||||
["text-neutral-800", "dark:text-neutral-200"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeHeadingStyle(
|
|
||||||
button: any,
|
|
||||||
addClasses: any,
|
|
||||||
removeClasses: any
|
|
||||||
) {
|
|
||||||
let heading = button.querySelector("span");
|
|
||||||
if (heading) {
|
|
||||||
heading.classList.remove(...removeClasses);
|
|
||||||
heading.classList.add(...addClasses);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabButtons = document.querySelectorAll("[data-target]");
|
|
||||||
|
|
||||||
if (tabButtons.length > 0) {
|
|
||||||
changeHeadingStyle(
|
|
||||||
tabButtons[0],
|
|
||||||
["text-orange-400", "dark:text-orange-300"],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
tabButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
tabButtons.forEach((btn) => setButtonInactive(btn, button));
|
|
||||||
activateButton(button);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
---
|
|
||||||
// Importing necessary components
|
|
||||||
import MainLayout from "@/layouts/MainLayout.astro";
|
|
||||||
import PrimaryCTA from "@components/ui/buttons/PrimaryCTA.astro";
|
|
||||||
import CardSmall from "@components/ui/cards/CardSmall.astro";
|
|
||||||
import CardWide from "@components/ui/cards/CardWide.astro";
|
|
||||||
import FeaturesStatsAlt from "@components/sections/features/FeaturesStatsAlt.astro";
|
|
||||||
import TestimonialsSectionAlt from "@components/sections/testimonials/TestimonialsSectionAlt.astro";
|
|
||||||
|
|
||||||
// Importing necessary functions from Astro
|
|
||||||
import { getCollection } from "astro:content";
|
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
import { SITE } from "@data/constants";
|
|
||||||
|
|
||||||
// Fetching all the product related content and sorting it by main.id
|
|
||||||
const product: CollectionEntry<"products">[] = (
|
|
||||||
await getCollection("products", ({ id }) => {
|
|
||||||
return id.startsWith("en/");
|
|
||||||
})
|
|
||||||
).sort(
|
|
||||||
(a: CollectionEntry<"products">, b: CollectionEntry<"products">) =>
|
|
||||||
a.data.main.id - b.data.main.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Define variables for page content
|
|
||||||
const title: string = "Products";
|
|
||||||
const subTitle: string =
|
|
||||||
"Explore the durability and precision of ScrewFast tools, designed for both professionals and enthusiasts. Each of our products is crafted with precision and built to last, ensuring you have the right tool for every job.";
|
|
||||||
|
|
||||||
// Testimonial data that will be rendered in the component
|
|
||||||
const testimonials = [
|
|
||||||
// First testimonial data
|
|
||||||
{
|
|
||||||
content:
|
|
||||||
" \"Since switching to ScrewFast's hardware tools, the efficiency on our construction sites has skyrocketed. The durability of the hex bolts and precision of the machine screws are simply unmatched. It's refreshing to work with a company that truly understands the daily demands of the industry.\" ",
|
|
||||||
author: "Jason Clark",
|
|
||||||
role: "Site Foreman | TopBuild",
|
|
||||||
avatarSrc:
|
|
||||||
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=1374&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=facearea&facepad=2&w=320&h=320&q=80",
|
|
||||||
avatarAlt: "Image Description",
|
|
||||||
},
|
|
||||||
// Second testimonial data
|
|
||||||
{
|
|
||||||
content:
|
|
||||||
" \"As an interior designer, I'm always looking for high-quality materials and tools that help bring my visions to life. ScrewFast's mixed screws assortment has been a game-changer for my projects, providing the perfect blend of quality and variety. The outstanding customer support was just the cherry on top!\" ",
|
|
||||||
author: "Maria Gonzalez",
|
|
||||||
role: "Interior Designer | Creative Spaces",
|
|
||||||
avatarSrc:
|
|
||||||
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=1376&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D8&auto=format&fit=facearea&facepad=2&w=320&h=320&q=80",
|
|
||||||
avatarAlt: "Image Description",
|
|
||||||
},
|
|
||||||
// Third testimonial data
|
|
||||||
{
|
|
||||||
content:
|
|
||||||
" \"I’ve been a professional carpenter for over 15 years, and I can sincerely say that ScrewFast’s tap bolts and nuts are some of the best I've used. They grip like no other, and I have full confidence in every joint and fixture. Plus, the service is impeccable – they truly care about my project's success.\" ",
|
|
||||||
author: "Richard Kim",
|
|
||||||
role: "Master Carpenter | WoodWright",
|
|
||||||
avatarSrc:
|
|
||||||
"https://images.unsplash.com/photo-1474176857210-7287d38d27c6?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D8&auto=format&fit=facearea&facepad=2&w=320&h=320&q=80",
|
|
||||||
avatarAlt: "Image Description",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const pageTitle: string = `Products | ${SITE.title}`;
|
|
||||||
---
|
|
||||||
|
|
||||||
<MainLayout
|
|
||||||
title={pageTitle}
|
|
||||||
structuredData={{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "WebPage",
|
|
||||||
"@id": "https://screwfast.uk/products",
|
|
||||||
"url": "https://screwfast.uk/products",
|
|
||||||
"name": "Hardware Tools | ScrewFast",
|
|
||||||
"description": "Explore the durability and precision of ScrewFast tools, designed for both professionals and enthusiasts.",
|
|
||||||
"isPartOf": {
|
|
||||||
"@type": "WebSite",
|
|
||||||
"url": "https://screwfast.uk",
|
|
||||||
"name": "ScrewFast",
|
|
||||||
"description": "ScrewFast offers top-tier hardware tools and expert construction services to meet all your project needs."
|
|
||||||
},
|
|
||||||
"inLanguage": "fr-FR"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
||||||
>
|
|
||||||
<div class="mb-4 flex items-center justify-between gap-8 sm:mb-8 md:mb-12">
|
|
||||||
<div class="flex items-center gap-12">
|
|
||||||
<h1
|
|
||||||
class="text-balance text-2xl font-bold tracking-tight text-neutral-800 dark:text-neutral-200 md:text-4xl md:leading-tight"
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
{
|
|
||||||
subTitle && (
|
|
||||||
<p class="hidden max-w-screen-sm text-pretty text-neutral-600 dark:text-neutral-400 md:block">
|
|
||||||
{subTitle}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<PrimaryCTA title="Customer Stories" url="#testimonials" noArrow={true} />
|
|
||||||
</div>
|
|
||||||
<!--Displaying products in alternating styles. Alternative product gets different card styling.-->
|
|
||||||
<!--Maps through all product entries and displays them with either CardSmall or CardWide based on their position.-->
|
|
||||||
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3 md:gap-6 xl:gap-8">
|
|
||||||
{
|
|
||||||
product.map((product, index) => {
|
|
||||||
const position = index % 4;
|
|
||||||
if (position === 0 || position === 3) {
|
|
||||||
return <CardSmall product={product} />;
|
|
||||||
} else {
|
|
||||||
return <CardWide product={product} />;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<!--Features statistics section-->
|
|
||||||
<FeaturesStatsAlt
|
|
||||||
title="Why Choose ScrewFast?"
|
|
||||||
subTitle="Transform your ideas into tangible results with ScrewFast tools. Whether you're starting with a sketch on a napkin or diving into a comprehensive construction project, our tools are engineered to help you build with confidence."
|
|
||||||
benefits={[
|
|
||||||
"Robust and reliable tools for long-lasting performance.",
|
|
||||||
"Innovative solutions tailored to modern construction needs.",
|
|
||||||
"Customer support dedicated to your project's success.",
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<!--Testimonials section-->
|
|
||||||
<TestimonialsSectionAlt
|
|
||||||
title="What Our Customers Say"
|
|
||||||
testimonials={testimonials}
|
|
||||||
/>
|
|
||||||
</MainLayout>
|
|
||||||
Loading…
Add table
Reference in a new issue