diff --git a/README.md b/README.md index d1c51be..4bd0c0f 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Important part of every project are **[tests](https://github.com/Gramli/WeatherA In this solution, each 'code' project has its own unit test project and every **unit test** project copy the same directory structure as 'code' project, which is very helpful for orientation in test project. Infrastructure project has also **integration tests**, because for format conversion is used third party library and we want to know that conversion works always as expected (for example when we update library version). ## Frontend Example -The frontend is a simple Angular 18 project that demonstrates how to upload and download files as blobs or FormData from the C# API. Files are saved to the Downloads folder using the [file-saver](https://www.npmjs.com/package/file-saver) library. For styling, the project utilizes [Bootstrap](https://getbootstrap.com/). Additionally, there are examples of displaying modals with [ng-bootstrap](https://www.npmjs.com/package/@ng-bootstrap/ng-bootstrap) and toasts/notifications with [angular-notifier](https://www.npmjs.com/package/gramli-angular-notifier). +The frontend is a simple Angular project that demonstrates how to upload and download files as blobs or FormData from the C# API. Files are saved to the Downloads folder using the [file-saver](https://www.npmjs.com/package/file-saver) library. For styling, the project utilizes [Bootstrap](https://getbootstrap.com/). It also includes examples of displaying modals with [ng-bootstrap](https://www.npmjs.com/package/@ng-bootstrap/ng-bootstrap) and showing toasts/notifications with [angular-notifier](https://www.npmjs.com/package/gramli-angular-notifier). Additionally, it demonstrates how to use translations in an Angular project with the [@ngx-translate/core](https://ngx-translate.org/) library, including a custom implementation of TranslateLoader to handle multiple translation files. ## Technologies * [ASP.NET Core 9](https://learn.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-9.0) diff --git a/src/File.API/EndpointBuilders/FileEndpointsBuilder.cs b/src/File.API/EndpointBuilders/FileEndpointsBuilder.cs index e13e271..df44aff 100644 --- a/src/File.API/EndpointBuilders/FileEndpointsBuilder.cs +++ b/src/File.API/EndpointBuilders/FileEndpointsBuilder.cs @@ -18,8 +18,8 @@ public static class FileEndpointsBuilder public static IEndpointRouteBuilder BuildFileEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) { return endpointRouteBuilder - .MapGroup("file") .MapVersionGroup(1) + .MapGroup("files") .BuildUploadEndpoints() .BuildDownloadEndpoints() .BuildGetEndpoints() @@ -29,7 +29,7 @@ public static IEndpointRouteBuilder BuildFileEndpoints(this IEndpointRouteBuilde private static IEndpointRouteBuilder BuildUploadEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) { - endpointRouteBuilder.MapPost("upload", + endpointRouteBuilder.MapPost("/upload", async (IFormFile file, [FromServices] IAddFilesCommandHandler handler, CancellationToken cancellationToken) => await handler.SendAsync(new AddFilesCommand([new FormFileProxy(file)]), cancellationToken)) .DisableAntiforgery() @@ -41,16 +41,16 @@ await handler.SendAsync(new AddFilesCommand([new FormFileProxy(file)]), cancella private static IEndpointRouteBuilder BuildDownloadEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) { - endpointRouteBuilder.MapGet("download", - async ([FromQuery] int id, [FromServices] IDownloadFileQueryHandler handler, CancellationToken cancellationToken) => + endpointRouteBuilder.MapGet("/{id}/download", + async (int id, [FromServices] IDownloadFileQueryHandler handler, CancellationToken cancellationToken) => await handler.GetFileAsync(new DownloadFileQuery(id), cancellationToken)) .DisableAntiforgery() .Produces() .WithName("DownloadFile") .WithTags("Get"); - endpointRouteBuilder.MapGet("downloadAsJson", - async ([FromQuery] int id, [FromServices] IDownloadFileQueryHandler handler, CancellationToken cancellationToken) => + endpointRouteBuilder.MapGet("/{id}/download/json", + async (int id, [FromServices] IDownloadFileQueryHandler handler, CancellationToken cancellationToken) => await handler.GetJsonFileAsync(new DownloadFileQuery(id), cancellationToken)) .DisableAntiforgery() .ProducesDataResponse() @@ -62,7 +62,7 @@ await handler.GetJsonFileAsync(new DownloadFileQuery(id), cancellationToken)) private static IEndpointRouteBuilder BuildGetEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) { - endpointRouteBuilder.MapGet("files-info", + endpointRouteBuilder.MapGet("/", async ([FromServices] IGetFilesInfoQueryHandler handler, CancellationToken cancellationToken) => await handler.SendAsync(EmptyRequest.Instance, cancellationToken)) .ProducesDataResponse>() @@ -73,9 +73,13 @@ await handler.SendAsync(EmptyRequest.Instance, cancellationToken)) private static IEndpointRouteBuilder BuildParseEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) { - endpointRouteBuilder.MapPost("export", - async ([FromBody]ExportFileQuery parseFileQuery,[FromServices] IExportFileQueryHandler handler, CancellationToken cancellationToken) => - await handler.GetFileAsync(parseFileQuery, cancellationToken)) + endpointRouteBuilder.MapGet("{id}/export", + async (int id, [FromQuery]string extension,[FromServices] IExportFileQueryHandler handler, CancellationToken cancellationToken) => + await handler.GetFileAsync(new ExportFileQuery + { + Id = id, + Extension = extension + }, cancellationToken)) .DisableAntiforgery() .ProducesDataResponse() .WithName("Export") @@ -85,7 +89,7 @@ await handler.GetFileAsync(parseFileQuery, cancellationToken)) private static IEndpointRouteBuilder BuildExportEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) { - endpointRouteBuilder.MapPost("convert", + endpointRouteBuilder.MapPost("/convert", async (IFormFile file, [FromForm]string formatToConvert, [FromServices] IConvertToQueryHandler handler, CancellationToken cancellationToken) => await handler.GetFileAsync(new ConvertToQuery(new FormFileProxy(file), formatToConvert), cancellationToken)) .DisableAntiforgery() diff --git a/src/File.API/File.API.csproj b/src/File.API/File.API.csproj index 5bf4b07..ec84d76 100644 --- a/src/File.API/File.API.csproj +++ b/src/File.API/File.API.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/File.Frontend/angular.json b/src/File.Frontend/angular.json index 424769d..1329000 100644 --- a/src/File.Frontend/angular.json +++ b/src/File.Frontend/angular.json @@ -35,7 +35,8 @@ { "glob": "**/*", "input": "public" - } + }, + "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", diff --git a/src/File.Frontend/package-lock.json b/src/File.Frontend/package-lock.json index 4fa740f..86cbf5f 100644 --- a/src/File.Frontend/package-lock.json +++ b/src/File.Frontend/package-lock.json @@ -21,6 +21,8 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", "@ng-bootstrap/ng-bootstrap": "^18.0.0", + "@ngx-translate/core": "^17.0.0", + "@ngx-translate/http-loader": "^17.0.0", "@popperjs/core": "^2.11.8", "@types/node": "^22.7.2", "bootstrap": "^5.3.3", @@ -5200,6 +5202,32 @@ "webpack": "^5.54.0" } }, + "node_modules/@ngx-translate/core": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-17.0.0.tgz", + "integrity": "sha512-Rft2D5ns2pq4orLZjEtx1uhNuEBerUdpFUG1IcqtGuipj6SavgB8SkxtNQALNDA+EVlvsNCCjC2ewZVtUeN6rg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16", + "@angular/core": ">=16" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-17.0.0.tgz", + "integrity": "sha512-hgS8sa0ARjH9ll3PhkLTufeVXNI2DNR2uFKDhBgq13siUXzzVr/a31M6zgecrtwbA34iaBV01hsTMbMS8V7iIw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16", + "@angular/core": ">=16" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/src/File.Frontend/package.json b/src/File.Frontend/package.json index c8724b1..a04c99f 100644 --- a/src/File.Frontend/package.json +++ b/src/File.Frontend/package.json @@ -23,6 +23,8 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", "@ng-bootstrap/ng-bootstrap": "^18.0.0", + "@ngx-translate/core": "^17.0.0", + "@ngx-translate/http-loader": "^17.0.0", "@popperjs/core": "^2.11.8", "@types/node": "^22.7.2", "bootstrap": "^5.3.3", diff --git a/src/File.Frontend/src/app/app.component.html b/src/File.Frontend/src/app/app.component.html index d0b0e1f..95ffeb5 100644 --- a/src/File.Frontend/src/app/app.component.html +++ b/src/File.Frontend/src/app/app.component.html @@ -1,7 +1,7 @@

File.Frontend

-

Processing...

+

{{ 'processing' | translate}}

Processing... - - - - - + + + + + @@ -34,16 +34,16 @@

Processing...

class="btn btn-info btn-sm mx-1" (click)="onDownloadFile(item.id)" > - Download + {{ 'download' | translate }} @@ -67,10 +67,10 @@

Processing...

diff --git a/src/File.Frontend/src/app/app.component.ts b/src/File.Frontend/src/app/app.component.ts index d5c7a50..35143a5 100644 --- a/src/File.Frontend/src/app/app.component.ts +++ b/src/File.Frontend/src/app/app.component.ts @@ -9,6 +9,7 @@ import { saveAs } from 'file-saver'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { FileLoadingService } from './services/file-loading.service'; import { NotificationAdapterService } from './services/notification-adapter.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-root', @@ -26,7 +27,8 @@ export class AppComponent implements OnInit { constructor( protected fileService: FileLoadingService, private ngbModal: NgbModal, - private notifierService: NotificationAdapterService + private notifierService: NotificationAdapterService, + private translateService: TranslateService ) {} public ngOnInit(): void { diff --git a/src/File.Frontend/src/app/app.module.ts b/src/File.Frontend/src/app/app.module.ts index 21bf1ba..7b63f25 100644 --- a/src/File.Frontend/src/app/app.module.ts +++ b/src/File.Frontend/src/app/app.module.ts @@ -2,12 +2,24 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { provideHttpClient } from '@angular/common/http'; +import { HttpClient, provideHttpClient } from '@angular/common/http'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { FormsModule } from '@angular/forms'; import { SelectExtensionModalComponent } from './components/select-extension-modal.component'; import { NotifierModule } from 'gramli-angular-notifier'; +import { MultiTranslateHttpLoader } from './translate/multi-translate-http-loader'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; + +export function HttpLoaderFactory(http: HttpClient): TranslateLoader { + return new MultiTranslateHttpLoader(http, [ + { prefix: './assets/i18n/', suffix: '.json' }, + { prefix: './assets/i18n/components/', suffix: '.json' }, + ]); +} @NgModule({ declarations: [AppComponent, SelectExtensionModalComponent], @@ -17,6 +29,15 @@ import { NotifierModule } from 'gramli-angular-notifier'; FontAwesomeModule, NgbModule, FormsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient], + }, + lang: 'en', + fallbackLang: 'en' + }), NotifierModule.withConfig({ position: { horizontal: { @@ -25,7 +46,9 @@ import { NotifierModule } from 'gramli-angular-notifier'; }, }), ], - providers: [provideHttpClient()], + providers: [ + provideHttpClient() + ], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/File.Frontend/src/app/components/select-extension-modal.component.html b/src/File.Frontend/src/app/components/select-extension-modal.component.html index 729c6fc..8351928 100644 --- a/src/File.Frontend/src/app/components/select-extension-modal.component.html +++ b/src/File.Frontend/src/app/components/select-extension-modal.component.html @@ -1,6 +1,6 @@
- +
IdNameFileNameTypeAction{{ 'table.id' | translate }}{{ 'table.name' | translate }}{{ 'table.fileName' | translate }}{{ 'table.type' | translate }}{{ 'table.action' | translate }}