From f05f56d42a9936b6a5319b0ca719f2ea3bb166db Mon Sep 17 00:00:00 2001 From: Owen Rees Date: Wed, 10 Dec 2025 22:55:18 +0100 Subject: [PATCH] rudimentary display of dynamic data --- public/worker-IHe0lHNYRx4fu2ZouPahC.js | 1 + src/app/[locale]/(main)/match/MatchForm.tsx | 42 ++- .../match/components/FeuilleMatchPDF.tsx | 279 +++++++++++++----- .../(main)/match/components/pdf/Header.tsx | 18 ++ .../(main)/match/components/pdf/InfoTable.tsx | 31 ++ .../(main)/match/components/pdf/styles.ts | 66 +++++ src/server/generateFeuilleDeMatch.ts | 37 +-- yarn.lock | 5 + 8 files changed, 361 insertions(+), 118 deletions(-) create mode 100644 public/worker-IHe0lHNYRx4fu2ZouPahC.js create mode 100644 src/app/[locale]/(main)/match/components/pdf/Header.tsx create mode 100644 src/app/[locale]/(main)/match/components/pdf/InfoTable.tsx create mode 100644 src/app/[locale]/(main)/match/components/pdf/styles.ts diff --git a/public/worker-IHe0lHNYRx4fu2ZouPahC.js b/public/worker-IHe0lHNYRx4fu2ZouPahC.js new file mode 100644 index 0000000..8587849 --- /dev/null +++ b/public/worker-IHe0lHNYRx4fu2ZouPahC.js @@ -0,0 +1 @@ +self.__WB_DISABLE_DEV_LOGS=!0; \ No newline at end of file diff --git a/src/app/[locale]/(main)/match/MatchForm.tsx b/src/app/[locale]/(main)/match/MatchForm.tsx index 5210488..5a6aca8 100644 --- a/src/app/[locale]/(main)/match/MatchForm.tsx +++ b/src/app/[locale]/(main)/match/MatchForm.tsx @@ -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"} - diff --git a/src/app/[locale]/(main)/match/components/FeuilleMatchPDF.tsx b/src/app/[locale]/(main)/match/components/FeuilleMatchPDF.tsx index a107a69..e5a7a4b 100644 --- a/src/app/[locale]/(main)/match/components/FeuilleMatchPDF.tsx +++ b/src/app/[locale]/(main)/match/components/FeuilleMatchPDF.tsx @@ -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 ( -// -// -// -// FEUILLE DE MATCH INTERCLUBS -// Competition: {data.competition} - Groupe: A - Ronde: {data.ronde} -// Lieu: {data.lieu} - Date: {data.date ? new Date(data.date).toLocaleDateString('fr-FR') : ''} -// -// -// {/* Team Headers */} -// -// -// Equipe A: {data.team1} -// -// -// Equipe B: {data.team2} -// -// -// -// {/* Table Header */} -// -// Ech. -// Noms Prénoms (A) -// Elo -// Res. -// Noms Prénoms (B) -// Elo -// -// -// {/* Table Rows */} -// {boards.map((_, i) => ( -// -// {i + 1} -// {data.team1_players?.[i]?.name || ''} -// {data.team1_players?.[i]?.elo || ''} -// {/* Result placeholder */} -// {data.team2_players?.[i]?.name || ''} -// {data.team2_players?.[i]?.elo || ''} -// -// ))} -// -// -// Capitaine A -// Arbitre -// Capitaine B -// -// -// -// ); -// }; +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 ( + + +
+ + + + {data.competition} + + + + + {/*Team Container*/} + + {/*Team Headers*/} + + + CLUB RECEVANT (ayant les blancs sur l'échiquier 1) + Code Club + + + {data.team1} + 2 + + + + + + + CLUB SE DEPLACANT ( ayant les noirs sur l'échiquier 1) + + Code Club + + + {data.team2} + 2 + + + + + {/*Board Header Container*/} + + + Ech. + Nom + Code FFE + ELO + Result + + + + Ech. + Nom + Code FFE + ELO + Result + + + + {/*Boards Container*/} + + + {boards.map((_, i) => ( + + + {`${i + 1}`} {i === 0 || i % 2 == 0 ? "B" : "N"} + + {data.team1_players[i]?.name || ""} + {data.team1_players[i]?.nrFFE || ""} + {data.team1_players[i]?.elo || ""} + + ))} + + + + {boards.map((_, i) => ( + + + {`${i + 1}`} {i === 0 || i % 2 == 0 ? "N" : "B"} + + {data.team2_players[i]?.name || ""} + {data.team2_players[i]?.nrFFE || ""} + {data.team2_players[i]?.elo || ""} + + ))} + + + + {/* Table Header */} + {/**/} + {/* Ech.*/} + {/* Noms Prénoms (A)*/} + {/* Elo*/} + {/* Res.*/} + {/* Noms Prénoms (B)*/} + {/* Elo*/} + {/**/} + + {/* Table Rows */} + {/*{boards.map((_, i) => (*/} + {/* */} + {/* {i + 1}*/} + {/* */} + {/* {data.team1_players?.[i]?.name || ""}*/} + {/* */} + {/* */} + {/* {data.team1_players?.[i]?.elo || ""}*/} + {/* */} + {/* /!* Result placeholder *!/*/} + {/* */} + {/* {data.team2_players?.[i]?.name || ""}*/} + {/* */} + {/* */} + {/* {data.team2_players?.[i]?.elo || ""}*/} + {/* */} + {/* */} + {/*))}*/} + + {/**/} + {/* Capitaine A*/} + {/* Arbitre*/} + {/* Capitaine B*/} + {/**/} + + + ); +}; diff --git a/src/app/[locale]/(main)/match/components/pdf/Header.tsx b/src/app/[locale]/(main)/match/components/pdf/Header.tsx new file mode 100644 index 0000000..13296c0 --- /dev/null +++ b/src/app/[locale]/(main)/match/components/pdf/Header.tsx @@ -0,0 +1,18 @@ +import { Text, View } from "@react-pdf/renderer"; + +import { styles } from "@/app/[locale]/(main)/match/components/pdf/styles"; + +const Header = () => ( + + FEDERATION FRANCAISE DES ECHECS + FEUILLE DE MATCH + + AGREE PAR LE MINISTERE DE LA JEUNESSE DES SPORTS ET DES LOISIRS + + + MEMBRE FONDATEUR DE LA FEDERATION INTERNATIONALE DES ECHECS + + +); + +export default Header; diff --git a/src/app/[locale]/(main)/match/components/pdf/InfoTable.tsx b/src/app/[locale]/(main)/match/components/pdf/InfoTable.tsx new file mode 100644 index 0000000..b8a6b6c --- /dev/null +++ b/src/app/[locale]/(main)/match/components/pdf/InfoTable.tsx @@ -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) => ( + + {/* Header Row */} + + DATE + LIEU + RONDE + + + {/* Data Row */} + + + {date ? new Date(date).toLocaleDateString("fr-FR") : " "} + + {lieu || " "} + {ronde || " "} + + +); + +export default InfoTable; diff --git a/src/app/[locale]/(main)/match/components/pdf/styles.ts b/src/app/[locale]/(main)/match/components/pdf/styles.ts new file mode 100644 index 0000000..c359344 --- /dev/null +++ b/src/app/[locale]/(main)/match/components/pdf/styles.ts @@ -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, + }, +}); diff --git a/src/server/generateFeuilleDeMatch.ts b/src/server/generateFeuilleDeMatch.ts index 06529d6..4c95699 100644 --- a/src/server/generateFeuilleDeMatch.ts +++ b/src/server/generateFeuilleDeMatch.ts @@ -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; diff --git a/yarn.lock b/yarn.lock index 1e27fb1..ae0be01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== +"@ag-media/react-pdf-table@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@ag-media/react-pdf-table/-/react-pdf-table-2.0.3.tgz#113554b583b46e41a098cf64fecb5decd59ba004" + integrity sha512-IscjfAOKwsyQok9YmzvuToe6GojN7J8hF0kb8C+K8qZX1DvhheGO+hRSAPxbv2nKMbSpvk7CIhSqJEkw++XVWg== + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"