Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Small improvements after upgrading to .NET 9, React 19, and Lingui v5 #647

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@

# 👋 Welcome to PlatformPlatform

Kick-start building top-tier B2B & B2C cloud SaaS products with sleek design, fully localized and accessible, vertical slice architecture, automated and fast DevOps, and top-notch security. All in one place – at zero cost.
Kick-start building top-tier B2B & B2C cloud SaaS products with sleek design, fully localized and accessible, vertical slice architecture, automated and fast DevOps, and top-notch security.

This is in the box:

* **Backend** - .NET and C# adhering to the principles of vertical slice architecture, DDD, CQRS, and clean code
* **Frontend** - React using TypeScript, with a sleek fully localized UI and a mature accessible design system
* **Backend** - .NET 9 and C# adhering to the principles of vertical slice architecture, DDD, CQRS, and clean code
* **Frontend** - React 19 and TypeScript, fully localized using React Aria components for world-class accessibility
* **CI/CD** - GitHub actions for fast passwordless deployments of application (Docker) and infrastructure (Bicep)
* **Infrastructure** - Cost efficient and scalable Azure PaaS services like Azure Container Apps, Azure SQL, etc.
* **Developer CLI** - Extendable .NET CLI for DevEx - set up CI/CD is one command and a couple of questions
Expand Down Expand Up @@ -61,7 +61,7 @@ For development, you need .NET, Docker, and Node. And GitHub and Azure CLI for s

```powershell
@(
"Microsoft.DotNet.SDK.8",
"Microsoft.DotNet.SDK.9",
"Git.Git",
"Docker.DockerDesktop",
"OpenJS.NodeJS",
Expand All @@ -80,7 +80,8 @@ Open a terminal and run the following commands:

- Install [Homebrew](https://brew.sh/), a package manager for Mac
- `brew install --cask dotnet-sdk`
- `brew install git docker node azure-cli gh`
- `brew install --cask docker`
- `brew install git node azure-cli gh`

</details>

Expand Down Expand Up @@ -127,10 +128,10 @@ Open a terminal and run the following commands:
sudo apt-get update
```

- Install .NET SDK 8.0, Node, GitHub CLI
- Install .NET SDK 9.0, Node, GitHub CLI

```bash
sudo apt-get install -y dotnet-sdk-8.0 nodejs gh
sudo apt-get install -y dotnet-sdk-9.0 nodejs gh
```

- Install Azure CLI
Expand All @@ -157,7 +158,7 @@ Open a terminal and run the following commands:

</details>

## 1. Fork and clone the repository
## 1. Clone the repository

Forking is only required to configure GitHub repository with continuous deployments to Azure ([step 3](#4-set-up-cicd-with-passwordless-deployments-from-github-to-azure)).

Expand Down Expand Up @@ -211,7 +212,8 @@ PlatformPlatform is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) contain
│ │ ├─ Core # Core business logic, application use cases, and infrastructure
│ │ ├─ Workers # Background workers for long-running tasks and event processing
│ │ └─ Tests # Tests for the Api, Core, and Workers
│ ├─ shared-kernel # Reusable components for all self-contained systems
│ ├─ shared-kernel # Reusable components and default configuration for all systems
│ ├─ shared-webapp # Reusable and styled React Aria Components that affect all systems
│ ├─ [saas-scs] # [Your SCS] Create your SaaS product as a self-contained system
│ └─ back-office # A self-contained system for operations and support (empty for now)
├─ cloud-infrastructure # Contains Bash and Bicep scripts (IaC) for Azure resources
Expand All @@ -225,11 +227,11 @@ PlatformPlatform is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) contain

# Technologies

### .NET 8 Backend With Vertical Sliced Architecture, DDD, CQRS, Minimal API, and Aspire
### .NET 9 Backend With Vertical Sliced Architecture, DDD, CQRS, Minimal API, and Aspire

The backend is built using the most popular, mature, and commonly used technologies in the .NET ecosystem:

- [.NET 8](https://dotnet.microsoft.com) and [C# 12](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp)
- [.NET 9](https://dotnet.microsoft.com) and [C# 13](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp)
- [.NET Aspire](https://aka.ms/dotnet-aspire)
- [YARP](https://microsoft.github.io/reverse-proxy)
- [ASP.NET Minimal API](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis)
Expand Down Expand Up @@ -258,11 +260,11 @@ Although some features like multi-tenancy are not yet implemented, the current i

</details>

### React Frontend With TypeScript, React Aria Components, and Node
### React 19 Frontend With TypeScript, React Aria Components, and Node

The frontend is built with these technologies:

- [React](https://react.dev)
- [React 19](https://react.dev)
- [TypeScript](https://www.typescriptlang.org)
- [React Aria Components](https://react-spectrum.adobe.com/react-aria/react-aria-components.html)
- [Tanstack Router](https://tanstack.com)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
{
var group = routes.MapGroup(RoutesPrefix).WithTags("Tenants").RequireAuthorization();

group.MapGet("/{id}", async Task<ApiResult<TenantResponse>> (TenantId id, IMediator mediator)
=> await mediator.Send(new GetTenantQuery(id))
group.MapGet("/{id}", async Task<ApiResult<TenantResponse>> ([AsParameters] GetTenantQuery query, IMediator mediator)
=> await mediator.Send(query)
).Produces<TenantResponse>();

group.MapPut("/{id}", async Task<ApiResult> (TenantId id, UpdateTenantCommand command, IMediator mediator)
Expand Down
4 changes: 2 additions & 2 deletions application/account-management/Api/Endpoints/UserEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
=> await mediator.Send(query)
).Produces<GetUsersResponse>();

group.MapGet("/{id}", async Task<ApiResult<UserResponse>> (UserId id, IMediator mediator)
=> await mediator.Send(new GetUserQuery(id))
group.MapGet("/{id}", async Task<ApiResult<UserResponse>> ([AsParameters] GetUserQuery query, IMediator mediator)
=> await mediator.Send(query)
).Produces<UserResponse>();

group.MapPost("/", async Task<ApiResult> (CreateUserCommand command, IMediator mediator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ public async Task GetTenant_WhenTenantInvalidTenantId_ShouldReturnBadRequest()
var response = await AuthenticatedHttpClient.GetAsync($"/api/account-management/tenants/{invalidTenantId}");

// Assert
await response.ShouldHaveErrorStatusCode(HttpStatusCode.BadRequest, $"""Failed to bind parameter "TenantId id" from "{invalidTenantId}".""");
await response.ShouldHaveErrorStatusCode(HttpStatusCode.BadRequest, $"""Failed to bind parameter "TenantId Id" from "{invalidTenantId}".""");
}
}
2 changes: 1 addition & 1 deletion application/account-management/Tests/Users/GetUserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ public async Task GetUser_WhenInvalidUserId_ShouldReturnBadRequest()
var response = await AuthenticatedHttpClient.GetAsync($"/api/account-management/users/{invalidUserId}");

// Assert
await response.ShouldHaveErrorStatusCode(HttpStatusCode.BadRequest, $"""Failed to bind parameter "UserId id" from "{invalidUserId}".""");
await response.ShouldHaveErrorStatusCode(HttpStatusCode.BadRequest, $"""Failed to bind parameter "UserId Id" from "{invalidUserId}".""");
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createFileRoute } from "@tanstack/react-router";
import { Trans } from "@lingui/macro";
import { Trans } from "@lingui/react/macro";
import { useApi } from "@/shared/lib/api/client";
import { TopMenu } from "@/shared/components/topMenu";
import { SharedSideMenu } from "@/shared/components/SharedSideMenu";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useCallback, useEffect } from "react";
import { useFormState } from "react-dom";
import { useActionState, useCallback, useEffect } from "react";
import { Button } from "@repo/ui/components/Button";
import { TextField } from "@repo/ui/components/TextField";
import { Heading } from "@repo/ui/components/Heading";
Expand All @@ -22,7 +21,7 @@ export default function InviteUserModal({ isOpen, onOpenChange }: Readonly<Invit
onOpenChange(false);
}, [onOpenChange]);

let [{ success, errors, title, message }, action, isPending] = useFormState(
let [{ success, errors, title, message }, action, isPending] = useActionState(
api.actionPost("/api/account-management/users/invite"),
{ success: null }
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Button } from "@repo/ui/components/Button";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import InviteUserModal from "./-components/InviteUserModal";
import { Trans } from "@lingui/macro";
import { Trans } from "@lingui/react/macro";

const userPageSearchSchema = z.object({
pageOffset: z.number().default(0).optional(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Schemas } from "@/shared/lib/api/client";
import { t } from "@lingui/macro";
import { t } from "@lingui/core/macro";

interface LoginState {
loginId: Schemas["LoginId"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Link } from "@repo/ui/components/Link";
import { Content, Heading, IllustratedMessage } from "@repo/ui/components/IllustratedMessage";
import { getLoginState } from "./-shared/loginState";
import { loginPath } from "@repo/infrastructure/auth/constants";
import { Trans } from "@lingui/macro";
import { Trans } from "@lingui/react/macro";

export const Route = createFileRoute("/login/expired")({
component: () => (
Expand Down
5 changes: 2 additions & 3 deletions application/account-management/WebApp/routes/login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import { Link } from "@repo/ui/components/Link";
import logoMarkUrl from "@/shared/images/logo-mark.svg";
import poweredByUrl from "@/shared/images/powered-by.svg";
import { TextField } from "@repo/ui/components/TextField";
import { useFormState } from "react-dom";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { useState } from "react";
import { useActionState, useState } from "react";
import { api } from "@/shared/lib/api/client";
import { setLoginState } from "./-shared/loginState";
import { FormErrorMessage } from "@repo/ui/components/FormErrorMessage";
Expand All @@ -33,7 +32,7 @@ export const Route = createFileRoute("/login/")({
export function LoginForm() {
const [email, setEmail] = useState("");

const [{ success, errors, data, title, message }, action, isPending] = useFormState(
const [{ success, errors, data, title, message }, action, isPending] = useActionState(
api.actionPost("/api/account-management/authentication/login/start"),
{ success: null }
);
Expand Down
5 changes: 2 additions & 3 deletions application/account-management/WebApp/routes/login/verify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import { useExpirationTimeout } from "@repo/ui/hooks/useExpiration";
import logoMarkUrl from "@/shared/images/logo-mark.svg";
import poweredByUrl from "@/shared/images/powered-by.svg";
import { getLoginState } from "./-shared/loginState";
import { useFormState } from "react-dom";
import { api } from "@/shared/lib/api/client";
import { FormErrorMessage } from "@repo/ui/components/FormErrorMessage";
import { loggedInPath } from "@repo/infrastructure/auth/constants";
import { useEffect } from "react";
import { useActionState, useEffect } from "react";

export const Route = createFileRoute("/login/verify")({
component: () => (
Expand All @@ -35,7 +34,7 @@ export function CompleteLoginForm() {
const { email, loginId, expireAt } = getLoginState();
const { expiresInString, isExpired } = useExpirationTimeout(expireAt);

const [{ success, title, message, errors }, action] = useFormState(
const [{ success, title, message, errors }, action] = useActionState(
api.actionPost("/api/account-management/authentication/login/{id}/complete"),
{
success: null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Schemas } from "@/shared/lib/api/client";
import { t } from "@lingui/macro";
import { t } from "@lingui/core/macro";

interface SignupState {
signupId: Schemas["SignupId"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Link } from "@repo/ui/components/Link";
import { Content, Heading, IllustratedMessage } from "@repo/ui/components/IllustratedMessage";
import { getSignupState } from "./-shared/signupState";
import { signUpPath } from "@repo/infrastructure/auth/constants";
import { Trans } from "@lingui/macro";
import { Trans } from "@lingui/react/macro";

export const Route = createFileRoute("/signup/expired")({
component: () => (
Expand Down
5 changes: 2 additions & 3 deletions application/account-management/WebApp/routes/signup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import logoMarkUrl from "@/shared/images/logo-mark.svg";
import poweredByUrl from "@/shared/images/powered-by.svg";
import { TextField } from "@repo/ui/components/TextField";
import { Form } from "@repo/ui/components/Form";
import { useFormState } from "react-dom";
import { useState } from "react";
import { useActionState, useState } from "react";
import { api, useApi } from "@/shared/lib/api/client";
import { setSignupState } from "./-shared/signupState";
import { FormErrorMessage } from "@repo/ui/components/FormErrorMessage";
Expand All @@ -36,7 +35,7 @@ export const Route = createFileRoute("/signup/")({
export function StartSignupForm() {
const [email, setEmail] = useState("");

const [{ success, errors, data, title, message }, action, isPending] = useFormState(
const [{ success, errors, data, title, message }, action, isPending] = useActionState(
api.actionPost("/api/account-management/signups/start"),
{ success: null }
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ import { OneTimeCodeInput } from "@repo/ui/components/OneTimeCodeInput";
import { useExpirationTimeout } from "@repo/ui/hooks/useExpiration";
import logoMarkUrl from "@/shared/images/logo-mark.svg";
import poweredByUrl from "@/shared/images/powered-by.svg";
import { useFormState } from "react-dom";
import { getSignupState } from "./-shared/signupState";
import { api } from "@/shared/lib/api/client";
import { FormErrorMessage } from "@repo/ui/components/FormErrorMessage";
import { signedUpPath } from "@repo/infrastructure/auth/constants";
import { useEffect } from "react";
import { useActionState, useEffect } from "react";

export const Route = createFileRoute("/signup/verify")({
component: () => (
Expand All @@ -35,7 +34,7 @@ export function CompleteSignupForm() {
const { email, signupId, expireAt } = getSignupState();
const { expiresInString, isExpired } = useExpirationTimeout(expireAt);

const [{ success, title, message, errors }, action] = useFormState(
const [{ success, title, message, errors }, action] = useActionState(
api.actionPost("/api/account-management/signups/{id}/complete"),
{
success: null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ErrorComponentProps } from "@tanstack/react-router";
import ErrorIllustration from "@spectrum-icons/illustrations/Error";
import { Content, Heading, IllustratedMessage } from "@repo/ui/components/IllustratedMessage";
import { Button } from "@repo/ui/components/Button";
import { Trans } from "@lingui/macro";
import { Trans } from "@lingui/react/macro";

export function ErrorMessage({ error, reset }: Readonly<ErrorComponentProps>) {
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { t } from "@lingui/macro";
import { t } from "@lingui/core/macro";
import { Image } from "@repo/ui/components/Image";
import heroMobileBlurImage from "@/public/images/hero-mobile-blur.webp";
import heroDesktopBlurImage from "@/public/images/hero-desktop-blur.webp";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useFormState } from "react-dom";
import { useActionState, useCallback, useEffect, useRef, useState } from "react";
import { FileTrigger, Form, Heading, Label } from "react-aria-components";
import { Menu, MenuItem, MenuSeparator, MenuTrigger } from "@repo/ui/components/Menu";
import { CameraIcon, Trash2Icon, XIcon } from "lucide-react";
Expand Down Expand Up @@ -63,7 +62,7 @@ export default function UserProfileModal({ isOpen, onOpenChange, userId }: Reado
}, [onOpenChange, avatarPreviewUrl]);

// Handle form submission
let [{ success, errors, title, message }, action, isPending] = useFormState(
let [{ success, errors, title, message }, action, isPending] = useActionState(
api.actionPut("/api/account-management/users/{id}"),
{ success: null }
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HeroImage } from "@/shared/components/HeroImage";
import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher";
import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector";
import { Button } from "@repo/ui/components/Button";
import { t } from "@lingui/macro";
import { t } from "@lingui/core/macro";
import { LifeBuoyIcon } from "lucide-react";

interface HorizontalHeroLayoutProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createFileRoute } from "@tanstack/react-router";
import { Trans } from "@lingui/macro";
import { Trans } from "@lingui/react/macro";
import { TopMenu } from "@/shared/components/topMenu";
import { SharedSideMenu } from "@/shared/components/SharedSideMenu";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MenuButton, SideMenu, SideMenuSpacer } from "@repo/ui/components/SideMenu";
import { BoxIcon, HomeIcon } from "lucide-react";
import { t } from "@lingui/macro";
import { t } from "@lingui/core/macro";

type SharedSideMenuProps = {
children?: React.ReactNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public class DevelopmentTokenSigningClient

private static string UserSecretsId => EntryAssembly.GetCustomAttribute<UserSecretsIdAttribute>()!.UserSecretsId;

public string Issuer => "https://localhost:9000";
public string Issuer => "Localhost";

public string Audience => "https://localhost:9000";
public string Audience => "Localhost";

public SigningCredentials GetSigningCredentials()
{
Expand Down
Loading