diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*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 \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..df7ee90 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,42 @@ +stages: + - build + - test + - publish + +variables: + DOCKER_IMAGE: registry.leon-hoppe.de/leon.hoppe/portfolio + +build: + stage: build + image: mcr.microsoft.com/dotnet/sdk:9.0 + script: + - dotnet restore + - dotnet build --configuration Release --no-restore + artifacts: + paths: + - "**/bin/Release" + expire_in: 10 minutes + +test: + stage: test + image: mcr.microsoft.com/dotnet/sdk:9.0 + script: + - dotnet test --verbosity normal + dependencies: + - build + +publish: + stage: publish + image: docker:latest + services: + - name: docker:dind + alias: docker + script: + - export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//') + - docker login -u leon.hoppe -p ${CI_REGISTRY_PASSWORD} registry.leon-hoppe.de + - docker build -t $DOCKER_IMAGE/api:$VERSION -f src/Portfolio.Api/Dockerfile . + - docker build -t $DOCKER_IMAGE/web:$VERSION -f src/Portfolio.Web/Dockerfile . + - docker push $DOCKER_IMAGE/api:$VERSION + - docker push $DOCKER_IMAGE/web:$VERSION + only: + - tags diff --git a/Portfolio.sln b/Portfolio.sln index a55ff74..fa6cdb8 100644 --- a/Portfolio.sln +++ b/Portfolio.sln @@ -1,8 +1,42 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{D8B3EB3B-9FFA-48AF-BDCF-35A540E5DF98}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Portfolio.Host", "src\Portfolio.Host\Portfolio.Host.csproj", "{7043E058-AEC6-4063-AA46-57C44E2F3BC2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Portfolio.Shared", "src\Portfolio.Shared\Portfolio.Shared.csproj", "{D4ADF9DA-4A2B-4C31-92F0-B43D0F4EDE1C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Portfolio.Api", "src\Portfolio.Api\Portfolio.Api.csproj", "{F502B467-0B4D-4DD8-BBD8-0F2BD7860965}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Portfolio.Web", "src\Portfolio.Web\Portfolio.Web.csproj", "{92368C80-5525-4F44-8FEA-03969E50931C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {7043E058-AEC6-4063-AA46-57C44E2F3BC2} = {D8B3EB3B-9FFA-48AF-BDCF-35A540E5DF98} + {D4ADF9DA-4A2B-4C31-92F0-B43D0F4EDE1C} = {D8B3EB3B-9FFA-48AF-BDCF-35A540E5DF98} + {F502B467-0B4D-4DD8-BBD8-0F2BD7860965} = {D8B3EB3B-9FFA-48AF-BDCF-35A540E5DF98} + {92368C80-5525-4F44-8FEA-03969E50931C} = {D8B3EB3B-9FFA-48AF-BDCF-35A540E5DF98} + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7043E058-AEC6-4063-AA46-57C44E2F3BC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7043E058-AEC6-4063-AA46-57C44E2F3BC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7043E058-AEC6-4063-AA46-57C44E2F3BC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7043E058-AEC6-4063-AA46-57C44E2F3BC2}.Release|Any CPU.Build.0 = Release|Any CPU + {D4ADF9DA-4A2B-4C31-92F0-B43D0F4EDE1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4ADF9DA-4A2B-4C31-92F0-B43D0F4EDE1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4ADF9DA-4A2B-4C31-92F0-B43D0F4EDE1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4ADF9DA-4A2B-4C31-92F0-B43D0F4EDE1C}.Release|Any CPU.Build.0 = Release|Any CPU + {F502B467-0B4D-4DD8-BBD8-0F2BD7860965}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F502B467-0B4D-4DD8-BBD8-0F2BD7860965}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F502B467-0B4D-4DD8-BBD8-0F2BD7860965}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F502B467-0B4D-4DD8-BBD8-0F2BD7860965}.Release|Any CPU.Build.0 = Release|Any CPU + {92368C80-5525-4F44-8FEA-03969E50931C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92368C80-5525-4F44-8FEA-03969E50931C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92368C80-5525-4F44-8FEA-03969E50931C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92368C80-5525-4F44-8FEA-03969E50931C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection EndGlobal diff --git a/src/Portfolio.Api/Dockerfile b/src/Portfolio.Api/Dockerfile new file mode 100644 index 0000000..70e4d47 --- /dev/null +++ b/src/Portfolio.Api/Dockerfile @@ -0,0 +1,30 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src + +COPY ["src/Portfolio.Shared/Portfolio.Shared.csproj", "./Portfolio.Shared/"] +RUN dotnet restore "Portfolio.Shared/Portfolio.Shared.csproj" + +COPY ["src/Portfolio.Api/Portfolio.Api.csproj", "./Portfolio.Api/"] +RUN dotnet restore "Portfolio.Api/Portfolio.Api.csproj" + +COPY ["src/Portfolio.Shared/", "./Portfolio.Shared/"] +COPY ["src/Portfolio.Api", "./Portfolio.Api/"] + +WORKDIR "/src/Portfolio.Api" +RUN dotnet build "Portfolio.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Portfolio.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Portfolio.Api.dll"] diff --git a/src/Portfolio.Api/Portfolio.Api.csproj b/src/Portfolio.Api/Portfolio.Api.csproj new file mode 100644 index 0000000..35db154 --- /dev/null +++ b/src/Portfolio.Api/Portfolio.Api.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + enable + enable + Linux + + + + + + + + + .dockerignore + + + + + + + + diff --git a/src/Portfolio.Api/Program.cs b/src/Portfolio.Api/Program.cs new file mode 100644 index 0000000..e850aef --- /dev/null +++ b/src/Portfolio.Api/Program.cs @@ -0,0 +1,18 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); +builder.AddServiceDefaults(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) { + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); +app.MapDefaultEndpoints(); + +app.Run(); diff --git a/src/Portfolio.Api/Properties/launchSettings.json b/src/Portfolio.Api/Properties/launchSettings.json new file mode 100644 index 0000000..39182d4 --- /dev/null +++ b/src/Portfolio.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5129", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7228;http://localhost:5129", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Portfolio.Api/appsettings.Development.json b/src/Portfolio.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Portfolio.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Portfolio.Api/appsettings.json b/src/Portfolio.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/Portfolio.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Portfolio.Host/Portfolio.Host.csproj b/src/Portfolio.Host/Portfolio.Host.csproj new file mode 100644 index 0000000..5f44a98 --- /dev/null +++ b/src/Portfolio.Host/Portfolio.Host.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + net9.0 + enable + enable + true + d6312aa1-33db-42d9-8204-b00161666c4d + + + + + + + + + + + + diff --git a/src/Portfolio.Host/Program.cs b/src/Portfolio.Host/Program.cs new file mode 100644 index 0000000..e976287 --- /dev/null +++ b/src/Portfolio.Host/Program.cs @@ -0,0 +1,9 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var api = builder.AddProject("api"); + +builder.AddProject("web") + .WithReference(api) + .WaitFor(api); + +builder.Build().Run(); \ No newline at end of file diff --git a/src/Portfolio.Host/Properties/launchSettings.json b/src/Portfolio.Host/Properties/launchSettings.json new file mode 100644 index 0000000..17c54d9 --- /dev/null +++ b/src/Portfolio.Host/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:17188;http://localhost:15187", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22234" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:15187", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19200", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20273" + } + } + } +} diff --git a/src/Portfolio.Host/appsettings.Development.json b/src/Portfolio.Host/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Portfolio.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Portfolio.Host/appsettings.json b/src/Portfolio.Host/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/src/Portfolio.Host/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/Portfolio.Shared/Extensions.cs b/src/Portfolio.Shared/Extensions.cs new file mode 100644 index 0000000..c5ea557 --- /dev/null +++ b/src/Portfolio.Shared/Extensions.cs @@ -0,0 +1,110 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions { + public static TBuilder AddServiceDefaults(this TBuilder builder) + where TBuilder : IHostApplicationBuilder { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) + where TBuilder : IHostApplicationBuilder { + builder.Logging.AddOpenTelemetry(logging => { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) + where TBuilder : IHostApplicationBuilder { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) + where TBuilder : IHostApplicationBuilder { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} \ No newline at end of file diff --git a/src/Portfolio.Shared/Portfolio.Shared.csproj b/src/Portfolio.Shared/Portfolio.Shared.csproj new file mode 100644 index 0000000..6a8950a --- /dev/null +++ b/src/Portfolio.Shared/Portfolio.Shared.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/src/Portfolio.Web/Components/App.razor b/src/Portfolio.Web/Components/App.razor new file mode 100644 index 0000000..939e292 --- /dev/null +++ b/src/Portfolio.Web/Components/App.razor @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Layout/MainLayout.razor b/src/Portfolio.Web/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..e3b2918 --- /dev/null +++ b/src/Portfolio.Web/Components/Layout/MainLayout.razor @@ -0,0 +1,23 @@ +@inherits LayoutComponentBase + +
+ + +
+
+ About +
+ +
+ @Body +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
\ No newline at end of file diff --git a/src/Portfolio.Web/Components/Layout/MainLayout.razor.css b/src/Portfolio.Web/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..38d1f25 --- /dev/null +++ b/src/Portfolio.Web/Components/Layout/MainLayout.razor.css @@ -0,0 +1,98 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + 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/src/Portfolio.Web/Components/Layout/NavMenu.razor b/src/Portfolio.Web/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..0b37b9d --- /dev/null +++ b/src/Portfolio.Web/Components/Layout/NavMenu.razor @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Layout/NavMenu.razor.css b/src/Portfolio.Web/Components/Layout/NavMenu.razor.css new file mode 100644 index 0000000..a2aeace --- /dev/null +++ b/src/Portfolio.Web/Components/Layout/NavMenu.razor.css @@ -0,0 +1,105 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + min-height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/src/Portfolio.Web/Components/Pages/Counter.razor b/src/Portfolio.Web/Components/Pages/Counter.razor new file mode 100644 index 0000000..15c11c3 --- /dev/null +++ b/src/Portfolio.Web/Components/Pages/Counter.razor @@ -0,0 +1,19 @@ +@page "/counter" +@rendermode InteractiveServer + +Counter + +

Counter

+ +

Current count: @currentCount

+ + + +@code { + private int currentCount = 0; + + private void IncrementCount() { + currentCount++; + } + +} \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Pages/Error.razor b/src/Portfolio.Web/Components/Pages/Error.razor new file mode 100644 index 0000000..06de831 --- /dev/null +++ b/src/Portfolio.Web/Components/Pages/Error.razor @@ -0,0 +1,35 @@ +@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; + +} \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Pages/Home.razor b/src/Portfolio.Web/Components/Pages/Home.razor new file mode 100644 index 0000000..dfcdf75 --- /dev/null +++ b/src/Portfolio.Web/Components/Pages/Home.razor @@ -0,0 +1,7 @@ +@page "/" + +Home + +

Hello, world!

+ +Welcome to your new app. \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Pages/Weather.razor b/src/Portfolio.Web/Components/Pages/Weather.razor new file mode 100644 index 0000000..f8204c6 --- /dev/null +++ b/src/Portfolio.Web/Components/Pages/Weather.razor @@ -0,0 +1,61 @@ +@page "/weather" +@attribute [StreamRendering] + +Weather + +

Weather

+ +

This component demonstrates showing data.

+ +@if (forecasts == null) { +

+ Loading... +

+} +else { + + + + + + + + + + + @foreach (var forecast in forecasts) { + + + + + + + } + +
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+} + +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() { + // Simulate asynchronous loading to demonstrate streaming rendering + await Task.Delay(500); + + var startDate = DateOnly.FromDateTime(DateTime.Now); + var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; + forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast { + Date = startDate.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }).ToArray(); + } + + private class WeatherForecast { + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Routes.razor b/src/Portfolio.Web/Components/Routes.razor new file mode 100644 index 0000000..ae94e9e --- /dev/null +++ b/src/Portfolio.Web/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Portfolio.Web/Components/_Imports.razor b/src/Portfolio.Web/Components/_Imports.razor new file mode 100644 index 0000000..0822e89 --- /dev/null +++ b/src/Portfolio.Web/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 Portfolio.Web +@using Portfolio.Web.Components \ No newline at end of file diff --git a/src/Portfolio.Web/Dockerfile b/src/Portfolio.Web/Dockerfile new file mode 100644 index 0000000..a857689 --- /dev/null +++ b/src/Portfolio.Web/Dockerfile @@ -0,0 +1,30 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src + +COPY ["src/Portfolio.Shared/Portfolio.Shared.csproj", "./Portfolio.Shared/"] +RUN dotnet restore "Portfolio.Shared/Portfolio.Shared.csproj" + +COPY ["src/Portfolio.Web/Portfolio.Web.csproj", "./Portfolio.Web/"] +RUN dotnet restore "Portfolio.Web/Portfolio.Web.csproj" + +COPY ["src/Portfolio.Shared/", "./Portfolio.Shared/"] +COPY ["src/Portfolio.Web", "./Portfolio.Web/"] + +WORKDIR "/src/Portfolio.Web" +RUN dotnet build "Portfolio.Web.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Portfolio.Web.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Portfolio.Web.dll"] diff --git a/src/Portfolio.Web/Portfolio.Web.csproj b/src/Portfolio.Web/Portfolio.Web.csproj new file mode 100644 index 0000000..92326bb --- /dev/null +++ b/src/Portfolio.Web/Portfolio.Web.csproj @@ -0,0 +1,67 @@ + + + + net9.0 + enable + enable + Linux + + + + + .dockerignore + + + + + + + + + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.min.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.min.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.min.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.min.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.min.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.min.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.min.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.min.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.min.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.min.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.min.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.min.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.min.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.min.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.min.css" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.min.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.js" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.js.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.min.js" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.bundle.min.js.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.js" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.js.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.min.js" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.esm.min.js.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.js" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.js.map" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.min.js" /> + <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\js\bootstrap.min.js.map" /> + + + diff --git a/src/Portfolio.Web/Program.cs b/src/Portfolio.Web/Program.cs new file mode 100644 index 0000000..d82cbbf --- /dev/null +++ b/src/Portfolio.Web/Program.cs @@ -0,0 +1,29 @@ +using Portfolio.Web.Components; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +builder.AddServiceDefaults(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) { + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.MapDefaultEndpoints(); + +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); \ No newline at end of file diff --git a/src/Portfolio.Web/Properties/launchSettings.json b/src/Portfolio.Web/Properties/launchSettings.json new file mode 100644 index 0000000..168993b --- /dev/null +++ b/src/Portfolio.Web/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5075", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7202;http://localhost:5075", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/src/Portfolio.Web/appsettings.Development.json b/src/Portfolio.Web/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Portfolio.Web/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Portfolio.Web/appsettings.json b/src/Portfolio.Web/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/Portfolio.Web/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Portfolio.Web/wwwroot/app.css b/src/Portfolio.Web/wwwroot/app.css new file mode 100644 index 0000000..e69de29 diff --git a/src/Portfolio.Web/wwwroot/favicon.png b/src/Portfolio.Web/wwwroot/favicon.png new file mode 100644 index 0000000..a1dc44a --- /dev/null +++ b/src/Portfolio.Web/wwwroot/favicon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e265ac0f2dda1e5dfa65b1adf330722bb3ef7789115283604d8cd19f098f1f08 +size 1148