wwwfiberdirekt/src/blocks/FDDataTableBlock/Component.tsx

255 lines
8.3 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import type { FDDataTableBlock as Props, Media } from '@/payload-types'
type TableData = {
headers: string[]
rows: string[][]
}
function detectDelimiter(line: string): string {
const semicolons = (line.match(/;/g) || []).length
const commas = (line.match(/,/g) || []).length
return semicolons >= commas ? ';' : ','
}
function parseCSVLine(line: string, delimiter: string): string[] {
const result: string[] = []
let current = ''
let inQuotes = false
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (char === '"') {
inQuotes = !inQuotes
} else if (char === delimiter && !inQuotes) {
result.push(current.trim())
current = ''
} else {
current += char
}
}
result.push(current.trim())
return result
}
async function parseFile(url: string, filename: string): Promise<TableData> {
const response = await fetch(url)
const isExcel = /\.(xlsx|xls)$/i.test(filename)
if (isExcel) {
const buffer = await response.arrayBuffer()
const ExcelJS = await import('exceljs')
const workbook = new ExcelJS.Workbook()
await workbook.xlsx.load(buffer)
const sheet = workbook.worksheets[0]
const data: string[][] = []
sheet.eachRow((row) => {
const cells = (row.values as unknown[])
.slice(1)
.map((c) => {
if (c === null || c === undefined) return ''
if (typeof c === 'object' && 'text' in c) return String((c as any).text ?? '')
if (typeof c === 'object' && 'result' in c) return String((c as any).result ?? '')
return String(c)
})
data.push(cells)
})
const nonEmpty = data.filter((r) => r.some((c) => c.trim()))
const [headerRow, ...bodyRows] = nonEmpty
return { headers: headerRow || [], rows: bodyRows }
} else {
const text = await response.text()
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)
const delimiter = detectDelimiter(lines[0] || '')
const [headerLine, ...bodyLines] = lines
return {
headers: parseCSVLine(headerLine, delimiter),
rows: bodyLines.map((l) => parseCSVLine(l, delimiter)),
}
}
}
export const FDDataTableBlockComponent: React.FC<Props> = ({
heading,
description,
dataSource = 'upload',
file,
headers: manualHeaders = [],
rows: manualRows = [],
sectionBackground = 'white',
headerStyle = 'navy',
stripeRows = true,
bordered = false,
firstColumnBold = false,
anchorId,
}) => {
const [tableData, setTableData] = useState<TableData | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const resolveData = useCallback(async () => {
if (dataSource === 'manual') {
const headers = (manualHeaders || []).map((h) => h.text || '')
const rows = (manualRows || []).map((r) =>
(r.cells || '').split(',').map((c: string) => c.trim()),
)
setTableData({ headers, rows })
return
}
const media = file as Media | undefined
if (!media?.url) {
setTableData(null)
return
}
setLoading(true)
setError(null)
try {
const filename = (media as any).filename || media.url
const data = await parseFile(media.url, filename)
setTableData(data)
} catch (e) {
console.error('Table parse error:', e)
setError('Kunde inte läsa filen. Kontrollera att det är en giltig CSV- eller Excel-fil.')
setTableData(null)
} finally {
setLoading(false)
}
}, [dataSource, file, manualHeaders, manualRows])
useEffect(() => { resolveData() }, [resolveData])
const isDark = sectionBackground === 'navy'
// Navy stays navy. White/gray adapt to OS dark mode.
const bgClass =
sectionBackground === 'navy' ? 'bg-fd-navy' :
sectionBackground === 'gray' ? 'bg-fd-surface-alt dark:bg-fd-navy' :
'bg-white dark:bg-fd-navy'
const titleClass = isDark
? 'text-fd-yellow'
: 'text-fd-navy dark:text-fd-yellow'
const bodyClass = isDark
? 'text-white'
: 'text-fd-navy dark:text-white'
// Header row style — navy header stays navy on dark bg; other styles get dark: adjustments
const headerBgClass =
headerStyle === 'yellow' ? 'bg-fd-yellow text-fd-navy' :
headerStyle === 'mint' ? 'bg-fd-mint text-fd-navy' :
headerStyle === 'gray' ? 'bg-gray-200 text-fd-navy dark:bg-white/10 dark:text-white' :
isDark ? 'bg-white/10 text-white' :
'bg-fd-navy text-white dark:bg-white/10'
const getRowBg = (i: number) => {
if (!stripeRows) return isDark ? 'bg-white/5' : 'bg-white dark:bg-white/5'
if (isDark) return i % 2 === 0 ? 'bg-white/5' : 'bg-white/10'
return i % 2 === 0
? 'bg-white dark:bg-white/5'
: 'bg-fd-surface-alt dark:bg-white/10'
}
const borderClass = bordered
? isDark
? 'border border-white/10'
: 'border border-fd-navy/10 dark:border-white/10'
: ''
const cellBorderClass = bordered
? isDark
? 'border-r border-b border-white/10 last:border-r-0'
: 'border-r border-b border-fd-navy/10 dark:border-white/10 last:border-r-0'
: ''
return (
<section id={anchorId || undefined} className={`w-full py-16 md:py-20 lg:py-[99px] ${bgClass}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
{(heading || description) && (
<div className="mb-10 md:mb-12">
{heading && (
<h2 className={`font-joey-heavy text-fd-h1 mb-4 ${titleClass}`}>
{heading}
</h2>
)}
{description && (
<p className={`font-joey text-fd-body-lg max-w-[680px] ${bodyClass}`}>
{description}
</p>
)}
</div>
)}
{loading && (
<div className={`flex items-center justify-center py-16 font-joey text-fd-body ${bodyClass} opacity-60`}>
Laddar tabell...
</div>
)}
{error && (
<div className="flex items-center justify-center py-16">
<p className="font-joey text-red-400 text-fd-small">{error}</p>
</div>
)}
{!loading && !error && !tableData && (
<div className={`flex items-center justify-center py-16 font-joey text-fd-body ${bodyClass} opacity-40`}>
{dataSource === 'upload'
? 'Ladda upp en CSV- eller Excel-fil för att visa tabellen.'
: 'Lägg till kolumnrubriker och rader ovan.'}
</div>
)}
{!loading && !error && tableData && tableData.headers.length > 0 && (
<div className="w-full overflow-hidden rounded-[20px] md:rounded-[30px]">
<div className="w-full overflow-x-auto" style={{ WebkitOverflowScrolling: 'touch' }}>
<table className={`w-full min-w-full border-collapse ${borderClass}`}>
<thead>
<tr>
{tableData.headers.map((header, i) => (
<th
key={i}
className={`px-4 md:px-6 py-3 md:py-4 text-left font-joey-bold text-fd-small whitespace-nowrap ${headerBgClass} ${cellBorderClass}`}
>
{header}
</th>
))}
</tr>
</thead>
<tbody>
{tableData.rows.map((row, rowIndex) => (
<tr key={rowIndex} className={getRowBg(rowIndex)}>
{tableData.headers.map((_, colIndex) => (
<td
key={colIndex}
className={`px-4 md:px-6 py-3 md:py-4 font-joey text-fd-small ${bodyClass} ${cellBorderClass} ${
colIndex === 0 && firstColumnBold ? 'font-joey-bold' : ''
}`}
>
{row[colIndex] ?? ''}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{!loading && !error && tableData && tableData.rows.length > 0 && (
<p className={`mt-3 font-joey text-fd-xs opacity-40 ${bodyClass}`}>
{tableData.rows.length} rader · {tableData.headers.length} kolumner
</p>
)}
</div>
</section>
)
}