feat: Footer widget with email link and CV download, added to root layout

This commit is contained in:
Ilia Mashkov
2026-05-18 14:15:25 +03:00
parent 0552a2a8e5
commit 7e542597d0
5 changed files with 77 additions and 1 deletions
+5 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { fraunces, publicSans } from '$shared/lib'; import { fraunces, publicSans } from '$shared/lib';
import { Footer } from '$widgets/Footer';
import './globals.css'; import './globals.css';
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -13,7 +14,10 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<body className={`${fraunces.variable} ${publicSans.variable}`}>{children}</body> <body className={`${fraunces.variable} ${publicSans.variable}`}>
{children}
<Footer />
</body>
</html> </html>
); );
} }
+1
View File
@@ -0,0 +1 @@
export { Footer } from './ui/Footer/Footer';
@@ -0,0 +1,47 @@
import { render, screen } from '@testing-library/react';
import { Footer } from './Footer';
describe('Footer', () => {
describe('structure', () => {
it('renders a footer element', () => {
const { container } = render(<Footer />);
expect(container.querySelector('footer')).toBeInTheDocument();
});
it('has brutal-border-top separator', () => {
const { container } = render(<Footer />);
expect(container.querySelector('footer')).toHaveClass('brutal-border-top');
});
});
describe('email link', () => {
it('renders the contact email as a mailto link', () => {
render(<Footer />);
const link = screen.getByRole('link', { name: /hello@allmy\.work/i });
expect(link).toHaveAttribute('href', 'mailto:hello@allmy.work');
});
});
describe('CV download', () => {
it('renders a CV download link', () => {
render(<Footer />);
expect(screen.getByRole('link', { name: /download cv/i })).toBeInTheDocument();
});
it('CV link points to the cv file', () => {
render(<Footer />);
expect(screen.getByRole('link', { name: /download cv/i })).toHaveAttribute('href', '/cv.pdf');
});
it('CV link has download attribute', () => {
render(<Footer />);
expect(screen.getByRole('link', { name: /download cv/i })).toHaveAttribute('download');
});
it('CV link has button styling', () => {
render(<Footer />);
const link = screen.getByRole('link', { name: /download cv/i });
expect(link).toHaveClass('brutal-border', 'uppercase');
});
});
});
+23
View File
@@ -0,0 +1,23 @@
import { CONTACT_LINKS } from '$shared/lib';
/**
* Site-wide footer with contact email and CV download link.
*/
export function Footer() {
return (
<footer className="brutal-border-top px-8 py-6 lg:px-16 lg:py-8">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<a href={`mailto:${CONTACT_LINKS.email}`} className="text-sm opacity-60 hover:opacity-100 transition-opacity">
{CONTACT_LINKS.email}
</a>
<a
href="/cv.pdf"
download
className="brutal-border px-4 py-2 bg-blue text-cream text-sm uppercase tracking-wider btn-transition self-start sm:self-auto"
>
Download CV
</a>
</div>
</footer>
);
}
+1
View File
@@ -1 +1,2 @@
export * from './Footer';
export * from './Navigation'; export * from './Navigation';