-
+
Vocalinux
diff --git a/web/src/components/__tests__/optimized-image.test.tsx b/web/src/components/__tests__/optimized-image.test.tsx
new file mode 100644
index 0000000..e395fa8
--- /dev/null
+++ b/web/src/components/__tests__/optimized-image.test.tsx
@@ -0,0 +1,143 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { OptimizedImage, VocalinuxLogo } from '../optimized-image';
+
+// Mock Next.js Image component
+jest.mock('next/image', () => ({
+ __esModule: true,
+ default: (props: any) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { priority, ...imgProps } = props;
+ // eslint-disable-next-line @next/next/no-img-element
+ return ;
+ },
+}));
+
+describe('OptimizedImage', () => {
+ const defaultProps = {
+ src: '/test-image.png',
+ alt: 'Test Image',
+ width: 100,
+ height: 100,
+ };
+
+ it('renders without crashing', () => {
+ render( );
+ const img = screen.getByAltText('Test Image');
+ expect(img).toBeInTheDocument();
+ });
+
+ it('renders a picture element with WebP source', () => {
+ const { container } = render( );
+ const picture = container.querySelector('picture');
+ expect(picture).toBeInTheDocument();
+
+ const source = container.querySelector('source');
+ expect(source).toBeInTheDocument();
+ expect(source).toHaveAttribute('srcset', '/test-image.webp');
+ expect(source).toHaveAttribute('type', 'image/webp');
+ });
+
+ it('provides PNG fallback for browsers without WebP support', () => {
+ render( );
+ const img = screen.getByAltText('Test Image');
+ expect(img).toHaveAttribute('src', '/test-image.png');
+ });
+
+ it('converts jpg to webp in source srcset', () => {
+ const { container } = render(
+
+ );
+ const source = container.querySelector('source');
+ expect(source).toHaveAttribute('srcset', '/image.webp');
+ });
+
+ it('converts jpeg to webp in source srcset', () => {
+ const { container } = render(
+
+ );
+ const source = container.querySelector('source');
+ expect(source).toHaveAttribute('srcset', '/image.webp');
+ });
+
+ it('handles case-insensitive extensions', () => {
+ const { container } = render(
+
+ );
+ const source = container.querySelector('source');
+ expect(source).toHaveAttribute('srcset', '/image.webp');
+ });
+
+ it('applies custom className to picture element', () => {
+ const { container } = render(
+
+ );
+ const picture = container.querySelector('picture');
+ expect(picture).toHaveClass('custom-class');
+ });
+
+ it('passes priority prop to Image component', () => {
+ render( );
+ const img = screen.getByAltText('Test Image');
+ // Priority images should not have loading="lazy"
+ expect(img).not.toHaveAttribute('loading', 'lazy');
+ });
+
+ it('sets correct dimensions on the image', () => {
+ render( );
+ const img = screen.getByAltText('Test Image');
+ expect(img).toHaveAttribute('width', '200');
+ expect(img).toHaveAttribute('height', '150');
+ });
+});
+
+describe('VocalinuxLogo', () => {
+ it('renders without crashing', () => {
+ render( );
+ const img = screen.getByAltText('Vocalinux');
+ expect(img).toBeInTheDocument();
+ });
+
+ it('renders a picture element with WebP source for logo', () => {
+ const { container } = render( );
+ const picture = container.querySelector('picture');
+ expect(picture).toBeInTheDocument();
+
+ const source = container.querySelector('source');
+ expect(source).toBeInTheDocument();
+ expect(source).toHaveAttribute('srcset', '/vocalinux.webp');
+ expect(source).toHaveAttribute('type', 'image/webp');
+ });
+
+ it('provides PNG fallback for the logo', () => {
+ render( );
+ const img = screen.getByAltText('Vocalinux');
+ expect(img).toHaveAttribute('src', '/vocalinux.png');
+ });
+
+ it('uses default dimensions when not specified', () => {
+ render( );
+ const img = screen.getByAltText('Vocalinux');
+ expect(img).toHaveAttribute('width', '32');
+ expect(img).toHaveAttribute('height', '32');
+ });
+
+ it('accepts custom dimensions', () => {
+ render( );
+ const img = screen.getByAltText('Vocalinux');
+ expect(img).toHaveAttribute('width', '64');
+ expect(img).toHaveAttribute('height', '64');
+ });
+
+ it('applies custom className', () => {
+ const { container } = render( );
+ const picture = container.querySelector('picture');
+ expect(picture).toHaveClass('logo-class');
+ });
+
+ it('passes priority prop for eager loading', () => {
+ render( );
+ const img = screen.getByAltText('Vocalinux');
+ expect(img).not.toHaveAttribute('loading', 'lazy');
+ });
+});
diff --git a/web/src/components/icons.tsx b/web/src/components/icons.tsx
index 6b3137f..ae03953 100644
--- a/web/src/components/icons.tsx
+++ b/web/src/components/icons.tsx
@@ -3,8 +3,11 @@
import React from "react";
import Image from "next/image";
-// Logo component using the actual application icon
-export const VocalinuxLogo: React.FC<{ className?: string }> = ({ className = "" }) => {
+// Re-export from optimized-image component for backward compatibility
+export { VocalinuxLogo } from "./optimized-image";
+
+// Legacy export - use VocalinuxLogo from optimized-image instead
+export const VocalinuxLogoLegacy: React.FC<{ className?: string }> = ({ className = "" }) => {
return (
= ({
+ src,
+ alt,
+ width,
+ height,
+ className = "",
+ priority = false,
+}) => {
+ // Get WebP version of the image
+ const webpSrc = src.replace(/\.(png|jpg|jpeg)$/i, '.webp');
+
+ // For Next.js Image component with priority and proper optimization
+ // we use the Next.js Image with WebP source
+ return (
+
+
+
+
+ );
+};
+
+/**
+ * Specialized Vocalinux logo component with WebP support
+ */
+export const VocalinuxLogo: React.FC<{
+ className?: string;
+ width?: number;
+ height?: number;
+ priority?: boolean;
+}> = ({ className = "", width = 32, height = 32, priority = false }) => {
+ return (
+
+
+
+
+ );
+};