rudimentary display of dynamic data

This commit is contained in:
2025-12-10 22:55:18 +01:00
parent 89f01c2f80
commit f05f56d42a
8 changed files with 361 additions and 118 deletions
+19 -23
View File
@@ -33,34 +33,24 @@ const MatchForm = ({ clubs }: IProps) => {
const onSubmit = async (data: MatchFormValues) => {
try {
await generateFeuilleDeMatch(data);
const result = await generateFeuilleDeMatch(data);
// // Destructure the result from the safe action
// const result = await generateFeuilleDeMatch(data);
//
// if (result?.data?.pdfBase64) {
// // 1. Convert Base64 to Blob
// const byteCharacters = atob(result.data.pdfBase64);
// const byteNumbers = new Array(byteCharacters.length);
// for (let i = 0; i < byteCharacters.length; i++) {
// byteNumbers[i] = byteCharacters.charCodeAt(i);
// }
// const byteArray = new Uint8Array(byteNumbers);
// const blob = new Blob([byteArray], { type: "application/pdf" });
//
// // 2. Create URL
// const url = URL.createObjectURL(blob);
//
// // 3. Open in new tab
// window.open(url, "_blank");
// }
if (result?.data?.pdfBase64) {
const byteCharacters = atob(result.data.pdfBase64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
window.open(url, "_blank");
}
setResponseMessage({
type: "success",
message: "generated feuille de match",
});
// form.reset(); // do not reset the form
} catch (err: unknown) {
console.log(err);
@@ -128,7 +118,13 @@ const MatchForm = ({ clubs }: IProps) => {
{form.formState.isSubmitting ? "Generating" : "Generate"}
</Button>
<Button onClick={() => form.reset()} type="button" intent="secondary">
<Button
onClick={() => {
form.reset();
}}
type="button"
intent="secondary"
>
{"Clear"}
</Button>
</div>
@@ -1,77 +1,202 @@
// import React from 'react';
// import { Page, Text, View, Document, StyleSheet, Font } from '@react-pdf/renderer';
// import { MatchFormValues } from '@/app/[locale]/(main)/match/components/MatchForm'; // Adjust path
//
// // Register fonts if needed (optional)
// // Font.register({ family: 'Roboto', src: '...' });
//
// const styles = StyleSheet.create({
// page: { flexDirection: 'column', padding: 30, fontSize: 10 },
// header: { marginBottom: 20, textAlign: 'center' },
// title: { fontSize: 18, fontWeight: 'bold', marginBottom: 10 },
// section: { margin: 10, padding: 10, flexGrow: 1 },
// row: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#000', alignItems: 'center', height: 24 },
// cell: { flex: 1, padding: 2 },
// cellSmall: { width: 40, padding: 2, borderLeftWidth: 1, borderLeftColor: '#000' },
// headerRow: { flexDirection: 'row', backgroundColor: '#E4E4E4', borderBottomWidth: 1, borderBottomColor: '#000', height: 30, alignItems: 'center' },
// });
//
// interface PdfProps {
// data: MatchFormValues;
// }
//
// export const FeuilleMatchPdf = ({ data }: PdfProps) => {
// // Determine max boards based on team size, default to 4 or 8
// const boardsCount = Math.max(data.team1_players?.length || 0, data.team2_players?.length || 0) || 8;
// const boards = Array.from({ length: boardsCount });
//
// return (
// <Document>
// <Page size="A4" style={styles.page}>
// <View style={styles.header}>
// <Text style={styles.title}>FEUILLE DE MATCH INTERCLUBS</Text>
// <Text>Competition: {data.competition} - Groupe: A - Ronde: {data.ronde}</Text>
// <Text>Lieu: {data.lieu} - Date: {data.date ? new Date(data.date).toLocaleDateString('fr-FR') : ''}</Text>
// </View>
//
// {/* Team Headers */}
// <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 10 }}>
// <View style={{ width: '45%' }}>
// <Text style={{ fontWeight: 'bold' }}>Equipe A: {data.team1}</Text>
// </View>
// <View style={{ width: '45%' }}>
// <Text style={{ fontWeight: 'bold' }}>Equipe B: {data.team2}</Text>
// </View>
// </View>
//
// {/* Table Header */}
// <View style={styles.headerRow}>
// <Text style={styles.cellSmall}>Ech.</Text>
// <Text style={styles.cell}>Noms Prénoms (A)</Text>
// <Text style={styles.cellSmall}>Elo</Text>
// <Text style={styles.cellSmall}>Res.</Text>
// <Text style={styles.cell}>Noms Prénoms (B)</Text>
// <Text style={styles.cellSmall}>Elo</Text>
// </View>
//
// {/* Table Rows */}
// {boards.map((_, i) => (
// <View key={i} style={styles.row}>
// <Text style={styles.cellSmall}>{i + 1}</Text>
// <Text style={styles.cell}>{data.team1_players?.[i]?.name || ''}</Text>
// <Text style={styles.cellSmall}>{data.team1_players?.[i]?.elo || ''}</Text>
// <Text style={styles.cellSmall}></Text> {/* Result placeholder */}
// <Text style={styles.cell}>{data.team2_players?.[i]?.name || ''}</Text>
// <Text style={styles.cellSmall}>{data.team2_players?.[i]?.elo || ''}</Text>
// </View>
// ))}
//
// <View style={{ marginTop: 50, flexDirection: 'row', justifyContent: 'space-around' }}>
// <Text>Capitaine A</Text>
// <Text>Arbitre</Text>
// <Text>Capitaine B</Text>
// </View>
// </Page>
// </Document>
// );
// };
import { Document, Font, Page, Text, View } from "@react-pdf/renderer";
import { type MatchFormValues } from "@/app/[locale]/(main)/match/MatchForm";
import Header from "@/app/[locale]/(main)/match/components/pdf/Header";
import InfoTable from "@/app/[locale]/(main)/match/components/pdf/InfoTable";
import { styles } from "./pdf/styles";
// Register fonts if needed (optional)
// Font.register({ family: 'Roboto', src: '...' });
interface PdfProps {
data: MatchFormValues;
}
export const FeuilleMatchPdf = ({ data }: PdfProps) => {
const boardsCount = Math.max(
data.team1_players?.length || 0,
data.team2_players?.length || 0,
);
const boards = Array.from({ length: boardsCount });
return (
<Document>
<Page size="A4" orientation={"landscape"} style={styles.page}>
<Header />
<View style={{ width: "75%", marginTop: 20, marginLeft: 100 }}>
<Text style={{ fontWeight: "bold", fontSize: 14, marginLeft: 100 }}>
{data.competition}
</Text>
<InfoTable date={data.date} lieu={data.lieu} ronde={data.ronde} />
</View>
{/*Team Container*/}
<View
style={{
width: "90%",
marginTop: 20,
flexDirection: "row",
justifyContent: "space-between",
}}
>
{/*Team Headers*/}
<View style={{ border: "1px solid black" }}>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
fontSize: 8,
}}
>
<Text>CLUB RECEVANT (ayant les blancs sur l'échiquier 1)</Text>
<Text>Code Club</Text>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
fontSize: 12,
fontWeight: "bold",
}}
>
<Text>{data.team1}</Text>
<Text>2</Text>
</View>
</View>
<View style={{ border: "1px solid black" }}>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
fontSize: 8,
}}
>
<Text>
CLUB SE DEPLACANT ( ayant les noirs sur l'échiquier 1)
</Text>
<Text>Code Club</Text>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
fontSize: 12,
fontWeight: "bold",
}}
>
<Text>{data.team2}</Text>
<Text>2</Text>
</View>
</View>
</View>
{/*Board Header Container*/}
<View
style={{
width: "90%",
marginTop: 20,
flexDirection: "row",
justifyContent: "space-between",
}}
>
<View style={styles.boardHeaderRow}>
<Text>Ech.</Text>
<Text>Nom</Text>
<Text>Code FFE</Text>
<Text>ELO</Text>
<Text>Result</Text>
</View>
<View style={styles.boardHeaderRow}>
<Text>Ech.</Text>
<Text>Nom</Text>
<Text>Code FFE</Text>
<Text>ELO</Text>
<Text>Result</Text>
</View>
</View>
{/*Boards Container*/}
<View
style={{
width: "90%",
marginTop: 20,
flexDirection: "row",
justifyContent: "space-between",
}}
>
<View style={{ flexDirection: "column" }}>
{boards.map((_, i) => (
<View key={`team1-board-${i}`} style={{ flexDirection: "row" }}>
<Text>
{`${i + 1}`} {i === 0 || i % 2 == 0 ? "B" : "N"}
</Text>
<Text>{data.team1_players[i]?.name || ""}</Text>
<Text>{data.team1_players[i]?.nrFFE || ""}</Text>
<Text>{data.team1_players[i]?.elo || ""}</Text>
</View>
))}
</View>
<View style={{ flexDirection: "column" }}>
{boards.map((_, i) => (
<View key={`team2-board-${i}`} style={{ flexDirection: "row" }}>
<Text>
{`${i + 1}`} {i === 0 || i % 2 == 0 ? "N" : "B"}
</Text>
<Text>{data.team2_players[i]?.name || ""}</Text>
<Text>{data.team2_players[i]?.nrFFE || ""}</Text>
<Text>{data.team2_players[i]?.elo || ""}</Text>
</View>
))}
</View>
</View>
{/* Table Header */}
{/*<View style={styles.headerRow}>*/}
{/* <Text style={styles.cellSmall}>Ech.</Text>*/}
{/* <Text style={styles.cell}>Noms Prénoms (A)</Text>*/}
{/* <Text style={styles.cellSmall}>Elo</Text>*/}
{/* <Text style={styles.cellSmall}>Res.</Text>*/}
{/* <Text style={styles.cell}>Noms Prénoms (B)</Text>*/}
{/* <Text style={styles.cellSmall}>Elo</Text>*/}
{/*</View>*/}
{/* Table Rows */}
{/*{boards.map((_, i) => (*/}
{/* <View key={i} style={styles.row}>*/}
{/* <Text style={styles.cellSmall}>{i + 1}</Text>*/}
{/* <Text style={styles.cell}>*/}
{/* {data.team1_players?.[i]?.name || ""}*/}
{/* </Text>*/}
{/* <Text style={styles.cellSmall}>*/}
{/* {data.team1_players?.[i]?.elo || ""}*/}
{/* </Text>*/}
{/* <Text style={styles.cellSmall}></Text> /!* Result placeholder *!/*/}
{/* <Text style={styles.cell}>*/}
{/* {data.team2_players?.[i]?.name || ""}*/}
{/* </Text>*/}
{/* <Text style={styles.cellSmall}>*/}
{/* {data.team2_players?.[i]?.elo || ""}*/}
{/* </Text>*/}
{/* </View>*/}
{/*))}*/}
{/*<View*/}
{/* style={{*/}
{/* marginTop: 50,*/}
{/* flexDirection: "row",*/}
{/* justifyContent: "space-around",*/}
{/* }}*/}
{/*>*/}
{/* <Text>Capitaine A</Text>*/}
{/* <Text>Arbitre</Text>*/}
{/* <Text>Capitaine B</Text>*/}
{/*</View>*/}
</Page>
</Document>
);
};
@@ -0,0 +1,18 @@
import { Text, View } from "@react-pdf/renderer";
import { styles } from "@/app/[locale]/(main)/match/components/pdf/styles";
const Header = () => (
<View style={styles.header}>
<Text style={styles.title}>FEDERATION FRANCAISE DES ECHECS</Text>
<Text style={styles.title}>FEUILLE DE MATCH</Text>
<Text style={{ fontSize: 8, fontWeight: "bold", marginBottom: 2 }}>
AGREE PAR LE MINISTERE DE LA JEUNESSE DES SPORTS ET DES LOISIRS
</Text>
<Text style={{ fontSize: 8, fontWeight: "bold", marginBottom: 2 }}>
MEMBRE FONDATEUR DE LA FEDERATION INTERNATIONALE DES ECHECS
</Text>
</View>
);
export default Header;
@@ -0,0 +1,31 @@
import { Text, View } from "@react-pdf/renderer";
import { styles } from "./styles";
interface IProps {
date: Date;
lieu: string;
ronde: number;
}
const InfoTable = ({ date, lieu, ronde }: IProps) => (
<View style={styles.infoTable}>
{/* Header Row */}
<View style={styles.infoRow}>
<Text style={styles.infoCellHeader}>DATE</Text>
<Text style={styles.infoCellHeader}>LIEU</Text>
<Text style={[styles.infoCellHeader, styles.lastCell]}>RONDE</Text>
</View>
{/* Data Row */}
<View style={styles.infoRow}>
<Text style={styles.infoCell}>
{date ? new Date(date).toLocaleDateString("fr-FR") : " "}
</Text>
<Text style={styles.infoCell}>{lieu || " "}</Text>
<Text style={[styles.infoCell, styles.lastCell]}>{ronde || " "}</Text>
</View>
</View>
);
export default InfoTable;
@@ -0,0 +1,66 @@
import { StyleSheet } from "@react-pdf/renderer";
export const styles = StyleSheet.create({
page: { flexDirection: "column", padding: 30, fontSize: 10 },
header: { marginBottom: 20, textAlign: "center" },
title: { fontSize: 18, fontWeight: "bold", marginBottom: 4 },
section: { margin: 10, padding: 10, flexGrow: 1 },
row: {
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: "#000",
alignItems: "center",
height: 24,
},
cell: { flex: 1, padding: 2 },
cellSmall: {
width: 40,
padding: 2,
borderLeftWidth: 1,
borderLeftColor: "#000",
},
// headerRow: {
// flexDirection: "row",
// backgroundColor: "#E4E4E4",
// borderBottomWidth: 1,
// borderBottomColor: "#000",
// height: 30,
// alignItems: "center",
// },
boardHeaderRow: {
flexDirection: "row",
justifyContent: "space-between",
},
infoTable: {
borderWidth: 1,
borderColor: "#000",
marginTop: 5,
marginBottom: 10,
},
infoRow: {
flexDirection: "row",
},
infoCellHeader: {
flex: 1,
padding: 5,
textAlign: "center",
fontWeight: "bold",
// backgroundColor: "#E4E4E4",
borderRightWidth: 1,
borderRightColor: "#000",
borderBottomWidth: 1,
borderBottomColor: "#000",
fontSize: 10,
},
infoCell: {
flex: 1,
padding: 5,
textAlign: "center",
borderRightWidth: 1,
borderRightColor: "#000",
fontSize: 10,
},
lastCell: {
borderRightWidth: 0,
},
});
+19 -18
View File
@@ -1,5 +1,8 @@
"use server";
import { renderToStream } from "@react-pdf/renderer";
import { FeuilleMatchPdf } from "@/app/[locale]/(main)/match/components/FeuilleMatchPDF";
import { matchSchema } from "@/schemas";
import { actionClient } from "@/server/safeAction";
@@ -7,25 +10,23 @@ export const generateFeuilleDeMatch = actionClient
.schema(matchSchema)
.action(async ({ parsedInput }) => {
try {
console.log("generateFeuilleDeMatch", parsedInput);
// 1. Render PDF to Node Stream
const stream = await renderToStream(
FeuilleMatchPdf({ data: parsedInput }),
);
// console.log("Generating PDF for:", parsedInput.competition);
//
// // 1. Render PDF to Node Stream
// const stream = await renderToStream(FeuilleMatchPdf({ data: parsedInput }));
//
// // 2. Convert Stream to Buffer
// const chunks: Buffer[] = [];
// for await (const chunk of stream) {
// chunks.push(Buffer.from(chunk));
// }
// const pdfBuffer = Buffer.concat(chunks);
//
// // 3. Return Base64 String
// return {
// pdfBase64: pdfBuffer.toString('base64'),
// filename: `match_${parsedInput.competition}_${parsedInput.ronde}.pdf`
// };
// 2. Convert Stream to Buffer
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
const pdfBuffer = Buffer.concat(chunks);
// 3. Return Base64 String
return {
pdfBase64: pdfBuffer.toString("base64"),
filename: `match_${parsedInput.competition}_${parsedInput.ronde}.pdf`,
};
} catch (error) {
console.log(error);
throw error;