Skip to content

Commit 3254736

Browse files
feat: add footer component (#14)
* feat: footer component * feat: add base tailwind config for colors * feat: responsive footer, added supported social media options with defaults * fix: opinionated footer styling for icon width and line-height
1 parent 022ff13 commit 3254736

File tree

12 files changed

+438
-6
lines changed

12 files changed

+438
-6
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from "react";
2+
import { Meta } from "@storybook/react";
3+
4+
import Footer from "../footer";
5+
6+
export default {
7+
title: "Components/Footer",
8+
argTypes: {
9+
colorMode: { control: { type: "radio" }, options: ["light", "dark"], defaultValue: "light" }
10+
}
11+
} as Meta;
12+
13+
export const UnModifiedFooter = (args: any) => {
14+
const { colorMode } = args;
15+
const isDark = colorMode === "dark";
16+
return (
17+
<div className={isDark ? "dark" : ""}>
18+
<Footer>
19+
<Footer.Socials
20+
platforms={[
21+
{
22+
entity: "github",
23+
entityLink: "https://github.com/bitcoindevs",
24+
iconProps: {
25+
className: "hover:text-orange-400"
26+
}
27+
},
28+
{
29+
entity: "discord",
30+
entityLink: "https://discord.gg/bitcoindev",
31+
iconProps: {
32+
className: "hover:text-orange-400"
33+
}
34+
},
35+
{
36+
entity: "twitter",
37+
entityLink: "https://twitter.com/bitcoindevs",
38+
iconProps: {
39+
className: "hover:text-orange-400"
40+
}
41+
},
42+
{
43+
entity: "nostr",
44+
entityLink: "https://discord.gg/bitcoindevs",
45+
iconProps: {
46+
className: "hover:text-orange-400"
47+
}
48+
},
49+
]}
50+
/>
51+
<Footer.Public dshboardLink="https://visits.bitcoindevs.xyz/share/0Beh7BUzocqrtgA5/bitcoin-search" />
52+
<Footer.About
53+
entityLink="https://bitcoindevs.xyz"
54+
entityName="Bitcoin Dev Project"
55+
/>
56+
<Footer.Feedback feedbackLink="https://bitcoindevs.xyz/" />
57+
</Footer>
58+
</div>
59+
);
60+
};

src/components/footer/Footer.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from "react";
2+
import { FooterPartsPrimitiveProps } from "./types";
3+
4+
interface FooterRootProps extends FooterPartsPrimitiveProps<HTMLDivElement> {
5+
separator?: React.ReactElement;
6+
}
7+
8+
export const Separator = () => (
9+
<div className="h-5 border xl:h-6 xl:border-2 border-custom-stroke hidden xl:block" />
10+
);
11+
12+
const Footer = ({ children, className, separator, ...rest}: React.PropsWithChildren<FooterRootProps>) => {
13+
14+
const viewSeparator = separator ?? <Separator />;
15+
16+
const renderChildrenWithSeparator = () => {
17+
const newChildren: React.ReactNode[] = [];
18+
React.Children.forEach(children, (child, index) => {
19+
if (React.isValidElement(child)) {
20+
const displayName = (child.type as any).displayName;
21+
newChildren.push(child);
22+
if (index < React.Children.count(children) - 1) {
23+
newChildren.push(React.cloneElement(viewSeparator, { key: `${displayName}-separator-${index}` }));
24+
}
25+
}
26+
});
27+
return newChildren;
28+
};
29+
30+
return (
31+
<div>
32+
<div className={`flex flex-col md:flex-row w-full justify-between sm:items-stretch md:items-center bg-white dark:bg-black gap-[20px] md:gap-[24px] mx-auto max-w-[1920px] p-2 ${className}`}>
33+
{renderChildrenWithSeparator()}
34+
</div>
35+
</div>
36+
);
37+
};
38+
39+
export default Footer;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from "react"
2+
import { FooterPartsPrimitiveProps } from "./types"
3+
4+
interface FooterAboutProps extends FooterPartsPrimitiveProps<HTMLDivElement> {
5+
entityLink?: string
6+
entityName?: string
7+
}
8+
9+
const FooterAbout = (props: React.PropsWithChildren<FooterAboutProps>) => {
10+
const { className: classname, children, entityLink, entityName, ...rest } = props
11+
if (children) {
12+
<div {...rest} className={classname}>
13+
{props.children}
14+
</div>
15+
}
16+
return (
17+
<div {...rest} className={`leading-none md:leading-tight text-sm text-gray-500 dark:text-gray-400 ${classname}`}>
18+
Built with <span>🧡</span> by the{" "}
19+
<a
20+
href={entityLink ?? "https://bitcoindevs.xyz/"}
21+
target="_blank"
22+
rel="noreferrer"
23+
className="underline font-medium text-custom-brightOrange-100"
24+
>
25+
{entityName ?? "Bitcoin Dev Project"}
26+
</a>
27+
</div>
28+
)
29+
}
30+
31+
FooterAbout.displayName = "FooterAbout"
32+
export default FooterAbout
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from "react";
2+
import { FooterPartsPrimitiveProps } from "./types";
3+
4+
interface FooterFeedbackProps extends FooterPartsPrimitiveProps<HTMLDivElement> {
5+
feedbackLink: string;
6+
}
7+
8+
const FooterFeedback = (
9+
props: React.PropsWithChildren<FooterFeedbackProps>
10+
) => {
11+
const { className: classname, children, feedbackLink, ...rest } = props;
12+
if (children) {
13+
<div {...rest} className={props.className}>
14+
{props.children}
15+
</div>;
16+
}
17+
return (
18+
<div
19+
{...rest}
20+
className={`leading-none md:leading-tight flex flex-col sm:flex-row items-stretch sm:items-center text-sm text-gray-500 dark:text-gray-400 gap-[20px] md:gap-[24px] ${classname}`}
21+
>
22+
<span>We&apos;d love to hear your feedback on this project?</span>
23+
<a
24+
href={feedbackLink}
25+
target="_blank"
26+
rel="noreferrer"
27+
className="leading-none w-fit min-w-fit mx-auto text-base font-medium md:font-semibold py-4 px-5 rounded-[10px] text-[#FAFAFA] dark:text-[#292929] bg-[#292929] dark:bg-[#FAFAFA]"
28+
>
29+
Give Feedback
30+
</a>
31+
</div>
32+
);
33+
};
34+
35+
FooterFeedback.displayName = "FooterFeedback";
36+
export default FooterFeedback;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from "react"
2+
import { FooterPartsPrimitiveProps } from "./types"
3+
4+
interface FooterLegalProps extends FooterPartsPrimitiveProps<HTMLDivElement> {}
5+
6+
const FooterLegal = (props: React.PropsWithChildren<FooterLegalProps>) => {
7+
const { className: classname, children, ...rest } = props
8+
if (children) {
9+
return (
10+
<div className={`${props.className} text-sm text-gray-500 dark:text-gray-400`}>
11+
{props.children}
12+
</div>
13+
)
14+
}
15+
return (
16+
<div {...rest} className={`text-sm text-gray-500 dark:text-gray-400 flex gap-2 justify-between ${classname}`}>
17+
<p>© {new Date().getFullYear()} ChaincodeLabs</p>
18+
<p>Privacy Policy</p>
19+
<p>Terms of Use</p>
20+
</div>
21+
)
22+
}
23+
FooterLegal.displayName = "FooterLegal"
24+
export default FooterLegal
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from "react";
2+
import { FooterPartsPrimitiveProps } from "./types";
3+
4+
interface FooterPublicProps extends FooterPartsPrimitiveProps<HTMLAnchorElement> {
5+
dshboardLink: string;
6+
}
7+
8+
9+
const FooterPublic = ({ className: classname, dshboardLink, ...rest }: FooterPublicProps) => {
10+
return (
11+
<a
12+
href={dshboardLink}
13+
target="_blank"
14+
rel="noreferrer"
15+
className={`leading-none md:leading-tight text-sm text-gray-500 dark:text-gray-400 underline ${classname}`}
16+
{...rest}
17+
>
18+
View our public visitor count
19+
</a>
20+
);
21+
};
22+
23+
export default FooterPublic;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from "react";
2+
import { FooterPartsPrimitiveProps } from "./types";
3+
import { TwitterXIcon, GithubIcon, DiscordIcon, NostrIcon } from "../../icons";
4+
5+
type SupportedSocialMedia = "twitter" | "github" | "discord" | "nostr";
6+
7+
type ManadatorySocialMediaProps<T> = {
8+
entityLink: string;
9+
iconProps?: React.SVGProps<SVGSVGElement>;
10+
} & T;
11+
12+
13+
type SocialMediaProps =
14+
| ManadatorySocialMediaProps<{
15+
entity: SupportedSocialMedia;
16+
icon?: React.ReactElement;
17+
}>
18+
| ManadatorySocialMediaProps<{
19+
entity: Exclude<string, SupportedSocialMedia>;
20+
icon: React.ReactElement;
21+
}>;
22+
23+
interface FooterSocialsProps extends FooterPartsPrimitiveProps<HTMLDivElement> {
24+
platforms: SocialMediaProps[];
25+
}
26+
27+
const Platform = ({ platform }: { platform: SocialMediaProps }) => {
28+
const { entity, entityLink, icon, iconProps } = platform;
29+
const { className, ...rest } = iconProps ?? {};
30+
const getIcon = (entity: SocialMediaProps["entity"]) => {
31+
if (icon) {
32+
return React.cloneElement(icon, { ...rest, className });
33+
}
34+
if (entity === "twitter") {
35+
return <TwitterXIcon className={`w-full ${className}`} {...rest} />;
36+
}
37+
if (entity === "github") {
38+
return <GithubIcon className={`w-full ${className}`} {...rest} />;
39+
}
40+
if (entity === "discord") {
41+
return <DiscordIcon className={`w-full ${className}`} {...rest} />;
42+
}
43+
if (entity === "nostr") {
44+
return <NostrIcon className={`w-full ${className}`} {...rest} />;
45+
}
46+
};
47+
const iconElement = getIcon(entity);
48+
49+
return (
50+
<div className="flex items-center justify-center w-full max-w-[40px] min-w-[24px]">
51+
<a
52+
href={entityLink}
53+
target="_blank"
54+
rel="noreferrer"
55+
className="underline font-medium"
56+
>
57+
{iconElement}
58+
</a>
59+
</div>
60+
);
61+
};
62+
63+
/**
64+
* FooterSocials Component
65+
* @description Renders social media icons with links in the footer.
66+
* @param {FooterSocialsProps} props - The component props
67+
* @param {SocialMediaProps[]} props.platforms - Array of social media platform configurations
68+
* @remarks
69+
* Provides corresponding icons for twitter, github, discord, and nostr entities.
70+
* If a custom string is passed as entity, the icon prop is required.
71+
*/
72+
73+
export const FooterSocials = (
74+
props: React.PropsWithChildren<FooterSocialsProps>
75+
) => {
76+
const { className: classname, children, platforms, ...rest } = props;
77+
if (children) {
78+
<div {...rest} className={classname}>
79+
{props.children}
80+
</div>;
81+
}
82+
return (
83+
<div
84+
{...rest}
85+
className={`text-black mb-[6px] md:mb-0 dark:text-white flex w-fit max-w-full gap-[24px] ${classname}`}
86+
>
87+
{platforms.map((platform, index) => (
88+
<Platform key={platform.entity} platform={platform} />
89+
))}
90+
</div>
91+
);
92+
};
93+
94+
FooterSocials.displayName = "FooterSocials";

src/components/footer/index.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from "react";
2+
import FooterRoot from "./Footer";
3+
import FooterAbout from "./FooterAbout";
4+
import FooterFeedback from "./FooterFeedback";
5+
import FooterLegal from "./FooterLegal";
6+
import { FooterSocials } from "./FooterSocials";
7+
import FooterPublic from "./FooterPublic";
8+
9+
const Footer: typeof FooterRoot & {
10+
About: typeof FooterAbout;
11+
Feedback: typeof FooterFeedback;
12+
Legal: typeof FooterLegal;
13+
Socials: typeof FooterSocials;
14+
Public: typeof FooterPublic;
15+
} = (props) => {
16+
return <FooterRoot {...props} />;
17+
};
18+
19+
// Attach subcomponents as properties
20+
21+
Footer.About = FooterAbout;
22+
Footer.Feedback = FooterFeedback;
23+
Footer.Legal = FooterLegal;
24+
Footer.Socials = FooterSocials;
25+
Footer.Public = FooterPublic;
26+
27+
export default Footer;

src/components/footer/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface FooterPartsPrimitiveProps<T>
2+
extends React.HTMLAttributes<T> {
3+
className?: string;
4+
}

src/icons/DiscordIcon.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ const DiscordIcon = ({
99
}: SVGProps<SVGSVGElement> & { pathProps?: SVGProps<SVGPathElement> }) => (
1010
// height is destructed and unused, scaling is defined by width
1111
// pathProps is destructured and unused
12-
1312
<svg
1413
width={width}
15-
viewBox="0 0 45 45"
14+
viewBox="0 0 41 33"
1615
fill="none"
1716
xmlns="http://www.w3.org/2000/svg"
1817
{...props}
1918
>
20-
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9997 6.9165C13.652 6.9165 11.1202 7.48117 8.34634 8.84334C8.1873 8.9212 8.05422 9.04346 7.96318 9.19534C6.05834 12.3707 2.33301 21.2092 2.33301 31.6665C2.33253 31.9044 2.42457 32.1332 2.58968 32.3045C4.33501 34.103 5.92084 35.4267 7.62218 36.3617C9.33268 37.3003 11.1147 37.8228 13.223 38.0758C13.3988 38.0972 13.577 38.0672 13.7361 37.9894C13.8952 37.9117 14.0284 37.7895 14.1195 37.6377L15.0545 36.0757C13.6923 35.599 12.3008 34.9665 11.4428 34.1085C11.3116 33.9816 11.2069 33.8298 11.1349 33.662C11.0629 33.4943 11.0251 33.3138 11.0236 33.1312C11.0221 32.9487 11.057 32.7676 11.1262 32.5987C11.1954 32.4297 11.2976 32.2763 11.4267 32.1472C11.5559 32.0182 11.7095 31.9161 11.8785 31.8471C12.0475 31.778 12.2286 31.7433 12.4111 31.745C12.5937 31.7467 12.7741 31.7847 12.9418 31.8568C13.1096 31.929 13.2612 32.0338 13.388 32.1652C13.8555 32.6327 14.9537 33.1643 16.5047 33.6612C17.7862 33.9967 20.0173 34.4165 22.4997 34.4165C24.982 34.4165 27.2132 33.9967 28.4947 33.6612C30.0457 33.1643 31.1438 32.6308 31.6113 32.1652C31.7372 32.0301 31.889 31.9217 32.0577 31.8466C32.2264 31.7714 32.4084 31.731 32.5931 31.7278C32.7777 31.7245 32.9611 31.7585 33.1323 31.8276C33.3035 31.8968 33.459 31.9997 33.5896 32.1303C33.7201 32.2608 33.8231 32.4164 33.8922 32.5876C33.9614 32.7588 33.9953 32.9422 33.9921 33.1268C33.9888 33.3114 33.9484 33.4935 33.8733 33.6622C33.7981 33.8308 33.6898 33.9826 33.5547 34.1085C32.6985 34.9665 31.307 35.599 29.943 36.0757L30.8798 37.6377C30.9707 37.7898 31.1038 37.9124 31.2629 37.9905C31.422 38.0686 31.6004 38.0989 31.7763 38.0777C33.8847 37.8228 35.6667 37.3003 37.3772 36.3617C39.0785 35.4267 40.6643 34.103 42.4078 32.3045C42.5736 32.1335 42.6663 31.9047 42.6663 31.6665C42.6663 21.2092 38.941 12.3707 37.0362 9.19534C36.9451 9.04346 36.812 8.9212 36.653 8.84334C33.8792 7.48117 31.3473 6.9165 27.9997 6.9165C27.8074 6.91665 27.62 6.97728 27.464 7.0898C27.3081 7.20232 27.1914 7.36105 27.1307 7.5435L26.5147 9.3915C25.2175 8.97154 23.8631 8.75508 22.4997 8.74984C21.1362 8.75508 19.7818 8.97154 18.4847 9.3915L17.8687 7.5435C17.8079 7.36105 17.6913 7.20232 17.5353 7.0898C17.3794 6.97728 17.192 6.91665 16.9997 6.9165ZM18.833 23.8748C18.833 25.6458 17.601 27.0832 16.083 27.0832C14.565 27.0832 13.333 25.6458 13.333 23.8748C13.333 22.1038 14.565 20.6665 16.083 20.6665C17.601 20.6665 18.833 22.1038 18.833 23.8748ZM28.9163 27.0832C30.4343 27.0832 31.6663 25.6458 31.6663 23.8748C31.6663 22.1038 30.4343 20.6665 28.9163 20.6665C27.3983 20.6665 26.1663 22.1038 26.1663 23.8748C26.1663 25.6458 27.3983 27.0832 28.9163 27.0832Z" fill="currentColor"/>
19+
<path
20+
fill-rule="evenodd"
21+
clip-rule="evenodd"
22+
d="M14.9997 0.916504C11.652 0.916504 9.12018 1.48117 6.34634 2.84334C6.1873 2.9212 6.05422 3.04346 5.96318 3.19534C4.05834 6.37067 0.33301 15.2092 0.33301 25.6665C0.332528 25.9044 0.424568 26.1332 0.589676 26.3045C2.33501 28.103 3.92084 29.4267 5.62218 30.3617C7.33268 31.3003 9.11468 31.8228 11.223 32.0758C11.3988 32.0972 11.577 32.0672 11.7361 31.9894C11.8952 31.9117 12.0284 31.7895 12.1195 31.6377L13.0545 30.0757C11.6923 29.599 10.3008 28.9665 9.44284 28.1085C9.31158 27.9816 9.2069 27.8298 9.13492 27.662C9.06293 27.4943 9.02509 27.3138 9.02359 27.1312C9.02209 26.9487 9.05697 26.7676 9.12618 26.5987C9.1954 26.4297 9.29757 26.2763 9.42673 26.1472C9.55589 26.0182 9.70946 25.9161 9.87848 25.8471C10.0475 25.778 10.2286 25.7433 10.4111 25.745C10.5937 25.7467 10.7741 25.7847 10.9418 25.8568C11.1096 25.929 11.2612 26.0338 11.388 26.1652C11.8555 26.6327 12.9537 27.1643 14.5047 27.6612C15.7862 27.9967 18.0173 28.4165 20.4997 28.4165C22.982 28.4165 25.2132 27.9967 26.4947 27.6612C28.0457 27.1643 29.1438 26.6308 29.6113 26.1652C29.7372 26.0301 29.889 25.9217 30.0577 25.8466C30.2264 25.7714 30.4084 25.731 30.5931 25.7278C30.7777 25.7245 30.9611 25.7585 31.1323 25.8276C31.3035 25.8968 31.459 25.9997 31.5896 26.1303C31.7201 26.2608 31.8231 26.4164 31.8922 26.5876C31.9614 26.7588 31.9953 26.9422 31.9921 27.1268C31.9888 27.3114 31.9484 27.4935 31.8733 27.6622C31.7981 27.8308 31.6898 27.9826 31.5547 28.1085C30.6985 28.9665 29.307 29.599 27.943 30.0757L28.8798 31.6377C28.9707 31.7898 29.1038 31.9124 29.2629 31.9905C29.422 32.0686 29.6004 32.0989 29.7763 32.0777C31.8847 31.8228 33.6667 31.3003 35.3772 30.3617C37.0785 29.4267 38.6643 28.103 40.4078 26.3045C40.5736 26.1335 40.6663 25.9047 40.6663 25.6665C40.6663 15.2092 36.941 6.37067 35.0362 3.19534C34.9451 3.04346 34.812 2.9212 34.653 2.84334C31.8792 1.48117 29.3473 0.916504 25.9997 0.916504C25.8074 0.91665 25.62 0.977275 25.464 1.0898C25.3081 1.20232 25.1914 1.36105 25.1307 1.5435L24.5147 3.3915C23.2175 2.97154 21.8631 2.75508 20.4997 2.74984C19.1362 2.75508 17.7818 2.97154 16.4847 3.3915L15.8687 1.5435C15.8079 1.36105 15.6913 1.20232 15.5353 1.0898C15.3794 0.977275 15.192 0.91665 14.9997 0.916504ZM16.833 17.8748C16.833 19.6458 15.601 21.0832 14.083 21.0832C12.565 21.0832 11.333 19.6458 11.333 17.8748C11.333 16.1038 12.565 14.6665 14.083 14.6665C15.601 14.6665 16.833 16.1038 16.833 17.8748ZM26.9163 21.0832C28.4343 21.0832 29.6663 19.6458 29.6663 17.8748C29.6663 16.1038 28.4343 14.6665 26.9163 14.6665C25.3983 14.6665 24.1663 16.1038 24.1663 17.8748C24.1663 19.6458 25.3983 21.0832 26.9163 21.0832Z"
23+
fill="currentColor"
24+
/>
2125
</svg>
2226
);
2327

0 commit comments

Comments
 (0)