Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
bae2ebf22a |
@ -1,14 +1,13 @@
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { themes } from '@storybook/theming';
|
||||
import { ThemeProvider } from "../src/app/[lang]/dashboard/components/client/theme-provider/theme-provider";
|
||||
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
|
||||
import { getI18n, LANGS } from "../src/bootstrap/i18n/i18n"
|
||||
import { DARK_MODE_EVENT_NAME, UPDATE_DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
|
||||
import { initI18next, LANGS } from "../src/bootstrap/i18n/i18n"
|
||||
import { addons } from '@storybook/preview-api';
|
||||
import { i18n } from "i18next";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
const channel = addons.getChannel();
|
||||
import "../src/app/globals.css"
|
||||
|
||||
/**
|
||||
*
|
||||
* This function will expand the object with nested properties
|
||||
@ -70,7 +69,7 @@ const preview = {
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
setI18n((await getI18n({ lng: locale })).i18n);
|
||||
setI18n((await initI18next({ lng: locale })).i18n);
|
||||
})()
|
||||
}, [])
|
||||
|
||||
|
25
README.md
25
README.md
@ -1,27 +1,4 @@
|
||||
# Nextjs clean architecture boilerplate
|
||||
|
||||
## Table of content
|
||||
|
||||
- Overview
|
||||
- Technologies
|
||||
- Architecture
|
||||
- Folder Structure
|
||||
- Getting started
|
||||
- Guildline
|
||||
|
||||
## Overview
|
||||
This project is a starting point for your medium to large scale projects with Nextjs, to make sure having a structured, maintainable and reusable base for your project based on best practices in clean architecture, DDD approach for business logics, MVVM for the frontend part, storybook and vitest for testing logics and ui part and also functional programming with error handling for business logics.
|
||||
|
||||
## Motivation
|
||||
Nextjs and many other new SSR tools provide a really good and new approach to handle frontend applications, with new tools to bring a new good experience for users. But as they're new and they just tried to bring new tools and features and also frontend community, didn't talk about software engineering and best practices approach for this tools.
|
||||
|
||||
So in many cases we see many teams uses nextjs to just use its tools and features as much as they can but they don't care about the best practices, architecture and software engineering approach. So there are many projects with Nextjs which is not maintainable and specially in medium to large scale applications, through the time it'll be really hard to manage to even impossible to add new features and it cause business failures.
|
||||
|
||||
So I decided to make a base firm and maintanable boilerplate for most faviorite SSR framework, which is Nextjs and use all my experiences and best practices which fits Nextjs features and abilities to make a structured, robust and maintanable basement for SSR projects.
|
||||
|
||||
I personally used this boilerplate for several enterprise web-applications and it's completely tested and you can rest assured to use it safely.
|
||||
|
||||
> Note: I'll be happy to get your issues and problems or any other opinion to make it better together. To know how to contribute please visite the CONTRIBUTE.md file.
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
@ -1,53 +0,0 @@
|
||||
@startuml Failure Class Diagram
|
||||
abstract BaseFailure {
|
||||
- String BASE_FAILURE_MESSAGE = "failure"
|
||||
+ String message = this.BASE_FAILURE_MESSAGE
|
||||
|
||||
constructor(key: string)
|
||||
}
|
||||
|
||||
class UserFailure {
|
||||
constructor(key: string)
|
||||
}
|
||||
|
||||
note left
|
||||
Extends parent message key by sending
|
||||
`user` keyword as a domain to parent.
|
||||
So in `UserFailure` our `message`
|
||||
property will be `failure.user`.
|
||||
end note
|
||||
|
||||
UserFailure --|> BaseFailure
|
||||
|
||||
|
||||
class UserModificationFailure {
|
||||
constructor(key: string)
|
||||
}
|
||||
note right
|
||||
Extends parent message key by sending
|
||||
`modification` keyword to parent.
|
||||
So in `UserModificationFailure` our `message`
|
||||
property will be `failure.user.modification`
|
||||
end note
|
||||
UserModificationFailure --|> UserFailure
|
||||
|
||||
class UserModificationAlreadyExistsFailure {
|
||||
constructor()
|
||||
}
|
||||
note left
|
||||
Extends parent message key by sending
|
||||
`alreadyExists` keyword to parent.
|
||||
So in `UserModificationAlreadyExistsFailure`
|
||||
our `message` property will be
|
||||
`failure.user.modification.AlreadyExists`
|
||||
end note
|
||||
|
||||
class DeviceFailure {
|
||||
constructor()
|
||||
}
|
||||
|
||||
DeviceFailure --|> BaseFailure
|
||||
UserModificationAlreadyExistsFailure --|> UserModificationFailure
|
||||
|
||||
|
||||
@enduml
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 14 KiB |
@ -1,246 +0,0 @@
|
||||
# Error handling with failure
|
||||
|
||||
## What is failure
|
||||
In each application and in each logic, there can be failures in the process, and based on their complexity, there can be a few or many possible scenarios for these failures.
|
||||
|
||||
In software development, we always try to have more control over these failures to:
|
||||
|
||||
Avoid possible bugs
|
||||
Help users understand the state of the application with proper messages
|
||||
Control processes for side effects
|
||||
Monitor the behavior of the application
|
||||
So, having a specific way of handling errors to achieve all these requirements in our app helps us build a more robust, reliable, and maintainable application.
|
||||
|
||||
Many frameworks provide their own ways to handle these failures, which they may call exceptions, failures, or other terms. But we shouldn't always depend on the behavior of frameworks for our logic and apps. Besides, many frameworks do not provide error handling tools, so we need to design a reliable architecture for the error handling in our application.
|
||||
|
||||
## Failure handling with base failure
|
||||
To have granular control over failures and define a specific type for all errors, we can use the power of inheritance and abstraction in OOP.
|
||||
|
||||
We can define an abstract class as our base failure, which serves as the specific type for all failures in the application.
|
||||
|
||||
```ts
|
||||
export default abstract class BaseFailure<META_DATA> {
|
||||
|
||||
metadata: META_DATA | undefined;
|
||||
|
||||
constructor(metadata?: META_DATA) {
|
||||
this.metadata = metadata ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
As you can see, it's just a simple abstract class that takes some metadata about the error details in any form. But wait, this is just the beginning of the story, we can explore many ideas with this failure.
|
||||
|
||||
## How to write a simple failure
|
||||
So, to create a simple failure, we can define our failure in any domain for any scenario we need, like this:
|
||||
|
||||
```ts
|
||||
export default class CreateUserFailure extends BaseFailure<{ userId: string }> {
|
||||
constructor(metadata?: { userId: string }) {
|
||||
super(metadata);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So in our logics for creating user we can return specific type of failure for creating user.
|
||||
|
||||
## Combination with Functional programming
|
||||
Functional programming is a deep topic that we cannot fully cover here. For more details, you can check out various courses and books available online.
|
||||
|
||||
However, for this article, we focus on one of the most useful functors in functional programming and how failure fits perfectly with it. This concept is the Either type.
|
||||
|
||||
Either is an algebraic data type (ADT) that represents computations that can return either a success or a failure. It consists of two possible values:
|
||||
|
||||
- Right(value): Represents a successful result.
|
||||
- Left(error): Represents a failure or unexpected result.
|
||||
|
||||
You're guessing right, our base failure will serve as the Left value in Either, allowing us to handle errors in a structured and functional way.
|
||||
|
||||
```ts
|
||||
Either<
|
||||
BaseFailure<unknown>,
|
||||
ResponseType
|
||||
>
|
||||
```
|
||||
So as we always have specific type for handling unexpected results, so we can define a new type for either in our app.
|
||||
```ts
|
||||
export type ApiEither<ResponseType> = Either<
|
||||
BaseFailure<unknown>,
|
||||
ResponseType
|
||||
>;
|
||||
```
|
||||
So, for any API calls, we can use the Either type to handle both success and failure cases.
|
||||
|
||||
Additionally, for asynchronous processes, we use TaskEither, which works similarly to Either but is designed for handling asynchronous operations.
|
||||
|
||||
|
||||
```ts
|
||||
type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
|
||||
```
|
||||
|
||||
For example, when creating a customer repository to handle all API calls for customers, we can use this type to manage success and failure cases.
|
||||
|
||||
```ts
|
||||
export default interface CustomerRepo {
|
||||
fetchList(query: string): ApiTask<Customer[]>;
|
||||
}
|
||||
```
|
||||
And in the repo we can have this pipe to get customer data:
|
||||
|
||||
> Note: In functional programming, a pipe is a composition of functions where the output of one function is passed as the input to the next, allowing for a clean, readable flow of data transformations.
|
||||
|
||||
```ts
|
||||
pipe(
|
||||
tryCatch(
|
||||
async () => {
|
||||
...// calling api and returning result
|
||||
},
|
||||
(l) => failureOr(l, new NetworkFailure(l as Error)),
|
||||
),
|
||||
map(this.customersDto.bind(this)),
|
||||
) as ApiTask<Customer[]>;
|
||||
```
|
||||
As you can see, in the try-catch block, which is the constructor of ApiEither, we define the right response in the first callback and the failure in the second callback argument.
|
||||
|
||||
failureOr is just a helper function that takes an error and converts it into a specific failure type, NetworkFailure in this example.
|
||||
|
||||
This ensures that during the process of fetching customer data, we always know the unexpected result will be of a specific type.
|
||||
```ts
|
||||
export function failureOr(
|
||||
reason: unknown,
|
||||
failure: BaseFailure<any>,
|
||||
): BaseFailure<any> {
|
||||
if (reason instanceof BaseFailure) {
|
||||
return reason;
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
```
|
||||
So in the process of fetching customer we know the unexpected result, always will be a speicfic type.
|
||||
So in any layer we can get the failure do some logics on left response based on its metadata and turn the failure shape to any other failure shape and use it for different purposes.
|
||||
|
||||
## Usecases of this idea
|
||||
|
||||
There are many situations where, if an important process encounters problems, we want to have control over it. We need to know when and why these issues happened and store that information in one of the monitoring tools.
|
||||
|
||||
For example, when a CreateUserFailure occurs in the repository layer, we can send a log with the specific time and relevant parameter data to any logging or monitoring tool.
|
||||
|
||||
### Monitoring on bugs with dev failures
|
||||
There are many situations, especially in frontend applications, where unexpected behavior occurs due to development mistakes or bugs. For example, when bugs or data changes in APIs happen, it's possible to face unexpected behaviors. In such cases, we want to show a specific message or redirect the user to an error page with a clear message.
|
||||
|
||||
Additionally, in frontend applications, logs may not be directly available in these situations, as the issue occurs on the user's system. To handle this, we can send metadata as a log to an API when encountering development failures.
|
||||
|
||||
To achieve this, we can simply define another abstract failure like this:
|
||||
|
||||
```ts
|
||||
export default abstract class BaseDevFailure<
|
||||
META_DATA,
|
||||
> extends BaseFailure<META_DATA> {}
|
||||
```
|
||||
As you can see, it’s just another failure that extends from the base failure.
|
||||
|
||||
For example, in some parts of the application, when sending dynamic arguments into the domain layer, there's a possibility of sending unexpected data. In such situations, we can define a specific development failure like this:
|
||||
```ts
|
||||
export default class ArgumentsFailure<
|
||||
META_DATA,
|
||||
> extends BaseDevFailure<META_DATA> {
|
||||
constructor(metadata?: META_DATA) {
|
||||
super(metadata);
|
||||
}
|
||||
}
|
||||
```
|
||||
So we can consider this scenario in our logics and facing with this failure we can make a log request to our log api even from frontend applications, so on facing with this situation they can show a descent message to user to contact to support team at the same time they store the bug log to have full controll on these situations.
|
||||
|
||||
### Manage translations and error messages with failure
|
||||
With this approach, we can go a step further than just error handling and even manage translations and display related messages in frontend applications automatically.
|
||||
|
||||
For each process and scenario, we should define a specific failure. At the same time, for each failure, we should display a corresponding message in the selected language based on the user's preference.
|
||||
|
||||
We can use this idea to automate both the error handling and message translation process.
|
||||
|
||||
To achieve this, we can pass a unique string key from the constructor based on the failure scenario. Our base failure will look like this:
|
||||
```ts
|
||||
export default abstract class BaseFailure<META_DATA> {
|
||||
private readonly BASE_FAILURE_MESSAGE = "failure";
|
||||
|
||||
/**
|
||||
* Use this message as key lang for failure messages
|
||||
*/
|
||||
message = this.BASE_FAILURE_MESSAGE;
|
||||
|
||||
metadata: META_DATA | undefined;
|
||||
|
||||
constructor(key: string, metadata?: META_DATA) {
|
||||
this.message = makeFailureMessage(this.message, key);
|
||||
this.metadata = metadata ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Message key and it'll add it to the failure message key hierarchy
|
||||
*/
|
||||
export function makeFailureMessage(message: string, key: string) {
|
||||
if (!key) return message;
|
||||
return `${message}.${key}`;
|
||||
}
|
||||
|
||||
```
|
||||
As you can see, we have a message property, which contains `BASE_FAILURE_MESSAGE`, the base key for all failure messages. It also accepts a key from the constructor, and with the makeFailureMessage function, it concatenates the new key with the message, shaping a unique message for each failure.
|
||||
|
||||
Each failure can have its own key passed from its constructor.
|
||||
|
||||
In the end, we can have a chained message key that we can use as the message key for each failure.
|
||||
|
||||
For example, for a failure like `UserAlreadyExistsFailure`, we can have a parent failure for all user domain failures, like this:
|
||||
|
||||
|
||||
|
||||
```ts
|
||||
export default class UserFailure extends BaseFailure {
|
||||
constructor(key: string) {
|
||||
super(makeFailureMessage("user", key));
|
||||
}
|
||||
}
|
||||
```
|
||||
and now we can define our failure:
|
||||
```ts
|
||||
export default class UserAlreadyExistsFailure extends UserFailure {
|
||||
constructor() {
|
||||
super("alreadyExists");
|
||||
}
|
||||
}
|
||||
```
|
||||
so the result of message for `UserAlreadyExistsFailure`, will be `failure.user.alreadyExists`.
|
||||
|
||||
At the same time, in another part of our project, we're using a langKey object to specify the translation key. This object, like the failure structure, follows the domain and folder structure to specify the language key.
|
||||
|
||||
```ts
|
||||
const langKey = {
|
||||
// ...
|
||||
failure: {
|
||||
user: {
|
||||
alreadyExists: "failure.user.alreadyExists",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
So, we can use our failure message key to retrieve the language key. By passing it to the translation method, we can get the translated failure message and automate the process of displaying the error message based on the failure we encounter.
|
||||
|
||||
```ts
|
||||
const usecaseResponse = await getUsersUsecase() as Promise<Either<BaseFailure, User[]>>
|
||||
|
||||
if (!isLeft(usecaseResponse)) return;
|
||||
if (!(usecaseResponse instanceOf BaseFailure)) return;
|
||||
|
||||
const translatedFailureMessage = t(usecaseResponse.left.message)
|
||||
```
|
||||
This is the final version, class diagram for our failur architecture:
|
||||

|
||||
|
||||
## Conclusion
|
||||
In this article, we've explored how to handle failures effectively in software applications by combining error handling with functional programming concepts like the Either type.
|
||||
|
||||
Furthermore, by integrating these failure handling mechanisms with automated processes for translating and displaying error messages, we create a more seamless experience for users, no matter the scenario. This approach not only improves the user experience by offering clear and context-specific messages, but it also provides valuable insights through monitoring and logging, enabling teams to quickly address issues.
|
||||
|
||||
Ultimately, this architecture supports building more robust, maintainable, and user-friendly applications, which I have used in many of my own projects, especially in frontend ones.
|
||||
|
@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/app/components/button/button";
|
||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
||||
import { useDI } from "@/bootstrap/di/di-context";
|
||||
import { useRef } from "react";
|
||||
|
||||
export default function CreateRandomInvoiceContainer() {
|
||||
const di = useDI();
|
||||
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));
|
||||
|
||||
return <Button vm={vm.current} />;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { DocumentIcon } from "@/app/components/icons/document";
|
||||
import HomeIcon from "@/app/components/icons/home";
|
||||
import { UserIcon } from "@/app/components/icons/user";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
type LinkItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: (props: { className?: string }) => JSX.Element;
|
||||
};
|
||||
export default function navLinkPersonalVM() {
|
||||
const pathname = usePathname();
|
||||
// Map of links to display in the side navigation.
|
||||
// Depending on the size of the application, this would be stored in a database.
|
||||
const links: LinkItem[] = [
|
||||
{ name: "Home", href: "/dashboard", icon: HomeIcon },
|
||||
{
|
||||
name: "Invoices",
|
||||
href: "/dashboard/invoices",
|
||||
icon: DocumentIcon,
|
||||
},
|
||||
{ name: "Customers", href: "/dashboard/customers", icon: UserIcon },
|
||||
];
|
||||
return {
|
||||
links,
|
||||
isLinkActive: (link: LinkItem) => pathname === link.href,
|
||||
};
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import HomeIcon from "@/app/components/icons/home";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
|
||||
type LinkItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: (props: { className?: string }) => JSX.Element;
|
||||
};
|
||||
|
||||
/**
|
||||
* Beside of reusable vm each View can have it's own personal vm to handle it's ownlogics.
|
||||
* Difference between personal vm and other vms which extends BaseVM, is that
|
||||
* personal vm directly will be called inside of view and instinctly connected to the view,
|
||||
* so they come together always and there is no need to be connected with interface for reusable
|
||||
* vms.
|
||||
*/
|
||||
export default function navLinkPersonalVM() {
|
||||
const pathname = usePathname();
|
||||
// Map of links to display in the side navigation.
|
||||
// Depending on the size of the application, this would be stored in a database.
|
||||
const links = useRef<LinkItem[]>([
|
||||
{ name: "Home", href: "/dashboard", icon: HomeIcon },
|
||||
]).current;
|
||||
return {
|
||||
links,
|
||||
isLinkActive: (link: LinkItem) => pathname.includes(link.href),
|
||||
};
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm";
|
||||
import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link-vm";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
"use client"
|
||||
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
|
@ -5,13 +5,7 @@ import {
|
||||
InboxIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
/**
|
||||
* Controllers are bridge between feature layer and application layer.
|
||||
* They decide, feature layer will be cached or not, where to run in client or server
|
||||
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||
* client state managements.
|
||||
*/
|
||||
export default function getCardIcon(props: {
|
||||
export default function cardController(props: {
|
||||
type: "invoices" | "customers" | "pending" | "collected";
|
||||
}) {
|
||||
const { type } = props;
|
@ -1,4 +1,4 @@
|
||||
import getCardIcon from "@/app/[lang]/dashboard/components/server/card-icon";
|
||||
import cardController from "@/app/[lang]/dashboard/components/server/card/card-controller";
|
||||
|
||||
export function Card({
|
||||
title,
|
||||
@ -9,7 +9,7 @@ export function Card({
|
||||
value: number | string;
|
||||
type: "invoices" | "customers" | "pending" | "collected";
|
||||
}) {
|
||||
const { Icon } = getCardIcon({ type });
|
||||
const { Icon } = cardController({ type });
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-gray-50 p-2 shadow-sm">
|
@ -0,0 +1,7 @@
|
||||
import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info-usecase";
|
||||
import { connection } from "next/server";
|
||||
|
||||
export default function cardsController() {
|
||||
connection();
|
||||
return fetchSummaryInfoUsecase();
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { Card } from "@/app/[lang]/dashboard/components/server/card";
|
||||
import fetchSummaryInfoController from "@/app/[lang]/dashboard/controller/fetch-summary-info.controller";
|
||||
import { Card } from "@/app/[lang]/dashboard/components/server/card/card";
|
||||
import cardsController from "@/app/[lang]/dashboard/components/server/cards/cards-controller";
|
||||
|
||||
export default async function CardWrapper() {
|
||||
const { customersNumber, invoicesNumber, invoicesSummary } =
|
||||
await fetchSummaryInfoController();
|
||||
await cardsController();
|
||||
|
||||
return (
|
||||
<>
|
@ -0,0 +1,7 @@
|
||||
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase";
|
||||
import { connection } from "next/server";
|
||||
|
||||
export default function latestInvoicesController() {
|
||||
connection();
|
||||
return fetchCustomerInvoicesUsecase();
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import latestInvoicesController from "@/app/[lang]/dashboard/controller/latest-invoices.controller";
|
||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
||||
import Button from "@/app/components/button/button";
|
||||
import CreateRandomInvoiceContainer from "@/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice";
|
||||
import latestInvoicesController from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { isLeft } from "fp-ts/lib/Either";
|
||||
@ -40,7 +39,6 @@ export default async function LatestInvoices() {
|
||||
</p>
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col md:col-span-4">
|
||||
<h2 className="mb-4 text-xl md:text-2xl">Latest Invoices</h2>
|
||||
@ -50,7 +48,7 @@ export default async function LatestInvoices() {
|
||||
<ArrowPathIcon className="h-5 w-5 text-gray-500" />
|
||||
<h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>
|
||||
</div>
|
||||
<Button vmKey={CreateRandomInvoiceButtonVM} />
|
||||
<CreateRandomInvoiceContainer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -1,12 +1,6 @@
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
|
||||
import fetchRevenuesUsecase from "@/feature/core/revenue/domain/usecase/fetch-revenues.usecase";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
import fetchRevenuesUsecase from "@/feature/core/revenue/domain/usecase/fetch-revenues-usecase";
|
||||
|
||||
/**
|
||||
* Controllers are bridge between feature layer and application layer.
|
||||
* They decide, feature layer will be cached or not, where to run in client or server
|
||||
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||
* client state managements.
|
||||
*/
|
||||
export default async function revenueChartController() {
|
||||
const revenue = await fetchRevenuesUsecase();
|
||||
const chartHeight = 350;
|
@ -1,4 +1,4 @@
|
||||
import revenueChartController from "@/app/[lang]/dashboard/controller/revenue-chart.controller";
|
||||
import revenueChartController from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller";
|
||||
import { CalendarIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export default async function RevenueChart() {
|
@ -1,27 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { ApiEither } from "@/feature/common/data/api-task";
|
||||
import serverDi from "@/feature/common/server.di";
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||
import {
|
||||
CreateInvoiceUsecase,
|
||||
createInvoiceUsecaseKey,
|
||||
} from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||
import { connection } from "next/server";
|
||||
|
||||
/**
|
||||
* Controllers are bridge between feature layer and application layer.
|
||||
* They decide, feature layer will be cached or not, where to run in client or server
|
||||
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||
* client state managements.
|
||||
*/
|
||||
export default async function createInvoiceController(
|
||||
params: InvoiceParam,
|
||||
): Promise<ApiEither<string>> {
|
||||
connection();
|
||||
const usecase = serverDi(invoiceModuleKey).resolve<CreateInvoiceUsecase>(
|
||||
createInvoiceUsecaseKey,
|
||||
);
|
||||
return usecase(params);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info.usecase";
|
||||
import { connection } from "next/server";
|
||||
|
||||
/**
|
||||
* Controllers are bridge between feature layer and application layer.
|
||||
* They decide, feature layer will be cached or not, where to run in client or server
|
||||
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||
* client state managements.
|
||||
*/
|
||||
export default function fetchSummaryInfoController() {
|
||||
connection();
|
||||
return fetchSummaryInfoUsecase();
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices.usecase";
|
||||
import { connection } from "next/server";
|
||||
|
||||
/**
|
||||
* Controllers are bridge between feature layer and application layer.
|
||||
* They decide, feature layer will be cached or not, where to run in client or server
|
||||
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||
* client state managements.
|
||||
*/
|
||||
export default function latestInvoicesController() {
|
||||
connection();
|
||||
return fetchCustomerInvoicesUsecase();
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import SideNav from "@/app/[lang]/dashboard/components/server/sidenav";
|
||||
import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard.app-module";
|
||||
import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard-app-module";
|
||||
import { DiContext } from "@/bootstrap/di/di-context";
|
||||
import { useRef } from "react";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeletons";
|
||||
import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeletons/skeletons";
|
||||
|
||||
export default function Loading() {
|
||||
return <DashboardSkeleton />;
|
||||
|
@ -1,20 +1,16 @@
|
||||
import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller";
|
||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
|
||||
|
||||
/**
|
||||
* Each page can have its own di to connect all vms, usecases or controllers
|
||||
*/
|
||||
export default function dashboardAppModule() {
|
||||
const dashboardDi = di.createChildContainer();
|
||||
|
||||
dashboardDi.register(createInvoiceUsecase.name, {
|
||||
useValue: createInvoiceUsecase,
|
||||
});
|
||||
dashboardDi.register(
|
||||
CreateRandomInvoiceButtonVM.name,
|
||||
CreateRandomInvoiceButtonVM,
|
||||
CreateRandomInvoiceButtonVM,
|
||||
);
|
||||
|
||||
dashboardDi.register(createInvoiceController.name, {
|
||||
useValue: createInvoiceController,
|
||||
});
|
||||
return dashboardDi;
|
||||
}
|
@ -1,14 +1,28 @@
|
||||
import {
|
||||
LatestInvoicesSkeleton,
|
||||
RevenueChartSkeleton,
|
||||
} from "@/app/[lang]/dashboard/components/server/skeletons";
|
||||
import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart";
|
||||
} from "@/app/[lang]/dashboard/components/server/skeletons/skeletons";
|
||||
import CardWrapper from "@/app/[lang]/dashboard/components/server/cards/cards";
|
||||
import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices";
|
||||
import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart";
|
||||
import { Suspense } from "react";
|
||||
import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices";
|
||||
import { getServerTranslation, LANGS } from "@/bootstrap/i18n/i18n";
|
||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
||||
|
||||
export default async function Dashboard() {
|
||||
export default async function Dashboard(props: {
|
||||
params: Promise<{ lang: LANGS }>;
|
||||
}) {
|
||||
const { params } = props;
|
||||
const { lang } = await params;
|
||||
const { t } = await getServerTranslation(lang);
|
||||
return (
|
||||
<main>
|
||||
<h1 className="mb-4 text-xl md:text-2xl">
|
||||
{t(langKey.global.dashboard)}
|
||||
</h1>
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<CardWrapper />
|
||||
</div>
|
||||
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
|
||||
<Suspense fallback={<RevenueChartSkeleton />}>
|
||||
<RevenueChart />
|
||||
|
@ -1,27 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller";
|
||||
import ButtonVm from "@/app/components/button/button.i-vm";
|
||||
import ButtonVm from "@/app/components/button/button-vm";
|
||||
import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action";
|
||||
import useThrottle from "@/bootstrap/helpers/hooks/use-throttle";
|
||||
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
|
||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
|
||||
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Viewmodel for the button view to connect to business logics and all UI logics
|
||||
* For UI logics, all translations, states, sideeffects and events will be handled
|
||||
* in this layer.
|
||||
*/
|
||||
export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> {
|
||||
private createInvoice: typeof createInvoiceController;
|
||||
private createInvoice: typeof createInvoiceUsecase;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.createInvoice = this.di.resolve(createInvoiceController.name);
|
||||
this.createInvoice = this.di.resolve(createInvoiceUsecase.name);
|
||||
}
|
||||
|
||||
useVM(): ButtonVm {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ThemeProvider } from "@/app/[lang]/dashboard/components/client/theme-provider/theme-provider";
|
||||
import { getI18n, LANGS } from "@/bootstrap/i18n/i18n";
|
||||
import { initI18next, LANGS } from "@/bootstrap/i18n/i18n";
|
||||
import TranslationsProvider from "@/bootstrap/i18n/i18n-provider";
|
||||
import localFont from "next/font/local";
|
||||
import { PropsWithChildren } from "react";
|
||||
@ -20,7 +20,7 @@ export default async function layout(
|
||||
) {
|
||||
const { params, children } = props;
|
||||
const { lang } = await params;
|
||||
await getI18n({ lng: lang });
|
||||
const { resources } = await initI18next({ lng: lang });
|
||||
return (
|
||||
<html lang={lang} suppressHydrationWarning>
|
||||
<body
|
||||
@ -32,7 +32,9 @@ export default async function layout(
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<TranslationsProvider lng={lang}>{children}</TranslationsProvider>
|
||||
<TranslationsProvider lng={lang} resources={resources}>
|
||||
{children}
|
||||
</TranslationsProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,13 +1,4 @@
|
||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
||||
import { getServerTranslation, LANGS } from "@/bootstrap/i18n/i18n";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home(props: {
|
||||
params: Promise<{ lang: LANGS }>;
|
||||
}) {
|
||||
const { params } = props;
|
||||
const { lang } = await params;
|
||||
const { t } = await getServerTranslation(lang);
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col p-6">
|
||||
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
|
||||
@ -17,12 +8,6 @@ export default async function Home(props: {
|
||||
<strong>Welcome to Acme.</strong> This is the example for the ,
|
||||
brought to you by Vercel.
|
||||
</p>
|
||||
<Link
|
||||
className="flex rounded-md bg-primary-foreground p-3 ml-auto mr-auto text-white"
|
||||
href="dashboard"
|
||||
>
|
||||
{t(langKey.global.dashboard)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
7
src/app/components/button/button-vm.ts
Normal file
7
src/app/components/button/button-vm.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default interface ButtonVm {
|
||||
props: {
|
||||
title: string;
|
||||
isDisable: boolean;
|
||||
};
|
||||
onClick(): void;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Interface for viewmodel of button. this is bridge between view and viewmodel.
|
||||
* With this interface, view is adapter and vms will be implementations in bridge pattern
|
||||
*/
|
||||
export default interface ButtonVm {
|
||||
props: {
|
||||
title: string;
|
||||
isDisable: boolean;
|
||||
};
|
||||
onClick(): void;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
|
||||
import ButtonVm from "@/app/components/button/button.i-vm";
|
||||
import ButtonVm from "@/app/components/button/button-vm";
|
||||
import { ReactNode } from "react";
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
|
@ -1,7 +1,12 @@
|
||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
||||
import Button from "@/app/components/button/button";
|
||||
import { DiContext, useDI } from "@/bootstrap/di/di-context";
|
||||
import mockedModuleDi from "@/bootstrap/di/mocked-module-di";
|
||||
import Story from "@/bootstrap/helpers/view/storybook-base-template-type";
|
||||
import getArgVM from "@/bootstrap/helpers/view/storybook-with-arg-vm";
|
||||
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
|
||||
import type { Meta } from "@storybook/react";
|
||||
import { useRef } from "react";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "general/Button",
|
||||
@ -27,3 +32,34 @@ export const Primary: Story = {
|
||||
return <Button vm={vm} memoizedByVM={false} />;
|
||||
},
|
||||
};
|
||||
|
||||
export const WithVM: Story = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const di = mockedModuleDi([
|
||||
{
|
||||
token: CreateRandomInvoiceButtonVM,
|
||||
provider: CreateRandomInvoiceButtonVM,
|
||||
},
|
||||
{
|
||||
token: createInvoiceUsecase.name,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, no-console
|
||||
provider: (args: any) => console.log("clicked", args),
|
||||
},
|
||||
]);
|
||||
return <Story di={di} />;
|
||||
},
|
||||
],
|
||||
render: (_, globalProps) => {
|
||||
function Child() {
|
||||
const di = useDI();
|
||||
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));
|
||||
return <Button vm={vm.current} memoizedByVM={false} />;
|
||||
}
|
||||
return (
|
||||
<DiContext.Provider value={globalProps.di}>
|
||||
<Child />
|
||||
</DiContext.Provider>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@ -2,9 +2,6 @@ import { constructor } from "tsyringe/dist/typings/types";
|
||||
|
||||
export const isServer = typeof window === "undefined";
|
||||
|
||||
/**
|
||||
* Checks if the given value is a class
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function isClass(fn: any): fn is constructor<unknown> {
|
||||
return typeof fn === "function" && /^(class|function [A-Z])/.test(fn);
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
/**
|
||||
* To connect tailwind classes.
|
||||
* @param inputs Tailwind classes
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
@ -1,14 +1,11 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
"use client";
|
||||
|
||||
// import gdi from "@/bootstrap/di/init-di";
|
||||
/* eslint-disable react/display-name */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
|
||||
"use client";
|
||||
|
||||
import { useDI } from "@/bootstrap/di/di-context";
|
||||
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
||||
import { Component, ReactNode, FC, PropsWithChildren, memo } from "react";
|
||||
import { InjectionToken } from "tsyringe";
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Connector Component */
|
||||
@ -26,6 +23,7 @@ interface IVvmConnector<IVM, PROPS> extends PropsWithChildren {
|
||||
const VvmConnector = memo(
|
||||
<IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => {
|
||||
const { View, Vm, restProps, children } = props;
|
||||
|
||||
const vm = Vm.useVM();
|
||||
|
||||
const allProps = {
|
||||
@ -47,7 +45,8 @@ const VvmConnector = memo(
|
||||
type IVMParent = Record<string, any>;
|
||||
type IPropParent = Record<string, any> | undefined;
|
||||
|
||||
type BaseProps<PROPS extends IPropParent = undefined> = {
|
||||
type BaseProps<IVM extends IVMParent, PROPS extends IPropParent = undefined> = {
|
||||
vm: IBaseVM<IVM>;
|
||||
restProps?: PROPS;
|
||||
/**
|
||||
* By default it's true.
|
||||
@ -58,24 +57,6 @@ type BaseProps<PROPS extends IPropParent = undefined> = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
type BasePropsWithVM<
|
||||
IVM extends IVMParent,
|
||||
PROPS extends IPropParent = undefined,
|
||||
> = BaseProps<PROPS> & {
|
||||
/**
|
||||
* Directly instantiated vm
|
||||
*/
|
||||
vm: IBaseVM<IVM>;
|
||||
};
|
||||
|
||||
type BasePropsWithVMKey<PROPS extends IPropParent = undefined> =
|
||||
BaseProps<PROPS> & {
|
||||
/**
|
||||
* TSyringe key for vm to be injected
|
||||
*/
|
||||
vmKey: InjectionToken;
|
||||
};
|
||||
|
||||
export type BuildProps<
|
||||
IVM extends IVMParent,
|
||||
PROPS extends IPropParent = undefined,
|
||||
@ -85,36 +66,10 @@ export type BuildProps<
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export type ViewProps<
|
||||
IVM extends IVMParent,
|
||||
PROPS extends IPropParent = undefined,
|
||||
> = BasePropsWithVM<IVM, PROPS> | BasePropsWithVMKey<PROPS>;
|
||||
|
||||
/**
|
||||
* Base view is base component for all views in mvvm architecture which gets
|
||||
* vm as props and connect it to the view and memoize the component by default
|
||||
* to just render just on changes of its vm.
|
||||
*/
|
||||
export default abstract class BaseView<
|
||||
IVM extends IVMParent,
|
||||
PROPS extends IPropParent = undefined,
|
||||
> extends Component<ViewProps<IVM, PROPS>> {
|
||||
private vm: IBaseVM<IVM> | undefined;
|
||||
|
||||
constructor(props: ViewProps<IVM, PROPS>) {
|
||||
super(props);
|
||||
this.vm = this.initVm;
|
||||
}
|
||||
|
||||
private get initVm() {
|
||||
if (Object.hasOwn(this.props, "vmKey")) {
|
||||
const { vmKey } = this.props as BasePropsWithVMKey<PROPS>;
|
||||
const di = useDI();
|
||||
return di.resolve(vmKey) as IBaseVM<IVM>;
|
||||
}
|
||||
return (this.props as BasePropsWithVM<IVM, PROPS>).vm;
|
||||
}
|
||||
|
||||
> extends Component<BaseProps<IVM, PROPS>> {
|
||||
protected get componentName() {
|
||||
return this.constructor.name;
|
||||
}
|
||||
@ -122,16 +77,9 @@ export default abstract class BaseView<
|
||||
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
|
||||
|
||||
render(): ReactNode {
|
||||
const { restProps, memoizedByVM, children, ...rest } = this.props;
|
||||
const { vm, restProps, memoizedByVM, children, ...rest } = this.props;
|
||||
|
||||
VvmConnector.displayName = this.componentName;
|
||||
const vm = memoizedByVM ? this.vm : this.initVm;
|
||||
if (!vm) {
|
||||
const isVmKey = Object.hasOwn(this.props, "vmKey");
|
||||
const message = isVmKey
|
||||
? "vm is not defined, check your di configuration"
|
||||
: "pass correct vm";
|
||||
throw new Error(`Vm is not defined${message}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<VvmConnector
|
||||
@ -144,4 +92,5 @@ export default abstract class BaseView<
|
||||
</VvmConnector>
|
||||
);
|
||||
}
|
||||
/* -------------------------------------------------------------------------- */
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
||||
|
||||
/**
|
||||
* To use with mvvm library to make a vm based on props so you can pass the result to the view
|
||||
* @param vmObj All properties which view needs to get from vm.
|
||||
* @returns Vm which is suitable to be passed to the view
|
||||
* @example const vm = getArgVM(globalData.parsedProps.vm);
|
||||
return <Button vm={vm} memoizedByVM={false} />;
|
||||
*/
|
||||
const getArgVM = <IVM>(vmObj: IVM) => {
|
||||
class VM implements IBaseVM<IVM> {
|
||||
useVM(): IVM {
|
||||
|
@ -5,12 +5,6 @@ import { NoOverride } from "@/bootstrap/helpers/type-helper";
|
||||
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
||||
import { useState } from "react";
|
||||
|
||||
/**
|
||||
* Base class for all viewmodels. It provides
|
||||
* - dependency injection: To get closes di which serves from di provider
|
||||
* - rerender method: to rerender your component manually
|
||||
* - produce method: to produce your vm dynamically by passing and attaching dependencies to it
|
||||
*/
|
||||
export default abstract class BaseVM<
|
||||
IVM,
|
||||
DEP extends object | undefined = undefined,
|
||||
|
@ -1,6 +1,3 @@
|
||||
/**
|
||||
* All viewmodels should implement this interface.
|
||||
*/
|
||||
export default interface IBaseVM<VM> {
|
||||
useVM(): VM;
|
||||
}
|
||||
|
@ -1,8 +1,3 @@
|
||||
/**
|
||||
* main language keys which will be used for translation to avoid using strings directly and be
|
||||
* a single source of truth in all changes between all languages dictionaries.
|
||||
* All languages dictionaries should have the same keys by having this object type.
|
||||
*/
|
||||
const langKey = {
|
||||
global: {
|
||||
home: "global.home",
|
||||
|
@ -1,24 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { getI18n, LANGS } from "@/bootstrap/i18n/i18n";
|
||||
import { PropsWithChildren, useEffect, useState } from "react";
|
||||
import { i18n } from "i18next";
|
||||
import storeLang from "@/bootstrap/i18n/store-lang-action";
|
||||
import { i18nInstance, initI18next, LANGS } from "@/bootstrap/i18n/i18n";
|
||||
import { Resource } from "i18next";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export default function TranslationsProvider({
|
||||
children,
|
||||
lng,
|
||||
}: PropsWithChildren & { lng: LANGS }) {
|
||||
const [i18n, setI18n] = useState<i18n>();
|
||||
resources,
|
||||
}: PropsWithChildren & { lng: LANGS; resources: Resource }) {
|
||||
if (!resources) return children;
|
||||
initI18next({ lng, resources });
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
storeLang(lng);
|
||||
setI18n((await getI18n({ lng })).i18n);
|
||||
})();
|
||||
}, [lng]);
|
||||
|
||||
if (!i18n) return null;
|
||||
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
||||
return <I18nextProvider i18n={i18nInstance}>{children}</I18nextProvider>;
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ export enum LANGS {
|
||||
RU = "ru",
|
||||
}
|
||||
|
||||
export const getI18n = async (params: {
|
||||
export const initI18next = async (params: {
|
||||
lng: LANGS;
|
||||
resources?: Resource;
|
||||
ns?: string;
|
||||
@ -43,9 +43,13 @@ export async function getServerTranslation(
|
||||
ns?: string,
|
||||
options: { keyPrefix?: string } = {},
|
||||
) {
|
||||
const { i18n } = await getI18n({ lng });
|
||||
await initI18next({ lng });
|
||||
return {
|
||||
t: i18n.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options?.keyPrefix),
|
||||
t: i18nInstance.getFixedT(
|
||||
lng,
|
||||
Array.isArray(ns) ? ns[0] : ns,
|
||||
options?.keyPrefix,
|
||||
),
|
||||
i18n: i18nInstance,
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { LANGS } from "@/bootstrap/i18n/i18n";
|
||||
import { cookieName } from "@/bootstrap/i18n/settings";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export default async function storeLang(lng: LANGS) {
|
||||
(await cookies()).set(cookieName, lng);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Either } from "fp-ts/lib/Either";
|
||||
import { TaskEither } from "fp-ts/lib/TaskEither";
|
||||
import BaseFailure from "@/feature/common/failures/base.failure";
|
||||
import BaseFailure from "@/feature/common/failures/base-failure";
|
||||
|
||||
type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
|
||||
export type ApiEither<ResponseType> = Either<
|
||||
|
@ -1,14 +1,8 @@
|
||||
import { makeFailureMessage } from "@/feature/common/failures/failure-helpers";
|
||||
|
||||
/**
|
||||
* This class can be used as a base class for creating custom failure classes.
|
||||
* With this class you can set message and metadata, with messages and extending
|
||||
* you can create your failure messages hierarchy and automatically by syncing langKey
|
||||
* with the hirerarchy of failure messages.
|
||||
* For example if you pass a key of `user` to the constructor of `UserCreationFailure`
|
||||
* so in langKey you can have failure message `faiure.user` so automatically,
|
||||
* you can show translated error message everywhere in the app.
|
||||
* Also you can use this failure message to have grained control over failures.
|
||||
* This is a class called BaseFailure that extends the Error class. It is
|
||||
* used as a base class for creating custom failure classes.
|
||||
*/
|
||||
export default abstract class BaseFailure<META_DATA> {
|
||||
/* ------------------------------- Attributes ------------------------------- */
|
@ -1,4 +1,4 @@
|
||||
import BaseDevFailure from "@/feature/common/failures/dev/base-dev.failure";
|
||||
import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
|
||||
|
||||
/**
|
||||
* Failure for needed arguments in a method but sent wrong one
|
5
src/feature/common/failures/dev/base-dev-failure.ts
Normal file
5
src/feature/common/failures/dev/base-dev-failure.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import BaseFailure from "@/feature/common/failures/base-failure";
|
||||
|
||||
export default abstract class BaseDevFailure<
|
||||
META_DATA,
|
||||
> extends BaseFailure<META_DATA> {}
|
@ -1,10 +0,0 @@
|
||||
import BaseFailure from "@/feature/common/failures/base.failure";
|
||||
|
||||
/**
|
||||
* This is a base class for development failures. All dev failures means we as a developer
|
||||
* made a mistake in the process and we should fix it and can be used in monitoring and
|
||||
* should be handled in hotfix ASAP.
|
||||
*/
|
||||
export default abstract class BaseDevFailure<
|
||||
META_DATA,
|
||||
> extends BaseFailure<META_DATA> {}
|
@ -1,7 +1,7 @@
|
||||
import BaseDevFailure from "@/feature/common/failures/dev/base-dev.failure";
|
||||
import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
|
||||
|
||||
/**
|
||||
* This is a failure when we didn't provice specific dependency.
|
||||
* This is a failure of not having specific dependency
|
||||
*/
|
||||
export default class DependencyFailure<
|
||||
META_DATA,
|
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import BaseFailure from "@/feature/common/failures/base.failure";
|
||||
import BaseFailure from "@/feature/common/failures/base-failure";
|
||||
|
||||
/**
|
||||
* This method is supposed to save previous failure of TaskEither
|
||||
|
@ -1,4 +1,4 @@
|
||||
import BaseFailure from "./base.failure";
|
||||
import BaseFailure from "./base-failure";
|
||||
|
||||
/**
|
||||
* Failure for HTTP response when response dosn't have base structure
|
@ -1,7 +1,7 @@
|
||||
import BaseFailure from "./base.failure";
|
||||
import BaseFailure from "./base-failure";
|
||||
|
||||
/**
|
||||
* Failure for params failure. which means some params are missing or not valid
|
||||
* Failure for params failure
|
||||
*/
|
||||
export default class ParamsFailure<META_DATA> extends BaseFailure<META_DATA> {
|
||||
/* ------------------------------- Constructor ------------------------------ */
|
@ -1,14 +1,14 @@
|
||||
import getCustomerInvoiceDi from "@/feature/core/customer-invoice/data/module/customer-invoice.di";
|
||||
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice.module-key";
|
||||
import getCustomerInvoiceDi from "@/feature/core/customer-invoice/data/module/customer-invoice-di";
|
||||
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
|
||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||
import getCustomerDi from "@/feature/core/customer/data/module/customer-di";
|
||||
import getInvoiceDi from "@/feature/core/invoice/data/module/invoice.di";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||
import getInvoiceDi from "@/feature/core/invoice/data/module/invoice-di";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info.module-key";
|
||||
import getSummaryInfoDi from "@/feature/core/summary-info/data/module/summary-info.di";
|
||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue.module-key";
|
||||
import getRevenueDi from "@/feature/core/revenue/data/module/revenue.di";
|
||||
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info-module-key";
|
||||
import getSummaryInfoDi from "@/feature/core/summary-info/data/module/summary-info-di";
|
||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
|
||||
import getRevenueDi from "@/feature/core/revenue/data/module/revenue-di";
|
||||
|
||||
const memoizedDis: Record<string, DependencyContainer> = {};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import CustomerInvoiceDbRepo from "@/feature/core/customer-invoice/data/repo/customer-invoice-db.repo";
|
||||
import { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice.repo";
|
||||
import CustomerInvoiceDbRepo from "@/feature/core/customer-invoice/data/repo/customer-invoice-db-repo";
|
||||
import { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
|
||||
export default function getCustomerInvoiceDi(): DependencyContainer {
|
@ -1,10 +1,10 @@
|
||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||
import ApiTask from "@/feature/common/data/api-task";
|
||||
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
||||
import NetworkFailure from "@/feature/common/failures/network.failure";
|
||||
import NetworkFailure from "@/feature/common/failures/network-failure";
|
||||
import { formatCurrency } from "@/feature/common/feature-helpers";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice.entity";
|
||||
import CustomerInvoiceRepo from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice.repo";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
||||
import CustomerInvoiceRepo from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||
import { pipe } from "fp-ts/lib/function";
|
||||
import { tryCatch } from "fp-ts/lib/TaskEither";
|
||||
import postgres from "postgres";
|
@ -1,5 +1,5 @@
|
||||
import ApiTask from "@/feature/common/data/api-task";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice.entity";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
||||
|
||||
export default interface CustomerInvoiceRepo {
|
||||
fetchList(): ApiTask<CustomerInvoice[]>;
|
@ -1,10 +1,11 @@
|
||||
import "server-only";
|
||||
import { ApiEither } from "@/feature/common/data/api-task";
|
||||
import serverDi from "@/feature/common/server.di";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice.entity";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
||||
import CustomerInvoiceRepo, {
|
||||
customerInvoiceRepoKey,
|
||||
} from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice.repo";
|
||||
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice.module-key";
|
||||
} from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
|
||||
|
||||
export default function fetchCustomerInvoicesUsecase(): Promise<
|
||||
ApiEither<CustomerInvoice[]>
|
@ -1,7 +1,7 @@
|
||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||
import ApiTask from "@/feature/common/data/api-task";
|
||||
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
||||
import NetworkFailure from "@/feature/common/failures/network.failure";
|
||||
import NetworkFailure from "@/feature/common/failures/network-failure";
|
||||
import { formatCurrency } from "@/feature/common/feature-helpers";
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||
import CustomerRepo from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import serverDi from "@/feature/common/server.di";
|
||||
import "server-only";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||
import CustomerRepo, {
|
||||
customerRepoKey,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import "server-only";
|
||||
import { ApiEither } from "@/feature/common/data/api-task";
|
||||
import serverDi from "@/feature/common/server.di";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||
import CustomerRepo, {
|
||||
|
@ -1,16 +1,11 @@
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import invoiceDbRepo from "@/feature/core/invoice/data/repo/invoice-db.repo";
|
||||
import { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice-impl.usecase";
|
||||
import { createInvoiceUsecaseKey } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
|
||||
import invoiceDbRepo from "@/feature/core/invoice/data/repo/invoice-db-repo";
|
||||
import { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
|
||||
export default function getInvoiceDi(): DependencyContainer {
|
||||
const invoiceDi = di.createChildContainer();
|
||||
|
||||
invoiceDi.register(invoiceRepoKey, invoiceDbRepo);
|
||||
invoiceDi.register(createInvoiceUsecaseKey, {
|
||||
useValue: createInvoiceUsecase,
|
||||
});
|
||||
return invoiceDi;
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
import "server-only";
|
||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||
import ApiTask from "@/feature/common/data/api-task";
|
||||
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
||||
import NetworkFailure from "@/feature/common/failures/network.failure";
|
||||
import NetworkFailure from "@/feature/common/failures/network-failure";
|
||||
import { formatCurrency } from "@/feature/common/feature-helpers";
|
||||
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
|
||||
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
import { pipe } from "fp-ts/lib/function";
|
||||
import { tryCatch } from "fp-ts/lib/TaskEither";
|
||||
import postgres from "postgres";
|
@ -1,6 +1,6 @@
|
||||
import ApiTask from "@/feature/common/data/api-task";
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
|
||||
export default interface InvoiceRepo {
|
||||
fetchAllInvoicesAmount(): Promise<number>;
|
@ -1,21 +1,22 @@
|
||||
"use server";
|
||||
|
||||
import { ApiEither } from "@/feature/common/data/api-task";
|
||||
import ParamsFailure from "@/feature/common/failures/params.failure";
|
||||
import serverDi from "@/feature/common/server.di";
|
||||
import ParamsFailure from "@/feature/common/failures/params-failure";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import InvoiceRepo, {
|
||||
invoiceRepoKey,
|
||||
} from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import {
|
||||
InvoiceParam,
|
||||
invoiceSchema,
|
||||
} from "@/feature/core/invoice/domain/param/invoice.param";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||
import { CreateInvoiceUsecase } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
|
||||
} from "@/feature/core/invoice/domain/param/invoice-param";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
import { pipe } from "fp-ts/lib/function";
|
||||
import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither";
|
||||
|
||||
const createInvoiceUsecase: CreateInvoiceUsecase = async (
|
||||
export default async function createInvoiceUsecase(
|
||||
params: InvoiceParam,
|
||||
): Promise<ApiEither<string>> => {
|
||||
): Promise<ApiEither<string>> {
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
||||
|
||||
return pipe(
|
||||
@ -28,6 +29,4 @@ const createInvoiceUsecase: CreateInvoiceUsecase = async (
|
||||
}),
|
||||
chain((params) => repo.createInvoice(params)),
|
||||
)();
|
||||
};
|
||||
|
||||
export default createInvoiceUsecase;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { ApiEither } from "@/feature/common/data/api-task";
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||
|
||||
export type CreateInvoiceUsecase = (
|
||||
param: InvoiceParam,
|
||||
) => Promise<ApiEither<string>>;
|
||||
|
||||
export const createInvoiceUsecaseKey = "createInvoiceUsecaseKey";
|
@ -1,8 +1,9 @@
|
||||
import serverDi from "@/feature/common/server.di";
|
||||
import "server-only";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import InvoiceRepo, {
|
||||
invoiceRepoKey,
|
||||
} from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
|
||||
export default function fetchAllInvoicesAmountUsecase(): Promise<number> {
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
@ -1,9 +1,10 @@
|
||||
import serverDi from "@/feature/common/server.di";
|
||||
import "server-only";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import InvoiceRepo, {
|
||||
invoiceRepoKey,
|
||||
} from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
|
||||
export default function fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
@ -1,6 +1,6 @@
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db.repo";
|
||||
import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue.i-repo";
|
||||
import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db-repo";
|
||||
import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
||||
|
||||
export default function getRevenueDi() {
|
||||
const revenueDi = di.createChildContainer();
|
@ -1,6 +1,6 @@
|
||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
|
||||
import RevenueRepo from "@/feature/core/revenue/domain/i-repo/revenue.i-repo";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
import RevenueRepo from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
||||
import postgres from "postgres";
|
||||
|
||||
export type RevenueDbResponse = {
|
@ -1,4 +1,4 @@
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
|
||||
export default interface RevenueRepo {
|
||||
fetchRevenues(): Promise<Revenue[]>;
|
@ -1,9 +1,10 @@
|
||||
import serverDi from "@/feature/common/server.di";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
|
||||
import "server-only";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
import RevenueRepo, {
|
||||
revenueRepoKey,
|
||||
} from "@/feature/core/revenue/domain/i-repo/revenue.i-repo";
|
||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue.module-key";
|
||||
} from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
|
||||
|
||||
export default function fetchRevenuesUsecase(): Promise<Revenue[]> {
|
||||
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey);
|
@ -1,6 +1,6 @@
|
||||
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase";
|
||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount.usecase";
|
||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary.usecase";
|
||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount-usecase";
|
||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary";
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
|
||||
export default function getSummaryInfoDi() {
|
@ -1,9 +1,10 @@
|
||||
import serverDi from "@/feature/common/server.di";
|
||||
import "server-only";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase";
|
||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount.usecase";
|
||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary.usecase";
|
||||
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info.module-key";
|
||||
import SummaryInfo from "@/feature/core/summary-info/domain/value-object/summary-info.value-object";
|
||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount-usecase";
|
||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary";
|
||||
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info-module-key";
|
||||
import SummaryInfo from "@/feature/core/summary-info/domain/value-object/summary-info";
|
||||
|
||||
export default async function fetchSummaryInfoUsecase(): Promise<SummaryInfo> {
|
||||
try {
|
@ -1,4 +1,4 @@
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
|
||||
export default class SummaryInfo {
|
||||
customersNumber: number;
|
@ -1,9 +1,6 @@
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import * as serverDi from "@/feature/common/server.di";
|
||||
import * as serverDi from "@/feature/common/server-di";
|
||||
|
||||
/**
|
||||
* To mock and get server di
|
||||
*/
|
||||
export default function mockDi() {
|
||||
vi.spyOn(serverDi, "default").mockReturnValue(di);
|
||||
return di;
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { Mock } from "moq.ts";
|
||||
|
||||
/**
|
||||
* To get mock object to mock objects and classes
|
||||
*/
|
||||
export function getMock<T>() {
|
||||
return new Mock<T>();
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import CustomerRepo, {
|
||||
import { getMock } from "@/test/common/mock/mock-factory";
|
||||
import { describe } from "vitest";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import CustomerFakeFactory from "@/test/common/fake-factory/customer/customer.fake-factory";
|
||||
import CustomerFakeFactory from "@/test/common/fake-factory/customer/customer-fake-factory";
|
||||
import fetchCustomersUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-usecase";
|
||||
import mockDi from "@/test/common/mock/mock-di";
|
||||
import { right } from "fp-ts/lib/TaskEither";
|
Loading…
x
Reference in New Issue
Block a user