255 lines
8.3 KiB
TypeScript
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>
|
|
)
|
|
}
|