Kurs vibe coding z AI · Moduł 8: Slack Clone
Eksport danych - raporty CSV i PDF
W tej lekcji2
Panel wygląda świetnie, ale kwatermistrz chce mieć liczby w arkuszu kalkulacyjnym, a armator raport w PDF. Zamiast przepisywać tabelę ręcznie, dodamy dwa przyciski: Export CSV i Export PDF. Oba pliki powstaną w przeglądarce, bez udziału serwera.
Eksport do CSV
CSV to zwykły tekst: pierwszy wiersz zawiera nagłówki, a każdy kolejny jeden rekord z wartościami oddzielonymi przecinkami. Funkcja exportToCSV składa taki tekst i każe przeglądarce go pobrać, a DataTable wywołuje ją przyciskiem:
function exportToCSV(data: any[], filename: string) {
const headers = Object.keys(data[0]).join(',');
const rows = data.map(row => Object.values(row).join(','));
const csv = [headers, ...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
}
function DataTable({ data }: { data: any[] }) {
return (
<div>
<button
onClick={() => exportToCSV(data, 'report.csv')}
className="bg-green-600 text-white px-4 py-2 rounded mb-4"
>
Export CSV
</button>
<table className="w-full border">
{/* Table content */}
</table>
</div>
);
}Kroki są cztery. Object.keys(data[0]).join(',') tworzy wiersz nagłówków z kluczy pierwszego obiektu, Object.values(row).join(',') robi to samo z wartościami, a całość łączymy znakiem nowej linii (join('\n')). Potem powstaje Blob z typem text/csv, czyli plik trzymany w pamięci przeglądarki. URL.createObjectURL(blob) daje mu tymczasowy adres, a link.download = filename i link.click() uruchamiają pobieranie pod nazwą report.csv. Dane w tabeli zostają bez zmian. Ten sam wzorzec pobierze każdy plik zbudowany w przeglądarce, np. JSON.
Na produkcję przydadzą się trzy poprawki. Wartość z przecinkiem, cudzysłowem albo nową linią rozjedzie kolumny, więc otocz ją cudzysłowami, a cudzysłów w środku podwój, tak jak każe standard CSV:
const escapeCell = (value: unknown) => `"${String(value).replace(/"/g, '""')}"`;Funkcja zwraca nowy tekst komórki i nie rusza oryginalnej wartości. Po pobraniu zwolnij adres przez URL.revokeObjectURL(url), a na samym początku sprawdź data.length, bo pusta tablica wysypie funkcję na data[0].
Eksport do PDF
PDF wygeneruje biblioteka jsPDF, a tabelę narysuje jej dodatek jspdf-autotable. Obie instalujesz razem, a samo polecenie nie zmienia jeszcze żadnego komponentu:
npm install jspdf jspdf-autotablePo instalacji eksport mieści się w kilku linijkach: tworzysz dokument, wpisujesz tytuł, rysujesz tabelę i zapisujesz plik.
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
function exportToPDF(data: any[], filename: string) {
const doc = new jsPDF();
doc.text('Analytics Report', 14, 15);
autoTable(doc, {
head: [Object.keys(data[0])],
body: data.map(row => Object.values(row))
});
doc.save(filename);
}new jsPDF() tworzy dokument, domyślnie A4 z wymiarami w milimetrach. doc.text('Analytics Report', 14, 15) wpisuje tytuł 14 mm od lewej i 15 mm od góry, a autoTable rysuje tabelę z nagłówkami (head) i wierszami (body). head to tablica wierszy nagłówka, stąd podwójne nawiasy wokół Object.keys(data[0]), a body zawiera po jednej tablicy wartości na rekord. doc.save(filename) pobiera gotowy plik, a tabela na stronie zostaje bez zmian. Standardowe czcionki PDF nie mają polskich znaków, więc dla ą czy ł dodaj własną czcionkę TTF. Eksport wywołuj w komponencie klienckim, np. w onClick, bo pobieranie dzieje się w przeglądarce.
Rada Kapitana Redbearda: arkusz może potraktować wartość zaczynającą się od =, +, - albo @ jak formułę. Gdy eksportujesz dane wpisane przez użytkowników, poprzedź takie wartości apostrofem, żeby nikt nie przemycił formuły w ładunku.
Zapamiętaj: CSV to tekst w Blobie, PDF to dokument z jsPDF, a oba trafiają do użytkownika przez pobranie w przeglądarce. W edytorze poniżej masz raport kanału z eksportem do CSV, a zadania sprawdzą kroki eksportu.
Kod do tej lekcji: App.jsx
// Eksport danych - pobieranie pliku CSV
import { useState } from 'react';
const reportData = [
{ channel: 'treasure-hunts', messages: 142, members: 8 },
{ channel: 'general', messages: 89, members: 12 },
{ channel: 'navigation', messages: 56, members: 5 }
];
function exportToCSV(data, filename) {
const headers = Object.keys(data[0]).join(',');
const rows = data.map(row => Object.values(row).join(','));
const csv = [headers, ...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
}
export default function App() {
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-3xl mx-auto bg-white border rounded p-6">
<div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-bold">Raport kanałów</h1>
<button
onClick={() => exportToCSV(reportData, 'report.csv')}
className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 text-sm font-semibold"
>
Eksportuj CSV
</button>
</div>
<table className="w-full border text-left text-sm">
<thead className="bg-gray-50">
<tr>
{Object.keys(reportData[0]).map(key => (
<th key={key} className="border px-3 py-2 font-semibold">{key}</th>
))}
</tr>
</thead>
<tbody>
{reportData.map((row, idx) => (
<tr key={idx}>
{Object.values(row).map((val, i) => (
<td key={i} className="border px-3 py-2">{val}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki typ MIME ustawiamy w Blob podczas eksportu CSV?
2. Jak tworzymy wiersz nagłówków CSV z obiektu danych?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Eksport danych do pliku CSV
- Układanie w pionie
Uporządkuj kroki pobierania pliku CSV w przeglądarce: