Skip to content

Commit 1862822

Browse files
committed
feat: add footer and jsonld schemas
1 parent 9bcee34 commit 1862822

File tree

9 files changed

+295
-12
lines changed

9 files changed

+295
-12
lines changed

app/[lang]/layout.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint-env node */
22
import { JetBrains_Mono as FontMono, Inter as FontSans } from "next/font/google";
33
import {
4-
Footer,
54
LastUpdated,
65
Layout,
76
LocaleSwitch,
@@ -14,6 +13,7 @@ import { getDictionary } from '../_dictionaries/get-dictionary';
1413
import PlausibleProvider from 'next-plausible';
1514
import { DiscordIcon, GitHubIcon } from "nextra/icons";
1615
import '../../styles/globals.css';
16+
import { Footer } from "../_components/footer";
1717
import { GITHUB_REPO_URL, PROJECT_URL } from "../_constants/project";
1818
import { metadataSEO } from "../metadata";
1919

@@ -59,16 +59,7 @@ export default async function RootLayout({ children, params }) {
5959
)
6060

6161
const footer = (
62-
<Footer>
63-
<div className="flex flex-col items-start justify-start">
64-
<p className="text-gray-500">
65-
&copy; {new Date().getFullYear()} UX Patterns for Devs
66-
</p>
67-
<p className="text-gray-500">
68-
Made with ❤️ by <a href="https://thedaviddias.com" target="_blank" rel="noopener noreferrer" className="font-bold underline">David Dias</a> for the Open-Source Community.
69-
</p>
70-
</div>
71-
</Footer>
62+
<Footer lang={lang} />
7263
)
7364
return (
7465
<html lang={lang} suppressHydrationWarning>

app/_components/footer.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
Footer as NextraFooter,
3+
} from 'nextra-theme-docs';
4+
5+
import { FOOTER_MENU_LINKS } from '../_constants/footer';
6+
import { getDictionary } from '../_dictionaries/get-dictionary';
7+
import { LinkCustom } from './link-custom';
8+
import { SOCIAL_LINKS } from './social';
9+
10+
const FooterMenuLinks = ({ lang }: { lang: string }) => {
11+
return (
12+
<div>
13+
<h3 className="small-title">General</h3>
14+
<ul className="mt-3 space-y-1">
15+
{FOOTER_MENU_LINKS(lang).map(({ path, label }) => (
16+
<li key={label}>
17+
<LinkCustom href={path} className="dark:!text-white">
18+
{label}
19+
</LinkCustom>
20+
</li>
21+
))}
22+
</ul>
23+
</div>
24+
)
25+
}
26+
27+
const FooterSocialLinks = () => {
28+
return (
29+
<div>
30+
<h3 className="small-title">Social</h3>
31+
<ul className="mt-3 space-y-1">
32+
{SOCIAL_LINKS.map(({ label, shortlink }) => (
33+
<li key={label}>
34+
<LinkCustom href={shortlink} className="dark:!text-white">
35+
{label}
36+
</LinkCustom>
37+
</li>
38+
))}
39+
</ul>
40+
</div>
41+
)
42+
}
43+
44+
export const Footer = async ({ lang }: { lang: string }) => {
45+
const dictionary = await getDictionary(lang)
46+
47+
return (
48+
<NextraFooter className="mt-auto sm:pt-8 w-full">
49+
<div
50+
className="main-footer transform w-full"
51+
aria-labelledby="footer-heading"
52+
>
53+
<h2 id="footer-heading" className="sr-only">
54+
Footer
55+
</h2>
56+
<div className="mx-auto max-w-5xl px-2 py-12 sm:px-6 lg:px-8 lg:py-7">
57+
<div className="flex flex-col-reverse sm:flex-row print:hidden">
58+
<div className="w-full flex-grow text-left sm:mb-0 sm:w-1/2 md:pr-24 lg:w-[40%]">
59+
<span className="mb-5 block text-xl font-bold">{dictionary.name}</span>
60+
<p
61+
className="text-sm text-gray-500 dark:text-gray-400">
62+
{dictionary.description}
63+
</p>
64+
<div className="flex space-x-6"></div>
65+
</div>
66+
67+
<div className="flex w-full !max-w-full flex-shrink-0 flex-grow justify-between text-gray-500 sm:w-1/2 lg:w-[40%] dark:text-gray-400">
68+
<FooterMenuLinks lang={lang} />
69+
<FooterSocialLinks />
70+
</div>
71+
</div>
72+
<div className="mt-8 flex items-start flex-col border-t border-gray-200 pt-3 dark:border-gray-400">
73+
<p className="text-gray-500">
74+
&copy; {new Date().getFullYear()} UX Patterns for Devs
75+
</p>
76+
<p className="text-gray-500">
77+
Made with ❤️ by <a href="https://thedaviddias.com" target="_blank" rel="noopener noreferrer" className="font-bold underline">David Dias</a> for the Open-Source Community.
78+
</p>
79+
</div>
80+
</div>
81+
</div>
82+
</NextraFooter >
83+
)
84+
}

app/_components/json-ld.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BASE_URL } from '@app/_constants/project'
22

33
interface JsonLdProps {
4-
data: Record<string, any>
4+
data: Record<string, unknown>
55
}
66

77
export function JsonLd({ data }: JsonLdProps) {

app/_components/link-custom.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import clsx from 'clsx'
2+
import Link, { LinkProps } from 'next/link'
3+
import React from 'react'
4+
5+
type CustomLinkProps = LinkProps & {
6+
href: string
7+
className?: string
8+
children: React.ReactNode
9+
rel?: string
10+
icon?: boolean
11+
}
12+
13+
export const LinkCustom: React.FC<CustomLinkProps> = ({
14+
href,
15+
className,
16+
children,
17+
icon = true,
18+
...rest
19+
}) => {
20+
const isInternalLink = href && (href.startsWith('/') || href.startsWith('#'))
21+
22+
if (!isInternalLink) {
23+
return (
24+
<a
25+
href={href}
26+
rel="noopener noreferrer"
27+
target="_blank"
28+
className={clsx(
29+
'cursor-pointer inline-flex',
30+
className
31+
)}
32+
{...rest}
33+
>
34+
{children}
35+
{icon && (
36+
<span className="inline-flex items-center">
37+
<svg
38+
stroke="currentColor"
39+
fill="none"
40+
strokeWidth="2"
41+
viewBox="0 0 24 24"
42+
strokeLinecap="round"
43+
strokeLinejoin="round"
44+
focusable="false"
45+
aria-hidden="true"
46+
height="1em"
47+
width="1em"
48+
xmlns="http://www.w3.org/2000/svg"
49+
>
50+
<line x1="7" y1="17" x2="17" y2="7"></line>
51+
<polyline points="7 7 17 7 17 17"></polyline>
52+
</svg>
53+
</span>
54+
)}
55+
</a>
56+
)
57+
}
58+
59+
return (
60+
<Link
61+
href={href}
62+
className={clsx('cursor-pointer', className)}
63+
{...rest}
64+
>
65+
{children}
66+
</Link>
67+
)
68+
}

app/_components/social.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { siBluesky, siDiscord, siGithub, siLinkedin, siX } from 'simple-icons'
2+
import { SimpleIconComponent } from './browser-support'
3+
4+
interface SocialLink {
5+
label: string
6+
link: string
7+
shortlink: string
8+
icon?: React.ReactNode
9+
rel?: 'me'
10+
}
11+
12+
export const SOCIAL_LINKS: SocialLink[] = [
13+
{
14+
label: 'X',
15+
link: 'https://x.com/thedaviddias',
16+
shortlink: 'https://ddias.link/x',
17+
rel: 'me',
18+
icon: (
19+
<SimpleIconComponent
20+
icon={siX}
21+
className="w-10 h-10 p-1.5 border rounded-full transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200 dark:border-gray-700"
22+
/>
23+
),
24+
},
25+
{
26+
label: 'BlueSky',
27+
link: 'https://bsky.social/thedaviddias.com',
28+
shortlink: 'https://ddias.link/bsky',
29+
rel: 'me',
30+
icon: (
31+
<SimpleIconComponent
32+
icon={siBluesky}
33+
className="w-10 h-10 p-1.5 border rounded-full transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200 dark:border-gray-700"
34+
/>
35+
),
36+
},
37+
{
38+
label: 'LinkedIn',
39+
link: 'https://www.linkedin.com/in/thedaviddias',
40+
shortlink: 'https://ddias.link/linkedin',
41+
rel: 'me',
42+
icon: (
43+
<SimpleIconComponent
44+
icon={siLinkedin}
45+
className="w-10 h-10 p-1.5 border rounded-full transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200 dark:border-gray-700"
46+
/>
47+
),
48+
},
49+
{
50+
label: 'Github',
51+
link: 'https://github.com/thedaviddias',
52+
shortlink: 'https://ddias.link/github',
53+
rel: 'me',
54+
icon: (
55+
<SimpleIconComponent
56+
icon={siGithub}
57+
className="w-10 h-10 p-1.5 border rounded-full transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200 dark:border-gray-700"
58+
/>
59+
),
60+
},
61+
{
62+
label: 'Join my Discord',
63+
link: 'https://discord.gg/EG6tmxsESP',
64+
shortlink: 'https://ddias.link/discord',
65+
icon: (
66+
<SimpleIconComponent
67+
icon={siDiscord}
68+
className="w-10 h-10 p-1.5 border rounded-full transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200 dark:border-gray-700"
69+
/>
70+
),
71+
},
72+
{
73+
label: 'Newsletter',
74+
link: 'https://thedaviddias.substack.com',
75+
shortlink: 'https://ddias.link/newsletter',
76+
rel: 'me',
77+
},
78+
]

app/_constants/footer.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const FOOTER_MENU_LINKS = (lang: string) => [
2+
{
3+
path: '/',
4+
label: 'Home'
5+
},
6+
{
7+
path: '/docs',
8+
label: 'Documentation'
9+
},
10+
{
11+
path: `/${lang}/about/about`,
12+
label: 'About'
13+
}
14+
]

app/_dictionaries/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export default {
44
system: 'System',
55
backToTop: 'Scroll to top',
66
lastUpdated: 'Last updated on',
7+
name: 'UX Patterns for Devs',
8+
description: 'UX Patterns for Developers is a collection of UX patterns for everyone but specially towards developers who want to understand how to build effective UI components accessible and usable.',
79
logo: {
810
title: 'All you need to know about UX Patterns'
911
},
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { BASE_URL } from '@app/_constants/project'
2+
3+
interface BreadcrumbItem {
4+
title: string
5+
url?: string
6+
}
7+
8+
export function generateBreadcrumbSchema(breadcrumbs: BreadcrumbItem[]) {
9+
return {
10+
"@context": "https://schema.org",
11+
"@type": "BreadcrumbList",
12+
"itemListElement": breadcrumbs.map((crumb, index) => {
13+
const isLastItem = index === breadcrumbs.length - 1
14+
const baseItem = {
15+
"@type": "ListItem",
16+
"position": index + 1,
17+
"name": crumb.title,
18+
}
19+
20+
if (!isLastItem && crumb.url) {
21+
baseItem["item"] = `${BASE_URL}${crumb.url}`
22+
}
23+
24+
return baseItem
25+
})
26+
}
27+
}

content/en/docs/forms/button.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ end
9292
- Should maintain the button's original width
9393
- Prevents multiple clicks during loading
9494
- Typically uses a spinner or progress indicator
95+
- Add loading text alongside spinners (e.g., "Saving..." vs just a spinner)
9596

9697
5. **Visual States**
9798

@@ -130,6 +131,7 @@ end
130131
- Provide visual feedback for all interactive states
131132
- Ensure keyboard navigation works properly
132133
- Include loading states for asynchronous actions
134+
- Use descriptive loading states with both text and visual indicators
133135
- Maintain ARIA labels and roles
134136

135137
**Don'ts ❌**
@@ -163,6 +165,23 @@ end
163165
- Don't make disabled buttons look interactive
164166
- Don't use inconsistent corner radius within button groups
165167
- Avoid having buttons looking like links or vice versa
168+
- Don't use spinners without descriptive text
169+
170+
### Mobile & Touch Considerations
171+
172+
**Do's ✅**
173+
174+
- Place primary actions within thumb-friendly zones
175+
- Use full-width buttons for important actions on mobile
176+
- Implement haptic feedback for touch interactions
177+
- Maintain sufficient spacing between touch targets (minimum 8px)
178+
179+
**Don'ts ❌**
180+
181+
- Don't place critical actions in hard-to-reach corners
182+
- Don't rely solely on hover states for mobile feedback
183+
- Don't make touch targets smaller than 44x44px
184+
- Don't crowd multiple buttons together on mobile views
166185

167186
### Layout & Positioning
168187

0 commit comments

Comments
 (0)