Compare commits

..

No commits in common. "86d6b52a3989cef0c73404f2a7552db02606485f" and "5a29c54a6ac9a014c814a5663503332cb2247308" have entirely different histories.

10 changed files with 99 additions and 93 deletions

View File

@ -1,14 +1,13 @@
import React from "react"; import React, { useRef } from "react";
import { themes } from '@storybook/theming'; import { themes } from '@storybook/theming';
import { ThemeProvider } from "../src/app/[lang]/dashboard/components/client/theme-provider/theme-provider"; import { ThemeProvider } from "../src/app/[lang]/dashboard/components/client/theme-provider/theme-provider";
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode'; import { DARK_MODE_EVENT_NAME, UPDATE_DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
import { getI18n, LANGS } from "../src/bootstrap/i18n/i18n" import { initI18next, LANGS } from "../src/bootstrap/i18n/i18n"
import { addons } from '@storybook/preview-api'; import { addons } from '@storybook/preview-api';
import { i18n } from "i18next"; import { i18n } from "i18next";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
const channel = addons.getChannel(); const channel = addons.getChannel();
import "../src/app/globals.css" import "../src/app/globals.css"
/** /**
* *
* This function will expand the object with nested properties * This function will expand the object with nested properties
@ -70,7 +69,7 @@ const preview = {
React.useEffect(() => { React.useEffect(() => {
(async () => { (async () => {
setI18n((await getI18n({ lng: locale })).i18n); setI18n((await initI18next({ lng: locale })).i18n);
})() })()
}, []) }, [])

View File

@ -0,0 +1,16 @@
"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";
/**
* From a parent component Vm and view will be connected together.
*/
export default function CreateRandomInvoiceContainer() {
const di = useDI();
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));
return <Button vm={vm.current} />;
}

View File

@ -1,4 +1,6 @@
import { DocumentIcon } from "@/app/components/icons/document";
import HomeIcon from "@/app/components/icons/home"; import HomeIcon from "@/app/components/icons/home";
import { UserIcon } from "@/app/components/icons/user";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useRef } from "react"; import { useRef } from "react";
@ -21,9 +23,15 @@ export default function navLinkPersonalVM() {
// Depending on the size of the application, this would be stored in a database. // Depending on the size of the application, this would be stored in a database.
const links = useRef<LinkItem[]>([ const links = useRef<LinkItem[]>([
{ name: "Home", href: "/dashboard", icon: HomeIcon }, { name: "Home", href: "/dashboard", icon: HomeIcon },
{
name: "Invoices",
href: "/dashboard/invoices",
icon: DocumentIcon,
},
{ name: "Customers", href: "/dashboard/customers", icon: UserIcon },
]).current; ]).current;
return { return {
links, links,
isLinkActive: (link: LinkItem) => pathname.includes(link.href), isLinkActive: (link: LinkItem) => pathname === link.href,
}; };
} }

View File

@ -1,6 +1,5 @@
import CreateRandomInvoiceContainer from "@/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice";
import latestInvoicesController from "@/app/[lang]/dashboard/controller/latest-invoices.controller"; 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 { ArrowPathIcon } from "@heroicons/react/24/outline"; import { ArrowPathIcon } from "@heroicons/react/24/outline";
import clsx from "clsx"; import clsx from "clsx";
import { isLeft } from "fp-ts/lib/Either"; import { isLeft } from "fp-ts/lib/Either";
@ -40,7 +39,6 @@ export default async function LatestInvoices() {
</p> </p>
</div> </div>
)); ));
return ( return (
<div className="flex w-full flex-col md:col-span-4"> <div className="flex w-full flex-col md:col-span-4">
<h2 className="mb-4 text-xl md:text-2xl">Latest Invoices</h2> <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" /> <ArrowPathIcon className="h-5 w-5 text-gray-500" />
<h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3> <h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>
</div> </div>
<Button vmKey={CreateRandomInvoiceButtonVM} /> <CreateRandomInvoiceContainer />
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,3 @@
import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller";
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm"; import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
import di from "@/bootstrap/di/init-di"; import di from "@/bootstrap/di/init-di";
@ -9,12 +8,8 @@ export default function dashboardAppModule() {
const dashboardDi = di.createChildContainer(); const dashboardDi = di.createChildContainer();
dashboardDi.register( dashboardDi.register(
CreateRandomInvoiceButtonVM.name, CreateRandomInvoiceButtonVM,
CreateRandomInvoiceButtonVM, CreateRandomInvoiceButtonVM,
); );
dashboardDi.register(createInvoiceController.name, {
useValue: createInvoiceController,
});
return dashboardDi; return dashboardDi;
} }

View File

@ -2,13 +2,27 @@ import {
LatestInvoicesSkeleton, LatestInvoicesSkeleton,
RevenueChartSkeleton, RevenueChartSkeleton,
} from "@/app/[lang]/dashboard/components/server/skeletons"; } 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 RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart";
import { Suspense } from "react"; 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"; import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices";
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 ( return (
<main> <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"> <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
<Suspense fallback={<RevenueChartSkeleton />}> <Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart /> <RevenueChart />

View File

@ -1,5 +1,3 @@
"use client";
import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller"; 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.i-vm";
import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action"; import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action";
@ -7,6 +5,7 @@ import useThrottle from "@/bootstrap/helpers/hooks/use-throttle";
import BaseVM from "@/bootstrap/helpers/vm/base-vm"; import BaseVM from "@/bootstrap/helpers/vm/base-vm";
import langKey from "@/bootstrap/i18n/dictionaries/lang-key"; 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/create-invoice.usecase";
import { faker } from "@faker-js/faker"; import { faker } from "@faker-js/faker";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -17,11 +16,11 @@ import { useTranslation } from "react-i18next";
* in this layer. * in this layer.
*/ */
export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> { export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> {
private createInvoice: typeof createInvoiceController; private createInvoice: CreateInvoiceUsecase;
constructor() { constructor() {
super(); super();
this.createInvoice = this.di.resolve(createInvoiceController.name); this.createInvoice = createInvoiceController;
} }
useVM(): ButtonVm { useVM(): ButtonVm {

View File

@ -1,13 +1,4 @@
import langKey from "@/bootstrap/i18n/dictionaries/lang-key"; export default function Home() {
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);
return ( return (
<main className="flex min-h-screen flex-col p-6"> <main className="flex min-h-screen flex-col p-6">
<div className="mt-4 flex grow flex-col gap-4 md:flex-row"> <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 , <strong>Welcome to Acme.</strong> This is the example for the ,
brought to you by Vercel. brought to you by Vercel.
</p> </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>
</div> </div>
</main> </main>

View File

@ -1,7 +1,12 @@
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
import Button from "@/app/components/button/button"; 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 Story from "@/bootstrap/helpers/view/storybook-base-template-type";
import getArgVM from "@/bootstrap/helpers/view/storybook-with-arg-vm"; import getArgVM from "@/bootstrap/helpers/view/storybook-with-arg-vm";
import { createInvoiceUsecaseKey } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
import type { Meta } from "@storybook/react"; import type { Meta } from "@storybook/react";
import { useRef } from "react";
const meta: Meta = { const meta: Meta = {
title: "general/Button", title: "general/Button",
@ -27,3 +32,36 @@ export const Primary: Story = {
return <Button vm={vm} memoizedByVM={false} />; return <Button vm={vm} memoizedByVM={false} />;
}, },
}; };
export const WithVM: Story = {
decorators: [
(Story) => {
const di = useRef(
mockedModuleDi([
{
token: CreateRandomInvoiceButtonVM,
provider: CreateRandomInvoiceButtonVM,
},
{
token: createInvoiceUsecaseKey,
// eslint-disable-next-line @typescript-eslint/no-explicit-any, no-console
provider: (args: any) => console.log("clicked", args),
},
]),
);
return (
<DiContext.Provider value={di.current}>
<Story di={di.current} />
</DiContext.Provider>
);
},
],
render: () => {
function Child() {
const di = useDI();
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));
return <Button vm={vm.current} memoizedByVM={false} />;
}
return <Child />;
},
};

View File

@ -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 react/display-name */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/jsx-props-no-spreading */ /* 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 IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
import { Component, ReactNode, FC, PropsWithChildren, memo } from "react"; import { Component, ReactNode, FC, PropsWithChildren, memo } from "react";
import { InjectionToken } from "tsyringe";
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Connector Component */ /* Connector Component */
@ -26,6 +23,7 @@ interface IVvmConnector<IVM, PROPS> extends PropsWithChildren {
const VvmConnector = memo( const VvmConnector = memo(
<IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => { <IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => {
const { View, Vm, restProps, children } = props; const { View, Vm, restProps, children } = props;
const vm = Vm.useVM(); const vm = Vm.useVM();
const allProps = { const allProps = {
@ -47,7 +45,8 @@ const VvmConnector = memo(
type IVMParent = Record<string, any>; type IVMParent = Record<string, any>;
type IPropParent = Record<string, any> | undefined; 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; restProps?: PROPS;
/** /**
* By default it's true. * By default it's true.
@ -58,24 +57,6 @@ type BaseProps<PROPS extends IPropParent = undefined> = {
children?: ReactNode; 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< export type BuildProps<
IVM extends IVMParent, IVM extends IVMParent,
PROPS extends IPropParent = undefined, PROPS extends IPropParent = undefined,
@ -85,11 +66,6 @@ export type BuildProps<
children?: ReactNode; 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 * 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 * vm as props and connect it to the view and memoize the component by default
@ -98,23 +74,7 @@ export type ViewProps<
export default abstract class BaseView< export default abstract class BaseView<
IVM extends IVMParent, IVM extends IVMParent,
PROPS extends IPropParent = undefined, PROPS extends IPropParent = undefined,
> extends Component<ViewProps<IVM, PROPS>> { > extends Component<BaseProps<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;
}
protected get componentName() { protected get componentName() {
return this.constructor.name; return this.constructor.name;
} }
@ -122,16 +82,9 @@ export default abstract class BaseView<
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode; protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
render(): ReactNode { render(): ReactNode {
const { restProps, memoizedByVM, children, ...rest } = this.props; const { vm, restProps, memoizedByVM, children, ...rest } = this.props;
VvmConnector.displayName = this.componentName; 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 ( return (
<VvmConnector <VvmConnector
@ -144,4 +97,5 @@ export default abstract class BaseView<
</VvmConnector> </VvmConnector>
); );
} }
/* -------------------------------------------------------------------------- */
} }