feat(Footer): icon-only CV download below sm
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
export interface Props {
|
||||||
|
/**
|
||||||
|
* CSS classes on the svg element
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File with downward arrow — download icon (Lucide file-down).
|
||||||
|
*/
|
||||||
|
export function FileDownIcon({ className }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden={true}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||||
|
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||||
|
<path d="M12 18v-6" />
|
||||||
|
<path d="m9 15 3 3 3-3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { CloseIcon } from './CloseIcon';
|
export { CloseIcon } from './CloseIcon';
|
||||||
|
export { FileDownIcon } from './FileDownIcon';
|
||||||
export { MagnifyIcon } from './MagnifyIcon';
|
export { MagnifyIcon } from './MagnifyIcon';
|
||||||
|
|||||||
@@ -155,6 +155,12 @@ describe('Footer', () => {
|
|||||||
expect(link).toHaveClass('brutal-border', 'uppercase');
|
expect(link).toHaveClass('brutal-border', 'uppercase');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('CV link carries the download icon for the narrow-viewport fallback', async () => {
|
||||||
|
render(await Footer());
|
||||||
|
const link = screen.getByRole('link', { name: /download cv/i });
|
||||||
|
expect(link.querySelector('svg')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('does not render CV link when no cv field', async () => {
|
it('does not render CV link when no cv field', async () => {
|
||||||
vi.mocked(getFirstRecord).mockResolvedValue({ ...mockSettings, cv: '' });
|
vi.mocked(getFirstRecord).mockResolvedValue({ ...mockSettings, cv: '' });
|
||||||
render(await Footer());
|
render(await Footer());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { SiteSettingsRecord } from '$shared/api';
|
import type { SiteSettingsRecord } from '$shared/api';
|
||||||
import { getFirstRecord } from '$shared/api';
|
import { getFirstRecord } from '$shared/api';
|
||||||
|
import { FileDownIcon } from '$shared/assets/icons';
|
||||||
import { buildFileUrl } from '$shared/lib';
|
import { buildFileUrl } from '$shared/lib';
|
||||||
import { Button, InlineSvg, Link } from '$shared/ui';
|
import { Button, InlineSvg, Link } from '$shared/ui';
|
||||||
|
|
||||||
@@ -42,8 +43,16 @@ export async function Footer() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{cvUrl && (
|
{cvUrl && (
|
||||||
<Button href={cvUrl} download size="sm" className="self-start sm:self-auto">
|
<Button
|
||||||
Download CV
|
href={cvUrl}
|
||||||
|
download
|
||||||
|
size="sm"
|
||||||
|
aria-label="Download CV"
|
||||||
|
className="inline-flex items-center self-start sm:self-auto"
|
||||||
|
>
|
||||||
|
{/* Icon-only below sm, matching the contact row's icon/label switch */}
|
||||||
|
<FileDownIcon className="w-5 h-5 sm:hidden" />
|
||||||
|
<span className="hidden sm:inline">Download CV</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user