From 9136ea228409664c4902a2c03317f4caf247aa7d Mon Sep 17 00:00:00 2001 From: Luc Vandenbroucke Date: Sun, 23 Aug 2026 12:25:26 +0200 Subject: [PATCH] Add project files. --- QuizTools.slnx | 3 + QuizTools/Program.cs | 382 +++++++++++++++++++++++++++++ QuizTools/QuizTools.csproj | 26 ++ QuizTools/images/lswtech300-95.svg | 130 ++++++++++ 4 files changed, 541 insertions(+) create mode 100644 QuizTools.slnx create mode 100644 QuizTools/Program.cs create mode 100644 QuizTools/QuizTools.csproj create mode 100644 QuizTools/images/lswtech300-95.svg diff --git a/QuizTools.slnx b/QuizTools.slnx new file mode 100644 index 0000000..29b7daf --- /dev/null +++ b/QuizTools.slnx @@ -0,0 +1,3 @@ + + + diff --git a/QuizTools/Program.cs b/QuizTools/Program.cs new file mode 100644 index 0000000..e0d60f2 --- /dev/null +++ b/QuizTools/Program.cs @@ -0,0 +1,382 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using Dapper; +using Microsoft.Data.SqlClient; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuizTools +{ + internal class Program + { + const int MaxQuestionImageHeight = 60; + const int MaxAnswerImageHeight = 40; + static void Main(string[] args) + { + // 1. License Configuration + QuestPDF.Settings.License = LicenseType.Community; + + string connectionString = "Server=localhost;Database=idpbw;Trusted_Connection=True;TrustServerCertificate=True;"; + + Console.WriteLine("Fetching data from SQL Server..."); + var modules = FetchQuizData(connectionString); + + Console.WriteLine("Generating PDF Booklet..."); + GeneratePdfBooklet(modules, "Quiz_Review_Booklet.pdf"); + + Console.WriteLine("PDF generated successfully!"); + } + + // --- Data Models --- + public class ModuleModel + { + public int Id { get; set; } + public string NameNL { get; set; } = string.Empty; + public string NameFR { get; set; } = string.Empty; + public string NameEN { get; set; } = string.Empty; + public List Questions { get; set; } = new(); + } + + public class QuestionModel + { + public int Id { get; set; } + public byte[]? Image { get; set; } + public Dictionary Text { get; set; } = new(); + public List Answers { get; set; } = new(); + } + + public class AnswerModel + { + public int Id { get; set; } + public bool IsCorrect { get; set; } + public byte[]? Image { get; set; } + public Dictionary Text { get; set; } = new(); + } + + // --- Database Fetching --- + static List FetchQuizData(string connStr) + { + using IDbConnection db = new SqlConnection(connStr); + string query = @" +SELECT m.moduleId AS ModuleId, m.naamNL AS ModuleNameNL,m.naamFR AS ModuleNameFR, m.naamEN AS ModuleNameEN, q.vraagId AS QuestionId, q.NL AS QTextNL, q.FR AS QTextFR, q.EN AS QTextEN, q_img.Image AS QImage, a.antwId AS AnswerId, a.NL AS ATextNL, a.FR AS ATextFR, a.EN AS ATextEN, a.correct AS IsCorrect, a_img.Image AS AImage +FROM dbo.modules m + JOIN quiz.vragen q ON m.moduleId=q.moduleId + LEFT JOIN dbo.QuizImages q_img ON q.QuizImageId=q_img.QuizImageId + JOIN quiz.antwoorden a ON q.vraagId=a.vraagId + LEFT JOIN dbo.QuizImages a_img ON a.QuizImageId=a_img.QuizImageId + WHERE q.active=1 AND q.deleted=0 +ORDER BY m.moduleId, q.vraagId, a.antwId; +"; + + var rows = db.Query(query); + var modulesDict = new Dictionary(); + + foreach (var row in rows) + { + int modId = row.ModuleId; + if (!modulesDict.TryGetValue(modId, out var module)) + { + module = new ModuleModel + { + Id = modId, + NameNL = row.ModuleNameNL ?? string.Empty, + NameFR = row.ModuleNameFR ?? string.Empty, + NameEN = row.ModuleNameEN ?? string.Empty + }; + modulesDict[modId] = module; + } + + int qId = row.QuestionId; + var question = module.Questions.FirstOrDefault(q => q.Id == qId); + if (question == null) + { + byte[]? qImage = row.QImage is DBNull ? null : (byte[]?)row.QImage; + if (qImage != null && qImage.Length <100) qImage = null; + question = new QuestionModel { Id = qId, Image = qImage }; + module.Questions.Add(question); + } + question.Text["nl"] = row.QTextNL; + question.Text["fr"] = row.QTextFR; + question.Text["en"] = row.QTextEN; + + int aId = row.AnswerId; + var answer = question.Answers.FirstOrDefault(a => a.Id == aId); + if (answer == null) + { + byte[]? aImage = row.AImage is DBNull ? null : (byte[]?)row.AImage; + if (aImage != null && aImage.Length < 100) aImage = null; + answer = new AnswerModel { Id = aId, IsCorrect = row.IsCorrect, Image = aImage }; + question.Answers.Add(answer); + } + answer.Text["nl"] = row.ATextNL; + answer.Text["fr"] = row.ATextFR; + answer.Text["en"] = row.ATextEN; + } + + return modulesDict.Values.ToList(); + } + + // --- PDF Generation Logic --- + static void GeneratePdfBooklet(List modules, string outputPath) + { + Document.Create(container => + { + //container.WithSettings(settings => + //{ + // settings.PdfA = false; // Set to true if you need PDF/A compliance + //}); + + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(15, Unit.Millimetre); + page.DefaultTextStyle(x => x.FontFamily("Segoe UI").FontSize(9).FontColor("#1E293B")); + + page.Header().Element(ComposeDocumentHeader); + + page.Content().PaddingVertical(10).Column(column => + { + bool isFirstModule = true; + + foreach (var module in modules) + { + // Avoid inserting a blank page break before the very first cover page + if (!isFirstModule) + { + column.Item().PageBreak(); + } + isFirstModule = false; + + // 1. Render Full-Page Module Cover + column.Item().Element(c => ComposeModuleTitlePage(c, module)); + + // 2. Start Questions on a Fresh Page Right After Title + column.Item().PageBreak(); + + // 3. Render Question Cards for this Module + column.Item().Column(qCol => + { + qCol.Spacing(15); + foreach (var question in module.Questions) + { + qCol.Item().Element(c => ComposeQuestionCard(c, question)); + } + }); + } + }); + + page.Footer().Element(ComposeDocumentFooter); + }); + }).WithMetadata(new DocumentMetadata + { + Title = "Quiz Review Booklet", + Author = "LSW-Tech Luc Vandenbroucke", + Subject = "Multilingual Quiz Questions and Answers (NL / FR / EN)", + Keywords = "Quiz, HPH", + Creator = "QuizTools Application" + }).GeneratePdf(outputPath); + } + static void ComposeModuleTitlePage(IContainer container, ModuleModel module) + { + container.AlignCenter().AlignMiddle().Column(col => + { + // Badge Header + col.Item().AlignCenter().Background("#1E293B").PaddingHorizontal(12).PaddingVertical(4) + .CornerRadius(4).Text($"MODULE {module.Id}").Bold().FontSize(12).FontColor("#FFFFFF"); + + // Module Names Stacked + col.Item().PaddingTop(20).AlignCenter().Text(module.NameNL) + .Bold().FontSize(22).FontColor("#0F172A"); + + col.Item().PaddingTop(6).AlignCenter().Text(module.NameFR) + .SemiBold().FontSize(18).FontColor("#3B82F6"); + + col.Item().PaddingTop(6).AlignCenter().Text(module.NameEN) + .Medium().FontSize(16).FontColor("#10B981"); + + // Decorative Accent Bar + col.Item().PaddingTop(25).Width(100).Height(3).Background("#CBD5E1"); + + // Question Counter + col.Item().PaddingTop(15).AlignCenter().Text($"{module.Questions.Count} Questions") + .FontSize(11).FontColor("#64748B"); + }); + } + + // --- Header Component --- + static void ComposeDocumentHeader(IContainer container) + { + container.BorderBottom(1).BorderColor("#E2E8F0").PaddingBottom(5).Row(row => + { + row.RelativeItem().Text("Quiz Questions and Answers").Bold().FontSize(10).FontColor("#1E293B"); + row.AutoItem().Text("23/08/2026").FontSize(10).FontColor("#64748B"); + }); + } + + // --- First-Page Title Banner Component --- + static void ComposeTitleBanner(IContainer container) + { + container.Background("#F8FAFC").Border(1).BorderColor("#CBD5E1").CornerRadius(4).Padding(12).Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text("Quiz Questions and Answers").Bold().FontSize(18).FontColor("#0F172A"); + col.Item().PaddingTop(2).Text("Multilingual Module Review Export (NL / FR / EN)").FontSize(10).FontColor("#64748B"); + }); + row.AutoItem().AlignBottom().Text("Date: 23/08/2026").FontSize(10).FontColor("#64748B"); + }); + } + + // --- Footer Component --- + //static void ComposeDocumentFooter(IContainer container) + //{ + // container.BorderTop(1).BorderColor("#E2E8F0").PaddingTop(5).Row(row => + // { + // // Left side: Creator info & date + // row.RelativeItem().Text(x => + // { + // x.Span("Created by ").FontColor("#64748B"); + // x.Span("Luc Vandenbroucke").Bold().FontColor("#1E293B"); + // x.Span(" — 23.08.2026").FontColor("#64748B"); + // }); + + // // Right side: Dynamic Page Numbering + // row.AutoItem().Text(x => + // { + // x.Span("Page ").FontColor("#64748B"); + // x.CurrentPageNumber().FontColor("#1E293B").Bold(); + // x.Span(" of ").FontColor("#64748B"); + // x.TotalPages().FontColor("#1E293B").Bold(); + // }); + // }); + //} + + static string GetSvgContent(string filePath) + { + if (File.Exists(filePath)) + { + return File.ReadAllText(filePath); + } + + // Fallback minimal SVG dot if file is missing + return @""; + } + + static void ComposeDocumentFooter(IContainer container) + { + // Read your SVG string from file or a string variable + string mySvg = GetSvgContent("images\\lswtech300-95.svg"); + + container.BorderTop(1).BorderColor("#E2E8F0").PaddingTop(5).Row(row => + { + // Left side: Render SVG image (constrained in height) + row.RelativeItem().Row(leftRow => + { + leftRow.AutoItem() + .MaxHeight(16) // Keeps the SVG small and aligned with footer text + .Svg(mySvg); + + leftRow.AutoItem() + .PaddingLeft(6) + .AlignMiddle() + .Text("— 23.08.2026") + .FontSize(8) + .FontColor("#64748B"); + }); + + // Right side: Page Numbering + row.AutoItem().AlignMiddle().Text(x => + { + x.Span("Page ").FontColor("#64748B"); + x.CurrentPageNumber().FontColor("#1E293B").Bold(); + x.Span(" of ").FontColor("#64748B"); + x.TotalPages().FontColor("#1E293B").Bold(); + }); + }); + } + + static void ComposeModuleHeader(IContainer container, string title) + { + container.BorderBottom(2).BorderColor("#1E293B").PaddingBottom(5).Text(title) + .FontSize(16).Bold().FontColor("#1E293B"); + } + + static void ComposeQuestionCard(IContainer container, QuestionModel q) + { + // NO .ShowEntire() on the root container so multi-page questions can render + container.ShowEntire().Border(1).BorderColor("#CBD5E1").CornerRadius(4).Column(column => + { + // 1. Keep the card header together with the first text block if possible + column.Item().ShowEntire().Background("#1E293B").Padding(6) + .Text($"Question #{q.Id}").Bold().FontColor("#FFFFFF"); + + column.Item().Padding(10).Column(body => + { + // Question Image + if (q.Image != null && q.Image.Length > 0) + { + body.Item().PaddingVertical(6).AlignCenter().MaxHeight(MaxQuestionImageHeight ).Image(q.Image); + } + + // Language Text Blocks + ComposeLangBlock(body, "NL", q.Text.GetValueOrDefault("nl"), "#F97316"); + ComposeLangBlock(body, "FR", q.Text.GetValueOrDefault("fr"), "#3B82F6"); + ComposeLangBlock(body, "EN", q.Text.GetValueOrDefault("en"), "#10B981"); + + // Answers Section + body.Item().PaddingTop(8).Text("ANSWERS").Bold().FontSize(8).FontColor("#64748B"); + + foreach (var ans in q.Answers) + { + // Each individual answer option stays intact on one page + body.Item().PaddingTop(5).Element(c => ComposeAnswerCard(c, ans)); + } + }); + }); + } + + static void ComposeAnswerCard(IContainer container, AnswerModel a) + { + string borderColor = a.IsCorrect ? "#86EFAC" : "#E2E8F0"; + string bgColor = a.IsCorrect ? "#F0FDF4" : "#FFFFFF"; + + // .ShowEntire() here prevents individual answer choices from splitting mid-text + container.ShowEntire().Border(1).BorderColor(borderColor).Background(bgColor).CornerRadius(4).Padding(6).Column(col => + { + col.Item().Row(row => + { + row.RelativeItem().Text($"Option {a.Id}").Bold(); + if (a.IsCorrect) + { + row.AutoItem().Background("#16A34A").PaddingHorizontal(4).PaddingVertical(1) + .CornerRadius(2).Text("CORRECT").FontSize(7).Bold().FontColor("#FFFFFF"); + } + }); + + if (a.Image != null && a.Image.Length > 0) + { + col.Item().PaddingVertical(4).AlignCenter().MaxHeight(MaxAnswerImageHeight).Image(a.Image); + } + + ComposeLangBlock(col, "NL", a.Text.GetValueOrDefault("nl"), "#F97316"); + ComposeLangBlock(col, "FR", a.Text.GetValueOrDefault("fr"), "#3B82F6"); + ComposeLangBlock(col, "EN", a.Text.GetValueOrDefault("en"), "#10B981"); + }); + } + + static void ComposeLangBlock(ColumnDescriptor column, string tag, string? text, string accentColor) + { + if (string.IsNullOrWhiteSpace(text)) return; + + column.Item().PaddingTop(3).BorderLeft(3).BorderColor(accentColor).Background("#F8FAFC").Padding(4).Row(row => + { + row.AutoItem().PaddingRight(5).Text(tag).Bold().FontSize(8).FontColor("#64748B"); + row.RelativeItem().Text(text); + }); + } + } +} \ No newline at end of file diff --git a/QuizTools/QuizTools.csproj b/QuizTools/QuizTools.csproj new file mode 100644 index 0000000..518c3aa --- /dev/null +++ b/QuizTools/QuizTools.csproj @@ -0,0 +1,26 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + Always + + + + + + + + + + diff --git a/QuizTools/images/lswtech300-95.svg b/QuizTools/images/lswtech300-95.svg new file mode 100644 index 0000000..2c9c69d --- /dev/null +++ b/QuizTools/images/lswtech300-95.svg @@ -0,0 +1,130 @@ + + + + + + + LSW-Tech Logo + + + + + + + + + + + + + + + + + + + LSW-Tech Logo + 2026-06-12 + + + Luc Vandenbroucke + + + + + +