Passa al contenuto principale

Sviluppo Frontend

Guida completa allo sviluppo frontend con Next.js 14, React, TypeScript e Tailwind CSS nell'ecosistema Emblema.

πŸš€ Stack Tecnologico​

Core Framework​

  • Next.js 14: App Router, Server Components, API Routes
  • React 18: Components, Hooks, Suspense
  • TypeScript 5: Type safety completo
  • Tailwind CSS: Utility-first styling

UI e Componenti​

  • Shadcn/ui: Component library base
  • Radix UI: Primitive headless components
  • Lucide React: Icon system
  • Framer Motion: Animazioni

State Management​

  • SWR: Server state e caching
  • React Hook Form: Form state management
  • Zustand: Client state (se necessario)
  • Context API: State globale (settings, user)

Sviluppo e Build​

  • Turbo: Build orchestration
  • ESLint + Prettier: Code quality
  • pnpm: Package management

πŸ“ Struttura App Principale​

apps/www-emblema/
β”œβ”€β”€ app/ # Next.js 14 App Router
β”‚ β”œβ”€β”€ layout.tsx # Root layout
β”‚ β”œβ”€β”€ page.tsx # Home page
β”‚ β”œβ”€β”€ dashboard/ # Dashboard routes
β”‚ β”œβ”€β”€ documents/ # Document management
β”‚ β”œβ”€β”€ chat/ # Chat interface
β”‚ └── settings/ # Settings pages
β”‚ β”œβ”€β”€ api/ # API Routes
β”‚ β”‚ β”œβ”€β”€ v1/ # Versioned API
β”‚ β”‚ β”œβ”€β”€ auth/ # NextAuth endpoints
β”‚ β”‚ └── cron/ # Scheduled tasks
β”‚ β”œβ”€β”€ globals.css # Global styles
β”‚ └── layout.tsx # Root app layout
β”œβ”€β”€ components/ # React components
β”œβ”€β”€ hooks/ # Custom hooks
β”œβ”€β”€ lib/ # Utilities
β”œβ”€β”€ config/ # App configuration
β”œβ”€β”€ middleware.ts # Next.js middleware
β”œβ”€β”€ schema/ # Zod validation
└── types/ # Type definitions

🧩 Componenti e Pattern​

1. Component Organization​

Per Feature (Domain-Driven)​

// components/document/
β”œβ”€β”€ form.tsx # Document form
β”œβ”€β”€ list.tsx # Document list
β”œβ”€β”€ view.tsx # Single document view
β”œβ”€β”€ data-table.tsx # Sortable table
β”œβ”€β”€ preview.tsx # File preview
└── empty-state.tsx # Empty state

// components/chat/
β”œβ”€β”€ chat.tsx # Main chat component
β”œβ”€β”€ message.tsx # Single message
β”œβ”€β”€ input.tsx # Chat input
β”œβ”€β”€ sidebar.tsx # Chat sidebar
└── artifact.tsx # AI artifacts

Form Pattern con forwardRef​

import { forwardRef, useImperativeHandle } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { FormRef } from '@/types/form-refs';
import { UI_CONSTANTS } from '@/lib/constants';

interface DocumentFormProps extends BaseFormProps<DocumentSchema> {
onCancel?: () => void;
}

export const DocumentForm = forwardRef<FormRef<DocumentSchema>, DocumentFormProps>(
({ defaultValues, onSubmit, variant = 'page', onCancel }, ref) => {

const form = useForm<DocumentSchema>({
resolver: zodResolver(createDocumentSchema()),
defaultValues,
});

useImperativeHandle(ref, () => ({
submit: async () => await form.handleSubmit(onSubmit)(),
reset: (values) => form.reset(values || defaultValues),
getValues: () => form.getValues(),
isDirty: () => form.formState.isDirty,
isValid: () => form.formState.isValid,
}));

const showButtons = variant === 'page';

return (
<Form {...form}>
<form onSubmit={showButtons ? form.handleSubmit(onSubmit) : undefined}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome Documento</FormLabel>
<FormControl>
<Input
placeholder="Inserisci il nome del documento"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

{showButtons && (
<div className="flex gap-2 justify-end">
<Button type="button" variant="outline" onClick={onCancel}>
{UI_CONSTANTS.actions.cancel}
</Button>
<Button type="submit">
{UI_CONSTANTS.actions.save}
</Button>
</div>
)}
</form>
</Form>
);
}
);

DocumentForm.displayName = 'DocumentForm';

2. Custom Hooks Pattern​

Data Fetching Hook​

import useSWR from "swr";
import { useCallback } from "react";

interface UseDocumentsOptions {
filters?: {
status?: string;
knowledgeBaseId?: string;
};
enabled?: boolean;
}

export const useDocuments = (options: UseDocumentsOptions = {}) => {
const { filters, enabled = true } = options;

const searchParams = new URLSearchParams();
if (filters?.status) searchParams.set("status", filters.status);
if (filters?.knowledgeBaseId)
searchParams.set("kb_id", filters.knowledgeBaseId);

const { data, error, mutate, isLoading } = useSWR(
enabled ? `/api/v1/documents?${searchParams}` : null,
async (url: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch documents");
return response.json();
},
);

const createDocument = useCallback(
async (documentData: CreateDocumentInput) => {
const response = await fetch("/api/v1/documents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(documentData),
});

if (!response.ok) throw new Error("Failed to create document");

const newDocument = await response.json();
mutate(); // Refresh list
return newDocument;
},
[mutate],
);

return {
documents: data?.documents || [],
total: data?.total || 0,
error,
isLoading,
createDocument,
refresh: mutate,
};
};

File Upload Hook​

export const useFileUpload = () => {
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);

const uploadFile = useCallback(
async (file: File, options: UploadOptions = {}) => {
setIsUploading(true);
setError(null);
setProgress(0);

try {
const formData = new FormData();
formData.append("file", file);

if (options.knowledgeBaseId) {
formData.append("knowledge_base_id", options.knowledgeBaseId);
}

const response = await fetch("/api/v1/documents/upload", {
method: "POST",
body: formData,
// Track upload progress
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total,
);
setProgress(percentCompleted);
},
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || "Upload failed");
}

const result = await response.json();
setProgress(100);
return result;
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Upload failed";
setError(errorMessage);
throw err;
} finally {
setIsUploading(false);
}
},
[],
);

const reset = useCallback(() => {
setProgress(0);
setError(null);
setIsUploading(false);
}, []);

return {
uploadFile,
progress,
error,
isUploading,
reset,
};
};

3. Data Table Pattern​

import { useState } from 'react';
import { DataTable } from '@/components/data-table/data-table';
import { DocumentColumns } from './columns';

interface DocumentListProps {
knowledgeBaseId?: string;
}

export const DocumentList = ({ knowledgeBaseId }: DocumentListProps) => {
const [sorting, setSorting] = useState<SortingState>([]);
const [filtering, setFiltering] = useState<string>('');

const { documents, isLoading, total } = useDocuments({
filters: { knowledgeBaseId },
});

const columns = DocumentColumns();

if (isLoading) {
return <DocumentTableSkeleton />;
}

return (
<div className="space-y-4">
<DataTable
data={documents}
columns={columns}
sorting={sorting}
onSortingChange={setSorting}
filtering={filtering}
onFilteringChange={setFiltering}
totalItems={total}
emptyState={
<EmptyDocumentState
knowledgeBaseId={knowledgeBaseId}
onUpload={() => {/* handle upload */}}
/>
}
/>
</div>
);
};

🎨 Styling e Theming​

1. Tailwind Configuration​

// tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
// ... other colors
},
},
},
plugins: [require("tailwindcss-animate")],
};

2. CSS Variables per Theming​

/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
/* ... */
}

.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
/* ... */
}
}

@layer components {
.container {
@apply mx-auto max-w-7xl px-4 sm:px-6 lg:px-8;
}

.page-header {
@apply flex items-center justify-between pb-4 border-b;
}
}

3. Component Variants con CVA​

import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
);

interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}

🌍 Configurazione App​

1. Settings Context​

// contexts/settings-context.tsx
import { createContext, useContext, ReactNode } from 'react';

interface Settings {
maxFileSize: number;
supportedFileTypes: string[];
chunkingPresets: ChunkingPreset[];
defaultModel: string;
}

const SettingsContext = createContext<Settings | undefined>(undefined);

export const SettingsProvider = ({
children,
settings
}: {
children: ReactNode;
settings: Settings;
}) => {
return (
<SettingsContext.Provider value={settings}>
{children}
</SettingsContext.Provider>
);
};

export const useSettings = () => {
const context = useContext(SettingsContext);
if (!context) {
throw new Error('useSettings must be used within SettingsProvider');
}
return context;
};

2. Costanti UI​

// lib/constants.ts
export const UI_CONSTANTS = {
// Testi UI statici in italiano
actions: {
save: "Salva",
cancel: "Annulla",
delete: "Elimina",
export: "Esporta",
},

labels: {
name: "Nome",
description: "Descrizione",
status: "Stato",
createdAt: "Creato il",
},

messages: {
success: {
saved: "Salvato con successo",
deleted: "Eliminato con successo",
},
error: {
notFound: "Non trovato",
generic: "Si Γ¨ verificato un errore",
},
},

placeholders: {
search: "Cerca...",
enterName: "Inserisci il nome",
},
};

3. Feature Flags​

// lib/feature-flags.ts
export const FEATURES = {
CHAT_ARTIFACTS: process.env.NEXT_PUBLIC_FEATURE_CHAT_ARTIFACTS === "true",
MCP_SUPPORT: process.env.NEXT_PUBLIC_FEATURE_MCP === "true",
ADVANCED_CHUNKING:
process.env.NEXT_PUBLIC_FEATURE_ADVANCED_CHUNKING === "true",
};

// Uso nei componenti
if (FEATURES.CHAT_ARTIFACTS) {
// Mostra funzionalitΓ  artifacts
}

πŸ›‘οΈ Gestione Errori​

1. Error Boundaries​

// components/feedback/error-boundary.tsx
import React from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import { Button } from '@/components/ui/button';

interface ErrorFallbackProps {
error: Error;
resetErrorBoundary: () => void;
}

function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) {

return (
<div className="flex min-h-[400px] flex-col items-center justify-center space-y-4 text-center">
<div className="space-y-2">
<h2 className="text-2xl font-semibold">
Si Γ¨ verificato un errore
</h2>
<p className="text-muted-foreground max-w-md">
Qualcosa Γ¨ andato storto. Riprova o contatta il supporto se il problema persiste.
</p>
</div>

{process.env.NODE_ENV === 'development' && (
<details className="text-left text-sm text-red-600 bg-red-50 p-4 rounded-md max-w-2xl">
<summary className="cursor-pointer">Error Details</summary>
<pre className="mt-2 whitespace-pre-wrap">{error.message}</pre>
<pre className="mt-2 whitespace-pre-wrap text-xs">{error.stack}</pre>
</details>
)}

<Button onClick={resetErrorBoundary}>
Riprova
</Button>
</div>
);
}

export const ErrorBoundary = ({ children }: { children: React.ReactNode }) => {
return (
<ReactErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
console.error('Error caught by boundary:', error, errorInfo);
// Send to error reporting service
}}
>
{children}
</ReactErrorBoundary>
);
};

2. Toast Notifications​

// hooks/use-toast.ts (da Shadcn/ui)
import { toast } from "sonner";
import { UI_CONSTANTS } from "@/lib/constants";

export const useToast = () => {
return {
success: (message: string) => toast.success(message),
error: (message: string) => toast.error(message),

// Convenience methods
saveSuccess: () => toast.success(UI_CONSTANTS.messages.success.saved),
deleteSuccess: () => toast.success(UI_CONSTANTS.messages.success.deleted),
uploadSuccess: () => toast.success("Caricamento completato"),

saveError: () => toast.error("Impossibile salvare"),
deleteError: () => toast.error("Impossibile eliminare"),
uploadError: (error?: string) =>
toast.error(
error ? `Caricamento fallito: ${error}` : "Caricamento fallito",
),
};
};

πŸ”„ Server-Side Rendering (SSR)​

1. SSR-Safe Patterns​

// ❌ Problematico per SSR
const MyComponent = () => {
const width = window.innerWidth; // window non definito su server
return <div>Width: {width}</div>;
};

// βœ… SSR-Safe
const MyComponent = () => {
const [width, setWidth] = useState<number>(0);

useEffect(() => {
if (typeof window !== 'undefined') {
setWidth(window.innerWidth);

const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}
}, []);

return <div>Width: {width || 'Loading...'}</div>;
};

2. Dynamic Imports per Component Browser-Only​

import dynamic from 'next/dynamic';

// Component che usa API browser
const FileViewer = dynamic(() => import('./file-viewer'), {
ssr: false,
loading: () => <div>Loading viewer...</div>
});

// Hook SSR-safe
const useIsClient = () => {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

return isClient;
};

3. Next.js App Router Best Practices​

// app/dashboard/layout.tsx - Server Component
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex h-screen">
<aside className="w-64 bg-gray-100">
<Navigation />
</aside>
<main className="flex-1 overflow-auto">
{children}
</main>
</div>
);
}

// app/dashboard/documents/page.tsx - Server Component
export default async function DocumentsPage() {
// Data fetching sul server
const documents = await fetchDocuments();

return (
<div className="p-6">
<h1>Documents</h1>
<DocumentList initialData={documents} />
</div>
);
}

// components/document/list.tsx - Client Component
'use client';

export const DocumentList = ({
initialData
}: {
initialData: Document[]
}) => {
const { documents } = useDocuments({
initialData, // SWR userΓ  questi come dati iniziali
});

return (
<div>
{documents.map(doc => (
<DocumentCard key={doc.id} document={doc} />
))}
</div>
);
};

πŸ§ͺ Testing Frontend​

1. Unit Testing con Vitest​

// components/__tests__/document-form.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { DocumentForm } from '../document/form';
import { vi } from 'vitest';

const mockOnSubmit = vi.fn();

describe('DocumentForm', () => {
it('should render form fields', () => {
render(
<DocumentForm
defaultValues={{}}
onSubmit={mockOnSubmit}
/>
);

expect(screen.getByLabelText(/nome documento/i)).toBeInTheDocument();
expect(screen.getByLabelText(/descrizione/i)).toBeInTheDocument();
});

it('should validate required fields', async () => {
render(
<DocumentForm
defaultValues={{}}
onSubmit={mockOnSubmit}
/>
);

const submitButton = screen.getByRole('button', { name: /salva/i });
fireEvent.click(submitButton);

await waitFor(() => {
expect(screen.getByText(/nome Γ¨ richiesto/i)).toBeInTheDocument();
});
});

it('should submit valid form', async () => {
render(
<DocumentForm
defaultValues={{}}
onSubmit={mockOnSubmit}
/>
);

const nameInput = screen.getByLabelText(/nome documento/i);
fireEvent.change(nameInput, { target: { value: 'Test Document' } });

const submitButton = screen.getByRole('button', { name: /salva/i });
fireEvent.click(submitButton);

await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
name: 'Test Document',
});
});
});
});

2. Integration Testing​

// hooks/__tests__/use-documents.test.tsx
import { renderHook, waitFor } from "@testing-library/react";
import { useDocuments } from "../use-documents";
import { rest } from "msw";
import { setupServer } from "msw/node";

const server = setupServer(
rest.get("/api/v1/documents", (req, res, ctx) => {
return res(
ctx.json({
documents: [
{ id: "1", name: "Test Doc 1" },
{ id: "2", name: "Test Doc 2" },
],
total: 2,
}),
);
}),
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe("useDocuments", () => {
it("should fetch documents", async () => {
const { result } = renderHook(() => useDocuments());

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.documents).toHaveLength(2);
expect(result.current.total).toBe(2);
});
});

πŸš€ Performance Optimization​

1. Code Splitting​

// Lazy loading di pagine pesanti
const DocumentEditor = dynamic(() => import('./document-editor'), {
loading: () => <EditorSkeleton />,
});

// Lazy loading di componenti condizionali
const AdminPanel = dynamic(() => import('./admin-panel'), {
ssr: false,
});

2. Memoization​

import { memo, useMemo, useCallback } from 'react';

export const DocumentCard = memo(({ document }: { document: Document }) => {
const formattedDate = useMemo(() =>
formatDate(document.createdAt),
[document.createdAt]
);

const handleClick = useCallback(() => {
// Handle click
}, [document.id]);

return (
<div onClick={handleClick}>
<h3>{document.name}</h3>
<p>{formattedDate}</p>
</div>
);
});

3. Bundle Analysis​

# Analizza bundle size
pnpm build
pnpm dlx @next/bundle-analyzer

# Monitor performance in development
ANALYZE=true pnpm build

πŸ”§ Development Tools​

1. VS Code Setup​

// .vscode/settings.json
{
"typescript.preferences.importModuleSpecifier": "relative",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"emmet.includeLanguages": {
"typescript": "html",
"typescriptreact": "html"
},
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}

2. Debugging​

// React DevTools con display names
DocumentForm.displayName = "DocumentForm";
DocumentList.displayName = "DocumentList";

// Debug hooks
const useDebugValue = (value: any, formatter?: (value: any) => any) => {
if (process.env.NODE_ENV === "development") {
console.log("Debug:", formatter ? formatter(value) : value);
}
};

Questo setup frontend garantisce:

  • βœ… Type Safety: TypeScript completo con validazione Zod
  • βœ… Performance: Code splitting, memoization, bundle optimization
  • βœ… DX: Hot reload, debugging tools, error boundaries
  • βœ… Accessibility: Radix UI primitives, ARIA support
  • βœ… Configurazione: Settings context, feature flags, costanti UI
  • βœ… Testing: Unit e integration testing setup

Questa pagina ti Γ¨ stata utile?