From 0b04c89a9a5af7aef69f9638b9bed70739359502 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 Aug 2025 18:33:22 +0000 Subject: [PATCH 2/8] Initial exploration and project setup validation Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> --- packages-microsoft-prod.deb | Bin 0 -> 4308 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages-microsoft-prod.deb diff --git a/packages-microsoft-prod.deb b/packages-microsoft-prod.deb new file mode 100644 index 0000000000000000000000000000000000000000..1ce5d32aa77a1036ef466e849189babce65191c9 GIT binary patch literal 4308 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 YAtpMP0he8U?G^tF5oxCX??wptFBdKI^#A|> literal 0 HcmV?d00001 From c8b9fd320cb1035c0c15e5986e03c91f09901a1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 Aug 2025 18:42:47 +0000 Subject: [PATCH 3/8] Create comprehensive .github/copilot-instructions.md with validated build and run instructions Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> --- .github/copilot-instructions.md | 101 ++++++++++++++++++++++++++++++++ packages-microsoft-prod.deb | Bin 4308 -> 0 bytes 2 files changed, 101 insertions(+) create mode 100644 .github/copilot-instructions.md delete mode 100644 packages-microsoft-prod.deb diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..3df2c5f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,101 @@ +# ShiftScheduler +ShiftScheduler is a Blazor WebAssembly application for managing work shift schedules with calendar export capabilities. The application allows users to select shifts for each day of the month and export schedules as ICS (calendar) or PDF files. + +Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. + +## Working Effectively +- **CRITICAL**: Install .NET 9.0 SDK first: + - `wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb` + - `sudo dpkg -i packages-microsoft-prod.deb` + - `sudo apt-get update` + - `sudo apt-get remove -y dotnet-host-8.0 dotnet-runtime-8.0 aspnetcore-runtime-8.0 dotnet-sdk-8.0` (removes conflicts) + - `sudo apt-get install -y dotnet-sdk-9.0` + - Verify with `dotnet --version` (should show 9.0.x) + +- Bootstrap and build the application: + - `dotnet restore` -- takes 30 seconds. NEVER CANCEL. Set timeout to 60+ minutes. + - `dotnet build` -- takes 15-20 seconds. NEVER CANCEL. Set timeout to 60+ minutes. + - `dotnet clean` -- takes 3 seconds when needed + +- Run the application: + - Server: `cd Server && dotnet run` -- takes 15 seconds to start. NEVER CANCEL. + - Access at: `http://localhost:5000` + - Application will be available after Blazor WebAssembly loads (~10 seconds additional) + +- No unit tests exist in this project: + - `dotnet test` runs successfully but reports "Build succeeded" with 0 tests + +## Validation +- **MANUAL VALIDATION REQUIREMENT**: Always test the full user workflow after making changes: + 1. Navigate to `http://localhost:5000` + 2. Wait for Blazor WebAssembly to load (look for calendar grid) + 3. Click shift buttons (⚫ for "Frei", 🌴 for "Urlaub") to select shifts + 4. Verify tooltips appear showing shift names + 5. Test export functionality by clicking "Export to ICS" or "Export to PDF" + 6. Verify files download correctly + +- **API VALIDATION**: Test core endpoints: + - `curl http://localhost:5000/api/shift/shifts` (should return shift configurations) + - PDF export: `curl -X POST http://localhost:5000/api/shift/export_pdf -H "Content-Type: application/json" -d '{"2025-09-01": "Frei"}' --output test.pdf` + +- The application has 2 known build warnings that are non-breaking: + - Obsolete QuestPDF Image method warning + - Possible null reference in IcsExportService + +- No linting tools are configured. No specific formatting or linting commands to run. + +## Validation Scenarios +After making changes, always run through these scenarios: +1. **Calendar Interaction**: Select different shifts for various days and verify visual feedback +2. **Export Functions**: Test both ICS and PDF export with selected shifts +3. **API Responses**: Verify shift data loads correctly via API calls +4. **Application Startup**: Confirm the app starts without errors and loads the UI + +## Common Tasks +### Project Structure +``` +ShiftScheduler.sln # Main solution file +├── Client/ # Blazor WebAssembly frontend +│ ├── Pages/Index.razor # Main calendar UI +│ └── Program.cs # Client startup +├── Server/ # ASP.NET Core host +│ ├── Program.cs # Server startup and dependency injection +│ ├── Controllers/ShiftController.cs # API endpoints +│ └── appsettings.json # Shift configurations +├── Shared/ # Shared models +│ └── Shift.cs # Shift data model +└── Services/ # Business logic + ├── ShiftService.cs # Shift data management + ├── IcsExportService.cs # Calendar export (uses Ical.Net) + └── PdfExportService.cs # PDF export (uses QuestPDF) +``` + +### Key Configuration Files +- `Server/appsettings.json`: Contains shift definitions with Name, Icon, MorningTime, AfternoonTime +- `Server/Properties/launchSettings.json`: Development server configuration (port 5000) +- All `.csproj` files target .NET 9.0 + +### Dependencies +- **Client**: Microsoft.AspNetCore.Components.WebAssembly +- **Server**: Microsoft.AspNetCore.Components.WebAssembly.Server +- **Services**: QuestPDF (PDF generation), Ical.Net (calendar export) + +### Timing Expectations +- **NEVER CANCEL**: All build operations may take time but must complete +- `dotnet restore`: 30 seconds (first time), 1-2 seconds (subsequent) +- `dotnet build`: 15-20 seconds clean build, 5-10 seconds incremental +- `dotnet run` startup: 15 seconds until server ready + 10 seconds for WebAssembly load +- Application response: Immediate once loaded + +### Common Development Workflows +1. **Making UI Changes**: Edit `Client/Pages/Index.razor` or related files, then `dotnet run` in Server directory +2. **Adding New Shifts**: Update `Server/appsettings.json` "Shifts" section +3. **API Changes**: Modify `Server/Controllers/ShiftController.cs` +4. **Business Logic**: Update files in `Services/` directory +5. **Data Models**: Modify `Shared/Shift.cs` + +### Troubleshooting +- If build fails with "does not support targeting .NET 9.0": Install .NET 9.0 SDK as described above +- If application won't start: Check that no other service is using port 5000 +- If WebAssembly doesn't load: Wait longer, check browser console for JavaScript errors +- Export issues: Verify shift configurations in appsettings.json have proper time formats for ICS export \ No newline at end of file diff --git a/packages-microsoft-prod.deb b/packages-microsoft-prod.deb deleted file mode 100644 index 1ce5d32aa77a1036ef466e849189babce65191c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4308 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 YAtpMP0he8U?G^tF5oxCX??wptFBdKI^#A|> From 78678033f9c7f2c6f263718730b6fc5edf4658c9 Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Sun, 17 Aug 2025 21:05:54 +0200 Subject: [PATCH 4/8] Enhance PDF --- Services/PdfExportService.cs | 210 ++++++++++++++++++++++++----------- 1 file changed, 146 insertions(+), 64 deletions(-) diff --git a/Services/PdfExportService.cs b/Services/PdfExportService.cs index cc2cd21..c22f12e 100644 --- a/Services/PdfExportService.cs +++ b/Services/PdfExportService.cs @@ -8,91 +8,173 @@ namespace ShiftScheduler.Services { public class PdfExportService { + private static readonly Color borderColor = Colors.Grey.Darken1; + private static readonly float boarderWidth = 1; + static PdfExportService() { - QuestPDF.Settings.License = LicenseType.Community; + QuestPDF.Settings.License = LicenseType.Community; } public byte[] GenerateMonthlySchedulePdf(Dictionary schedule, List shifts) { var year = schedule.First().Key.Year; var month = schedule.First().Key.Month; - var daysInMonth = DateTime.DaysInMonth(year, month); - var monthDates = Enumerable.Range(1, daysInMonth) - .Select(day => new DateTime(year, month, day)) - .ToList(); var document = Document.Create(container => { container.Page(page => { page.Margin(30); - page.Size(PageSizes.A4); - page.DefaultTextStyle(x => x.FontSize(12)); + page.Size(PageSizes.A4.Landscape()); + page.DefaultTextStyle(x => x.FontSize(10)); page.Header() .Text($"Shift Schedule for {CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(month)} {year}") - .FontSize(18).Bold().AlignCenter(); - - page.Content().Table(table => - { - table.ColumnsDefinition(columns => - { - columns.RelativeColumn(2); // Date - columns.RelativeColumn(2); // Shift Name - columns.RelativeColumn(2); // Times - columns.RelativeColumn(1); // Icon - }); - - // Header - table.Header(header => - { - header.Cell().Text("Date").Bold(); - header.Cell().Text("Shift").Bold(); - header.Cell().Text("Times").Bold(); - header.Cell().Text("Icon").Bold(); - }); - - foreach (var date in monthDates) - { - var dateText = date.ToString("dd.MM.yyyy (dddd)", CultureInfo.InvariantCulture); - table.Cell().Text(dateText); - - var shiftName = schedule.ContainsKey(date) ? schedule[date] : null; - if (shiftName != null) - { - var shift = shifts.Single(s => s.Name.Equals(shiftName)); - table.Cell().Text(shift.Name); - table.Cell().Text($"{shift.MorningTime} - {shift.AfternoonTime}"); - - if (!string.IsNullOrWhiteSpace(shift.Icon)) - { - try - { - table.Cell().Image(shift.Icon, ImageScaling.FitArea); - } - catch - { - table.Cell().Text("[icon missing]"); - } - } - else - { - table.Cell().Text("-"); - } - } - else - { - table.Cell().Text("-"); - table.Cell().Text("-"); - table.Cell().Text("-"); - } - } - }); + .FontSize(16).Bold().AlignCenter(); + + page.Content().Element(e => ComposeTable(e, schedule, shifts)); }); }); return document.GeneratePdf(); } + + void ComposeTable(IContainer container, IReadOnlyDictionary schedule, IReadOnlyList shifts) + { + container.Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + }); + + var year = schedule.First().Key.Year; + var month = schedule.First().Key.Month; + var daysInMonth = DateTime.DaysInMonth(year, month); + var monthDates = Enumerable.Range(1, daysInMonth) + .Select(day => new DateTime(year, month, day)) + .ToList(); + var weeks = monthDates + .GroupBy(d => CultureInfo.InvariantCulture.Calendar.GetWeekOfYear( + d, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday)) + .ToList(); + foreach (var week in weeks) + { + RenderShiftForWeek(table, week, schedule, shifts); + } + }); + } + + private static void RenderShiftForWeek(TableDescriptor table, IGrouping week, + IReadOnlyDictionary schedule, IReadOnlyList shifts) + { + var weekDates = week.ToList(); + var dayInWeek = weekDates.Count; + foreach (var day in Enumerable.Range(0, dayInWeek)) + { + var dddd = weekDates.ElementAtOrDefault(day).ToString("dd.MM.yyyy (ddd)"); + table.Cell().Element(CellStyleFirst).Text(dddd); + } + foreach (var day in Enumerable.Range(dayInWeek, 7 - dayInWeek)) + { + RenderEmptyCell(table); + } + + foreach (var day in Enumerable.Range(0, dayInWeek)) + { + var shiftName = shifts.Single(s => s.Name == schedule[weekDates.ElementAtOrDefault(day)]).Name; + table.Cell().Element(CellStyleMiddle).Text(shiftName); + } + foreach (var day in Enumerable.Range(dayInWeek, 7 - dayInWeek)) + { + RenderEmptyCell(table); + } + + foreach (var day in Enumerable.Range(0, dayInWeek)) + { + var morningTime = shifts.Single(s => s.Name == schedule[weekDates.ElementAtOrDefault(day)]).MorningTime; + table.Cell().Element(CellStyleMiddle).Text(morningTime); + } + foreach (var day in Enumerable.Range(dayInWeek, 7 - dayInWeek)) + { + RenderEmptyCell(table); + } + + foreach (var day in Enumerable.Range(0, dayInWeek)) + { + var afternoonTime = shifts.Single(s => s.Name == schedule[weekDates.ElementAtOrDefault(day)]).AfternoonTime; + table.Cell().Element(CellStyleMiddle).Text(afternoonTime); + } + foreach (var day in Enumerable.Range(dayInWeek, 7 - dayInWeek)) + { + RenderEmptyCell(table); + } + + foreach (var day in Enumerable.Range(0, dayInWeek)) + { + var icon = shifts.Single(s => s.Name == schedule[weekDates.ElementAtOrDefault(day)]).Icon; + table.Cell().Element(CellStyleLast).Text(icon); + } + foreach (var day in Enumerable.Range(dayInWeek, 7 - dayInWeek)) + { + RenderEmptyCell(table); + } + } + + private static void RenderEmptyCell(TableDescriptor table) + { + table.Cell().Element(EmptyCellStyle).Text(""); + } + + private static IContainer CellStyleFirst(IContainer container) + { + return container + .Padding(0) + .BorderLeft(boarderWidth) + .BorderTop(boarderWidth) + .BorderRight(boarderWidth) + .BorderColor(borderColor) + .AlignCenter() + .AlignMiddle(); + } + + private static IContainer CellStyleMiddle(IContainer container) + { + return container + .Padding(0) + .BorderLeft(boarderWidth) + .BorderRight(boarderWidth) + .BorderColor(borderColor) + .AlignCenter() + .AlignMiddle(); + } + + private static IContainer CellStyleLast(IContainer container) + { + return container + .Padding(0) + .BorderLeft(boarderWidth) + .BorderBottom(boarderWidth) + .BorderRight(boarderWidth) + .BorderColor(borderColor) + .AlignCenter() + .AlignMiddle(); + } + + private static IContainer EmptyCellStyle(IContainer container) + { + return container + .Padding(0) + .BorderLeft(0) + .BorderBottom(0) + .BorderRight(0); + } } } From 76a15317294d939b858d3c476cb42343456343d4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 Aug 2025 19:40:52 +0200 Subject: [PATCH 5/8] Implement localStorage persistence for shift selections (#6) * Initial plan * Fix .NET framework version compatibility from 9.0 to 8.0 Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Implement localStorage persistence for shift selections Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Restore .NET 9.0 version and update Index.razor.cs per feedback Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Fix compilation error by making EditMonth and EditYear instance variables Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Minor changes --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> Co-authored-by: Claudio Schaad --- Client/Pages/Index.razor.cs | 62 ++++++++++++++++++++++++++++++++++--- Client/wwwroot/index.html | 2 -- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/Client/Pages/Index.razor.cs b/Client/Pages/Index.razor.cs index 582534b..ceda173 100644 --- a/Client/Pages/Index.razor.cs +++ b/Client/Pages/Index.razor.cs @@ -1,26 +1,34 @@ using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; using ShiftScheduler.Shared.Models; using System.Net.Http.Json; +using System.Text.Json; namespace ShiftScheduler.Client.Pages { public partial class Index : ComponentBase { [Inject] private HttpClient HttpClient { get; set; } = default!; + [Inject] private IJSRuntime JSRuntime { get; set; } = default!; + [Inject] private NavigationManager NavigationManager { get; set; } = default!; private List DaysInMonth { get; set; } = new(); private List Shifts { get; set; } = new(); private Dictionary SelectedSchedule { get; set; } = new(); + private int EditMonth { get; set; } + private int EditYear { get; set; } protected override async Task OnInitializedAsync() { - var (nextMonth, year) = GetNextMonthAndYear(); + (EditMonth, EditYear) = GetNextMonthAndYear(); - DaysInMonth = Enumerable.Range(1, DateTime.DaysInMonth(year, nextMonth)) - .Select(d => new DateTime(year, nextMonth, d)) + DaysInMonth = Enumerable.Range(1, DateTime.DaysInMonth(EditYear, EditMonth)) + .Select(d => new DateTime(EditYear, EditMonth, d)) .ToList(); Shifts = await HttpClient.GetFromJsonAsync>("api/shift/shifts") ?? new(); + + await LoadScheduleFromStorage(); } private (int nextMonth, int year) GetNextMonthAndYear() @@ -31,9 +39,10 @@ namespace ShiftScheduler.Client.Pages return (nextMonth, year); } - private void SelectShift(DateTime day, string shiftName) + private async Task SelectShift(DateTime day, string shiftName) { SelectedSchedule[day] = shiftName; + await SaveScheduleToStorage(); } private async Task ExportToIcs() @@ -54,6 +63,49 @@ namespace ShiftScheduler.Client.Pages NavigationManager.NavigateTo(fileUrl, true); } - [Inject] private NavigationManager NavigationManager { get; set; } = default!; + private async Task SaveScheduleToStorage() + { + try + { + var storageKey = $"ShiftSchedule_{EditYear}_{EditMonth:D2}"; + + var serializableSchedule = SelectedSchedule.ToDictionary( + kvp => kvp.Key.ToString("yyyy-MM-dd"), + kvp => kvp.Value + ); + + var json = JsonSerializer.Serialize(serializableSchedule); + await JSRuntime.InvokeVoidAsync("localStorage.setItem", storageKey, json); + } + catch (Exception) + { + // ignored + } + } + + private async Task LoadScheduleFromStorage() + { + try + { + var storageKey = $"ShiftSchedule_{EditYear}_{EditMonth:D2}"; + var json = await JSRuntime.InvokeAsync("localStorage.getItem", storageKey); + + if (!string.IsNullOrEmpty(json)) + { + var serializableSchedule = JsonSerializer.Deserialize>(json); + if (serializableSchedule != null) + { + SelectedSchedule = serializableSchedule.ToDictionary( + kvp => DateTime.Parse(kvp.Key), + kvp => kvp.Value + ); + } + } + } + catch (Exception) + { + SelectedSchedule = new Dictionary(); + } + } } } diff --git a/Client/wwwroot/index.html b/Client/wwwroot/index.html index da19b49..9dead99 100644 --- a/Client/wwwroot/index.html +++ b/Client/wwwroot/index.html @@ -27,8 +27,6 @@ 🗙 - - From 6267c01fc891ef80b2d6e1c9eaa9c1293ec9aaaf Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Tue, 19 Aug 2025 06:27:21 +0200 Subject: [PATCH 6/8] Update copilot-instructions.md common instructions --- .github/copilot-instructions.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3df2c5f..f073251 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -98,4 +98,8 @@ ShiftScheduler.sln # Main solution file - If build fails with "does not support targeting .NET 9.0": Install .NET 9.0 SDK as described above - If application won't start: Check that no other service is using port 5000 - If WebAssembly doesn't load: Wait longer, check browser console for JavaScript errors -- Export issues: Verify shift configurations in appsettings.json have proper time formats for ICS export \ No newline at end of file +- Export issues: Verify shift configurations in appsettings.json have proper time formats for ICS export + + ## Common Instructions + - Don't write code comments + - Use var instead of explicit type, ex. var x = 4; From ac6f471511e67f41d3a413816c016f4e4e9c058e Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Tue, 19 Aug 2025 06:28:00 +0200 Subject: [PATCH 7/8] Update copilot-instructions.md fix formatting --- .github/copilot-instructions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f073251..e937674 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -100,6 +100,6 @@ ShiftScheduler.sln # Main solution file - If WebAssembly doesn't load: Wait longer, check browser console for JavaScript errors - Export issues: Verify shift configurations in appsettings.json have proper time formats for ICS export - ## Common Instructions - - Don't write code comments - - Use var instead of explicit type, ex. var x = 4; +## Common Instructions +- Don't write code comments +- Use var instead of explicit type, ex. var x = 4; From e275cbd240c483adc1b5156bc60b27a8bf23a3eb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 Aug 2025 06:34:29 +0200 Subject: [PATCH 8/8] Add reset button with proper styling and improve export button appearance (#8) * Initial plan * Initial plan for reset button implementation Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Implement reset button with styling and export button improvements Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Remove code comments and clean up .deb file Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> --- .gitignore | 6 ++++- Client/Pages/Index.razor | 10 +++++--- Client/Pages/Index.razor.cs | 15 ++++++++++++ Client/Pages/Index.razor.css | 47 ++++++++++++++++++++++++++++++++---- 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index fd490fc..c60101d 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,8 @@ CodeCoverage/ # NUnit *.VisualState.xml TestResult.xml -nunit-*.xml \ No newline at end of file +nunit-*.xml + +# Temporary installation files +*.deb +*.rpm \ No newline at end of file diff --git a/Client/Pages/Index.razor b/Client/Pages/Index.razor index ac50d18..fa4ad60 100644 --- a/Client/Pages/Index.razor +++ b/Client/Pages/Index.razor @@ -25,7 +25,11 @@ } -
- - +
+ +
+ +
+ +
diff --git a/Client/Pages/Index.razor.cs b/Client/Pages/Index.razor.cs index ceda173..ea90886 100644 --- a/Client/Pages/Index.razor.cs +++ b/Client/Pages/Index.razor.cs @@ -107,5 +107,20 @@ namespace ShiftScheduler.Client.Pages SelectedSchedule = new Dictionary(); } } + + private async Task ResetSchedule() + { + SelectedSchedule.Clear(); + + try + { + var storageKey = $"ShiftSchedule_{EditYear}_{EditMonth:D2}"; + await JSRuntime.InvokeVoidAsync("localStorage.removeItem", storageKey); + } + catch (Exception) + { + // ignored + } + } } } diff --git a/Client/Pages/Index.razor.css b/Client/Pages/Index.razor.css index 8465dc8..c823f7e 100644 --- a/Client/Pages/Index.razor.css +++ b/Client/Pages/Index.razor.css @@ -52,14 +52,51 @@ } /* Export button */ -.export-btn { - margin-top: 20px; +.btn.export-btn { + margin: 5px; padding: 10px 20px; font-size: 16px; cursor: pointer; - background-color: #28a745; - color: white; - border: none; + background-color: #ADD8E6; /* Light blue */ + color: #333; + border: 2px solid #87CEEB; /* Slightly darker blue border */ + border-radius: 5px; + transition: background-color 0.2s ease; +} + +.btn.export-btn:hover { + background-color: #87CEEB; + border-color: #4682B4; +} + +/* Reset button */ +.reset-btn { + margin: 5px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + background-color: #FFB6C1; /* Light red/pink */ + color: #333; + border: 2px solid #FF91A4; /* Slightly darker red border */ + border-radius: 5px; + transition: background-color 0.2s ease; +} + +.reset-btn:hover { + background-color: #FF91A4; + border-color: #FF69B4; +} + +/* Reset section - separate from export buttons */ +.reset-section { + margin: 30px 0 20px 0; + text-align: center; +} + +/* Export buttons section */ +.export-buttons { + margin: 20px 0; + text-align: center; } /* Tooltip container */