Initial commit

This commit is contained in:
Nikolay Pecheniev
2026-01-22 13:44:27 +02:00
commit 523197247b
300 changed files with 57896 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
"use client";
// import Image from "next/image";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FooterColumns from "@/components/shared/FooterColumns";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
interface FooterBaseProps {
// logoSrc?: string;
logoText?: string;
// logoWidth?: number;
// logoHeight?: number;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
// logoClassName?: string;
logoTextClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
}
const FooterBase = ({
// logoSrc = "/brand/logowhite.svg",
logoText = "Webild",
// logoWidth = 120,
// logoHeight = 40,
columns,
copyrightText = `© 2025 | Webild`,
onPrivacyClick,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
// logoClassName = "",
logoTextClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
privacyButtonClassName = "",
}: FooterBaseProps) => {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full primary-button text-background py-15 mt-20", className)}
>
<div
className={cls("relative w-content-width mx-auto z-10", containerClassName)}
>
<div className="flex flex-col md:flex-row gap-10 md:gap-0 justify-between items-start mb-10">
{/* {logoSrc ? (
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt="Logo"
width={logoWidth}
height={logoHeight}
className={cls("object-contain", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : ( */}
<h2 className={cls("text-4xl font-medium text-background", logoTextClassName)}>
{logoText}
</h2>
{/* )} */}
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={cls("text-background/50", columnTitleClassName)}
columnItemClassName={cls("text-background", columnItemClassName)}
/>
</div>
<div
className={cls("w-full flex items-center justify-between pt-9 border-t border-background/20", copyrightContainerClassName)}
>
<span className={cls("text-background/50 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
<ButtonTextUnderline
text="Privacy Policy"
onClick={onPrivacyClick}
className={cls("text-background/50", privacyButtonClassName)}
/>
</div>
</div>
</footer>
);
};
FooterBase.displayName = "FooterBase";
export default FooterBase;

View File

@@ -0,0 +1,108 @@
"use client";
// import Image from "next/image";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FooterColumns from "@/components/shared/FooterColumns";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
interface FooterBaseCardProps {
// logoSrc?: string;
logoText?: string;
// logoWidth?: number;
// logoHeight?: number;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
// logoClassName?: string;
logoTextClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
}
const FooterBaseCard = ({
// logoSrc = "/brand/logowhite.svg",
logoText = "Webild",
// logoWidth = 120,
// logoHeight = 40,
columns,
copyrightText = `© 2025 | Webild`,
onPrivacyClick,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
cardClassName = "",
// logoClassName = "",
logoTextClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
privacyButtonClassName = "",
}: FooterBaseCardProps) => {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative w-full py-20", className)}
>
<div className={cls("relative w-content-width mx-auto card rounded-theme-capped p-10", containerClassName, cardClassName)}>
<div className="relative z-1 flex flex-col md:flex-row gap-10 md:gap-0 justify-between items-start mb-10">
{/* {logoSrc ? (
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt="Logo"
width={logoWidth}
height={logoHeight}
className={cls("object-contain", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : ( */}
<h2 className={cls("text-4xl font-medium", logoTextClassName)}>
{logoText}
</h2>
{/* )} */}
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
/>
</div>
<div
className={cls("relative z-1 w-full flex items-center justify-between pt-9 border-t border-foreground/20", copyrightContainerClassName)}
>
<span className={cls("text-foreground/50 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
<ButtonTextUnderline
text="Privacy Policy"
onClick={onPrivacyClick}
className={cls("text-foreground/50", privacyButtonClassName)}
/>
</div>
</div>
</footer>
);
};
FooterBaseCard.displayName = "FooterBaseCard";
export default FooterBaseCard;

View File

@@ -0,0 +1,126 @@
"use client";
import { useRef, useEffect, useState } from "react";
import FooterBase from "./FooterBase";
import { cls } from "@/lib/utils";
interface FooterColumn {
title: string;
items: Array<{
label: string;
href?: string;
onClick?: () => void;
}>;
}
interface FooterBaseRevealProps {
// logoSrc?: string;
// logoWidth?: number;
// logoHeight?: number;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
wrapperClassName?: string;
containerClassName?: string;
footerClassName?: string;
footerContainerClassName?: string;
// logoClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
}
const FooterBaseReveal = ({
// logoSrc,
// logoWidth,
// logoHeight,
columns,
copyrightText,
onPrivacyClick,
ariaLabel,
className = "",
wrapperClassName = "",
containerClassName = "",
footerClassName,
footerContainerClassName,
// logoClassName,
columnsClassName,
columnClassName,
columnTitleClassName,
columnItemClassName,
copyrightContainerClassName,
copyrightTextClassName,
privacyButtonClassName,
}: FooterBaseRevealProps) => {
const footerRef = useRef<HTMLDivElement>(null);
const [footerHeight, setFooterHeight] = useState<number>(0);
useEffect(() => {
const updateHeight = () => {
if (footerRef.current) {
const height = footerRef.current.offsetHeight;
setFooterHeight(height);
}
};
updateHeight();
const resizeObserver = new ResizeObserver(updateHeight);
const currentFooter = footerRef.current;
if (currentFooter) {
resizeObserver.observe(currentFooter);
}
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<section
className={cls("relative z-0 w-full mt-20", className)}
style={{
height: footerHeight ? `${footerHeight}px` : "auto",
clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)",
}}
>
<div
className={cls("fixed bottom-0 w-full flex items-center justify-center overflow-hidden", wrapperClassName)}
style={{ height: footerHeight ? `${footerHeight}px` : "auto" }}
>
<div ref={footerRef} className={cls("w-full", containerClassName)}>
<FooterBase
// logoSrc={logoSrc}
// logoWidth={logoWidth}
// logoHeight={logoHeight}
columns={columns}
copyrightText={copyrightText}
onPrivacyClick={onPrivacyClick}
ariaLabel={ariaLabel}
className={cls("mt-0", footerClassName)}
containerClassName={footerContainerClassName}
// logoClassName={logoClassName}
columnsClassName={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
copyrightContainerClassName={copyrightContainerClassName}
copyrightTextClassName={copyrightTextClassName}
privacyButtonClassName={privacyButtonClassName}
/>
</div>
</div>
</section>
);
};
FooterBaseReveal.displayName = "FooterBaseReveal";
export default FooterBaseReveal;

View File

@@ -0,0 +1,81 @@
"use client";
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
import SocialLinks from "@/components/shared/SocialLinks";
import { cls } from "@/lib/utils";
import type { SocialLink } from "@/components/shared/SocialLinks";
interface FooterCardProps {
// logoSrc?: string;
// logoAlt?: string;
logoText?: string;
logoLineHeight?: number;
copyrightText?: string;
socialLinks?: SocialLink[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
logoClassName?: string;
dividerClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
socialContainerClassName?: string;
socialIconClassName?: string;
}
const FooterCard = ({
// logoSrc,
// logoAlt = "Logo",
logoText = "Webild",
logoLineHeight = 1.1,
copyrightText = `© 2025 | Webild`,
socialLinks,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
cardClassName = "",
logoClassName = "",
dividerClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
socialContainerClassName = "",
socialIconClassName = "",
}: FooterCardProps) => {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative w-full py-20", className)}
>
<div className={cls("relative w-content-width mx-auto card rounded-theme-capped px-10", containerClassName, cardClassName)}>
<div className={cls("relative z-1 w-full", logoClassName)}>
<FillWidthText lineHeight={logoLineHeight}>
{logoText}
</FillWidthText>
</div>
<div className={cls("relative z-1 w-full h-px bg-accent/20 mb-6", dividerClassName)} />
<div
className={cls("relative z-1 w-full flex flex-col md:flex-row items-center justify-between gap-4 mb-6", copyrightContainerClassName)}
>
<span className={cls("text-accent/75 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
{socialLinks && socialLinks.length > 0 && (
<SocialLinks
socialLinks={socialLinks}
className={socialContainerClassName}
iconClassName={socialIconClassName}
/>
)}
</div>
</div>
</footer>
);
};
FooterCard.displayName = "FooterCard";
export default FooterCard;

View File

@@ -0,0 +1,120 @@
"use client";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
import { ChevronRight } from "lucide-react";
import { cls } from "@/lib/utils";
interface FooterColumn {
items: Array<{
label: string;
href?: string;
onClick?: () => void;
}>;
}
interface FooterLogoEmphasisProps {
// logoSrc?: string;
// logoAlt?: string;
columns: FooterColumn[];
logoText?: string;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoClassName?: string;
columnsClassName?: string;
columnClassName?: string;
itemClassName?: string;
iconClassName?: string;
buttonClassName?: string;
}
const FooterLogoEmphasis = ({
// logoSrc,
// logoAlt = "Logo",
columns,
logoText = "Webild",
ariaLabel = "Site footer",
className = "",
containerClassName = "",
logoClassName = "",
columnsClassName = "",
columnClassName = "",
itemClassName = "",
iconClassName = "",
buttonClassName = "",
}: FooterLogoEmphasisProps) => {
const columnCount = columns.length;
const useFlex = columnCount <= 3;
const gridColsClass = columnCount === 4
? "grid-cols-2 md:grid-cols-4"
: "grid-cols-2 md:grid-cols-5";
return (
<footer
className={cls(
"w-full py-15 mt-20 flex justify-center relative z-1 overflow-hidden primary-button text-background rounded-t-theme-capped",
className
)}
role="contentinfo"
aria-label={ariaLabel}
>
<div
className={cls(
"w-content-width mx-auto flex flex-col relative z-10",
"gap-10 md:gap-20",
containerClassName
)}
>
<div className={cls("relative z-1 w-full", logoClassName)}>
<FillWidthText lineHeight={1.1}>
{logoText}
</FillWidthText>
</div>
<div
className={cls(
"w-full mb-10",
useFlex
? cls(
"flex flex-col md:flex-row gap-8 md:gap-[var(--width-10)]",
columnCount === 1 ? "md:justify-center" : "md:justify-between"
)
: cls("grid gap-[var(--width-10)] md:gap-[calc(var(--width-10)/2)]", gridColsClass),
columnsClassName
)}
>
{columns.map((column, index) => (
<div
key={`column-${index}`}
className={cls("flex items-start flex-col gap-4", columnClassName)}
>
{column.items.map((item) => (
<div
key={`${item.label}-${index}`}
className={cls("flex items-center gap-2 text-base", itemClassName)}
>
<ChevronRight
className={cls("h-[1em] w-auto", iconClassName)}
strokeWidth={3}
aria-hidden="true"
/>
<ButtonTextUnderline
text={item.label}
href={item.href}
onClick={item.onClick}
className={cls("font-medium text-base", buttonClassName)}
/>
</div>
))}
</div>
))}
</div>
</div>
</footer>
);
};
FooterLogoEmphasis.displayName = "FooterLogoEmphasis";
export default FooterLogoEmphasis;

View File

@@ -0,0 +1,89 @@
"use client";
import { useRef, useEffect, useState } from "react";
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
import { cls } from "@/lib/utils";
interface FooterLogoRevealProps {
// logoSrc?: string;
// logoAlt?: string;
logoText?: string;
logoLineHeight?: number;
ariaLabel?: string;
className?: string;
wrapperClassName?: string;
containerClassName?: string;
logoClassName?: string;
}
const FooterLogoReveal = ({
// logoSrc,
// logoAlt = "Logo",
logoText = "Webild",
logoLineHeight = 1.1,
ariaLabel = "Site footer",
className = "",
wrapperClassName = "",
containerClassName = "",
logoClassName = "",
}: FooterLogoRevealProps) => {
const footerRef = useRef<HTMLDivElement>(null);
const [footerHeight, setFooterHeight] = useState<number>(0);
useEffect(() => {
const updateHeight = () => {
if (footerRef.current) {
const height = footerRef.current.offsetHeight;
setFooterHeight(height);
}
};
updateHeight();
const resizeObserver = new ResizeObserver(updateHeight);
const currentFooter = footerRef.current;
if (currentFooter) {
resizeObserver.observe(currentFooter);
}
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<section
aria-label={ariaLabel}
className={cls("relative z-0 w-full mt-20", className)}
style={{
height: footerHeight ? `${footerHeight}px` : "auto",
clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)",
}}
>
<div
className={cls("fixed bottom-0 w-full flex items-center justify-center overflow-hidden", wrapperClassName)}
style={{ height: footerHeight ? `${footerHeight}px` : "auto" }}
>
<div ref={footerRef} className={cls("w-full", containerClassName)}>
<footer
role="contentinfo"
className="relative w-full py-20 card"
>
<div className="w-content-width mx-auto flex flex-col relative z-10">
<div className={cls("relative z-1 w-full", logoClassName)}>
<FillWidthText lineHeight={logoLineHeight}>
{logoText}
</FillWidthText>
</div>
</div>
</footer>
</div>
</div>
</section>
);
};
FooterLogoReveal.displayName = "FooterLogoReveal";
export default FooterLogoReveal;

View File

@@ -0,0 +1,143 @@
"use client";
// import Image from "next/image";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FooterColumns from "@/components/shared/FooterColumns";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type FooterMediaProps = MediaProps & {
// logoSrc?: string;
logoText?: string;
// logoWidth?: number;
// logoHeight?: number;
columns: FooterColumn[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
// logoClassName?: string;
logoTextClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
};
const FooterMedia = ({
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Footer video",
// logoSrc = "/brand/logowhite.svg",
logoText = "Webild",
// logoWidth = 120,
// logoHeight = 40,
columns,
copyrightText = `© 2025 | Webild`,
onPrivacyClick,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
// logoClassName = "",
logoTextClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
privacyButtonClassName = "",
}: FooterMediaProps) => {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full mt-20", className)}
>
<div className={cls("w-full aspect-square md:aspect-[16/6] mask-fade-top-long", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover rounded-none!", mediaClassName)}
/>
</div>
<div className="primary-button text-background py-15">
<div
className={cls("relative w-content-width mx-auto z-10", containerClassName)}
>
<div className="flex flex-col md:flex-row gap-10 md:gap-0 justify-between items-start mb-10">
{/* {logoSrc ? (
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt="Logo"
width={logoWidth}
height={logoHeight}
className={cls("object-contain", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : ( */}
<h2 className={cls("text-4xl font-medium text-background", logoTextClassName)}>
{logoText}
</h2>
{/* )} */}
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={cls("text-background/50", columnTitleClassName)}
columnItemClassName={cls("text-background", columnItemClassName)}
/>
</div>
<div
className={cls("w-full flex items-center justify-between pt-9 border-t border-background/20", copyrightContainerClassName)}
>
<span className={cls("text-background/50 text-sm", copyrightTextClassName)}>
{copyrightText}
</span>
<ButtonTextUnderline
text="Privacy Policy"
onClick={onPrivacyClick}
className={cls("text-background/50", privacyButtonClassName)}
/>
</div>
</div>
</div>
</footer>
);
};
FooterMedia.displayName = "FooterMedia";
export default FooterMedia;

View File

@@ -0,0 +1,75 @@
"use client";
import { memo } from "react";
import FooterColumns from "@/components/shared/FooterColumns";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
interface FooterSimpleProps {
columns: FooterColumn[];
bottomLeftText: string;
bottomRightText: string;
ariaLabel?: string;
className?: string;
containerClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
dividerClassName?: string;
bottomContainerClassName?: string;
bottomLeftTextClassName?: string;
bottomRightTextClassName?: string;
}
const FooterSimple = ({
columns,
bottomLeftText,
bottomRightText,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
dividerClassName = "",
bottomContainerClassName = "",
bottomLeftTextClassName = "",
bottomRightTextClassName = "",
}: FooterSimpleProps) => {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative w-full pt-20 pb-10", className)}
>
<div className={cls("w-content-width md:w-60 mx-auto", containerClassName)}>
<FooterColumns
columns={columns}
className={cls("w-full! justify-between mb-10", columnsClassName)}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
/>
<div
className={cls("w-full h-px bg-foreground/20", dividerClassName)}
/>
<div
className={cls("w-full flex items-center justify-between pt-6", bottomContainerClassName)}
>
<p className={cls("text-foreground/50 text-sm", bottomLeftTextClassName)}>
{bottomLeftText}
</p>
<p className={cls("text-foreground/50 text-sm", bottomRightTextClassName)}>
{bottomRightText}
</p>
</div>
</div>
</footer>
);
};
FooterSimple.displayName = "FooterSimple";
export default memo(FooterSimple);