From b171dc9f3713fd9f5fbfef9afdde76609d0f28e1 Mon Sep 17 00:00:00 2001 From: maxstue Date: Wed, 20 Nov 2024 09:40:23 +0100 Subject: [PATCH 01/15] SAVE --- .../Extensions/ApplicationExtensions.cs | 2 - .../AuthorizationHandlerExtension.cs | 31 ------- .../Common/Extensions/CurrentUserExtension.cs | 51 ----------- .../Common/Extensions/ServiceExtensions.cs | 14 ++- .../Middleware/CurrentUserMiddleware.cs | 85 +++++++++++++++++++ .../api/Kijk.Api/Common/Models/CurrentUser.cs | 6 +- apps/api/Kijk.Api/Common/Models/Dtos.cs | 12 ++- .../Domain/Entities/EnergyConsumption.cs | 7 ++ apps/api/Kijk.Api/Domain/Entities/User.cs | 6 ++ .../Endpoints/EnergyConsumptionsEndpoint.cs | 14 +++ .../EnergyConsumptionsModule.cs | 11 +++ .../GetByEnergyConsumptions.cs | 70 +++++++++++++++ .../Modules/Transactions/GetByTransactions.cs | 2 +- apps/api/Kijk.Api/Program.cs | 1 + 14 files changed, 215 insertions(+), 97 deletions(-) delete mode 100644 apps/api/Kijk.Api/Common/Extensions/AuthorizationHandlerExtension.cs delete mode 100644 apps/api/Kijk.Api/Common/Extensions/CurrentUserExtension.cs create mode 100644 apps/api/Kijk.Api/Common/Middleware/CurrentUserMiddleware.cs create mode 100644 apps/api/Kijk.Api/Endpoints/EnergyConsumptionsEndpoint.cs create mode 100644 apps/api/Kijk.Api/Modules/EnergyConsumptions/EnergyConsumptionsModule.cs create mode 100644 apps/api/Kijk.Api/Modules/EnergyConsumptions/GetByEnergyConsumptions.cs diff --git a/apps/api/Kijk.Api/Common/Extensions/ApplicationExtensions.cs b/apps/api/Kijk.Api/Common/Extensions/ApplicationExtensions.cs index 16a8c96..9e22ccb 100644 --- a/apps/api/Kijk.Api/Common/Extensions/ApplicationExtensions.cs +++ b/apps/api/Kijk.Api/Common/Extensions/ApplicationExtensions.cs @@ -18,8 +18,6 @@ public static IApplicationBuilder UseCustomOpenApi(this IApplicationBuilder appl applicationBuilder.UseSwaggerUI( c => { - // c.UseRequestInterceptor( - // "(req) => { req.headers['Authorization'] = 'Bearer ' + window?.swaggerUIRedirectOauth2?.auth?.token?.access_token; return req; }"); c.DefaultModelsExpandDepth(0); c.DefaultModelExpandDepth(0); c.SwaggerEndpoint("v1/swagger.json", "Kijk Api v1.00"); diff --git a/apps/api/Kijk.Api/Common/Extensions/AuthorizationHandlerExtension.cs b/apps/api/Kijk.Api/Common/Extensions/AuthorizationHandlerExtension.cs deleted file mode 100644 index 45f0844..0000000 --- a/apps/api/Kijk.Api/Common/Extensions/AuthorizationHandlerExtension.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Kijk.Api.Common.Models; -using Microsoft.AspNetCore.Authorization; - -namespace Kijk.Api.Common.Extensions; - -public static class AuthorizationHandlerExtensions -{ - // Adds the current user requirement that will activate our authorization handler - public static AuthorizationPolicyBuilder RequireCurrentUser(this AuthorizationPolicyBuilder builder) - { - return builder.RequireAuthenticatedUser().AddRequirements(new CheckCurrentUserRequirement()); - } -} - -public class CheckCurrentUserRequirement : IAuthorizationRequirement -{ -} - -// This authorization handler verifies that the user exists even if there's a valid token -public class CheckCurrentUserAuthHandler(CurrentUser currentUser) : AuthorizationHandler -{ - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CheckCurrentUserRequirement requirement) - { - if (currentUser is { User.AuthId: not null } and { Principal: not null }) - { - context.Succeed(requirement); - } - - return Task.CompletedTask; - } -} diff --git a/apps/api/Kijk.Api/Common/Extensions/CurrentUserExtension.cs b/apps/api/Kijk.Api/Common/Extensions/CurrentUserExtension.cs deleted file mode 100644 index 2da72ac..0000000 --- a/apps/api/Kijk.Api/Common/Extensions/CurrentUserExtension.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Security.Claims; -using Kijk.Api.Common.Models; -using Kijk.Api.Domain.Entities; -using Kijk.Api.Persistence; -using Microsoft.AspNetCore.Authentication; - -namespace Kijk.Api.Common.Extensions; - -public static class CurrentUserExtensions -{ - public static IServiceCollection AddCurrentUser(this IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - return services; - } - - /// - /// This class gets only called if is NOT null. - /// - private sealed class ClaimsTransformation(CurrentUser currentUser, AppDbContext dbContext) : IClaimsTransformation - { - // We're not going to transform anything. We're using this as a hook into authorization - // to set the current user without adding custom middleware. - public async Task TransformAsync(ClaimsPrincipal principal) - { - var sub = principal.FindFirstValue(ClaimTypes.NameIdentifier); - - if (sub != null) - { - var email = principal.FindFirstValue(ClaimTypes.Email); - var userEntity = await dbContext.Users - .AsNoTracking() - .Where(x => x.AuthId == sub) - .Select(x => SimpleAuthUser.Create(x)) - .FirstOrDefaultAsync(); - - currentUser.Principal = principal; - // TODO use more values from token - currentUser.User = userEntity ?? new SimpleAuthUser( - Guid.NewGuid(), - sub, - AppConstants.CreateUserIdentifier, - email, - true); - } - - return await Task.FromResult(principal); - } - } -} diff --git a/apps/api/Kijk.Api/Common/Extensions/ServiceExtensions.cs b/apps/api/Kijk.Api/Common/Extensions/ServiceExtensions.cs index 001254f..f6b9bca 100644 --- a/apps/api/Kijk.Api/Common/Extensions/ServiceExtensions.cs +++ b/apps/api/Kijk.Api/Common/Extensions/ServiceExtensions.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using Humanizer; using Kijk.Api.Common.Filters; +using Kijk.Api.Common.Middleware; using Kijk.Api.Common.Models; using Kijk.Api.Common.Options; using Kijk.Api.Modules.App; @@ -97,7 +98,7 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, IC { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "bearerAuth" } }, - new string[] { } + [] } }); @@ -187,7 +188,7 @@ public static IServiceCollection AddAuth(this IServiceCollection services, IConf ValidateAudience = false, NameClaimType = ClaimTypes.NameIdentifier }; - x.Events = new JwtBearerEvents() + x.Events = new JwtBearerEvents { // Additional validation for AZP claim OnTokenValidated = context => @@ -203,18 +204,15 @@ public static IServiceCollection AddAuth(this IServiceCollection services, IConf }; }); - // State that represents the current user from the request - services.AddCurrentUser(); + services.AddScoped(); + services.AddTransient(); services.AddAuthorizationBuilder() - .AddPolicy(AppConstants.Policies.All, policy => policy.RequireClaim("id").RequireCurrentUser().Build()); + .AddPolicy(AppConstants.Policies.All, policy => policy.RequireClaim("id").RequireAuthenticatedUser().Build()); // .AddPolicy(AppConstants.Policies.User, policy => policy.RequireRole(AppConstants.Roles.User).RequireCurrentUser().Build()) // .AddPolicy(AppConstants.Policies.Admin, policy => policy.RequireRole(AppConstants.Roles.Admin).RequireCurrentUser().Build()) - // add current user handler - services.AddScoped(); - return services; } diff --git a/apps/api/Kijk.Api/Common/Middleware/CurrentUserMiddleware.cs b/apps/api/Kijk.Api/Common/Middleware/CurrentUserMiddleware.cs new file mode 100644 index 0000000..5f932c3 --- /dev/null +++ b/apps/api/Kijk.Api/Common/Middleware/CurrentUserMiddleware.cs @@ -0,0 +1,85 @@ +using System.Security.Claims; +using System.Text.Json; +using Kijk.Api.Common.Models; +using Kijk.Api.Persistence; + +namespace Kijk.Api.Common.Middleware; + +public class CurrentUserMiddleware(AppDbContext dbContext, CurrentUser currentUser) : IMiddleware +{ + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var (isSuccess, extAuthId) = await SetCurrentUser(context); + if (isSuccess) + { + await next(context); + } + else + { + await HandleError(context, extAuthId); + } + } + + private async Task<(bool, string?)> SetCurrentUser(HttpContext context) + { + var extAuthId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); + + if (extAuthId == null) + { + return (false, extAuthId); + } + + var email = context.User.FindFirstValue(ClaimTypes.Email); + var userEntity = await GetUserFromDb(extAuthId); + currentUser.Principal = context.User; + + if (context.Request.Path == "/sign-in" && userEntity is null) + { + // TODO use more values from token + currentUser.User = new SimpleAuthUser( + Guid.NewGuid(), + extAuthId, + Guid.Empty, + AppConstants.CreateUserIdentifier, + email, + true); + return (true, extAuthId); + } + + if (userEntity is null || userEntity.HouseholdId == Guid.Empty) + { + return (false, extAuthId); + } + + currentUser.User = userEntity; + return (true, extAuthId); + } + + private Task GetUserFromDb(string sub) + { + return dbContext.Users + .AsNoTracking() + .Where(x => x.AuthId == sub) + .Select(x => SimpleAuthUser.Create(x)) + .FirstOrDefaultAsync(); + } + + private static async Task HandleError(HttpContext context, string? extAuthId) + { + context.Response.ContentType = "application/json"; + var resp = ApiResponseBuilder.Error(AppError.Basic(AppErrorCodes.NotFoundError, $"User for id '{extAuthId}' was not found")); + SentToSentry(resp); + await context.Response.WriteAsync(JsonSerializer.Serialize(resp)); + } + + private static void SentToSentry(ApiResponse> resp) + { + SentrySdk.CaptureMessage( + resp.Data?[0].Message ?? "AuthError: Token or role is not valid", + opt => + { + opt.SetExtra("Response", resp); + opt.SetExtra("Code", resp.Data?[0].Code); + }); + } +} diff --git a/apps/api/Kijk.Api/Common/Models/CurrentUser.cs b/apps/api/Kijk.Api/Common/Models/CurrentUser.cs index 03e209b..125a4bb 100644 --- a/apps/api/Kijk.Api/Common/Models/CurrentUser.cs +++ b/apps/api/Kijk.Api/Common/Models/CurrentUser.cs @@ -20,8 +20,10 @@ public class CurrentUser public string Email => this.User.Email ?? throw new ArgumentNullException(ClaimTypes.Upn, "'Upn/Email' not found"); public List Permissions => Principal.FindAll(PermissionsClaim).Select(x => x.Value).ToList(); - + public bool IsAdmin => this.Permissions.Contains(AppConstants.Roles.Admin); - + public bool IsUser => this.Permissions.Contains(AppConstants.Roles.Admin); + + public Guid ActiveHouseholdId => this.User.HouseholdId; } diff --git a/apps/api/Kijk.Api/Common/Models/Dtos.cs b/apps/api/Kijk.Api/Common/Models/Dtos.cs index a9e3850..2ac7fa8 100644 --- a/apps/api/Kijk.Api/Common/Models/Dtos.cs +++ b/apps/api/Kijk.Api/Common/Models/Dtos.cs @@ -47,8 +47,16 @@ public static CategoryDto Create(Category category) => new(category.Id, category.Name, category.Color, category.Type, category.CreatorType); } -public record SimpleAuthUser(Guid Id, string AuthId, string Name, string? Email, bool? FirstTime = false) +public record SimpleAuthUser(Guid Id, string AuthId, Guid HouseholdId, string Name, string? Email, bool? FirstTime = false) { public static SimpleAuthUser Create(User user) => - new(user.Id, user.AuthId, user.Name, user.Email, user.FirstTime); + new(user.Id, user.AuthId, user.GetActiveHouseHoldId(), user.Name, user.Email, user.FirstTime); +} + +public record EnergyConsumptionDto(Guid Id, string Name, string? Description, decimal Value, EnergyConsumptionType Type, DateTime CreatedAt) +{ + public static EnergyConsumptionDto Create(EnergyConsumption energyConsumption) => + new( + energyConsumption.Id, energyConsumption.Name, energyConsumption.Description, energyConsumption.Value, energyConsumption.Type, + energyConsumption.CreatedAt); } diff --git a/apps/api/Kijk.Api/Domain/Entities/EnergyConsumption.cs b/apps/api/Kijk.Api/Domain/Entities/EnergyConsumption.cs index 7aebd42..66b3d09 100644 --- a/apps/api/Kijk.Api/Domain/Entities/EnergyConsumption.cs +++ b/apps/api/Kijk.Api/Domain/Entities/EnergyConsumption.cs @@ -8,6 +8,11 @@ public sealed class EnergyConsumption : BaseEntity public string? Description { get; set; } public required decimal Value { get; set; } public required EnergyConsumptionType Type { get; set; } + + /// + /// Represents the date of the energy consumption. + /// + public required DateTime Date { get; set; } public Guid HouseholdId { get; set; } @@ -16,6 +21,7 @@ public static EnergyConsumption Create( EnergyConsumptionType type, decimal value, Guid householdId, + DateTime date, string? description = default) { return new() @@ -25,6 +31,7 @@ public static EnergyConsumption Create( Description = description, Type = type, Value = value, + Date = date, HouseholdId = householdId }; } diff --git a/apps/api/Kijk.Api/Domain/Entities/User.cs b/apps/api/Kijk.Api/Domain/Entities/User.cs index 688999b..c6c07ad 100644 --- a/apps/api/Kijk.Api/Domain/Entities/User.cs +++ b/apps/api/Kijk.Api/Domain/Entities/User.cs @@ -19,6 +19,12 @@ public sealed class User : BaseEntity public List Categories { get; set; } = []; + /// + /// It should never be empty as it is set when the user is created. + /// + /// + public Guid GetActiveHouseHoldId() => UserHouseholds.FirstOrDefault(x => x.IsDefault)?.HouseholdId ?? Guid.Empty; + public User SetDefaultCategories(bool? useDefaultCategories, List defaultCategories) { if (useDefaultCategories == true) diff --git a/apps/api/Kijk.Api/Endpoints/EnergyConsumptionsEndpoint.cs b/apps/api/Kijk.Api/Endpoints/EnergyConsumptionsEndpoint.cs new file mode 100644 index 0000000..61d97a6 --- /dev/null +++ b/apps/api/Kijk.Api/Endpoints/EnergyConsumptionsEndpoint.cs @@ -0,0 +1,14 @@ +namespace Kijk.Api.Endpoints; + +public static class EnergyConsumptionsEndpoint +{ + public static IEndpointRouteBuilder MaEnergyConsumptionsEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) + { + var group = endpointRouteBuilder.MapGroup("/energy-consumptions") + .WithTags("EnergyConsumptions"); + + + + return endpointRouteBuilder; + } +} diff --git a/apps/api/Kijk.Api/Modules/EnergyConsumptions/EnergyConsumptionsModule.cs b/apps/api/Kijk.Api/Modules/EnergyConsumptions/EnergyConsumptionsModule.cs new file mode 100644 index 0000000..6cbcfd4 --- /dev/null +++ b/apps/api/Kijk.Api/Modules/EnergyConsumptions/EnergyConsumptionsModule.cs @@ -0,0 +1,11 @@ +namespace Kijk.Api.Modules.EnergyConsumptions; + +public static class EnergyConsumptionsModule +{ + public static IServiceCollection AddEnergyConsumptionsModule(this IServiceCollection services) + { + // services.AddScoped, CreateTransactionsValidator>(); + + return services; + } +} diff --git a/apps/api/Kijk.Api/Modules/EnergyConsumptions/GetByEnergyConsumptions.cs b/apps/api/Kijk.Api/Modules/EnergyConsumptions/GetByEnergyConsumptions.cs new file mode 100644 index 0000000..6aef924 --- /dev/null +++ b/apps/api/Kijk.Api/Modules/EnergyConsumptions/GetByEnergyConsumptions.cs @@ -0,0 +1,70 @@ +using System.Globalization; +using Kijk.Api.Common.Extensions; +using Kijk.Api.Common.Models; +using Kijk.Api.Persistence; +using Microsoft.AspNetCore.Mvc; + +namespace Kijk.Api.Modules.EnergyConsumptions; + +public static class GetByEnergyConsumptions +{ + private static readonly ILogger Logger = Log.ForContext(typeof(GetByEnergyConsumptions)); + + public static RouteGroupBuilder MapGetByEnergyConsumptions(this RouteGroupBuilder groupBuilder) + { + groupBuilder.MapGet("/", Handle) + .Produces>>() + .Produces>>(StatusCodes.Status400BadRequest) + .Produces>>(StatusCodes.Status404NotFound); + + return groupBuilder; + } + + /// + /// Retrieves all energy consumptions for the current user by year, month and type. + /// + /// + /// + /// + /// + /// + /// + /// + private static async Task Handle( + [FromQuery(Name = "year")] int? year, + [FromQuery(Name = "month")] string? month, + [FromQuery(Name = "type")] string? type, + AppDbContext dbContext, + CurrentUser currentUser, + CancellationToken cancellationToken) + { + try + { + var monthInt = month is not null ? DateTime.ParseExact(month, "MMMM", CultureInfo.InvariantCulture).Month : -1; + + var typeExists = Enum.TryParse(type, true, out var realType); + + var response = await dbContext.EnergyConsumptions + .AsNoTracking() + .Where(x => x.HouseholdId == currentUser.ActiveHouseholdId) + .If(year != null, q => q.Where(x => x.Date.Year == year)) + .If(monthInt != -1, q => q.Where(x => x.Date.Month == monthInt)) + .If(typeExists, q => q.Where(x => x.Type == realType)) + .Select( + x => new EnergyConsumptionDto( + x.Id, + x.Name, + x.Description, + x.Value, + x.Type, x.CreatedAt)) + .ToListAsync(cancellationToken); + + return TypedResults.Ok(ApiResponseBuilder.Success(response)); + } + catch (Exception e) + { + Logger.Warning(e, "Error: {Error}", e.Message); + return TypedResults.BadRequest(ApiResponseBuilder.Error(e.Message)); + } + } +} diff --git a/apps/api/Kijk.Api/Modules/Transactions/GetByTransactions.cs b/apps/api/Kijk.Api/Modules/Transactions/GetByTransactions.cs index 770be8a..15f4e43 100644 --- a/apps/api/Kijk.Api/Modules/Transactions/GetByTransactions.cs +++ b/apps/api/Kijk.Api/Modules/Transactions/GetByTransactions.cs @@ -21,7 +21,7 @@ public static RouteGroupBuilder MapGetByTransactions(this RouteGroupBuilder grou } /// - /// Retrieves all transactions for the current user by year and month. + /// Retrieves all transactions for the current user by year and month. /// /// /// diff --git a/apps/api/Kijk.Api/Program.cs b/apps/api/Kijk.Api/Program.cs index 52fcf52..09a0bee 100644 --- a/apps/api/Kijk.Api/Program.cs +++ b/apps/api/Kijk.Api/Program.cs @@ -64,6 +64,7 @@ app.UseCors(AppConstants.Policies.Cors) .UseAuthentication() .UseAuthorization(); + app.UseMiddleware(); app.UseResponseCompression(); From 49e3fe3495de0874a5b58fe1be6fdb249931e56c Mon Sep 17 00:00:00 2001 From: maxstue Date: Tue, 26 Nov 2024 16:49:39 +0100 Subject: [PATCH 02/15] feat(KIJK-266): add new email --- apps/client/src/shared/lib/constants.ts | 2 +- apps/web/constants/config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/shared/lib/constants.ts b/apps/client/src/shared/lib/constants.ts index 3689bd5..c3960b5 100644 --- a/apps/client/src/shared/lib/constants.ts +++ b/apps/client/src/shared/lib/constants.ts @@ -1,7 +1,7 @@ export const siteConfig = { name: 'kijk', url: 'https://kijk-maxstue.vercel.app/', - email: 'mail:maxstue2304@gmail.com', + email: 'mail:kijk@justmax.xyz', description: 'Beautifully designed household app built with shadcn/ui and nextjs', links: { github: 'https://github.com/maxstue/kijk', diff --git a/apps/web/constants/config.ts b/apps/web/constants/config.ts index 13ed313..5a1ba62 100644 --- a/apps/web/constants/config.ts +++ b/apps/web/constants/config.ts @@ -3,7 +3,7 @@ import { Route } from 'next/types'; export const siteConfig = { name: 'kijk', url: 'https://kijk-maxstue.vercel.app/', - email: 'mail:maxstue2304@gmail.com', + email: 'mail:kijk@justmax.xyz', docs_user: '/docs/user', docs_dev: '/docs/developer', description: 'Beautifully designed Budget book built with shadcn/ui and nextjs', From 6ffee49f01a7022c1b87059ca9a83ea3d6eb2f86 Mon Sep 17 00:00:00 2001 From: maxstue Date: Tue, 26 Nov 2024 19:01:15 +0100 Subject: [PATCH 03/15] =?UTF-8?q?upgrade=20client=20packages=20?= =?UTF-8?q?=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/components.json | 17 +- apps/client/eslint.config.js | 6 +- apps/client/package.json | 151 +- .../src/app/budget/budget-month-nav.tsx | 6 +- apps/client/src/app/home/overview.tsx | 2 +- apps/client/src/app/root/app-help.tsx | 2 +- apps/client/src/app/root/command-menu.tsx | 2 +- apps/client/src/index.css | 67 + apps/client/src/routeTree.gen.ts | 147 +- apps/client/src/routes/_protected.tsx | 2 +- apps/client/src/routes/_protected/budget.tsx | 8 +- .../shared/components/errors/app-error.tsx | 2 +- .../shared/components/theme-mode-switcher.tsx | 8 +- .../src/shared/components/ui/accordion.tsx | 28 +- .../src/shared/components/ui/alert-dialog.tsx | 49 +- .../client/src/shared/components/ui/alert.tsx | 26 +- .../client/src/shared/components/ui/badge.tsx | 32 +- .../src/shared/components/ui/breadcrumb.tsx | 90 + .../src/shared/components/ui/button.tsx | 47 +- .../src/shared/components/ui/calendar.tsx | 5 +- apps/client/src/shared/components/ui/card.tsx | 14 +- .../src/shared/components/ui/command.tsx | 10 +- .../src/shared/components/ui/context-menu.tsx | 43 +- .../src/shared/components/ui/drawer.tsx | 87 + .../shared/components/ui/dropdown-menu.tsx | 122 +- .../src/shared/components/ui/form/form.tsx | 16 +- .../src/shared/components/ui/hover-card.tsx | 31 +- .../client/src/shared/components/ui/input.tsx | 30 +- .../client/src/shared/components/ui/label.tsx | 7 +- .../src/shared/components/ui/menubar.tsx | 79 +- .../shared/components/ui/navigation-menu.tsx | 9 +- .../src/shared/components/ui/pagination.tsx | 81 + .../src/shared/components/ui/popover.tsx | 35 +- .../src/shared/components/ui/progress.tsx | 31 +- .../src/shared/components/ui/resizable.tsx | 37 + .../src/shared/components/ui/scroll-area.tsx | 39 +- .../src/shared/components/ui/select.tsx | 94 +- .../src/shared/components/ui/separator.tsx | 29 +- .../client/src/shared/components/ui/sheet.tsx | 48 +- .../src/shared/components/ui/sidebar.tsx | 637 +++ .../src/shared/components/ui/sonner.tsx | 27 + .../src/shared/components/ui/switch.tsx | 2 +- .../client/src/shared/components/ui/table.tsx | 4 +- .../src/shared/components/ui/textarea.tsx | 28 +- .../client/src/shared/components/ui/toast.tsx | 79 +- .../src/shared/components/ui/toggle-group.tsx | 49 + .../src/shared/components/ui/toggle.tsx | 38 +- .../src/shared/components/ui/tooltip.tsx | 29 +- apps/client/src/shared/hooks/use-mobile.tsx | 19 + apps/client/src/shared/hooks/use-stepper.ts | 8 +- apps/client/src/shared/hooks/use-toast.ts | 15 +- apps/client/src/shared/lib/auth-client.ts | 8 +- apps/client/src/shared/types/global.d.ts | 5 + apps/client/src/shared/utils/store.tsx | 15 +- apps/client/tailwind.config.ts | 70 +- pnpm-lock.yaml | 4992 ++++++++++------- 56 files changed, 4969 insertions(+), 2595 deletions(-) create mode 100644 apps/client/src/shared/components/ui/breadcrumb.tsx create mode 100644 apps/client/src/shared/components/ui/drawer.tsx create mode 100644 apps/client/src/shared/components/ui/pagination.tsx create mode 100644 apps/client/src/shared/components/ui/resizable.tsx create mode 100644 apps/client/src/shared/components/ui/sidebar.tsx create mode 100644 apps/client/src/shared/components/ui/sonner.tsx create mode 100644 apps/client/src/shared/components/ui/toggle-group.tsx create mode 100644 apps/client/src/shared/hooks/use-mobile.tsx create mode 100644 apps/client/src/shared/types/global.d.ts diff --git a/apps/client/components.json b/apps/client/components.json index 57039a2..68fec4c 100644 --- a/apps/client/components.json +++ b/apps/client/components.json @@ -4,13 +4,18 @@ "rsc": false, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "tailwind.config.ts", "css": "src/index.css", - "baseColor": "slate", - "cssVariables": true + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" }, "aliases": { - "components": "@/components", - "utils": "@/lib/utils" - } + "components": "@/shared/components", + "utils": "@/shared/lib/helpers", + "ui": "@/shared/components/ui", + "lib": "@/shared/lib", + "hooks": "@/shared/hooks" + }, + "iconLibrary": "lucide" } diff --git a/apps/client/eslint.config.js b/apps/client/eslint.config.js index 93e7772..4936bd3 100644 --- a/apps/client/eslint.config.js +++ b/apps/client/eslint.config.js @@ -1,6 +1,7 @@ // @ts-check import eslint from '@eslint/js'; import tanstackQueryPlugin from '@tanstack/eslint-plugin-query'; +import tanstackRouterRouter from '@tanstack/eslint-plugin-router' import prettierConfig from 'eslint-config-prettier'; import reactPlugin from 'eslint-plugin-react'; import reactHooksPlugin from 'eslint-plugin-react-hooks'; @@ -99,8 +100,10 @@ const typescriptConfig = { /** @type {import('typescript-eslint').ConfigWithExtends} */ const reactConfig = { name: 'react', + // @ts-ignore extends: [reactPlugin.configs.flat.recommended], plugins: { + // @ts-ignore 'react-hooks': reactHooksPlugin, 'react-refresh': reactRefreshPlugin, }, @@ -142,6 +145,7 @@ const unicornConfig = { 'unicorn/no-array-reduce': 'warn', 'unicorn/no-null': 'warn', 'unicorn/no-useless-undefined': 'warn', + 'unicorn/no-document-cookie': 'warn', 'unicorn/filename-case': [ 'error', { @@ -202,7 +206,7 @@ export default tseslint.config( prettierConfig, reactConfig, unicornConfig, - // @ts-ignore + ...tanstackRouterRouter.configs['flat/recommended'], ...tanstackQueryPlugin.configs['flat/recommended'], disableTypeChecked, ignoreFiles, diff --git a/apps/client/package.json b/apps/client/package.json index 37c5c5a..21de7fe 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -19,94 +19,101 @@ "clean:build": "rimraf ./dist" }, "dependencies": { - "@clerk/clerk-js": "^5.14.0", - "@clerk/clerk-react": "^5.4.0", - "@hookform/resolvers": "^3.9.0", - "@nivo/calendar": "^0.87.0", - "@nivo/core": "^0.87.0", - "@radix-ui/react-accordion": "^1.2.0", - "@radix-ui/react-alert-dialog": "^1.1.1", + "@clerk/clerk-js": "^5.35.0", + "@clerk/clerk-react": "^5.17.0", + "@hookform/resolvers": "^3.9.1", + "@nivo/calendar": "^0.88.0", + "@nivo/core": "^0.88.0", + "@radix-ui/react-accordion": "^1.2.1", + "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.1.0", - "@radix-ui/react-avatar": "^1.1.0", - "@radix-ui/react-checkbox": "^1.1.1", - "@radix-ui/react-collapsible": "^1.1.0", - "@radix-ui/react-context-menu": "^2.2.1", - "@radix-ui/react-dialog": "^1.1.1", - "@radix-ui/react-dropdown-menu": "^2.1.1", - "@radix-ui/react-hover-card": "^1.1.1", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.2", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-menubar": "^1.1.1", - "@radix-ui/react-navigation-menu": "^1.2.0", - "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-menubar": "^1.1.2", + "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-progress": "^1.1.0", - "@radix-ui/react-radio-group": "^1.2.0", - "@radix-ui/react-scroll-area": "^1.1.0", - "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-radio-group": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-switch": "^1.1.0", - "@radix-ui/react-tabs": "^1.1.0", - "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-toggle": "^1.1.0", - "@radix-ui/react-tooltip": "^1.1.2", - "@sentry/react": "^8.25.0", - "@sentry/vite-plugin": "^2.22.1", - "@tanstack/react-query": "5.51.23", - "@tanstack/react-router": "1.47.1", - "@tanstack/react-table": "^8.20.1", - "@tanstack/router-devtools": "1.47.1", - "axios": "^1.7.3", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "@sentry/react": "^8.40.0", + "@sentry/vite-plugin": "^2.22.6", + "@tanstack/react-query": "5.61.4", + "@tanstack/react-router": "1.82.12", + "@tanstack/react-table": "^8.20.5", + "@tanstack/router-devtools": "1.82.12", + "axios": "^1.7.8", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cmdk": "^1.0.0", - "date-fns": "^3.6.0", - "framer-motion": "^11.3.24", + "cmdk": "^1.0.4", + "date-fns": "^4.1.0", + "framer-motion": "^11.11.17", "immer": "^10.1.1", - "lucide-react": "^0.427.0", - "posthog-js": "^1.155.0", + "input-otp": "^1.4.1", + "lucide-react": "^0.461.0", + "next-themes": "^0.4.3", + "posthog-js": "^1.189.0", "react": "^18.3.1", - "react-day-picker": "^9.0.8", + "react-day-picker": "^9.4.0", "react-dom": "^18.3.1", - "react-error-boundary": "^4.0.13", - "react-hook-form": "^7.52.2", - "tailwind-merge": "^2.5.2", - "tailwind-variants": "^0.2.1", + "react-error-boundary": "^4.1.2", + "react-hook-form": "^7.53.2", + "react-resizable-panels": "^2.1.7", + "sonner": "^1.7.0", + "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.1", "zod": "^3.23.8", - "zustand": "^4.5.4" + "zustand": "^5.0.1" }, "devDependencies": { - "@eslint/compat": "^1.1.1", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.9.0", - "@ianvs/prettier-plugin-sort-imports": "^4.3.1", + "@eslint/compat": "^1.2.3", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.15.0", + "@ianvs/prettier-plugin-sort-imports": "^4.4.0", "@microsoft/eslint-formatter-sarif": "^3.1.0", - "@tanstack/eslint-plugin-query": "^5.51.15", - "@tanstack/react-query-devtools": "5.51.23", - "@tanstack/router-vite-plugin": "^1.47.0", - "@total-typescript/ts-reset": "^0.5.1", - "@types/node": "^22.2.0", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.19", - "eslint": "^9.9.0", + "@tanstack/eslint-plugin-query": "^5.61.4", + "@tanstack/eslint-plugin-router": "^1.82.12", + "@tanstack/react-query-devtools": "5.61.4", + "@tanstack/router-vite-plugin": "^1.82.10", + "@total-typescript/ts-reset": "^0.6.1", + "@types/node": "^22.10.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-react": "^7.35.0", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "eslint-plugin-unicorn": "^55.0.0", - "globals": "^15.9.0", - "knip": "^5.23.2", - "postcss": "^8.4.39", - "prettier": "^3.3.3", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "eslint-plugin-unicorn": "^56.0.1", + "globals": "^15.12.0", + "knip": "^5.38.0", + "postcss": "^8.4.49", + "prettier": "^3.4.1", "prettier-plugin-jsdoc": "^1.3.0", - "prettier-plugin-tailwindcss": "^0.6.6", - "tailwindcss": "^3.4.9", - "typescript": "^5.5.4", - "typescript-eslint": "^8.1.0", - "vite": "^5.4.0", - "vite-plugin-checker": "^0.7.0", - "vite-plugin-pwa": "^0.20.0" + "prettier-plugin-tailwindcss": "^0.6.9", + "tailwindcss": "^3.4.15", + "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0", + "vite": "^6.0.0", + "vite-plugin-checker": "^0.8.0", + "vite-plugin-pwa": "^0.21.0" } } diff --git a/apps/client/src/app/budget/budget-month-nav.tsx b/apps/client/src/app/budget/budget-month-nav.tsx index 17b6579..0311fd7 100644 --- a/apps/client/src/app/budget/budget-month-nav.tsx +++ b/apps/client/src/app/budget/budget-month-nav.tsx @@ -13,12 +13,16 @@ export function BudgetMonthNav({ className, ...props }: Props) { return ( ({ ...previous, month: item })} className={cn( buttonVariants({ variant: 'ghost' }), 'justify-start text-primary/65 data-[status=active]:bg-primary data-[status=active]:text-primary-foreground', )} + search={(previous) => ({ + ...previous, + month: item, + })} > {item} diff --git a/apps/client/src/app/home/overview.tsx b/apps/client/src/app/home/overview.tsx index da92164..1a78486 100644 --- a/apps/client/src/app/home/overview.tsx +++ b/apps/client/src/app/home/overview.tsx @@ -1,4 +1,4 @@ // TODO add other graph package export function Overview() { - return null; + return (<>); } diff --git a/apps/client/src/app/root/app-help.tsx b/apps/client/src/app/root/app-help.tsx index 3d39f3c..8b1e97a 100644 --- a/apps/client/src/app/root/app-help.tsx +++ b/apps/client/src/app/root/app-help.tsx @@ -40,7 +40,7 @@ export function AppHelp() { - +
diff --git a/apps/client/src/app/root/command-menu.tsx b/apps/client/src/app/root/command-menu.tsx index 163a3be..04cdc88 100644 --- a/apps/client/src/app/root/command-menu.tsx +++ b/apps/client/src/app/root/command-menu.tsx @@ -84,7 +84,7 @@ export function CommandMenu({ ...props }: Props) { )} onClick={handleOpen(true)} > - {props.isCollapsed ? null : Search...} + {props.isCollapsed ? undefined : Search...} rootRoute, } as any) const AuthRoute = AuthImport.update({ + id: '/auth', path: '/auth', getParentRoute: () => rootRoute, } as any) @@ -39,31 +41,37 @@ const ProtectedRoute = ProtectedImport.update({ } as any) const ProtectedIndexRoute = ProtectedIndexImport.update({ + id: '/', path: '/', getParentRoute: () => ProtectedRoute, } as any) const ProtectedWelcomeRoute = ProtectedWelcomeImport.update({ + id: '/welcome', path: '/welcome', getParentRoute: () => ProtectedRoute, } as any) const ProtectedSettingsRoute = ProtectedSettingsImport.update({ + id: '/settings', path: '/settings', getParentRoute: () => ProtectedRoute, } as any) const ProtectedHomeRoute = ProtectedHomeImport.update({ + id: '/home', path: '/home', getParentRoute: () => ProtectedRoute, } as any) const ProtectedBudgetRoute = ProtectedBudgetImport.update({ + id: '/budget', path: '/budget', getParentRoute: () => ProtectedRoute, } as any) const ProtectedSettingsSectionRoute = ProtectedSettingsSectionImport.update({ + id: '/$section', path: '/$section', getParentRoute: () => ProtectedSettingsRoute, } as any) @@ -140,21 +148,124 @@ declare module '@tanstack/react-router' { // Create and export the route tree -export const routeTree = rootRoute.addChildren({ - ProtectedRoute: ProtectedRoute.addChildren({ - ProtectedBudgetRoute, - ProtectedHomeRoute, - ProtectedSettingsRoute: ProtectedSettingsRoute.addChildren({ - ProtectedSettingsSectionRoute, - }), - ProtectedWelcomeRoute, - ProtectedIndexRoute, - }), - AuthRoute, - SsoCallbackRoute, -}) - -/* prettier-ignore-end */ +interface ProtectedSettingsRouteChildren { + ProtectedSettingsSectionRoute: typeof ProtectedSettingsSectionRoute +} + +const ProtectedSettingsRouteChildren: ProtectedSettingsRouteChildren = { + ProtectedSettingsSectionRoute: ProtectedSettingsSectionRoute, +} + +const ProtectedSettingsRouteWithChildren = + ProtectedSettingsRoute._addFileChildren(ProtectedSettingsRouteChildren) + +interface ProtectedRouteChildren { + ProtectedBudgetRoute: typeof ProtectedBudgetRoute + ProtectedHomeRoute: typeof ProtectedHomeRoute + ProtectedSettingsRoute: typeof ProtectedSettingsRouteWithChildren + ProtectedWelcomeRoute: typeof ProtectedWelcomeRoute + ProtectedIndexRoute: typeof ProtectedIndexRoute +} + +const ProtectedRouteChildren: ProtectedRouteChildren = { + ProtectedBudgetRoute: ProtectedBudgetRoute, + ProtectedHomeRoute: ProtectedHomeRoute, + ProtectedSettingsRoute: ProtectedSettingsRouteWithChildren, + ProtectedWelcomeRoute: ProtectedWelcomeRoute, + ProtectedIndexRoute: ProtectedIndexRoute, +} + +const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren( + ProtectedRouteChildren, +) + +export interface FileRoutesByFullPath { + '': typeof ProtectedRouteWithChildren + '/auth': typeof AuthRoute + '/sso-callback': typeof SsoCallbackRoute + '/budget': typeof ProtectedBudgetRoute + '/home': typeof ProtectedHomeRoute + '/settings': typeof ProtectedSettingsRouteWithChildren + '/welcome': typeof ProtectedWelcomeRoute + '/': typeof ProtectedIndexRoute + '/settings/$section': typeof ProtectedSettingsSectionRoute +} + +export interface FileRoutesByTo { + '/auth': typeof AuthRoute + '/sso-callback': typeof SsoCallbackRoute + '/budget': typeof ProtectedBudgetRoute + '/home': typeof ProtectedHomeRoute + '/settings': typeof ProtectedSettingsRouteWithChildren + '/welcome': typeof ProtectedWelcomeRoute + '/': typeof ProtectedIndexRoute + '/settings/$section': typeof ProtectedSettingsSectionRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/_protected': typeof ProtectedRouteWithChildren + '/auth': typeof AuthRoute + '/sso-callback': typeof SsoCallbackRoute + '/_protected/budget': typeof ProtectedBudgetRoute + '/_protected/home': typeof ProtectedHomeRoute + '/_protected/settings': typeof ProtectedSettingsRouteWithChildren + '/_protected/welcome': typeof ProtectedWelcomeRoute + '/_protected/': typeof ProtectedIndexRoute + '/_protected/settings/$section': typeof ProtectedSettingsSectionRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '' + | '/auth' + | '/sso-callback' + | '/budget' + | '/home' + | '/settings' + | '/welcome' + | '/' + | '/settings/$section' + fileRoutesByTo: FileRoutesByTo + to: + | '/auth' + | '/sso-callback' + | '/budget' + | '/home' + | '/settings' + | '/welcome' + | '/' + | '/settings/$section' + id: + | '__root__' + | '/_protected' + | '/auth' + | '/sso-callback' + | '/_protected/budget' + | '/_protected/home' + | '/_protected/settings' + | '/_protected/welcome' + | '/_protected/' + | '/_protected/settings/$section' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + ProtectedRoute: typeof ProtectedRouteWithChildren + AuthRoute: typeof AuthRoute + SsoCallbackRoute: typeof SsoCallbackRoute +} + +const rootRouteChildren: RootRouteChildren = { + ProtectedRoute: ProtectedRouteWithChildren, + AuthRoute: AuthRoute, + SsoCallbackRoute: SsoCallbackRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() /* ROUTE_MANIFEST_START { diff --git a/apps/client/src/routes/_protected.tsx b/apps/client/src/routes/_protected.tsx index 908fa4e..2f57923 100644 --- a/apps/client/src/routes/_protected.tsx +++ b/apps/client/src/routes/_protected.tsx @@ -43,7 +43,7 @@ function Protected() { )} {/* Content */}
-
+
diff --git a/apps/client/src/routes/_protected/budget.tsx b/apps/client/src/routes/_protected/budget.tsx index 085c24d..69c4e49 100644 --- a/apps/client/src/routes/_protected/budget.tsx +++ b/apps/client/src/routes/_protected/budget.tsx @@ -38,16 +38,16 @@ const searchSchema = z.object({ }); export const Route = createFileRoute('/_protected/budget')({ + validateSearch: searchSchema, loaderDeps: ({ search: { month, year } }) => ({ month, year }), - loader: ({ deps: { month, year }, context: { queryClient } }) => { - return queryClient.ensureQueryData(getTransactionsQuery(year, month)); - }, preSearchFilters: [ (search) => ({ ...search, }), ], - validateSearch: searchSchema, + loader: ({ deps: { month, year }, context: { queryClient } }) => { + return queryClient.ensureQueryData(getTransactionsQuery(year, month)); + }, component: BudgetPage, notFoundComponent: NotFound, pendingComponent: () => , diff --git a/apps/client/src/shared/components/errors/app-error.tsx b/apps/client/src/shared/components/errors/app-error.tsx index a0af1b1..bcc60fd 100644 --- a/apps/client/src/shared/components/errors/app-error.tsx +++ b/apps/client/src/shared/components/errors/app-error.tsx @@ -11,7 +11,7 @@ import { cn } from '@/shared/lib/helpers'; type Props = { resetErrorBoundary?: () => void } & Partial; const handleGotToRoot = () => { - window.location.href = '/'; + globalThis.location.href = '/'; }; export function AppError({ error, info, resetErrorBoundary }: Props) { diff --git a/apps/client/src/shared/components/theme-mode-switcher.tsx b/apps/client/src/shared/components/theme-mode-switcher.tsx index 46e97c9..992caa8 100644 --- a/apps/client/src/shared/components/theme-mode-switcher.tsx +++ b/apps/client/src/shared/components/theme-mode-switcher.tsx @@ -6,12 +6,12 @@ export function ThemeModeSwitcher() { const { mode } = useThemeStore(); useEffect(() => { - const root = window.document.documentElement; + const root = globalThis.document.documentElement; root.classList.remove('light', 'dark'); if (mode === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + const systemTheme = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; root.classList.add(systemTheme); return; @@ -21,14 +21,14 @@ export function ThemeModeSwitcher() { }, [mode]); useEffect(() => { - const root = window.document.documentElement; + const root = globalThis.document.documentElement; const listener = (event: MediaQueryListEvent) => { root.classList.remove('light', 'dark'); root.classList.add(event.matches ? 'dark' : 'light'); }; - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', listener); + globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', listener); }, []); // eslint-disable-next-line unicorn/no-useless-undefined diff --git a/apps/client/src/shared/components/ui/accordion.tsx b/apps/client/src/shared/components/ui/accordion.tsx index 84be93b..4046f75 100644 --- a/apps/client/src/shared/components/ui/accordion.tsx +++ b/apps/client/src/shared/components/ui/accordion.tsx @@ -1,4 +1,4 @@ -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'; +import * as React from 'react'; import * as AccordionPrimitive from '@radix-ui/react-accordion'; import { ChevronDown } from 'lucide-react'; @@ -6,17 +6,17 @@ import { cn } from '@/shared/lib/helpers'; const Accordion = AccordionPrimitive.Root; -const AccordionItem = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AccordionItem.displayName = 'AccordionItem'; -const AccordionTrigger = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( , - ComponentPropsWithoutRef +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( -
{children}
+
{children}
)); + AccordionContent.displayName = AccordionPrimitive.Content.displayName; export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/apps/client/src/shared/components/ui/alert-dialog.tsx b/apps/client/src/shared/components/ui/alert-dialog.tsx index 0f3ac59..8f98823 100644 --- a/apps/client/src/shared/components/ui/alert-dialog.tsx +++ b/apps/client/src/shared/components/ui/alert-dialog.tsx @@ -1,4 +1,4 @@ -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'; +import * as React from 'react'; import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; import { buttonVariants } from '@/shared/components/ui/button'; @@ -8,18 +8,15 @@ const AlertDialog = AlertDialogPrimitive.Root; const AlertDialogTrigger = AlertDialogPrimitive.Trigger; -const AlertDialogPortal = ({ ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => ( - -); -AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName; +const AlertDialogPortal = AlertDialogPrimitive.Portal; -const AlertDialogOverlay = forwardRef< - ElementRef, - Omit, 'children'> +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( , - ComponentPropsWithoutRef +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( , - ComponentPropsWithoutRef +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; -const AlertDialogDescription = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; -const AlertDialogAction = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; -const AlertDialogCancel = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', - variants: { - variant: { - default: 'bg-background text-foreground', - destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', +const alertVariants = cva( + 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', + }, + }, + defaultVariants: { + variant: 'default', }, }, - defaultVariants: { - variant: 'default', - }, -}); +); const Alert = React.forwardRef< HTMLDivElement, diff --git a/apps/client/src/shared/components/ui/badge.tsx b/apps/client/src/shared/components/ui/badge.tsx index 04f50e7..21d6720 100644 --- a/apps/client/src/shared/components/ui/badge.tsx +++ b/apps/client/src/shared/components/ui/badge.tsx @@ -1,23 +1,25 @@ import * as React from 'react'; -import { tv } from 'tailwind-variants'; +import { cva } from 'class-variance-authority'; import { cn } from '@/shared/lib/helpers'; -import type { VariantProps } from 'tailwind-variants'; +import type { VariantProps } from 'class-variance-authority'; -const badgeVariants = tv({ - base: 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', - variants: { - variant: { - default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', - secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', - destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', - outline: 'text-foreground', +const badgeVariants = cva( + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', + secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', }, }, - defaultVariants: { - variant: 'default', - }, -}); +); export interface BadgeProps extends React.HTMLAttributes, VariantProps {} @@ -25,4 +27,4 @@ function Badge({ className, variant, ...props }: BadgeProps) { return
; } -export { Badge }; +export { Badge, badgeVariants }; diff --git a/apps/client/src/shared/components/ui/breadcrumb.tsx b/apps/client/src/shared/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..b76577f --- /dev/null +++ b/apps/client/src/shared/components/ui/breadcrumb.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { ChevronRight, MoreHorizontal } from 'lucide-react'; + +import { cn } from '@/shared/lib/helpers'; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<'nav'> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>