diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67babe0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +## ========================================== +## 1. Fichiers de compilation & Cache .NET +## ========================================== +**/bin/ +**/obj/ +**/out/ + +## ========================================== +## 2. Visual Studio & IDE (VS Code, JetBrains) +## ========================================== +.vs/ +.vscode/ +*.suo +*.user +*.userosscache +*.sln.docstates + +## ========================================== +## 3. Packages NuGet temporaires +## ========================================== +nupkgs/ +*.nupkg +*.snupkg + +## ========================================== +## 4. Fichiers système +## ========================================== +Thumbs.db +[Dd]esktop.ini +.DS_Store \ No newline at end of file diff --git a/HA_Shopping_List.Server/Components/App.razor b/HA_Shopping_List.Server/Components/App.razor new file mode 100644 index 0000000..b90968a --- /dev/null +++ b/HA_Shopping_List.Server/Components/App.razor @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HA_Shopping_List.Server/Components/Layout/MainLayout.razor b/HA_Shopping_List.Server/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..f7226b6 --- /dev/null +++ b/HA_Shopping_List.Server/Components/Layout/MainLayout.razor @@ -0,0 +1,10 @@ +@inherits LayoutComponentBase + +
+ @Body +
+ + \ No newline at end of file diff --git a/HA_Shopping_List.Server/Components/Layout/MainLayout.razor.css b/HA_Shopping_List.Server/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..df8c10f --- /dev/null +++ b/HA_Shopping_List.Server/Components/Layout/MainLayout.razor.css @@ -0,0 +1,18 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/HA_Shopping_List.Server/Components/Pages/Error.razor b/HA_Shopping_List.Server/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/HA_Shopping_List.Server/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/HA_Shopping_List.Server/Components/Pages/Home.razor b/HA_Shopping_List.Server/Components/Pages/Home.razor new file mode 100644 index 0000000..3822bee --- /dev/null +++ b/HA_Shopping_List.Server/Components/Pages/Home.razor @@ -0,0 +1,445 @@ +@page "/" +@rendermode InteractiveServer +@using HA_Shopping_List.Server.Data +@using HA_Shopping_List.Server.Enum +@using Microsoft.EntityFrameworkCore +@inject IDbContextFactory DbFactory + +Liste de Courses + +
+ +

+ 🛒 Courses de la Maison +

+ + +
+ + + + + +
+ + + + + @if (currentTab == Tab.ShoppingList) + { + +
+
+ + + + @foreach (var suggestion in historySuggestions) + { + + } + + + + + + +
+
+ + +
+ @if (!activeItems.Any()) + { +
+ La liste est vide ! Utilisez le champ ci-dessus ou piochez dans l'historique. +
+ } + else + { + @foreach (var item in activeItems) + { +
+
+ @item.Name + @if (item.Quantity > 1) + { + + x@(item.Quantity) + + } +
+ +
+ +
+ + + @item.Quantity + + +
+ + + + + + +
+
+ } + } +
+ } + + + + + @if (currentTab == Tab.Cart) + { +
+
+

Articles actuellement dans le panier

+ @if (cartItems.Any()) + { + + } +
+ + @if (!cartItems.Any()) + { +
+ Votre chariot est vide pour l'instant. +
+ } + else + { + @foreach (var item in cartItems) + { +
+
+ @item.Name + @if (item.Quantity > 1) + { + (x@(item.Quantity)) + } +
+ +
+ } + } +
+ } + + + + + @if (currentTab == Tab.History) + { +
+ +
+ +
+ + @{ + var filteredHistory = string.IsNullOrWhiteSpace(searchQuery) + ? historyItems + : historyItems.Where(h => h.Name.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + @if (!filteredHistory.Any()) + { +
+ Aucun article trouvé dans l'historique. +
+ } + else + { + @foreach (var item in filteredHistory) + { +
+
+ @item.Name +
+ Dernier achat : @item.LastPurchasedAt.ToString("dd/MM/yyyy") +
+
+ +
+ + +
+
+ } + } +
+ } +
+ +@code { + private enum Tab { ShoppingList, Cart, History } + private Tab currentTab = Tab.ShoppingList; + + private List activeItems = new(); + private List cartItems = new(); + private List historyItems = new(); + private List historySuggestions = new(); + + private string newItemName = string.Empty; + private int newItemQuantity = 1; + private string searchQuery = string.Empty; + + protected override async Task OnInitializedAsync() + { + await LoadAllData(); + } + + private async Task LoadAllData() + { + using var db = await DbFactory.CreateDbContextAsync(); + + var all = await db.Items.ToListAsync(); + + activeItems = all.Where(i => i.Status == ShoppingItemStatus.Active) + .OrderBy(i => i.Name) + .ToList(); + + cartItems = all.Where(i => i.Status == ShoppingItemStatus.InCart) + .OrderBy(i => i.Name) + .ToList(); + + // L'historique regroupe les articles par nom distinct pour l'affichage et l'autocomplétion + historyItems = all.GroupBy(i => i.Name.Trim().ToLowerInvariant()) + .Select(g => g.OrderByDescending(x => x.LastPurchasedAt).First()) + .OrderBy(i => i.Name) + .ToList(); + + historySuggestions = historyItems.Select(h => h.Name).ToList(); + StateHasChanged(); + } + + private async Task AddItem() + { + if (string.IsNullOrWhiteSpace(newItemName)) return; + + using var db = await DbFactory.CreateDbContextAsync(); + var normalizedName = newItemName.Trim(); + + // Vérifie si l'article est déjà présent dans la liste active + var existingActive = await db.Items.FirstOrDefaultAsync(i => + i.Status == ShoppingItemStatus.Active && + i.Name.ToLower() == normalizedName.ToLower()); + + if (existingActive != null) + { + existingActive.Quantity += newItemQuantity; + } + else + { + db.Items.Add(new ShoppingItem + { + Name = normalizedName, + Quantity = newItemQuantity, + Status = ShoppingItemStatus.Active, + LastPurchasedAt = DateTime.Now + }); + } + + await db.SaveChangesAsync(); + + newItemName = string.Empty; + newItemQuantity = 1; + await LoadAllData(); + } + + private async Task MoveToCart(ShoppingItem item) + { + using var db = await DbFactory.CreateDbContextAsync(); + var entity = await db.Items.FindAsync(item.Id); + if (entity != null) + { + entity.Status = ShoppingItemStatus.InCart; + await db.SaveChangesAsync(); + } + await LoadAllData(); + } + + private async Task ReturnToList(ShoppingItem item) + { + using var db = await DbFactory.CreateDbContextAsync(); + var entity = await db.Items.FindAsync(item.Id); + if (entity != null) + { + entity.Status = ShoppingItemStatus.Active; + await db.SaveChangesAsync(); + } + await LoadAllData(); + } + + private async Task RemoveActiveItem(ShoppingItem item) + { + using var db = await DbFactory.CreateDbContextAsync(); + var entity = await db.Items.FindAsync(item.Id); + if (entity != null) + { + // Passe en statut Purchased pour rester consultable dans l'historique + entity.Status = ShoppingItemStatus.Purchased; + entity.LastPurchasedAt = DateTime.Now; + await db.SaveChangesAsync(); + } + await LoadAllData(); + } + + private async Task ClearCartAndArchive() + { + using var db = await DbFactory.CreateDbContextAsync(); + var inCart = await db.Items.Where(i => i.Status == ShoppingItemStatus.InCart).ToListAsync(); + + foreach (var item in inCart) + { + item.Status = ShoppingItemStatus.Purchased; + item.LastPurchasedAt = DateTime.Now; + } + + await db.SaveChangesAsync(); + await LoadAllData(); + } + + private async Task ReAddFromHistory(ShoppingItem item) + { + using var db = await DbFactory.CreateDbContextAsync(); + var existingActive = await db.Items.FirstOrDefaultAsync(i => + i.Status == ShoppingItemStatus.Active && + i.Name.ToLower() == item.Name.ToLower()); + + if (existingActive != null) + { + existingActive.Quantity += 1; + } + else + { + db.Items.Add(new ShoppingItem + { + Name = item.Name, + Quantity = 1, + Status = ShoppingItemStatus.Active, + LastPurchasedAt = DateTime.Now + }); + } + + await db.SaveChangesAsync(); + await LoadAllData(); + } + + private async Task DeleteFromHistory(int id) + { + using var db = await DbFactory.CreateDbContextAsync(); + var entity = await db.Items.FindAsync(id); + if (entity != null) + { + db.Items.Remove(entity); + await db.SaveChangesAsync(); + } + await LoadAllData(); + } + + private async Task IncreaseQuantity(ShoppingItem item) + { + using var db = await DbFactory.CreateDbContextAsync(); + var entity = await db.Items.FindAsync(item.Id); + if (entity != null) + { + entity.Quantity++; + await db.SaveChangesAsync(); + } + await LoadAllData(); + } + + private async Task DecreaseQuantity(ShoppingItem item) + { + using var db = await DbFactory.CreateDbContextAsync(); + var entity = await db.Items.FindAsync(item.Id); + if (entity != null) + { + if (entity.Quantity > 1) + { + entity.Quantity--; + await db.SaveChangesAsync(); + } + else + { + // Si la quantité passe en dessous de 1, l'article est retiré de la liste active vers l'historique + await RemoveActiveItem(item); + return; + } + } + await LoadAllData(); + } +} \ No newline at end of file diff --git a/HA_Shopping_List.Server/Components/Routes.razor b/HA_Shopping_List.Server/Components/Routes.razor new file mode 100644 index 0000000..f756e19 --- /dev/null +++ b/HA_Shopping_List.Server/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/HA_Shopping_List.Server/Components/_Imports.razor b/HA_Shopping_List.Server/Components/_Imports.razor new file mode 100644 index 0000000..f2b8813 --- /dev/null +++ b/HA_Shopping_List.Server/Components/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using HA_Shopping_List.Server +@using HA_Shopping_List.Server.Components diff --git a/HA_Shopping_List.Server/Data/AppDbContext.cs b/HA_Shopping_List.Server/Data/AppDbContext.cs new file mode 100644 index 0000000..93bc60d --- /dev/null +++ b/HA_Shopping_List.Server/Data/AppDbContext.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; +using System.Reflection.Emit; + +namespace HA_Shopping_List.Server.Data +{ + /// + /// Classe AppDbContext. + /// Implémente le + /// + /// + public class AppDbContext : DbContext + { + /// + /// Initialise une nouvelle instance de la classe . + /// + /// L'options. + public AppDbContext(DbContextOptions options) : base(options) { } + + /// + /// Obtient le items. + /// + /// La valeur "items" + public DbSet Items => Set(); + + /// + /// Appelé lorsque [model creating]. + /// + /// Le model builder. + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .HasIndex(i => i.Name); + } + } +} diff --git a/HA_Shopping_List.Server/Data/ShoppingItem.cs b/HA_Shopping_List.Server/Data/ShoppingItem.cs new file mode 100644 index 0000000..fa46a9e --- /dev/null +++ b/HA_Shopping_List.Server/Data/ShoppingItem.cs @@ -0,0 +1,46 @@ +using HA_Shopping_List.Server.Enum; + +namespace HA_Shopping_List.Server.Data +{ + /// + /// Classe ShoppingItem. + /// + public class ShoppingItem + { + /// + /// Obtient ou définit le identifier. + /// + /// La valeur "identifier" + public int Id { get; set; } + + /// + /// Obtient ou définit le name. + /// + /// La valeur "name" + public string Name { get; set; } = string.Empty; + + /// + /// Obtient ou définit le quantity. + /// + /// La valeur "quantity" + public int Quantity { get; set; } = 1; + + /// + /// Obtient ou définit le unit. + /// + /// La valeur "unit" + public string? Unit { get; set; } // Ex: kg, pack, bouteille, ou null + + /// + /// Obtient ou définit le status. + /// + /// La valeur "status" + public ShoppingItemStatus Status { get; set; } = ShoppingItemStatus.Active; + + /// + /// Obtient ou définit le last purchased at. + /// + /// La valeur "last purchased at" + public DateTime LastPurchasedAt { get; set; } = DateTime.Now; + } +} diff --git a/HA_Shopping_List.Server/Dockerfile b/HA_Shopping_List.Server/Dockerfile new file mode 100644 index 0000000..4c39c1f --- /dev/null +++ b/HA_Shopping_List.Server/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +RUN mkdir -p /data && chmod 777 /data +EXPOSE 8099 +ENV ASPNETCORE_URLS=http://+:8099 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["HA_Shopping_List.Server/HA_Shopping_List.Server.csproj", "HA_Shopping_List.Server/"] +RUN dotnet restore "./HA_Shopping_List.Server/HA_Shopping_List.Server.csproj" +COPY . . +WORKDIR "/src/HA_Shopping_List.Server" +RUN dotnet build "./HA_Shopping_List.Server.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./HA_Shopping_List.Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "HA_Shopping_List.Server.dll"] \ No newline at end of file diff --git a/HA_Shopping_List.Server/Enum/ShoppingItemStatus.cs b/HA_Shopping_List.Server/Enum/ShoppingItemStatus.cs new file mode 100644 index 0000000..722e24b --- /dev/null +++ b/HA_Shopping_List.Server/Enum/ShoppingItemStatus.cs @@ -0,0 +1,23 @@ +namespace HA_Shopping_List.Server.Enum +{ + /// + /// Énumération ShoppingItemStatus + /// + public enum ShoppingItemStatus + { + /// + /// Le Active. + /// + Active = 0, // Dans la liste de courses + + /// + /// Le In cart. + /// + InCart = 1, // Dans le chariot + + /// + /// Le Purchased. + /// + Purchased = 2 // Acheté / uniquement dans l'historique + } +} diff --git a/HA_Shopping_List.Server/HA_Shopping_List.Server.csproj b/HA_Shopping_List.Server/HA_Shopping_List.Server.csproj new file mode 100644 index 0000000..3bfdfe3 --- /dev/null +++ b/HA_Shopping_List.Server/HA_Shopping_List.Server.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + Linux + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/HA_Shopping_List.Server/Program.cs b/HA_Shopping_List.Server/Program.cs new file mode 100644 index 0000000..1a2b354 --- /dev/null +++ b/HA_Shopping_List.Server/Program.cs @@ -0,0 +1,59 @@ +using HA_Shopping_List.Server.Components; +using HA_Shopping_List.Server.Data; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// Blazor Server interactif +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(options => options.DetailedErrors = true); + +// Persistance dans /data (Home Assistant) ou dans le dossier local en Dev +var dbFolder = Directory.Exists("/data") ? "/data" : AppContext.BaseDirectory; +Directory.CreateDirectory(dbFolder); +var dbPath = Path.Combine(dbFolder, "shopping.db"); + +builder.Services.AddDbContextFactory(options => + options.UseSqlite($"Data Source={dbPath}")); + +var app = builder.Build(); + +// Création automatique de la base SQLite au démarrage +using (var scope = app.Services.CreateScope()) +{ + var factory = scope.ServiceProvider.GetRequiredService>(); + using var db = factory.CreateDbContext(); + db.Database.EnsureCreated(); +} + +#if DEBUG +app.Use(async (context, next) => +{ + context.Response.Headers.Append("Permissions-Policy", "unload=(self)"); + await next(); +}); +#endif + +// Gestion du préfixe dynamique Ingress de Home Assistant +app.Use(async (context, next) => +{ + if (context.Request.Headers.TryGetValue("X-Ingress-Path", out var ingressPath)) + { + context.Request.PathBase = ingressPath.ToString(); + } + await next(); +}); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); +} + +app.UseStaticFiles(); +app.UseAntiforgery(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +// Port Ingress standard +app.Run("http://0.0.0.0:8099"); \ No newline at end of file diff --git a/HA_Shopping_List.Server/Properties/launchSettings.json b/HA_Shopping_List.Server/Properties/launchSettings.json new file mode 100644 index 0000000..b279b86 --- /dev/null +++ b/HA_Shopping_List.Server/Properties/launchSettings.json @@ -0,0 +1,39 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:8099" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTP_PORTS": "8099" + }, + "publishAllPorts": true, + "useSSL": false + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:33927", + "sslPort": 0 + } + } +} \ No newline at end of file diff --git a/HA_Shopping_List.Server/appsettings.Development.json b/HA_Shopping_List.Server/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/HA_Shopping_List.Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/HA_Shopping_List.Server/appsettings.json b/HA_Shopping_List.Server/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/HA_Shopping_List.Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/HA_Shopping_List.Server/wwwroot/app.css b/HA_Shopping_List.Server/wwwroot/app.css new file mode 100644 index 0000000..e398853 --- /dev/null +++ b/HA_Shopping_List.Server/wwwroot/app.css @@ -0,0 +1,29 @@ +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} diff --git a/HA_Shopping_List.Server/wwwroot/favicon.ico b/HA_Shopping_List.Server/wwwroot/favicon.ico new file mode 100644 index 0000000..7016724 Binary files /dev/null and b/HA_Shopping_List.Server/wwwroot/favicon.ico differ diff --git a/HA_Shopping_List.slnx b/HA_Shopping_List.slnx new file mode 100644 index 0000000..8f6c5ab --- /dev/null +++ b/HA_Shopping_List.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docker_launch.ps1 b/docker_launch.ps1 new file mode 100644 index 0000000..4b653fb --- /dev/null +++ b/docker_launch.ps1 @@ -0,0 +1,44 @@ +function Start-DockerDesktop { + [CmdletBinding()] + param( + [Parameter()] + [string]$Path = "$env:ProgramFiles\Docker\Docker\Docker Desktop.exe", + + [Parameter()] + [int]$TimeoutSeconds = 120 + ) + + if (-not (Test-Path $Path)) { + Write-Error "Docker Desktop est introuvable au chemin : $Path" + return $false + } + + $dockerProcess = Get-Process -Name "Docker Desktop" -ErrorAction SilentlyContinue + if ($null -eq $dockerProcess) { + Write-Host "Démarrage de Docker Desktop..." -ForegroundColor Cyan + Start-Process -FilePath $Path + } else { + Write-Host "Le processus Docker Desktop est déjà en cours." -ForegroundColor Yellow + } + + Write-Host "En attente du moteur Docker..." -NoNewline + $elapsed = 0 + $interval = 2 + + while ($elapsed -lt $TimeoutSeconds) { + cmd.exe /c "docker info >nul 2>&1" + if ($LASTEXITCODE -eq 0) { + Write-Host " Prêt !" -ForegroundColor Green + docker --version + return $true + } + + Start-Sleep -Seconds $interval + Write-Host "." -NoNewline + $elapsed += $interval + } + + Write-Host "" + Write-Warning "Le moteur Docker n'a pas répondu dans le délai imparti ($TimeoutSeconds s)." + return $false +} \ No newline at end of file diff --git a/publish.ps1 b/publish.ps1 new file mode 100644 index 0000000..e14ea46 --- /dev/null +++ b/publish.ps1 @@ -0,0 +1,39 @@ +# S'assurer que le script s'arrête immédiatement en cas d'erreur +$ErrorActionPreference = "Stop" + +. "$PSScriptRoot\docker_launch.ps1" + +Write-Host "=== 1/5. Demarage de docker ===" -ForegroundColor Cyan +Start-DockerDesktop + +$RegistryUrl = "gitea.mragot1408.synology.me" +$User = "admin_gitea" +$Pass = "Z7Q2kKELozwno0Eve8b4FYnlCw" +$ImageName = "hashoppinglist" +$VersionNumber = "1.0.0" +$Version = "latest" + + +Write-Host "`n=== 2/5. Nettoyage et Compilation de la Solution (Release | Any CPU) ===" -ForegroundColor Cyan +dotnet restore +docker login https://$RegistryUrl -u $User -p $Pass + +Write-Host "`n=== 3/5. Build de Blazor / Docker ===" -ForegroundColor Cyan +dotnet build ./HA_Shopping_List.Server/HA_Shopping_List.Server.csproj --configuration Release -p:Platform="Any CPU" + +# Format obligatoire pour Gitea : Domaine/Utilisateur/NomImage (en minuscules) +$FullImage = "gitea.mragot1408.synology.me/$($User.ToLower())/$($ImageName.ToLower())" + +Write-Host "`n=== 4/5. Publication du Blazor / Docker ===" -ForegroundColor Cyan +docker build --no-cache ` + -t "${FullImage}:${VersionNumber}" ` + -t "${FullImage}:${Version}" ` + -f ./HA_Shopping_List.Server/Dockerfile . + +Write-Host "`n=== 5/5. Push du Blazor ===" -ForegroundColor Cyan +docker push "${FullImage}:${VersionNumber}" +docker push "${FullImage}:${Version}" + +Write-Host "`n==========================================" -ForegroundColor Green +Write-Host " Terminé avec succès !" -ForegroundColor Green +Write-Host "==========================================" -ForegroundColor Green \ No newline at end of file