diff --git a/src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx b/src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx index 68742c2..8db5bf3 100644 --- a/src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx +++ b/src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx @@ -5,6 +5,9 @@ import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random import { useDI } from "@/bootstrap/di/di-context"; import { useRef } from "react"; +/** + * From a parent component Vm and view will be connected together. + */ export default function CreateRandomInvoiceContainer() { const di = useDI(); const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM)); diff --git a/src/app/[lang]/dashboard/components/client/nav-links/nav-link-vm.ts b/src/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm.ts similarity index 65% rename from src/app/[lang]/dashboard/components/client/nav-links/nav-link-vm.ts rename to src/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm.ts index bdf58b2..9ddf695 100644 --- a/src/app/[lang]/dashboard/components/client/nav-links/nav-link-vm.ts +++ b/src/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm.ts @@ -2,17 +2,26 @@ 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"; +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: LinkItem[] = [ + const links = useRef([ { name: "Home", href: "/dashboard", icon: HomeIcon }, { name: "Invoices", @@ -20,7 +29,7 @@ export default function navLinkPersonalVM() { icon: DocumentIcon, }, { name: "Customers", href: "/dashboard/customers", icon: UserIcon }, - ]; + ]).current; return { links, isLinkActive: (link: LinkItem) => pathname === link.href, diff --git a/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx b/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx index edb724c..8c2f587 100644 --- a/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx +++ b/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx @@ -1,6 +1,6 @@ "use client"; -import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link-vm"; +import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm"; import clsx from "clsx"; import Link from "next/link"; diff --git a/src/app/[lang]/dashboard/components/client/theme-provider/theme-provider.tsx b/src/app/[lang]/dashboard/components/client/theme-provider/theme-provider.tsx index 08ad43b..65b6bf4 100644 --- a/src/app/[lang]/dashboard/components/client/theme-provider/theme-provider.tsx +++ b/src/app/[lang]/dashboard/components/client/theme-provider/theme-provider.tsx @@ -1,3 +1,5 @@ +/* eslint-disable react/jsx-props-no-spreading */ + "use client"; import * as React from "react"; @@ -7,6 +9,5 @@ export function ThemeProvider({ children, ...props }: React.ComponentProps) { - // eslint-disable-next-line react/jsx-props-no-spreading return {children}; } diff --git a/src/app/[lang]/dashboard/components/server/card/card.tsx b/src/app/[lang]/dashboard/components/server/card.tsx similarity index 86% rename from src/app/[lang]/dashboard/components/server/card/card.tsx rename to src/app/[lang]/dashboard/components/server/card.tsx index 0bb192a..3df00e2 100644 --- a/src/app/[lang]/dashboard/components/server/card/card.tsx +++ b/src/app/[lang]/dashboard/components/server/card.tsx @@ -1,4 +1,4 @@ -import cardController from "@/app/[lang]/dashboard/components/server/card/card-controller"; +import cardController from "@/app/[lang]/dashboard/controller/card.controller"; export function Card({ title, diff --git a/src/app/[lang]/dashboard/components/server/cards/cards.tsx b/src/app/[lang]/dashboard/components/server/cards.tsx similarity index 83% rename from src/app/[lang]/dashboard/components/server/cards/cards.tsx rename to src/app/[lang]/dashboard/components/server/cards.tsx index 91c641d..d506061 100644 --- a/src/app/[lang]/dashboard/components/server/cards/cards.tsx +++ b/src/app/[lang]/dashboard/components/server/cards.tsx @@ -1,5 +1,5 @@ -import { Card } from "@/app/[lang]/dashboard/components/server/card/card"; -import cardsController from "@/app/[lang]/dashboard/components/server/cards/cards-controller"; +import { Card } from "@/app/[lang]/dashboard/components/server/card"; +import cardsController from "@/app/[lang]/dashboard/controller/cards.controller"; export default async function CardWrapper() { const { customersNumber, invoicesNumber, invoicesSummary } = diff --git a/src/app/[lang]/dashboard/components/server/cards/cards-controller.ts b/src/app/[lang]/dashboard/components/server/cards/cards-controller.ts deleted file mode 100644 index 9d38e29..0000000 --- a/src/app/[lang]/dashboard/components/server/cards/cards-controller.ts +++ /dev/null @@ -1,7 +0,0 @@ -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(); -} diff --git a/src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices.tsx b/src/app/[lang]/dashboard/components/server/latest-invoices.tsx similarity index 94% rename from src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices.tsx rename to src/app/[lang]/dashboard/components/server/latest-invoices.tsx index e55a6e1..4d54d2b 100644 --- a/src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices.tsx +++ b/src/app/[lang]/dashboard/components/server/latest-invoices.tsx @@ -1,5 +1,5 @@ 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 latestInvoicesController from "@/app/[lang]/dashboard/controller/latest-invoices.controller"; import { ArrowPathIcon } from "@heroicons/react/24/outline"; import clsx from "clsx"; import { isLeft } from "fp-ts/lib/Either"; diff --git a/src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller.ts b/src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller.ts deleted file mode 100644 index 2b7df56..0000000 --- a/src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller.ts +++ /dev/null @@ -1,7 +0,0 @@ -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(); -} diff --git a/src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart.tsx b/src/app/[lang]/dashboard/components/server/revenue-chart.tsx similarity index 93% rename from src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart.tsx rename to src/app/[lang]/dashboard/components/server/revenue-chart.tsx index e6bc5a0..6e6381b 100644 --- a/src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart.tsx +++ b/src/app/[lang]/dashboard/components/server/revenue-chart.tsx @@ -1,4 +1,4 @@ -import revenueChartController from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller"; +import revenueChartController from "@/app/[lang]/dashboard/controller/revenue-chart.controller"; import { CalendarIcon } from "@heroicons/react/24/outline"; export default async function RevenueChart() { diff --git a/src/app/[lang]/dashboard/components/server/skeletons/skeletons.tsx b/src/app/[lang]/dashboard/components/server/skeletons.tsx similarity index 100% rename from src/app/[lang]/dashboard/components/server/skeletons/skeletons.tsx rename to src/app/[lang]/dashboard/components/server/skeletons.tsx diff --git a/src/app/[lang]/dashboard/components/server/card/card-controller.ts b/src/app/[lang]/dashboard/controller/card.controller.ts similarity index 59% rename from src/app/[lang]/dashboard/components/server/card/card-controller.ts rename to src/app/[lang]/dashboard/controller/card.controller.ts index 5f28e88..5db4b72 100644 --- a/src/app/[lang]/dashboard/components/server/card/card-controller.ts +++ b/src/app/[lang]/dashboard/controller/card.controller.ts @@ -5,6 +5,12 @@ 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 cardController(props: { type: "invoices" | "customers" | "pending" | "collected"; }) { diff --git a/src/app/[lang]/dashboard/controller/cards.controller.ts b/src/app/[lang]/dashboard/controller/cards.controller.ts new file mode 100644 index 0000000..93de870 --- /dev/null +++ b/src/app/[lang]/dashboard/controller/cards.controller.ts @@ -0,0 +1,13 @@ +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 cardsController() { + connection(); + return fetchSummaryInfoUsecase(); +} diff --git a/src/app/[lang]/dashboard/controller/create-invoice.controller.ts b/src/app/[lang]/dashboard/controller/create-invoice.controller.ts new file mode 100644 index 0000000..e07023f --- /dev/null +++ b/src/app/[lang]/dashboard/controller/create-invoice.controller.ts @@ -0,0 +1,27 @@ +"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> { + connection(); + const usecase = serverDi(invoiceModuleKey).resolve( + createInvoiceUsecaseKey, + ); + return usecase(params); +} diff --git a/src/app/[lang]/dashboard/controller/latest-invoices.controller.ts b/src/app/[lang]/dashboard/controller/latest-invoices.controller.ts new file mode 100644 index 0000000..aece969 --- /dev/null +++ b/src/app/[lang]/dashboard/controller/latest-invoices.controller.ts @@ -0,0 +1,13 @@ +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(); +} diff --git a/src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller.ts b/src/app/[lang]/dashboard/controller/revenue-chart.controller.ts similarity index 71% rename from src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller.ts rename to src/app/[lang]/dashboard/controller/revenue-chart.controller.ts index 55c0797..1986288 100644 --- a/src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller.ts +++ b/src/app/[lang]/dashboard/controller/revenue-chart.controller.ts @@ -1,6 +1,12 @@ -import Revenue from "@/feature/core/revenue/domain/entity/revenue"; -import fetchRevenuesUsecase from "@/feature/core/revenue/domain/usecase/fetch-revenues-usecase"; +import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity"; +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; diff --git a/src/app/[lang]/dashboard/layout.tsx b/src/app/[lang]/dashboard/layout.tsx index 308e2a7..41a0efc 100644 --- a/src/app/[lang]/dashboard/layout.tsx +++ b/src/app/[lang]/dashboard/layout.tsx @@ -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"; diff --git a/src/app/[lang]/dashboard/loading.tsx b/src/app/[lang]/dashboard/loading.tsx index 0002d3a..d8aec02 100644 --- a/src/app/[lang]/dashboard/loading.tsx +++ b/src/app/[lang]/dashboard/loading.tsx @@ -1,4 +1,4 @@ -import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeletons/skeletons"; +import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeletons"; export default function Loading() { return ; diff --git a/src/app/[lang]/dashboard/module/dashboard-app-module.ts b/src/app/[lang]/dashboard/module/dashboard.app-module.ts similarity index 52% rename from src/app/[lang]/dashboard/module/dashboard-app-module.ts rename to src/app/[lang]/dashboard/module/dashboard.app-module.ts index ffd92a1..b3ec20f 100644 --- a/src/app/[lang]/dashboard/module/dashboard-app-module.ts +++ b/src/app/[lang]/dashboard/module/dashboard.app-module.ts @@ -1,14 +1,12 @@ 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/create-invoice-impl.usecase"; -import { createInvoiceUsecaseKey } from "@/feature/core/invoice/domain/usecase/create-invoice/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(createInvoiceUsecaseKey, { - useValue: createInvoiceUsecase, - }); dashboardDi.register( CreateRandomInvoiceButtonVM, CreateRandomInvoiceButtonVM, diff --git a/src/app/[lang]/dashboard/page.tsx b/src/app/[lang]/dashboard/page.tsx index ea09ada..ee88963 100644 --- a/src/app/[lang]/dashboard/page.tsx +++ b/src/app/[lang]/dashboard/page.tsx @@ -1,13 +1,13 @@ import { LatestInvoicesSkeleton, RevenueChartSkeleton, -} 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"; +} from "@/app/[lang]/dashboard/components/server/skeletons"; +import CardWrapper from "@/app/[lang]/dashboard/components/server/cards"; +import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart"; import { Suspense } from "react"; import { getServerTranslation, LANGS } from "@/bootstrap/i18n/i18n"; import langKey from "@/bootstrap/i18n/dictionaries/lang-key"; +import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices"; export default async function Dashboard(props: { params: Promise<{ lang: LANGS }>; diff --git a/src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts b/src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts index ed42813..246508d 100644 --- a/src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts +++ b/src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts @@ -1,23 +1,26 @@ -import ButtonVm from "@/app/components/button/button-vm"; +import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller"; +import ButtonVm from "@/app/components/button/button.i-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 { - CreateInvoiceUsecase, - createInvoiceUsecaseKey, -} from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase"; +import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param"; +import { CreateInvoiceUsecase } from "@/feature/core/invoice/domain/usecase/create-invoice/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 { private createInvoice: CreateInvoiceUsecase; constructor() { super(); - this.createInvoice = this.di.resolve(createInvoiceUsecaseKey); + this.createInvoice = createInvoiceController; } useVM(): ButtonVm { diff --git a/src/app/[lang]/layout.tsx b/src/app/[lang]/layout.tsx index 7eeae97..2703929 100644 --- a/src/app/[lang]/layout.tsx +++ b/src/app/[lang]/layout.tsx @@ -1,5 +1,5 @@ import { ThemeProvider } from "@/app/[lang]/dashboard/components/client/theme-provider/theme-provider"; -import { initI18next, LANGS } from "@/bootstrap/i18n/i18n"; +import { getI18n, 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 initI18next({ lng: lang }); + await getI18n({ lng: lang }); return ( { return typeof fn === "function" && /^(class|function [A-Z])/.test(fn); diff --git a/src/bootstrap/helpers/lib/ui-utils.ts b/src/bootstrap/helpers/lib/ui-utils.ts index a5ef193..f8373a9 100644 --- a/src/bootstrap/helpers/lib/ui-utils.ts +++ b/src/bootstrap/helpers/lib/ui-utils.ts @@ -1,6 +1,10 @@ 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)); } diff --git a/src/bootstrap/helpers/view/base-view.tsx b/src/bootstrap/helpers/view/base-view.tsx index b0d4f57..914c9fc 100644 --- a/src/bootstrap/helpers/view/base-view.tsx +++ b/src/bootstrap/helpers/view/base-view.tsx @@ -66,6 +66,11 @@ export type BuildProps< children?: ReactNode; }; +/** + * 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, diff --git a/src/bootstrap/helpers/view/storybook-with-arg-vm.ts b/src/bootstrap/helpers/view/storybook-with-arg-vm.ts index 210e173..7a3387c 100644 --- a/src/bootstrap/helpers/view/storybook-with-arg-vm.ts +++ b/src/bootstrap/helpers/view/storybook-with-arg-vm.ts @@ -1,5 +1,12 @@ 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