change le favicon

This commit is contained in:
Jalil Arfaoui 2025-01-05 23:24:09 +01:00
parent 47eef4ca6f
commit 5d67ab803a
7 changed files with 9 additions and 815 deletions

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

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

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

View file

@ -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,15 +12,15 @@ 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,
}, },
]; ];
export const GET: APIRoute = async () => { export const GET: APIRoute = async () => {

View file

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

View file

@ -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:
" \"Ive been a professional carpenter for over 15 years, and I can sincerely say that ScrewFasts 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>