Initial commit

This commit is contained in:
vitalijmulika
2025-12-25 13:52:13 +02:00
commit 4ef6fdd36a
307 changed files with 62808 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
"use client";
import { memo } from "react";
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 = memo<FooterBaseProps>(function 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 = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full primary-button text-background py-15", 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,109 @@
"use client";
import { memo } from "react";
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 = memo<FooterBaseCardProps>(function 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 = "",
}) {
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 { memo, 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 = memo<FooterBaseRevealProps>(function FooterBaseReveal({
logoSrc,
logoWidth,
logoHeight,
columns,
copyrightText,
onPrivacyClick,
ariaLabel,
className = "",
wrapperClassName = "",
containerClassName = "",
footerClassName,
footerContainerClassName,
logoClassName,
columnsClassName,
columnClassName,
columnTitleClassName,
columnItemClassName,
copyrightContainerClassName,
copyrightTextClassName,
privacyButtonClassName,
}) {
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", 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={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,133 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import FooterColumns from "@/components/shared/FooterColumns";
import SocialLinks from "@/components/shared/SocialLinks";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
import type { SocialLink } from "@/components/shared/SocialLinks";
interface FooterBaseSocialProps {
logoSrc?: string;
logoText?: string;
logoWidth?: number;
logoHeight?: number;
description: string;
columns: FooterColumn[];
socialLinks: SocialLink[];
copyrightText?: string;
onPrivacyClick?: () => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoClassName?: string;
logoTextClassName?: string;
descriptionClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
socialLinksClassName?: string;
socialIconClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
privacyButtonClassName?: string;
}
const FooterBaseSocial = memo<FooterBaseSocialProps>(function FooterBaseSocial({
logoSrc = "/brand/logowhite.svg",
logoText = "Webild",
logoWidth = 120,
logoHeight = 40,
description,
columns,
socialLinks,
copyrightText = `© 2025 | Webild`,
onPrivacyClick,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
logoClassName = "",
logoTextClassName = "",
descriptionClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
socialLinksClassName = "",
socialIconClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
privacyButtonClassName = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full primary-button text-background py-15", 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 mb-10">
<div className="relative flex flex-col justify-between gap-4">
{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>
)}
<p className={cls("text-background/50 text-base leading-tight text-balance md:max-w-[var(--width-20)]", descriptionClassName)}>
{description}
</p>
<SocialLinks
socialLinks={socialLinks}
className={cls("mt-auto", socialLinksClassName)}
iconClassName={socialIconClassName}
/>
</div>
<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>
);
});
FooterBaseSocial.displayName = "FooterBaseSocial";
export default FooterBaseSocial;

View File

@@ -0,0 +1,84 @@
"use client";
import { memo } from "react";
import FooterLogo from "@/components/sections/footer/FooterLogo";
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;
copyrightText?: string;
socialLinks?: SocialLink[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
logoClassName?: string;
svgClassName?: string;
dividerClassName?: string;
copyrightContainerClassName?: string;
copyrightTextClassName?: string;
socialContainerClassName?: string;
socialIconClassName?: string;
}
const FooterCard = memo<FooterCardProps>(function FooterCard({
// logoSrc,
// logoAlt = "Logo",
logoText = "Webild",
copyrightText = `© 2025 | Webild`,
socialLinks,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
cardClassName = "",
logoClassName = "",
svgClassName = "",
dividerClassName = "",
copyrightContainerClassName = "",
copyrightTextClassName = "",
socialContainerClassName = "",
socialIconClassName = "",
}) {
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)}>
<FooterLogo
// logoSrc={logoSrc}
// logoAlt={logoAlt}
logoText={logoText}
className={logoClassName}
svgClassName={svgClassName}
/>
<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,44 @@
"use client";
import { memo } from "react";
// import Image from "next/image";
import SvgTextLogo from "@/components/shared/SvgTextLogo/SvgTextLogo";
import { cls } from "@/lib/utils";
interface FooterLogoProps {
// logoSrc?: string;
// logoAlt?: string;
logoText?: string;
className?: string;
svgClassName?: string;
}
const FooterLogo = memo<FooterLogoProps>(function FooterLogo({
// logoSrc,
// logoAlt = "Logo",
logoText = "Webild",
className = "",
svgClassName = ""
}) {
return (
<div className={cls("relative z-1 w-full", className)}>
{/* {logoSrc ? (
<Image
src={logoSrc}
alt={logoAlt}
width={1000}
height={1000}
className="w-full h-auto object-contain"
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={logoAlt === ""}
/>
) : ( */}
<SvgTextLogo logoText={logoText} className={svgClassName} />
{/* )} */}
</div>
);
});
FooterLogo.displayName = "FooterLogo";
export default FooterLogo;

View File

@@ -0,0 +1,125 @@
"use client";
import { memo } from "react";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import { ChevronRight } from "lucide-react";
import FooterLogo from "./FooterLogo";
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 = memo<FooterLogoEmphasisProps>(
function FooterLogoEmphasis({
// logoSrc,
// logoAlt = "Logo",
columns,
logoText = "Webild",
ariaLabel = "Site footer",
className = "",
containerClassName = "",
logoClassName = "",
columnsClassName = "",
columnClassName = "",
itemClassName = "",
iconClassName = "",
buttonClassName = "",
}) {
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 flex justify-center relative z-1 overflow-hidden primary-button text-background rounded-t-theme-capped",
"py-15",
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
)}
>
<FooterLogo
// logoSrc={logoSrc}
// logoAlt={logoAlt}
logoText={logoText}
className={logoClassName}
/>
<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,91 @@
"use client";
import { memo, useRef, useEffect, useState } from "react";
import FooterLogo from "./FooterLogo";
import { cls } from "@/lib/utils";
interface FooterLogoRevealProps {
// logoSrc?: string;
// logoAlt?: string;
logoText?: string;
ariaLabel?: string;
className?: string;
wrapperClassName?: string;
containerClassName?: string;
logoClassName?: string;
svgClassName?: string;
}
const FooterLogoReveal = memo<FooterLogoRevealProps>(function FooterLogoReveal({
// logoSrc,
// logoAlt = "Logo",
logoText = "Webild",
ariaLabel = "Site footer",
className = "",
wrapperClassName = "",
containerClassName = "",
logoClassName = "",
svgClassName = "",
}) {
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", 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">
<FooterLogo
// logoSrc={logoSrc}
// logoAlt={logoAlt}
logoText={logoText}
className={logoClassName}
svgClassName={svgClassName}
/>
</div>
</footer>
</div>
</div>
</section>
);
});
FooterLogoReveal.displayName = "FooterLogoReveal";
export default FooterLogoReveal;

View File

@@ -0,0 +1,144 @@
"use client";
import { memo } from "react";
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 = memo<FooterMediaProps>(function 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 = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full", 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,114 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import SocialLinks from "@/components/shared/SocialLinks";
import FooterColumns from "@/components/shared/FooterColumns";
import { cls } from "@/lib/utils";
import type { SocialLink } from "@/components/shared/SocialLinks";
import type { FooterColumn } from "@/components/shared/FooterColumns";
interface FooterSocialProps {
logoSrc?: string;
logoText?: string;
logoWidth?: number;
logoHeight?: number;
columns: FooterColumn[];
socialLinks?: SocialLink[];
copyrightText?: string;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
logoTextClassName?: string;
copyrightTextClassName?: string;
socialContainerClassName?: string;
socialIconClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
}
const FooterSocial = memo<FooterSocialProps>(function FooterSocial({
logoSrc,
logoText = "Webild",
logoWidth = 120,
logoHeight = 40,
columns,
socialLinks,
copyrightText = "© Finerpoint, Inc. 2025",
ariaLabel = "Site footer",
className = "",
containerClassName = "",
logoContainerClassName = "",
logoClassName = "",
logoTextClassName = "",
copyrightTextClassName = "",
socialContainerClassName = "",
socialIconClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
}) {
return (
<footer
role="contentinfo"
aria-label={ariaLabel}
className={cls("relative overflow-hidden w-full text-foreground py-15", className)}
>
<div
className={cls("relative w-content-width mx-auto", containerClassName)}
>
<div className="flex flex-col md:flex-row gap-14 md:gap-20 justify-between">
<div className={cls("flex flex-col gap-15", logoContainerClassName)}>
<div className="flex flex-col gap-0">
{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>
)}
<p className={cls("text-sm text-accent/75", copyrightTextClassName)}>
{copyrightText}
</p>
</div>
{socialLinks && socialLinks.length > 0 && (
<SocialLinks
socialLinks={socialLinks}
className={socialContainerClassName}
iconClassName={socialIconClassName}
/>
)}
</div>
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
/>
</div>
</div>
</footer>
);
});
FooterSocial.displayName = "FooterSocial";
export default FooterSocial;

View File

@@ -0,0 +1,139 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import FooterColumns from "@/components/shared/FooterColumns";
import AvatarGroup from "@/components/shared/AvatarGroup";
import { cls } from "@/lib/utils";
import type { FooterColumn } from "@/components/shared/FooterColumns";
import type { Avatar } from "@/components/shared/AvatarGroup";
import type { LucideIcon } from "lucide-react";
interface ContactItem {
icon: LucideIcon;
text: string;
}
interface FooterSplitProps {
logoSrc?: string;
logoAlt?: string;
logoText?: string;
logoWidth?: number;
logoHeight?: number;
columns: FooterColumn[];
title: string;
avatars?: Avatar[];
contactItems: ContactItem[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
contentClassName?: string;
columnsClassName?: string;
columnClassName?: string;
columnTitleClassName?: string;
columnItemClassName?: string;
titleClassName?: string;
avatarGroupClassName?: string;
logoClassName?: string;
logoTextClassName?: string;
contactClassName?: string;
contactItemClassName?: string;
contactIconClassName?: string;
}
const FooterSplit = memo<FooterSplitProps>(function FooterSplit({
logoSrc,
logoAlt = "Logo",
logoText = "Webild",
logoWidth = 120,
logoHeight = 40,
columns,
title,
avatars,
contactItems,
ariaLabel = "Site footer",
className = "",
containerClassName = "",
cardClassName = "",
contentClassName = "",
columnsClassName = "",
columnClassName = "",
columnTitleClassName = "",
columnItemClassName = "",
titleClassName = "",
avatarGroupClassName = "",
logoClassName = "",
logoTextClassName = "",
contactClassName = "",
contactItemClassName = "",
contactIconClassName = "",
}) {
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 flex flex-col gap-40 p-10", containerClassName, cardClassName)}>
<div className={cls("relative z-1 flex flex-col md:flex-row gap-10", contentClassName)}>
<FooterColumns
columns={columns}
className={columnsClassName}
columnClassName={columnClassName}
columnTitleClassName={columnTitleClassName}
columnItemClassName={columnItemClassName}
/>
<div className="flex-1 flex flex-col items-start md:items-end text-left md:text-right gap-4">
<h2 className={cls("text-4xl font-medium w-full md:w-1/2 text-balance", titleClassName)}>
{title}
</h2>
{avatars && avatars.length > 0 && (
<AvatarGroup
avatars={avatars}
className={avatarGroupClassName}
/>
)}
</div>
</div>
<div className="relative z-1 flex flex-col-reverse md:flex-row items-start md:items-end justify-between gap-6">
{logoSrc ? (
<div className="w-full flex-shrink-0">
<Image
src={logoSrc}
alt={logoAlt}
width={logoWidth}
height={logoHeight}
className={cls("object-contain w-full md:w-20", logoClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
aria-hidden={true}
/>
</div>
) : (
<h2 className={cls("w-full text-9xl font-medium truncate", logoTextClassName)}>
{logoText}
</h2>
)}
<div className={cls("w-full md:w-auto flex flex-col gap-2 text-accent text-base", contactClassName)}>
{contactItems.map((item, index) => {
const Icon = item.icon;
return (
<div key={index} className={cls("w-full md:w-auto flex items-center gap-2", contactItemClassName)}>
<Icon className={cls("h-[1em] w-auto", contactIconClassName)} />
<span className="whitespace-nowrap truncate" >{item.text}</span>
</div>
);
})}
</div>
</div>
</div>
</footer>
);
});
FooterSplit.displayName = "FooterSplit";
export default FooterSplit;