develop #3

Merged
behnam merged 39 commits from develop into main 2024-11-26 15:47:00 +00:00
93 changed files with 1268 additions and 958 deletions
Showing only changes of commit 55c8298237 - Show all commits

View File

@ -9,5 +9,87 @@
}
}
],
"extends": ["next/core-web-vitals", "next/typescript"]
"plugins": [
"prettier"
],
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"alias": {
"map": [
[
"~",
"./src"
]
],
"extensions": [
".js",
".ts",
".tsx",
".d.ts",
".test.ts",
".json"
]
}
}
},
"rules": {
"no-use-before-define": "off",
"class-methods-use-this": "off",
"import/prefer-default-export": "off",
"import/no-cycle": "off",
"no-promise-executor-return": "off",
"@typescript-eslint/no-shadow": "off",
"react/require-default-props": "off",
"no-shadow": "off",
"prettier/prettier": [
"warn",
{
"printWidth": 80,
"tabWidth": 2,
"endOfLine":"auto",
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always"
}
],
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
],
"react/jsx-filename-extension": [
1,
{
"extensions": [
".ts",
".tsx"
]
}
],
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true
}
]
},
"extends": [
"airbnb",
"next/core-web-vitals",
"next/typescript",
"prettier"
]
}

View File

@ -6,7 +6,7 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start --port 4000",
"lint": "next lint",
"lint": "next lint --fix",
"test": "vitest",
"seed": "node -r dotenv/config ./src/bootstrap/boundaries/db/seed.js"
},
@ -46,10 +46,16 @@
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"eslint": "^8",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-next": "15.0.1",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-prettier": "^5.2.1",
"jsdom": "^25.0.1",
"moq.ts": "^10.0.8",
"postcss": "^8",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.1",
"typescript": "^5",
"vitest": "^2.1.4"

View File

@ -1,13 +1,13 @@
"use client"
"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"
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))
const di = useDI();
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));
return <Button vm={vm.current}/>
return <Button vm={vm.current} />;
}

View File

@ -1,29 +0,0 @@
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
}
}

View File

@ -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,
};
}

View File

@ -1,10 +1,11 @@
'use client'
import navLinkPersonalVM from '@/app/[lang]/dashboard/components/client/nav-links/nav-link-controller';
import clsx from 'clsx';
import Link from 'next/link'
"use client";
import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link-vm";
import clsx from "clsx";
import Link from "next/link";
export default function NavLinks() {
const { links, isLinkActive } = navLinkPersonalVM()
const { links, isLinkActive } = navLinkPersonalVM();
return (
<>
{links.map((link) => {
@ -14,9 +15,9 @@ export default function NavLinks() {
key={link.name}
href={link.href}
className={clsx(
'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
"flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3",
{
'bg-sky-100 text-blue-600': isLinkActive(link),
"bg-sky-100 text-blue-600": isLinkActive(link),
},
)}
>

View File

@ -3,17 +3,19 @@ import {
ClockIcon,
UserGroupIcon,
InboxIcon,
} from '@heroicons/react/24/outline';
} from "@heroicons/react/24/outline";
export default function cardController(props: { type: 'invoices' | 'customers' | 'pending' | 'collected'; }) {
const { type } = props
const iconMap = {
collected: BanknotesIcon,
customers: UserGroupIcon,
pending: ClockIcon,
invoices: InboxIcon,
};
return {
Icon: iconMap[type]
}
export default function cardController(props: {
type: "invoices" | "customers" | "pending" | "collected";
}) {
const { type } = props;
const iconMap = {
collected: BanknotesIcon,
customers: UserGroupIcon,
pending: ClockIcon,
invoices: InboxIcon,
};
return {
Icon: iconMap[type],
};
}

View File

@ -1,7 +1,5 @@
import cardController from "@/app/[lang]/dashboard/components/server/card/card-controller";
export function Card({
title,
value,
@ -9,9 +7,9 @@ export function Card({
}: {
title: string;
value: number | string;
type: 'invoices' | 'customers' | 'pending' | 'collected';
type: "invoices" | "customers" | "pending" | "collected";
}) {
const { Icon } = cardController({type})
const { Icon } = cardController({ type });
return (
<div className="rounded-xl bg-gray-50 p-2 shadow-sm">
@ -19,9 +17,7 @@ export function Card({
{Icon ? <Icon className="h-5 w-5 text-gray-700" /> : null}
<h3 className="ml-2 text-sm font-medium">{title}</h3>
</div>
<p
className="rounded-xl bg-white px-4 py-8 text-center text-2xl"
>
<p className="rounded-xl bg-white px-4 py-8 text-center text-2xl">
{value}
</p>
</div>

View File

@ -1,5 +1,5 @@
import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info-usecase";
export default function cardsController() {
return fetchSummaryInfoUsecase();
return fetchSummaryInfoUsecase();
}

View File

@ -1,20 +1,16 @@
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/card";
import cardsController from "@/app/[lang]/dashboard/components/server/cards/cards-controller";
export default async function CardWrapper() {
const {customersNumber, invoicesNumber, invoicesSummary } = await cardsController();
const { customersNumber, invoicesNumber, invoicesSummary } =
await cardsController();
return (
<>
<Card title="Collected" value={invoicesSummary.paid} type="collected" />
<Card title="Pending" value={invoicesSummary.pending} type="pending" />
<Card title="Total Invoices" value={invoicesNumber} type="invoices" />
<Card
title="Total Customers"
value={customersNumber}
type="customers"
/>
<Card title="Total Customers" value={customersNumber} type="customers" />
</>
);
}

View File

@ -1,5 +1,5 @@
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase";
export default async function latestInvoicesController() {
return await fetchCustomerInvoicesUsecase()
export default function latestInvoicesController() {
return fetchCustomerInvoicesUsecase();
}

View File

@ -1,61 +1,49 @@
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';
import Image from 'next/image';
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";
import Image from "next/image";
export default async function LatestInvoices() {
const latestInvoices = await latestInvoicesController();
if (isLeft(latestInvoices)) return <div>Error</div>
if (isLeft(latestInvoices)) return <div>Error</div>;
const invoices = latestInvoices.right.map((invoice, i) => {
return (
<div
key={invoice.id}
className={clsx(
'flex flex-row items-center justify-between py-4',
{
'border-t': i !== 0,
},
)}
>
<div className="flex items-center">
<Image
src={invoice.customerImageUrl}
alt={`${invoice.customerName}'s profile picture`}
className="mr-4 rounded-full"
width={32}
height={32}
/>
<div className="min-w-0">
<p className="truncate text-sm font-semibold md:text-base">
{invoice.customerName}
</p>
<p className="hidden text-sm text-gray-500 sm:block">
{invoice.customerEmail}
</p>
</div>
</div>
<p
className="truncate text-sm font-medium md:text-base"
>
{invoice.invoicesAmount}
</p>
</div>
);
})
const invoices = latestInvoices.right.map((invoice, i) => (
<div
key={invoice.id}
className={clsx("flex flex-row items-center justify-between py-4", {
"border-t": i !== 0,
})}
>
<div className="flex items-center">
<Image
src={invoice.customerImageUrl}
alt={`${invoice.customerName}'s profile picture`}
className="mr-4 rounded-full"
width={32}
height={32}
/>
<div className="min-w-0">
<p className="truncate text-sm font-semibold md:text-base">
{invoice.customerName}
</p>
<p className="hidden text-sm text-gray-500 sm:block">
{invoice.customerEmail}
</p>
</div>
</div>
<p className="truncate text-sm font-medium md:text-base">
{invoice.invoicesAmount}
</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>
<h2 className="mb-4 text-xl md:text-2xl">Latest Invoices</h2>
<div className="flex grow flex-col max-h-[66.5vh] justify-between rounded-xl bg-gray-50 p-4">
<div className="bg-white px-6 h-full overflow-y-auto">
{invoices}
</div>
<div className="bg-white px-6 h-full overflow-y-auto">{invoices}</div>
<div className="flex items-end mt-auto pb-2 pt-6">
<ArrowPathIcon className="h-5 w-5 text-gray-500" />
<h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>

View File

@ -11,8 +11,8 @@ export default async function revenueChartController() {
revenue,
chartHeight,
yAxisLabels,
topLabel
}
topLabel,
};
}
function generateYAxis(revenue: Revenue[]) {
@ -27,4 +27,4 @@ function generateYAxis(revenue: Revenue[]) {
}
return { yAxisLabels, topLabel };
};
}

View File

@ -1,8 +1,9 @@
import revenueChartController from '@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller';
import { CalendarIcon } from '@heroicons/react/24/outline';
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() {
const { chartHeight, revenue, topLabel, yAxisLabels } = await revenueChartController()
const { chartHeight, revenue, topLabel, yAxisLabels } =
await revenueChartController();
if (!revenue || revenue.length === 0) {
return <p className="mt-4 text-gray-400">No data available.</p>;
@ -10,9 +11,7 @@ export default async function RevenueChart() {
return (
<div className="w-full md:col-span-4">
<h2 className={` mb-4 text-xl md:text-2xl`}>
Recent Revenue
</h2>
<h2 className={` mb-4 text-xl md:text-2xl`}>Recent Revenue</h2>
<div className="rounded-xl bg-gray-50 p-4">
<div className="sm:grid-cols-13 mt-0 grid grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4">
<div
@ -31,7 +30,7 @@ export default async function RevenueChart() {
style={{
height: `${(chartHeight / topLabel) * month.revenue}px`,
}}
></div>
/>
<p className="-rotate-90 text-sm text-gray-400 sm:rotate-0">
{month.month}
</p>

View File

@ -1,5 +1,5 @@
import NavLinks from '@/app/[lang]/dashboard/components/client/nav-links/nav-links';
import Link from 'next/link';
import NavLinks from "@/app/[lang]/dashboard/components/client/nav-links/nav-links";
import Link from "next/link";
export default function SideNav() {
return (
@ -8,13 +8,11 @@ export default function SideNav() {
className="mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40"
href="/"
>
<div className="w-32 text-white md:w-40">
Home
</div>
<div className="w-32 text-white md:w-40">Home</div>
</Link>
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
<NavLinks />
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block" />
</div>
</div>
);

View File

@ -1,6 +1,6 @@
// Loading animation
const shimmer =
'before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent';
"before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent";
export function CardSkeleton() {
return (
@ -106,33 +106,36 @@ export function TableRowSkeleton() {
return (
<tr className="w-full border-b border-gray-100 last-of-type:border-none [&:first-child>td:first-child]:rounded-tl-lg [&:first-child>td:last-child]:rounded-tr-lg [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg">
{/* Customer Name and Image */}
<td className="relative overflow-hidden whitespace-nowrap py-3 pl-6 pr-3">
<td
aria-label="Customer name"
className="relative overflow-hidden whitespace-nowrap py-3 pl-6 pr-3"
>
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-gray-100"></div>
<div className="h-6 w-24 rounded bg-gray-100"></div>
<div className="h-8 w-8 rounded-full bg-gray-100" />
<div className="h-6 w-24 rounded bg-gray-100" />
</div>
</td>
{/* Email */}
<td className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-32 rounded bg-gray-100"></div>
<td aria-label="Email" className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-32 rounded bg-gray-100" />
</td>
{/* Amount */}
<td className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100"></div>
<td aria-label="Amount" className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100" />
</td>
{/* Date */}
<td className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100"></div>
<td aria-label="Date" className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100" />
</td>
{/* Status */}
<td className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100"></div>
<td aria-label="Status" className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100" />
</td>
{/* Actions */}
<td className="whitespace-nowrap py-3 pl-6 pr-3">
<td aria-label="Actions" className="whitespace-nowrap py-3 pl-6 pr-3">
<div className="flex justify-end gap-3">
<div className="h-[38px] w-[38px] rounded bg-gray-100"></div>
<div className="h-[38px] w-[38px] rounded bg-gray-100"></div>
<div className="h-[38px] w-[38px] rounded bg-gray-100" />
<div className="h-[38px] w-[38px] rounded bg-gray-100" />
</div>
</td>
</tr>
@ -144,19 +147,19 @@ export function InvoicesMobileSkeleton() {
<div className="mb-2 w-full rounded-md bg-white p-4">
<div className="flex items-center justify-between border-b border-gray-100 pb-8">
<div className="flex items-center">
<div className="mr-2 h-8 w-8 rounded-full bg-gray-100"></div>
<div className="h-6 w-16 rounded bg-gray-100"></div>
<div className="mr-2 h-8 w-8 rounded-full bg-gray-100" />
<div className="h-6 w-16 rounded bg-gray-100" />
</div>
<div className="h-6 w-16 rounded bg-gray-100"></div>
<div className="h-6 w-16 rounded bg-gray-100" />
</div>
<div className="flex w-full items-center justify-between pt-4">
<div>
<div className="h-6 w-16 rounded bg-gray-100"></div>
<div className="mt-2 h-6 w-24 rounded bg-gray-100"></div>
<div className="h-6 w-16 rounded bg-gray-100" />
<div className="mt-2 h-6 w-24 rounded bg-gray-100" />
</div>
<div className="flex justify-end gap-2">
<div className="h-10 w-10 rounded bg-gray-100"></div>
<div className="h-10 w-10 rounded bg-gray-100"></div>
<div className="h-10 w-10 rounded bg-gray-100" />
<div className="h-10 w-10 rounded bg-gray-100" />
</div>
</div>
</div>

View File

@ -1,19 +1,21 @@
"use client"
"use client";
import SideNav from "@/app/[lang]/dashboard/components/server/sidenav";
import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard-app-module";
import { DiContext } from "@/bootstrap/di/di-context";
import { useRef } from "react";
export default function Layout({ children }: { children: React.ReactNode }) {
const di = useRef(dashboardAppModule())
const di = useRef(dashboardAppModule());
return (
<DiContext.Provider value={di.current}>
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
<div className="w-full flex-none md:w-64">
<SideNav />
</div>
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">
{children}
</div>
</div>
</DiContext.Provider>
);

View File

@ -1,13 +1,16 @@
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";
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
export default function dashboardAppModule() {
const dashboardDi = di.createChildContainer()
const dashboardDi = di.createChildContainer();
dashboardDi.register(createInvoiceUsecase.name, {
useValue: createInvoiceUsecase
})
dashboardDi.register(CreateRandomInvoiceButtonVM, CreateRandomInvoiceButtonVM)
return dashboardDi
dashboardDi.register(createInvoiceUsecase.name, {
useValue: createInvoiceUsecase,
});
dashboardDi.register(
CreateRandomInvoiceButtonVM,
CreateRandomInvoiceButtonVM,
);
return dashboardDi;
}

View File

@ -1,4 +1,7 @@
import { LatestInvoicesSkeleton, RevenueChartSkeleton } from "@/app/[lang]/dashboard/components/server/skeletons/skeletons";
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";
@ -6,12 +9,15 @@ import { Suspense } from "react";
import { getServerTranslation } from "@/bootstrap/i18n/i18n";
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
export default async function Dashboard(props: {params: Promise<{lang: string}>}) {
const {lang} = await props.params
const { t } = await getServerTranslation(lang)
export default async function Dashboard(props: {
params: Promise<{ lang: string }>;
}) {
const { params } = props;
const { lang } = await params;
const { t } = await getServerTranslation(lang);
return (
<main>
<h1 className={`mb-4 text-xl md:text-2xl`}>
<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">
@ -19,12 +25,12 @@ export default async function Dashboard(props: {params: Promise<{lang: string}>}
</div>
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
<RevenueChart />
</Suspense>
<Suspense fallback={<LatestInvoicesSkeleton />}>
<LatestInvoices />
</Suspense>
</div>
</main>
)
);
}

View File

@ -8,39 +8,46 @@ import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-i
import { faker } from "@faker-js/faker";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> {
private createInvoice: typeof createInvoiceUsecase
private createInvoice: typeof createInvoiceUsecase;
constructor() {
super()
this.createInvoice = this.di.resolve(createInvoiceUsecase.name)
}
constructor() {
super();
this.createInvoice = this.di.resolve(createInvoiceUsecase.name);
}
useVM(): ButtonVm {
const router = useRouter()
const [action, isPending] = useServerAction(() => this.onClickHandler(router.refresh))
const throttledOnClick = useThrottle(action, 5000)
useVM(): ButtonVm {
const router = useRouter();
const [action, isPending] = useServerAction(() =>
this.onClickHandler(router.refresh),
);
const throttledOnClick = useThrottle(action, 5000);
const {t} = useTranslation()
const { t } = useTranslation();
return {
props: {
title: t(isPending ? langKey.global.loading : langKey.dashboard.invoice.createButton),
isDisable: isPending ? true : false
},
onClick: throttledOnClick.bind(this)
}
}
return {
props: {
title: t(
isPending
? langKey.global.loading
: langKey.dashboard.invoice.createButton,
),
isDisable: !!isPending,
},
onClick: throttledOnClick.bind(this),
};
}
async onClickHandler(refreshPage: () => void) {
const fakedParams: InvoiceParam = {
amount: faker.number.int({
min: 1,
max: 10
}),
status: "paid"
}
await this.createInvoice(fakedParams)
refreshPage()
}
async onClickHandler(refreshPage: () => void) {
const fakedParams: InvoiceParam = {
amount: faker.number.int({
min: 1,
max: 10,
}),
status: "paid",
};
await this.createInvoice(fakedParams);
refreshPage();
}
}

View File

@ -2,8 +2,14 @@ import { initI18next } from "@/bootstrap/i18n/i18n";
import TranslationsProvider from "@/bootstrap/i18n/i18n-provider";
import { PropsWithChildren } from "react";
export default async function layout(props: PropsWithChildren & {params: Promise<{lang: string}>}) {
const lang = (await props.params).lang
const { resources} = await initI18next({lng: lang})
return <TranslationsProvider lng={lang} resources={resources}>{props.children}</TranslationsProvider>
export default async function layout(
props: PropsWithChildren & { params: Promise<{ lang: string }> },
) {
const { lang } = await props.params;
const { resources } = await initI18next({ lng: lang });
return (
<TranslationsProvider lng={lang} resources={resources}>
{props.children}
</TranslationsProvider>
);
}

View File

@ -1,14 +1,12 @@
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">
<div className="flex flex-col justify-center gap-6 rounded-lg bg-gray-50 px-6 py-10 md:w-2/5 md:px-20">
<div />
<p className={`text-xl text-gray-800 md:text-3xl md:leading-normal`}>
<strong>Welcome to Acme.</strong> This is the example for the{' '}
, brought to you by Vercel.
<div />
<p className="text-xl text-gray-800 md:text-3xl md:leading-normal">
<strong>Welcome to Acme.</strong> This is the example for the ,
brought to you by Vercel.
</p>
</div>
</div>

View File

@ -1,7 +1,7 @@
export default interface ButtonVm {
props: {
title: string;
isDisable: boolean;
}
onClick(): void
props: {
title: string;
isDisable: boolean;
};
onClick(): void;
}

View File

@ -1,21 +1,25 @@
"use client"
"use client";
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
import ButtonVm from "@/app/components/button/button-vm";
import { ReactNode } from "react";
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/bootstrap/helpers/lib/ui-utils";
export default class Button extends BaseView<ButtonVm> {
protected Build(props: BuildProps<ButtonVm>): ReactNode {
const {vm} = props
protected Build(props: BuildProps<ButtonVm>): ReactNode {
const { vm } = props;
return <ButtonUi disabled={vm.props.isDisable} onClick={vm.onClick} >{vm.props.title}</ButtonUi>
}
return (
<ButtonUi disabled={vm.props.isDisable} onClick={vm.onClick}>
{vm.props.title}
</ButtonUi>
);
}
}
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
@ -43,27 +47,28 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const ButtonUi = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
)
}
)
ButtonUi.displayName = "Button"
);
},
);
ButtonUi.displayName = "Button";
export { buttonVariants }
export { buttonVariants };

View File

@ -1,5 +1,20 @@
export function DocumentIcon(props: {className?: string}) {
return (
<svg className={props.className} width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
)
export function DocumentIcon(props: { className?: string }) {
const { className } = props;
return (
<svg
className={className}
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
);
}

View File

@ -1,5 +1,20 @@
export default function HomeIcon(props: {className?: string}) {
return (
<svg className={props.className} width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.07926 0.222253C7.31275 -0.007434 7.6873 -0.007434 7.92079 0.222253L14.6708 6.86227C14.907 7.09465 14.9101 7.47453 14.6778 7.71076C14.4454 7.947 14.0655 7.95012 13.8293 7.71773L13 6.90201V12.5C13 12.7761 12.7762 13 12.5 13H2.50002C2.22388 13 2.00002 12.7761 2.00002 12.5V6.90201L1.17079 7.71773C0.934558 7.95012 0.554672 7.947 0.32229 7.71076C0.0899079 7.47453 0.0930283 7.09465 0.32926 6.86227L7.07926 0.222253ZM7.50002 1.49163L12 5.91831V12H10V8.49999C10 8.22385 9.77617 7.99999 9.50002 7.99999H6.50002C6.22388 7.99999 6.00002 8.22385 6.00002 8.49999V12H3.00002V5.91831L7.50002 1.49163ZM7.00002 12H9.00002V8.99999H7.00002V12Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
)
export default function HomeIcon(props: { className?: string }) {
const { className } = props;
return (
<svg
className={className}
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.07926 0.222253C7.31275 -0.007434 7.6873 -0.007434 7.92079 0.222253L14.6708 6.86227C14.907 7.09465 14.9101 7.47453 14.6778 7.71076C14.4454 7.947 14.0655 7.95012 13.8293 7.71773L13 6.90201V12.5C13 12.7761 12.7762 13 12.5 13H2.50002C2.22388 13 2.00002 12.7761 2.00002 12.5V6.90201L1.17079 7.71773C0.934558 7.95012 0.554672 7.947 0.32229 7.71076C0.0899079 7.47453 0.0930283 7.09465 0.32926 6.86227L7.07926 0.222253ZM7.50002 1.49163L12 5.91831V12H10V8.49999C10 8.22385 9.77617 7.99999 9.50002 7.99999H6.50002C6.22388 7.99999 6.00002 8.22385 6.00002 8.49999V12H3.00002V5.91831L7.50002 1.49163ZM7.00002 12H9.00002V8.99999H7.00002V12Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
);
}

View File

@ -1,7 +1,19 @@
export function UserIcon(props: {className?: string}) {
return (
<svg className={props.className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
)
export function UserIcon(props: { className?: string }) {
const { className } = props;
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
/>
</svg>
);
}

View File

@ -1,6 +1,7 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
@ -23,7 +24,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>

View File

@ -1,13 +1,12 @@
import postgres from "postgres";
const envs = process.env;
const dbConfigs = {
host: envs.POSTGRES_HOST,
port: Number(envs.POSTGRES_PORT),
username: envs.POSTGRES_USER,
password: envs.POSTGRES_PASS,
database: envs.POSTGRES_DB,
}
host: envs.POSTGRES_HOST,
port: Number(envs.POSTGRES_PORT),
username: envs.POSTGRES_USER,
password: envs.POSTGRES_PASS,
database: envs.POSTGRES_DB,
};
export const sql = postgres(dbConfigs);

View File

@ -2,73 +2,73 @@
// https://nextjs.org/learn/dashboard-app/fetching-data
const users = [
{
id: '410544b2-4001-4271-9855-fec4b6a6442a',
name: 'User',
email: 'user@nextmail.com',
password: '123456',
id: "410544b2-4001-4271-9855-fec4b6a6442a",
name: "User",
email: "user@nextmail.com",
password: "123456",
},
];
const customers = [
{
id: '3958dc9e-712f-4377-85e9-fec4b6a6442a',
name: 'Delba de Oliveira',
email: 'delba@oliveira.com',
image_url: '/customers/delba-de-oliveira.png',
id: "3958dc9e-712f-4377-85e9-fec4b6a6442a",
name: "Delba de Oliveira",
email: "delba@oliveira.com",
image_url: "/customers/delba-de-oliveira.png",
},
{
id: '3958dc9e-742f-4377-85e9-fec4b6a6442a',
name: 'Lee Robinson',
email: 'lee@robinson.com',
image_url: '/customers/lee-robinson.png',
id: "3958dc9e-742f-4377-85e9-fec4b6a6442a",
name: "Lee Robinson",
email: "lee@robinson.com",
image_url: "/customers/lee-robinson.png",
},
{
id: '3958dc9e-737f-4377-85e9-fec4b6a6442a',
name: 'Hector Simpson',
email: 'hector@simpson.com',
image_url: '/customers/hector-simpson.png',
id: "3958dc9e-737f-4377-85e9-fec4b6a6442a",
name: "Hector Simpson",
email: "hector@simpson.com",
image_url: "/customers/hector-simpson.png",
},
{
id: '50ca3e18-62cd-11ee-8c99-0242ac120002',
name: 'Steven Tey',
email: 'steven@tey.com',
image_url: '/customers/steven-tey.png',
id: "50ca3e18-62cd-11ee-8c99-0242ac120002",
name: "Steven Tey",
email: "steven@tey.com",
image_url: "/customers/steven-tey.png",
},
{
id: '3958dc9e-787f-4377-85e9-fec4b6a6442a',
name: 'Steph Dietz',
email: 'steph@dietz.com',
image_url: '/customers/steph-dietz.png',
id: "3958dc9e-787f-4377-85e9-fec4b6a6442a",
name: "Steph Dietz",
email: "steph@dietz.com",
image_url: "/customers/steph-dietz.png",
},
{
id: '76d65c26-f784-44a2-ac19-586678f7c2f2',
name: 'Michael Novotny',
email: 'michael@novotny.com',
image_url: '/customers/michael-novotny.png',
id: "76d65c26-f784-44a2-ac19-586678f7c2f2",
name: "Michael Novotny",
email: "michael@novotny.com",
image_url: "/customers/michael-novotny.png",
},
{
id: 'd6e15727-9fe1-4961-8c5b-ea44a9bd81aa',
name: 'Evil Rabbit',
email: 'evil@rabbit.com',
image_url: '/customers/evil-rabbit.png',
id: "d6e15727-9fe1-4961-8c5b-ea44a9bd81aa",
name: "Evil Rabbit",
email: "evil@rabbit.com",
image_url: "/customers/evil-rabbit.png",
},
{
id: '126eed9c-c90c-4ef6-a4a8-fcf7408d3c66',
name: 'Emil Kowalski',
email: 'emil@kowalski.com',
image_url: '/customers/emil-kowalski.png',
id: "126eed9c-c90c-4ef6-a4a8-fcf7408d3c66",
name: "Emil Kowalski",
email: "emil@kowalski.com",
image_url: "/customers/emil-kowalski.png",
},
{
id: 'CC27C14A-0ACF-4F4A-A6C9-D45682C144B9',
name: 'Amy Burns',
email: 'amy@burns.com',
image_url: '/customers/amy-burns.png',
id: "CC27C14A-0ACF-4F4A-A6C9-D45682C144B9",
name: "Amy Burns",
email: "amy@burns.com",
image_url: "/customers/amy-burns.png",
},
{
id: '13D07535-C59E-4157-A011-F8D2EF4E0CBB',
name: 'Balazs Orban',
email: 'balazs@orban.com',
image_url: '/customers/balazs-orban.png',
id: "13D07535-C59E-4157-A011-F8D2EF4E0CBB",
name: "Balazs Orban",
email: "balazs@orban.com",
image_url: "/customers/balazs-orban.png",
},
];
@ -76,108 +76,108 @@ const invoices = [
{
customer_id: customers[0].id,
amount: 15795,
status: 'pending',
date: '2022-12-06',
status: "pending",
date: "2022-12-06",
},
{
customer_id: customers[1].id,
amount: 20348,
status: 'pending',
date: '2022-11-14',
status: "pending",
date: "2022-11-14",
},
{
customer_id: customers[4].id,
amount: 3040,
status: 'paid',
date: '2022-10-29',
status: "paid",
date: "2022-10-29",
},
{
customer_id: customers[3].id,
amount: 44800,
status: 'paid',
date: '2023-09-10',
status: "paid",
date: "2023-09-10",
},
{
customer_id: customers[5].id,
amount: 34577,
status: 'pending',
date: '2023-08-05',
status: "pending",
date: "2023-08-05",
},
{
customer_id: customers[7].id,
amount: 54246,
status: 'pending',
date: '2023-07-16',
status: "pending",
date: "2023-07-16",
},
{
customer_id: customers[6].id,
amount: 666,
status: 'pending',
date: '2023-06-27',
status: "pending",
date: "2023-06-27",
},
{
customer_id: customers[3].id,
amount: 32545,
status: 'paid',
date: '2023-06-09',
status: "paid",
date: "2023-06-09",
},
{
customer_id: customers[4].id,
amount: 1250,
status: 'paid',
date: '2023-06-17',
status: "paid",
date: "2023-06-17",
},
{
customer_id: customers[5].id,
amount: 8546,
status: 'paid',
date: '2023-06-07',
status: "paid",
date: "2023-06-07",
},
{
customer_id: customers[1].id,
amount: 500,
status: 'paid',
date: '2023-08-19',
status: "paid",
date: "2023-08-19",
},
{
customer_id: customers[5].id,
amount: 8945,
status: 'paid',
date: '2023-06-03',
status: "paid",
date: "2023-06-03",
},
{
customer_id: customers[2].id,
amount: 8945,
status: 'paid',
date: '2023-06-18',
status: "paid",
date: "2023-06-18",
},
{
customer_id: customers[0].id,
amount: 8945,
status: 'paid',
date: '2023-10-04',
status: "paid",
date: "2023-10-04",
},
{
customer_id: customers[2].id,
amount: 1000,
status: 'paid',
date: '2022-06-05',
status: "paid",
date: "2022-06-05",
},
];
const revenue = [
{ month: 'Jan', revenue: 2000 },
{ month: 'Feb', revenue: 1800 },
{ month: 'Mar', revenue: 2200 },
{ month: 'Apr', revenue: 2500 },
{ month: 'May', revenue: 2300 },
{ month: 'Jun', revenue: 3200 },
{ month: 'Jul', revenue: 3500 },
{ month: 'Aug', revenue: 3700 },
{ month: 'Sep', revenue: 2500 },
{ month: 'Oct', revenue: 2800 },
{ month: 'Nov', revenue: 3000 },
{ month: 'Dec', revenue: 4800 },
{ month: "Jan", revenue: 2000 },
{ month: "Feb", revenue: 1800 },
{ month: "Mar", revenue: 2200 },
{ month: "Apr", revenue: 2500 },
{ month: "May", revenue: 2300 },
{ month: "Jun", revenue: 3200 },
{ month: "Jul", revenue: 3500 },
{ month: "Aug", revenue: 3700 },
{ month: "Sep", revenue: 2500 },
{ month: "Oct", revenue: 2800 },
{ month: "Nov", revenue: 3000 },
{ month: "Dec", revenue: 4800 },
];
module.exports = {

View File

@ -1,12 +1,14 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-require-imports */
const bcrypt = require("bcrypt");
const postgres = require("postgres");
const {
invoices,
customers,
revenue,
users,
} = require('./placeholder-data.js');
const bcrypt = require('bcrypt');
const postgres = require('postgres');
// eslint-disable-next-line import/extensions
} = require("./placeholder-data.js");
async function seedUsers(sql) {
try {
@ -42,7 +44,7 @@ async function seedUsers(sql) {
users: insertedUsers,
};
} catch (error) {
console.error('Error seeding users:', error);
console.error("Error seeding users:", error);
throw error;
}
}
@ -82,7 +84,7 @@ async function seedInvoices(sql) {
invoices: insertedInvoices,
};
} catch (error) {
console.error('Error seeding invoices:', error);
console.error("Error seeding invoices:", error);
throw error;
}
}
@ -121,7 +123,7 @@ async function seedCustomers(sql) {
customers: insertedCustomers,
};
} catch (error) {
console.error('Error seeding customers:', error);
console.error("Error seeding customers:", error);
throw error;
}
}
@ -156,7 +158,7 @@ async function seedRevenue(sql) {
revenue: insertedRevenue,
};
} catch (error) {
console.error('Error seeding revenue:', error);
console.error("Error seeding revenue:", error);
throw error;
}
}
@ -169,7 +171,7 @@ async function main() {
username: envs.POSTGRES_USER,
password: envs.POSTGRES_PASS,
database: envs.POSTGRES_DB,
}
};
const sql = postgres(dbConfigs);
@ -177,12 +179,11 @@ async function main() {
await seedCustomers(sql);
await seedInvoices(sql);
await seedRevenue(sql);
}
main().catch((err) => {
console.error(
'An error occurred while attempting to seed the database:',
"An error occurred while attempting to seed the database:",
err,
);
});

View File

@ -1,21 +1,19 @@
"use client"
"use client";
import di from "@/bootstrap/di/init-di";
import { createContext, use } from "react";
import { DependencyContainer } from "tsyringe";
const DiContext = createContext<null | DependencyContainer>(di)
const DiContext = createContext<null | DependencyContainer>(di);
const useDI = () => {
const di = use(DiContext)
const di = use(DiContext);
if (!di) {
throw new Error("Di has not provided")
}
if (!di) {
throw new Error("Di has not provided");
}
return di
}
return di;
};
export {
DiContext,
useDI,
}
export { DiContext, useDI };

View File

@ -1,5 +1,5 @@
// "use client"
import "reflect-metadata"
import "reflect-metadata";
import { container, DependencyContainer } from "tsyringe";
/**

View File

@ -1,2 +1 @@
export const isServer = typeof window === 'undefined'
export const isServer = typeof window === "undefined";

View File

@ -1,11 +1,11 @@
import { useState, useEffect, useTransition, useRef } from 'react';
import { useState, useEffect, useTransition, useRef } from "react";
/**
*
* @param action Main server action to run
* @param onFinished Callback to run after action
* @returns transitioned action to run and is pending variable
*/
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useServerAction = <P extends any[], R>(
action: (...args: P) => Promise<R>,

View File

@ -1,18 +1,22 @@
"use client"
"use client";
import { useEffect, useRef } from "react"
import { useRef } from "react";
/**
*
* @param callback
* @param time In miliseconds
*/
export default function useThrottle<T extends Function>(callback: T, time: number = 2000) {
const lastRun = useRef(Date.now())
export default function useThrottle<T extends () => unknown>(
callback: T,
time: number = 2000,
) {
const lastRun = useRef(Date.now());
return function() {
if (Date.now() - lastRun.current <= time) return;
lastRun.current = Date.now()
return callback()
}
// eslint-disable-next-line func-names
return function () {
if (Date.now() - lastRun.current <= time) return;
lastRun.current = Date.now();
callback();
};
}

View File

@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@ -1,4 +1,5 @@
"use client"
"use client";
// import gdi from "@/bootstrap/di/init-di";
/* eslint-disable react/display-name */
/* eslint-disable @typescript-eslint/no-explicit-any */
@ -23,7 +24,7 @@ const VvmConnector = memo(
<IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => {
const { View, Vm, restProps, children } = props;
const vm = Vm.useVM()
const vm = Vm.useVM();
const allProps = {
restProps,
@ -69,16 +70,16 @@ export default abstract class BaseView<
IVM extends IVMParent,
PROPS extends IPropParent = undefined,
> extends Component<BaseProps<IVM, PROPS>> {
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
protected get componentName() {
return this.constructor.name
return this.constructor.name;
}
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
render(): ReactNode {
const { vm, restProps, memoizedByVM, children, ...rest } = this.props;
VvmConnector.displayName = this.componentName
VvmConnector.displayName = this.componentName;
return (
<VvmConnector

View File

@ -1,4 +1,5 @@
"use client"
"use client";
import { useDI } from "@/bootstrap/di/di-context";
import { NoOverride } from "@/bootstrap/helpers/type-helper";
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
@ -31,8 +32,9 @@ export default abstract class BaseVM<
/* -------------------------------------------------------------------------- */
protected get di() {
return useDI()
return useDI();
}
/* -------------------------------------------------------------------------- */
/**
* You can use this hook in your useVm method to get rerender method

View File

@ -1,16 +1,16 @@
import langKey from "@/bootstrap/i18n/dictionaries/lang-key"
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
const en: typeof langKey = {
global: {
home: "Home",
loading: "Loading",
dashboard: "Dashboard"
global: {
home: "Home",
loading: "Loading",
dashboard: "Dashboard",
},
dashboard: {
invoice: {
createButton: "Create random Invoice",
},
dashboard: {
invoice: {
createButton: "Create random Invoice"
}
}
}
},
};
export default en
export default en;

View File

@ -1,14 +1,14 @@
const langKey = {
global: {
home: "global.home",
dashboard: "global.dashboard",
loading: "global.loading"
global: {
home: "global.home",
dashboard: "global.dashboard",
loading: "global.loading",
},
dashboard: {
invoice: {
createButton: "dashboard.invoice.createButton",
},
dashboard: {
invoice: {
createButton: "dashboard.invoice.createButton"
}
}
}
},
};
export default langKey;

View File

@ -1,16 +1,16 @@
import langKey from "@/bootstrap/i18n/dictionaries/lang-key"
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
const ru: typeof langKey = {
global: {
home: "Дом",
loading: "Загрузка",
dashboard: "Панель приборов"
global: {
home: "Дом",
loading: "Загрузка",
dashboard: "Панель приборов",
},
dashboard: {
invoice: {
createButton: "Создать случайный счет-фактуру",
},
dashboard: {
invoice: {
createButton: "Создать случайный счет-фактуру"
}
}
}
},
};
export default ru
export default ru;

View File

@ -1,13 +1,18 @@
"use client"
import { I18nextProvider } from "react-i18next"
"use client";
import { I18nextProvider } from "react-i18next";
import { initI18next } from "@/bootstrap/i18n/i18n";
import { createInstance, Resource } from "i18next";
import { PropsWithChildren } from "react";
export default function TranslationsProvider({children, lng, resources}: PropsWithChildren & {lng: string; resources: Resource}) {
const i18n = createInstance()
export default function TranslationsProvider({
children,
lng,
resources,
}: PropsWithChildren & { lng: string; resources: Resource }) {
const i18n = createInstance();
initI18next({lng, i18n, resources})
initI18next({ lng, i18n, resources });
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}

View File

@ -1,34 +1,51 @@
import { getOptions, languages } from '@/bootstrap/i18n/settings'
import { createInstance, i18n, Resource } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
import { getOptions, languages } from "@/bootstrap/i18n/settings";
import { createInstance, i18n, Resource } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
export const initI18next = async (params: {lng: string, i18n?: i18n, resources?: Resource, ns?: string}) => {
const { lng, i18n, ns, resources } = params
const i18nInstance = i18n ? i18n : createInstance()
export const initI18next = async (params: {
lng: string;
i18n?: i18n;
resources?: Resource;
ns?: string;
}) => {
const { lng, i18n, ns, resources } = params;
const i18nInstance = i18n || createInstance();
await i18nInstance
.use(initReactI18next)
.use(resourcesToBackend((language: string) => import(`./dictionaries/${language}.ts`)))
.use(
resourcesToBackend(
(language: string) => import(`./dictionaries/${language}.ts`),
),
)
.init({
...getOptions(lng, ns),
resources,
preload: resources ? [] : languages
},)
...getOptions(lng, ns),
resources,
preload: resources ? [] : languages,
});
await i18nInstance.init()
return {
i18n: i18nInstance,
resources: i18nInstance.services.resourceStore.data,
t: i18nInstance.t
}
}
export async function getServerTranslation(lng: string, ns?: string, options: {keyPrefix?: string} = {}) {
const i18nextInstance = (await initI18next({lng, ns})).i18n
await i18nInstance.init();
return {
t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options?.keyPrefix),
i18n: i18nextInstance
}
i18n: i18nInstance,
resources: i18nInstance.services.resourceStore.data,
t: i18nInstance.t,
};
};
export async function getServerTranslation(
lng: string,
ns?: string,
options: { keyPrefix?: string } = {},
) {
const i18nextInstance = (await initI18next({ lng, ns })).i18n;
return {
t: i18nextInstance.getFixedT(
lng,
Array.isArray(ns) ? ns[0] : ns,
options?.keyPrefix,
),
i18n: i18nextInstance,
};
}

View File

@ -1,9 +1,9 @@
export const fallbackLng = 'en'
export const languages = [fallbackLng, 'ru']
export const defaultNS = 'translation'
export const cookieName = 'i18next'
export const fallbackLng = "en";
export const languages = [fallbackLng, "ru"];
export const defaultNS = "translation";
export const cookieName = "i18next";
export function getOptions (lng = fallbackLng, ns = defaultNS) {
export function getOptions(lng = fallbackLng, ns = defaultNS) {
return {
// debug: true,
supportedLngs: languages,
@ -11,6 +11,6 @@ export function getOptions (lng = fallbackLng, ns = defaultNS) {
lng,
fallbackNS: defaultNS,
defaultNS,
ns
}
ns,
};
}

View File

@ -2,7 +2,10 @@ import { Either } from "fp-ts/lib/Either";
import { TaskEither } from "fp-ts/lib/TaskEither";
import BaseFailure from "@/feature/common/failures/base-failure";
type ApiTask<ResponseType> = TaskEither<BaseFailure, ResponseType>;
export type ApiEither<ResponseType> = Either<BaseFailure, ResponseType>;
type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
export type ApiEither<ResponseType> = Either<
BaseFailure<unknown>,
ResponseType
>;
export default ApiTask;

View File

@ -20,7 +20,7 @@ export default abstract class BaseFailure<META_DATA> {
/* -------------------------------------------------------------------------- */
constructor(key: string, metadata?: META_DATA) {
this.message = makeFailureMessage(this.message, key);
this.metadata = metadata ?? undefined
this.metadata = metadata ?? undefined;
}
/* -------------------------------------------------------------------------- */
}

View File

@ -3,7 +3,9 @@ import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
/**
* Failure for needed arguments in a method but sent wrong one
*/
export default class ArgumentsFailure<META_DATA> extends BaseDevFailure<META_DATA> {
export default class ArgumentsFailure<
META_DATA,
> extends BaseDevFailure<META_DATA> {
/* ------------------------------- Constructor ------------------------------ */
constructor(metadata?: META_DATA) {
super("arguments", metadata);

View File

@ -1,3 +1,5 @@
import BaseFailure from "@/feature/common/failures/base-failure";
export default abstract class BaseDevFailure<META_DATA> extends BaseFailure<META_DATA> {}
export default abstract class BaseDevFailure<
META_DATA,
> extends BaseFailure<META_DATA> {}

View File

@ -3,7 +3,9 @@ import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
/**
* This is a failure of not having specific dependency
*/
export default class DependencyFailure<META_DATA> extends BaseDevFailure<META_DATA> {
export default class DependencyFailure<
META_DATA,
> extends BaseDevFailure<META_DATA> {
constructor(metadata: META_DATA) {
super("DependencyFailure", metadata);
}

View File

@ -1,6 +1,5 @@
export const formatCurrency = (amount: number) => {
return (amount / 100).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
export const formatCurrency = (amount: number) =>
(amount / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
});
};

View File

@ -10,21 +10,21 @@ import getSummaryInfoDi from "@/feature/core/summary-info/data/module/summary-in
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> = {}
const memoizedDis: Record<string, DependencyContainer> = {};
export default function serverDi(module: string): DependencyContainer {
if (memoizedDis[module]) return memoizedDis[module]
const getDi = {
[customerKey]: getCustomerDi,
[customerInvoiceModuleKey]: getCustomerInvoiceDi,
[invoiceModuleKey]: getInvoiceDi,
[summaryInfoModuleKey]: getSummaryInfoDi,
[revenueModuleKey]: getRevenueDi,
}[module]
if (memoizedDis[module]) return memoizedDis[module];
const getDi = {
[customerKey]: getCustomerDi,
[customerInvoiceModuleKey]: getCustomerInvoiceDi,
[invoiceModuleKey]: getInvoiceDi,
[summaryInfoModuleKey]: getSummaryInfoDi,
[revenueModuleKey]: getRevenueDi,
}[module];
if (!getDi) throw new Error("Server Di didn't found for module: " + module)
if (!getDi) throw new Error(`Server Di didn't found for module: ${module}`);
const di = getDi()
memoizedDis[module] = di
return di
const di = getDi();
memoizedDis[module] = di;
return di;
}

View File

@ -4,8 +4,8 @@ import { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i
import { DependencyContainer } from "tsyringe";
export default function getCustomerInvoiceDi(): DependencyContainer {
const customerInvoiceDi = di.createChildContainer()
const customerInvoiceDi = di.createChildContainer();
customerInvoiceDi.register(customerInvoiceRepoKey, CustomerInvoiceDbRepo)
return customerInvoiceDi
customerInvoiceDi.register(customerInvoiceRepoKey, CustomerInvoiceDbRepo);
return customerInvoiceDi;
}

View File

@ -15,39 +15,44 @@ type customerInvoiceDbResponse = {
image_url: string;
email: string;
amount: string;
}
};
export default class CustomerInvoiceDbRepo implements CustomerInvoiceRepo {
fetchList(): ApiTask<CustomerInvoice[]> {
return pipe(
tryCatch(
async () => {
const response = await sql`
fetchList(): ApiTask<CustomerInvoice[]> {
return pipe(
tryCatch(
async () => {
const response = (await sql`
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC
LIMIT 20 ` as postgres.RowList<customerInvoiceDbResponse[]>;
LIMIT 20 `) as postgres.RowList<
customerInvoiceDbResponse[]
>;
return this.customerInvoicesDto(response)
},
(l) => failureOr(l, new NetworkFailure())
)
)
}
return this.customerInvoicesDto(response);
},
(l) => failureOr(l, new NetworkFailure()),
),
);
}
private customerInvoicesDto(dbCustomers: customerInvoiceDbResponse[]): CustomerInvoice[] {
return dbCustomers.map((customer) => this.customerInvoiceDto(customer));
}
private customerInvoiceDto(dbCustomer: customerInvoiceDbResponse): CustomerInvoice {
return new CustomerInvoice({
id: dbCustomer.id,
customerName: dbCustomer.name,
customerEmail: dbCustomer.email,
customerImageUrl: dbCustomer.image_url,
invoicesAmount: formatCurrency(+dbCustomer.amount),
})
}
private customerInvoicesDto(
dbCustomers: customerInvoiceDbResponse[],
): CustomerInvoice[] {
return dbCustomers.map((customer) => this.customerInvoiceDto(customer));
}
private customerInvoiceDto(
dbCustomer: customerInvoiceDbResponse,
): CustomerInvoice {
return new CustomerInvoice({
id: dbCustomer.id,
customerName: dbCustomer.name,
customerEmail: dbCustomer.email,
customerImageUrl: dbCustomer.image_url,
invoicesAmount: formatCurrency(+dbCustomer.amount),
});
}
}

View File

@ -1,21 +1,25 @@
export default class CustomerInvoice {
id: string;
customerName: string;
customerImageUrl: string;
customerEmail: string;
invoicesAmount: string;
id: string;
constructor({
id,
customerEmail,
customerImageUrl,
customerName,
invoicesAmount
}: CustomerInvoice) {
this.id = id;
this.customerEmail = customerEmail
this.customerImageUrl = customerImageUrl
this.customerName = customerName
this.invoicesAmount = invoicesAmount
}
customerName: string;
customerImageUrl: string;
customerEmail: string;
invoicesAmount: string;
constructor({
id,
customerEmail,
customerImageUrl,
customerName,
invoicesAmount,
}: CustomerInvoice) {
this.id = id;
this.customerEmail = customerEmail;
this.customerImageUrl = customerImageUrl;
this.customerName = customerName;
this.invoicesAmount = invoicesAmount;
}
}

View File

@ -1,8 +1,8 @@
import ApiTask from "@/feature/common/data/api-task"
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice"
import ApiTask from "@/feature/common/data/api-task";
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
export default interface CustomerInvoiceRepo {
fetchList(): ApiTask<CustomerInvoice[]>
fetchList(): ApiTask<CustomerInvoice[]>;
}
export const customerInvoiceRepoKey = "customerInvoiceRepoKey"
export const customerInvoiceRepoKey = "customerInvoiceRepoKey";

View File

@ -1,12 +1,18 @@
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";
import CustomerInvoiceRepo, { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
import CustomerInvoiceRepo, {
customerInvoiceRepoKey,
} from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
import { connection } from "next/server";
export default async function fetchCustomerInvoicesUsecase(): Promise<ApiEither<CustomerInvoice[]>> {
connection()
const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(customerInvoiceRepoKey)
return repo.fetchList()()
export default async function fetchCustomerInvoicesUsecase(): Promise<
ApiEither<CustomerInvoice[]>
> {
connection();
const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(
customerInvoiceRepoKey,
);
return repo.fetchList()();
}

View File

@ -1 +1 @@
export const customerInvoiceModuleKey = "customerInvoiceModuleKey"
export const customerInvoiceModuleKey = "customerInvoiceModuleKey";

View File

@ -1 +1 @@
export const customerKey = "customerKey"
export const customerKey = "customerKey";

View File

@ -4,8 +4,8 @@ import { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-
import { DependencyContainer } from "tsyringe";
export default function getCustomerDi(): DependencyContainer {
const customerDi = di.createChildContainer()
const customerDi = di.createChildContainer();
customerDi.register(customerRepoKey, CustomerDbRepo)
return customerDi
customerDi.register(customerRepoKey, CustomerDbRepo);
return customerDi;
}

View File

@ -1,7 +1,12 @@
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 { 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";
import { pipe } from "fp-ts/lib/function";
import { map, tryCatch } from "fp-ts/lib/TaskEither";
import postgres from "postgres";
type customerDbResponse = {
@ -12,57 +17,60 @@ type customerDbResponse = {
total_invoices: string;
total_pending: string;
total_paid: string;
}
};
export default class CustomerDbRepo implements CustomerRepo {
async fetchList(query: string): Promise<Customer[]> {
try {
const data = await sql`
SELECT
customers.id,
customers.name,
customers.email,
customers.image_url,
COUNT(invoices.id) AS total_invoices,
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
FROM customers
LEFT JOIN invoices ON customers.id = invoices.customer_id
WHERE
customers.name ILIKE ${`%${query}%`} OR
customers.email ILIKE ${`%${query}%`}
GROUP BY customers.id, customers.name, customers.email, customers.image_url
ORDER BY customers.name ASC
` as postgres.RowList<customerDbResponse[]>;
fetchList(query: string): ApiTask<Customer[]> {
return pipe(
tryCatch(
async () => {
const data = (await sql`
SELECT
customers.id,
customers.name,
customers.email,
customers.image_url,
COUNT(invoices.id) AS total_invoices,
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
FROM customers
LEFT JOIN invoices ON customers.id = invoices.customer_id
WHERE
customers.name ILIKE ${`%${query}%`} OR
customers.email ILIKE ${`%${query}%`}
GROUP BY customers.id, customers.name, customers.email, customers.image_url
ORDER BY customers.name ASC
`) as postgres.RowList<customerDbResponse[]>;
return data;
},
(l) => failureOr(l, new NetworkFailure(l as Error)),
),
map(this.customersDto.bind(this)),
) as ApiTask<Customer[]>;
}
return this.customersDto(data);
} catch (err) {
console.error('Database Error:', err);
throw new Error('Failed to fetch customer table.');
}
}
async fetchCustomersAmount(): Promise<number> {
const data =
(await sql`SELECT COUNT(*) FROM customers`) as postgres.RowList<
unknown[]
>;
return Number(data.count ?? "0");
}
async fetchCustomersAmount(): Promise<number> {
const data = await sql`SELECT COUNT(*) FROM customers`as postgres.RowList<unknown[]>;
return Number(data.count ?? '0');
}
private customersDto(dbCustomers: customerDbResponse[]): Customer[] {
return dbCustomers.map((customer) => this.customerDto(customer));
}
private customerDto(dbCustomer: customerDbResponse): Customer {
return new Customer({
id: dbCustomer.id,
name: dbCustomer.name,
email: dbCustomer.email,
imageUrl: dbCustomer.image_url,
totalInvoices: dbCustomer.total_invoices,
totalPending: formatCurrency(Number(dbCustomer.total_pending)),
totalPaid: formatCurrency(Number(dbCustomer.total_paid)),
})
}
private customersDto(dbCustomers: customerDbResponse[]): Customer[] {
return dbCustomers.map((customer) => this.customerDto(customer));
}
private customerDto(dbCustomer: customerDbResponse): Customer {
return new Customer({
id: dbCustomer.id,
name: dbCustomer.name,
email: dbCustomer.email,
imageUrl: dbCustomer.image_url,
totalInvoices: dbCustomer.total_invoices,
totalPending: formatCurrency(Number(dbCustomer.total_pending)),
totalPaid: formatCurrency(Number(dbCustomer.total_paid)),
});
}
}

View File

@ -1,27 +1,33 @@
export default class Customer {
id: string;
name: string;
email: string;
imageUrl: string;
totalInvoices: string;
totalPending: string;
totalPaid: string;
id: string;
constructor({
id,
email,
imageUrl,
name,
totalInvoices,
totalPaid,
totalPending
}: Customer) {
this.id = id;
this.name = name;
this.email = email;
this.imageUrl = imageUrl;
this.totalInvoices = totalInvoices;
this.totalPaid = totalPaid;
this.totalPending = totalPending;
}
name: string;
email: string;
imageUrl: string;
totalInvoices: string;
totalPending: string;
totalPaid: string;
constructor({
id,
email,
imageUrl,
name,
totalInvoices,
totalPaid,
totalPending,
}: Customer) {
this.id = id;
this.name = name;
this.email = email;
this.imageUrl = imageUrl;
this.totalInvoices = totalInvoices;
this.totalPaid = totalPaid;
this.totalPending = totalPending;
}
}

View File

@ -1,9 +1,9 @@
import ApiTask from "@/feature/common/data/api-task"
import Customer from "@/feature/core/customer/domain/entity/customer"
import ApiTask from "@/feature/common/data/api-task";
import Customer from "@/feature/core/customer/domain/entity/customer";
export default interface CustomerRepo {
fetchList(query: string): ApiTask<Customer[]>
fetchCustomersAmount(): Promise<number>
fetchList(query: string): ApiTask<Customer[]>;
fetchCustomersAmount(): Promise<number>;
}
export const customerRepoKey = "customerRepoKey"
export const customerRepoKey = "customerRepoKey";

View File

@ -1,8 +1,10 @@
import serverDi from "@/feature/common/server-di";
import { customerKey } from "@/feature/core/customer/customer-key";
import CustomerRepo, { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
import CustomerRepo, {
customerRepoKey,
} from "@/feature/core/customer/domain/i-repo/customer-repo";
export default async function fetchCustomersAmountUsecase(): Promise<number> {
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey)
return repo.fetchCustomersAmount()
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey);
return repo.fetchCustomersAmount();
}

View File

@ -1,14 +1,19 @@
"use server"
"use server";
import { ApiEither } from "@/feature/common/data/api-task";
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, { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
import CustomerRepo, {
customerRepoKey,
} from "@/feature/core/customer/domain/i-repo/customer-repo";
import { connection } from "next/server";
export default async function fetchCustomersUsecase(query: string): Promise<Customer[]> {
connection()
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey)
export default function fetchCustomersUsecase(
query: string,
): Promise<ApiEither<Customer[]>> {
connection();
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey);
return repo.fetchList(query)
return repo.fetchList(query)();
}

View File

@ -4,8 +4,8 @@ import { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-rep
import { DependencyContainer } from "tsyringe";
export default function getInvoiceDi(): DependencyContainer {
const invoiceDi = di.createChildContainer()
const invoiceDi = di.createChildContainer();
invoiceDi.register(invoiceRepoKey, invoiceDbRepo)
return invoiceDi
invoiceDi.register(invoiceRepoKey, invoiceDbRepo);
return invoiceDi;
}

View File

@ -10,57 +10,60 @@ import { pipe } from "fp-ts/lib/function";
import { tryCatch } from "fp-ts/lib/TaskEither";
import postgres from "postgres";
type InvoiceSummaryDbResponse = {paid: string, pending: string}
type InvoiceSummaryDbResponse = { paid: string; pending: string };
export default class InvoiceDbRepo implements InvoiceRepo {
async fetchAllInvoicesAmount(): Promise<number> {
const data = await sql`SELECT COUNT(*) FROM invoices` as postgres.RowList<unknown[]>;
async fetchAllInvoicesAmount(): Promise<number> {
const data = (await sql`SELECT COUNT(*) FROM invoices`) as postgres.RowList<
unknown[]
>;
return data.count ?? 0
}
return data.count ?? 0;
}
createInvoice(params: InvoiceParam): ApiTask<string> {
return pipe(
tryCatch(
async () => {
const firstCustomerIdDb = await sql`SELECT
createInvoice(params: InvoiceParam): ApiTask<string> {
return pipe(
tryCatch(
async () => {
const firstCustomerIdDb = await sql`SELECT
id FROM customers
ORDER BY id DESC
LIMIT 1
`
const customerId = firstCustomerIdDb.at(0)?.id
if (!customerId) throw new Error("There is no customer")
`;
const customerId = firstCustomerIdDb.at(0)?.id;
if (!customerId) throw new Error("There is no customer");
const { amount, status } = params;
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
const { amount, status } = params;
const amountInCents = amount * 100;
const date = new Date().toISOString().split("T")[0];
// Insert data into the database
const result = await sql`
// Insert data into the database
const result = await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
RETURNING id
`;
return result.at(0)?.id ?? ""
},
(l) => failureOr(l, new NetworkFailure(l as Error))
),
)
}
return result.at(0)?.id ?? "";
},
(l) => failureOr(l, new NetworkFailure(l as Error)),
),
);
}
async fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
const invoiceStatusPromise = await sql`SELECT
async fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
const invoiceStatusPromise = (await sql`SELECT
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
FROM invoices` as postgres.RowList<InvoiceSummaryDbResponse[]>;
FROM invoices`) as postgres.RowList<InvoiceSummaryDbResponse[]>;
return this.invoiceSummaryDto(invoiceStatusPromise.at(0))
return this.invoiceSummaryDto(invoiceStatusPromise.at(0));
}
}
private invoiceSummaryDto(dbResponse?: InvoiceSummaryDbResponse): InvoiceStatusSummary {
return new InvoiceStatusSummary({
paid: formatCurrency(Number(dbResponse?.paid ?? '0')),
pending: formatCurrency(Number(dbResponse?.pending ?? '0'))
})
}
private invoiceSummaryDto(
dbResponse?: InvoiceSummaryDbResponse,
): InvoiceStatusSummary {
return new InvoiceStatusSummary({
paid: formatCurrency(Number(dbResponse?.paid ?? "0")),
pending: formatCurrency(Number(dbResponse?.pending ?? "0")),
});
}
}

View File

@ -1,11 +1,11 @@
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"
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";
export default interface InvoiceRepo {
fetchAllInvoicesAmount(): Promise<number>
fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary>
createInvoice(params: InvoiceParam): ApiTask<string>
fetchAllInvoicesAmount(): Promise<number>;
fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary>;
createInvoice(params: InvoiceParam): ApiTask<string>;
}
export const invoiceRepoKey = "invoiceRepoKey"
export const invoiceRepoKey = "invoiceRepoKey";

View File

@ -1,10 +1,12 @@
import { z } from "zod";
export const invoiceSchema = z.object({
amount: z.coerce.number().gt(0, { message: 'Please enter an amount greater than $0.' }),
status: z.enum(['pending', 'paid'], {
invalid_type_error: 'Please select an invoice status.',
amount: z.coerce
.number()
.gt(0, { message: "Please enter an amount greater than $0." }),
status: z.enum(["pending", "paid"], {
invalid_type_error: "Please select an invoice status.",
}),
});
export type InvoiceParam = z.infer<typeof invoiceSchema>
export type InvoiceParam = z.infer<typeof invoiceSchema>;

View File

@ -1,24 +1,32 @@
"use server"
"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 InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
import { InvoiceParam, invoiceSchema } from "@/feature/core/invoice/domain/param/invoice-param";
import InvoiceRepo, {
invoiceRepoKey,
} 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 { pipe } from "fp-ts/lib/function";
import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither";
export default async function createInvoiceUsecase(params: InvoiceParam): Promise<ApiEither<string>> {
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
export default async function createInvoiceUsecase(
params: InvoiceParam,
): Promise<ApiEither<string>> {
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
return pipe(
fromNullable(new ParamsFailure())(params),
map((params) => invoiceSchema.safeParse(params)),
chain((params) => {
const isParamsValid = invoiceSchema.safeParse(params)
if (!isParamsValid.success) left(new ParamsFailure())
return right(params.data as InvoiceParam)
}),
chain((params) => repo.createInvoice(params))
)()
return pipe(
fromNullable(new ParamsFailure())(params),
map((params) => invoiceSchema.safeParse(params)),
chain((params) => {
const isParamsValid = invoiceSchema.safeParse(params);
if (!isParamsValid.success) left(new ParamsFailure());
return right(params.data as InvoiceParam);
}),
chain((params) => repo.createInvoice(params)),
)();
}

View File

@ -1,10 +1,13 @@
"use server"
"use server";
import serverDi from "@/feature/common/server-di";
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
import InvoiceRepo, {
invoiceRepoKey,
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
export default async function fetchAllInvoicesAmountUsecase(): Promise<number> {
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
return repo.fetchAllInvoicesAmount()
return repo.fetchAllInvoicesAmount();
}

View File

@ -1,9 +1,11 @@
import serverDi from "@/feature/common/server-di";
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
import InvoiceRepo, {
invoiceRepoKey,
} 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 async function fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
return repo.fetchInvoicesStatusSummary()
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
return repo.fetchInvoicesStatusSummary();
}

View File

@ -1,12 +1,10 @@
export default class InvoiceStatusSummary {
paid: string;
pending: string;
paid: string;
constructor({
paid,
pending
}: InvoiceStatusSummary) {
this.paid = paid;
this.pending = pending;
}
pending: string;
constructor({ paid, pending }: InvoiceStatusSummary) {
this.paid = paid;
this.pending = pending;
}
}

View File

@ -1 +1 @@
export const invoiceModuleKey = "invoiceModuleKey"
export const invoiceModuleKey = "invoiceModuleKey";

View File

@ -1,10 +1,10 @@
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-repo"
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-repo";
export default function getRevenueDi() {
const revenueDi = di.createChildContainer()
const revenueDi = di.createChildContainer();
revenueDi.register(revenueRepoKey, RevenueDbRepo)
return revenueDi
revenueDi.register(revenueRepoKey, RevenueDbRepo);
return revenueDi;
}

View File

@ -1,7 +1,6 @@
import { sql } from "@/bootstrap/boundaries/db/db";
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
import RevenueRepo from "@/feature/core/revenue/domain/i-repo/revenue-repo";
import { connection } from "next/server";
import postgres from "postgres";
export type RevenueDbResponse = {
@ -9,33 +8,30 @@ export type RevenueDbResponse = {
revenue: number;
};
export default class RevenueDbRepo implements RevenueRepo {
async fetchRevenues(): Promise<Revenue[]> {
try {
// Artificially delay a response for demo purposes.
// Don't do this in production :)
await new Promise((resolve) => setTimeout(resolve, 3000));
async fetchRevenues(): Promise<Revenue[]> {
try {
// Artificially delay a response for demo purposes.
// Don't do this in production :)
await new Promise((resolve) => setTimeout(resolve, 3000));
const data = await sql`SELECT * FROM revenue` as postgres.RowList<RevenueDbResponse[]>;
const data = (await sql`SELECT * FROM revenue`) as postgres.RowList<
RevenueDbResponse[]
>;
console.log('Data fetch completed after 3 seconds.');
return this.revenuesDto(data);
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch revenue data.');
}
return this.revenuesDto(data);
} catch {
throw new Error("Failed to fetch revenue data.");
}
}
private revenuesDto(dbResponse: RevenueDbResponse[]): Revenue[] {
return dbResponse.map((dbRevenue) => this.revenueDto(dbRevenue));
}
private revenuesDto(dbResponse: RevenueDbResponse[]): Revenue[] {
return dbResponse.map((dbRevenue) => this.revenueDto(dbRevenue))
}
private revenueDto(dbResponse: RevenueDbResponse): Revenue {
return new Revenue({
month: dbResponse.month,
revenue: dbResponse.revenue
})
}
private revenueDto(dbResponse: RevenueDbResponse): Revenue {
return new Revenue({
month: dbResponse.month,
revenue: dbResponse.revenue,
});
}
}

View File

@ -1,14 +1,10 @@
export default class Revenue {
month: string;
revenue: number;
month: string;
constructor(
{
month,
revenue
}: Revenue
) {
this.month = month
this.revenue = revenue
}
revenue: number;
constructor({ month, revenue }: Revenue) {
this.month = month;
this.revenue = revenue;
}
}

View File

@ -1,7 +1,7 @@
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
export default interface RevenueRepo {
fetchRevenues(): Promise<Revenue[]>
fetchRevenues(): Promise<Revenue[]>;
}
export const revenueRepoKey = "revenueRepoKey"
export const revenueRepoKey = "revenueRepoKey";

View File

@ -1 +1 @@
export const revenueModuleKey = "RevenueModuleKey"
export const revenueModuleKey = "RevenueModuleKey";

View File

@ -1,9 +1,11 @@
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-repo";
import RevenueRepo, {
revenueRepoKey,
} from "@/feature/core/revenue/domain/i-repo/revenue-repo";
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
export default async function fetchRevenuesUsecase(): Promise<Revenue[]> {
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey)
return repo.fetchRevenues()
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey);
return repo.fetchRevenues();
}

View File

@ -1,19 +1,19 @@
import di from "@/bootstrap/di/init-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"
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";
import di from "@/bootstrap/di/init-di";
export default function getSummaryInfoDi() {
const summaryInfoDi = di.createChildContainer()
const summaryInfoDi = di.createChildContainer();
summaryInfoDi.register(fetchAllInvoicesAmountUsecase.name, {
useValue: fetchAllInvoicesAmountUsecase
})
summaryInfoDi.register(fetchCustomersAmountUsecase.name, {
useValue: fetchCustomersAmountUsecase
})
summaryInfoDi.register(fetchInvoicesStatusSummary.name, {
useValue: fetchInvoicesStatusSummary
})
return summaryInfoDi
summaryInfoDi.register(fetchAllInvoicesAmountUsecase.name, {
useValue: fetchAllInvoicesAmountUsecase,
});
summaryInfoDi.register(fetchCustomersAmountUsecase.name, {
useValue: fetchCustomersAmountUsecase,
});
summaryInfoDi.register(fetchInvoicesStatusSummary.name, {
useValue: fetchInvoicesStatusSummary,
});
return summaryInfoDi;
}

View File

@ -1 +1 @@
export const summaryInfoModuleKey = "summaryInfoModuleKey"
export const summaryInfoModuleKey = "summaryInfoModuleKey";

View File

@ -7,28 +7,33 @@ import SummaryInfo from "@/feature/core/summary-info/domain/value-object/summary
import { connection } from "next/server";
export default async function fetchSummaryInfoUsecase(): Promise<SummaryInfo> {
connection()
connection();
try{
const summaryInfoDi = serverDi(summaryInfoModuleKey)
const invoicesAmountPromise = summaryInfoDi.resolve<typeof fetchAllInvoicesAmountUsecase>(fetchAllInvoicesAmountUsecase.name)()
const customersAmountPromise = summaryInfoDi.resolve<typeof fetchCustomersAmountUsecase>(fetchCustomersAmountUsecase.name)()
const invoiceSummaryPomise = summaryInfoDi.resolve<typeof fetchInvoicesStatusSummary>(fetchInvoicesStatusSummary.name)()
try {
const summaryInfoDi = serverDi(summaryInfoModuleKey);
const invoicesAmountPromise = summaryInfoDi.resolve<
typeof fetchAllInvoicesAmountUsecase
>(fetchAllInvoicesAmountUsecase.name)();
const customersAmountPromise = summaryInfoDi.resolve<
typeof fetchCustomersAmountUsecase
>(fetchCustomersAmountUsecase.name)();
const invoiceSummaryPomise = summaryInfoDi.resolve<
typeof fetchInvoicesStatusSummary
>(fetchInvoicesStatusSummary.name)();
const [invoicesAmount, customersAmount, invoicesSummary] = await Promise.all([
invoicesAmountPromise,
customersAmountPromise,
invoiceSummaryPomise,
]);
const [invoicesAmount, customersAmount, invoicesSummary] =
await Promise.all([
invoicesAmountPromise,
customersAmountPromise,
invoiceSummaryPomise,
]);
return new SummaryInfo({
invoicesNumber: invoicesAmount,
customersNumber: customersAmount,
invoicesSummary: invoicesSummary
})
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch card data.');
}
return new SummaryInfo({
invoicesNumber: invoicesAmount,
customersNumber: customersAmount,
invoicesSummary,
});
} catch {
throw new Error("Failed to fetch card data.");
}
}

View File

@ -1,17 +1,19 @@
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
export default class SummaryInfo {
customersNumber: number;
invoicesNumber: number;
invoicesSummary: InvoiceStatusSummary
customersNumber: number;
constructor({
customersNumber,
invoicesNumber,
invoicesSummary
}: SummaryInfo) {
this.customersNumber = customersNumber
this.invoicesNumber = invoicesNumber
this.invoicesSummary = invoicesSummary
}
invoicesNumber: number;
invoicesSummary: InvoiceStatusSummary;
constructor({
customersNumber,
invoicesNumber,
invoicesSummary,
}: SummaryInfo) {
this.customersNumber = customersNumber;
this.invoicesNumber = invoicesNumber;
this.invoicesSummary = invoicesSummary;
}
}

View File

@ -1,36 +1,41 @@
import { NextRequest, NextResponse } from 'next/server'
import acceptLanguage from 'accept-language'
import { cookieName, fallbackLng, languages } from '@/bootstrap/i18n/settings'
import { NextRequest, NextResponse } from "next/server";
import acceptLanguage from "accept-language";
import { cookieName, fallbackLng, languages } from "@/bootstrap/i18n/settings";
acceptLanguage.languages(languages)
acceptLanguage.languages(languages);
export const config = {
matcher: ["/((?!api|static|.*\\..*|_next).*)"]
}
matcher: ["/((?!api|static|.*\\..*|_next).*)"],
};
export function middleware(req: NextRequest) {
let lng
if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req?.cookies?.get(cookieName)?.value)
if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'))
if (!lng) lng = fallbackLng
let lng;
if (req.cookies.has(cookieName))
lng = acceptLanguage.get(req?.cookies?.get(cookieName)?.value);
if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"));
if (!lng) lng = fallbackLng;
// Redirect if lng in path is not supported
if (
!languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
!req.nextUrl.pathname.startsWith('/_next')
!languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
!req.nextUrl.pathname.startsWith("/_next")
) {
return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
return NextResponse.redirect(
new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
);
}
if (req.headers.has('referer')) {
const refererUrl = new URL(req?.headers?.get('referer') ?? "")
const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`))
const response = NextResponse.next()
if (req.headers.has("referer")) {
const refererUrl = new URL(req?.headers?.get("referer") ?? "");
const lngInReferer = languages.find((l) =>
refererUrl.pathname.startsWith(`/${l}`),
);
const response = NextResponse.next();
if (lngInReferer) {
response.cookies.set(cookieName, lngInReferer)
response.cookies.set(cookieName, lngInReferer);
}
return response
return response;
}
return NextResponse.next()
return NextResponse.next();
}

View File

@ -2,20 +2,21 @@ import Customer from "@/feature/core/customer/domain/entity/customer";
import { faker } from "@faker-js/faker";
export default class CustomerFakeFactory {
static getFakeCustomer(): Customer {
return new Customer({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
imageUrl: faker.image.url(),
totalInvoices: faker.number.int().toLocaleString(),
totalPaid: faker.finance.amount(),
totalPending: faker.number.int().toLocaleString(),
})
}
static getFakeCustomer(): Customer {
return new Customer({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
imageUrl: faker.image.url(),
totalInvoices: faker.number.int().toLocaleString(),
totalPaid: faker.finance.amount(),
totalPending: faker.number.int().toLocaleString(),
});
}
static getFakeCustomerList(length: number = 10): Customer[] {
return Array.from({length}).map(() => CustomerFakeFactory.getFakeCustomer())
}
static getFakeCustomerList(length: number = 10): Customer[] {
return Array.from({ length }).map(() =>
CustomerFakeFactory.getFakeCustomer(),
);
}
}

View File

@ -1,7 +1,7 @@
import di from "@/bootstrap/di/init-di"
import * as serverDi from "@/feature/common/server-di"
import di from "@/bootstrap/di/init-di";
import * as serverDi from "@/feature/common/server-di";
export default function mockDi() {
vi.spyOn(serverDi, "default").mockReturnValue(di)
return di
vi.spyOn(serverDi, "default").mockReturnValue(di);
return di;
}

View File

@ -1,48 +1,53 @@
import CustomerRepo, { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
import CustomerRepo, {
customerRepoKey,
} from "@/feature/core/customer/domain/i-repo/customer-repo";
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 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";
/* -------------------------------------------------------------------------- */
/* Faking */
/* -------------------------------------------------------------------------- */
const fakedCustomerList = CustomerFakeFactory.getFakeCustomerList()
const fakedCustomerList = CustomerFakeFactory.getFakeCustomerList();
/* -------------------------------------------------------------------------- */
/* Mocking */
/* -------------------------------------------------------------------------- */
const customerDi = mockDi()
const customerDi = mockDi();
const mockedFetchList = vi.fn<CustomerRepo['fetchList']>()
const MockedRepo = getMock<CustomerRepo>()
MockedRepo.setup((instance) => instance.fetchList).returns(mockedFetchList)
const mockedFetchList = vi.fn<CustomerRepo["fetchList"]>();
const MockedRepo = getMock<CustomerRepo>();
MockedRepo.setup((instance) => instance.fetchList).returns(mockedFetchList);
/* -------------------------------------------------------------------------- */
/* DI */
/* -------------------------------------------------------------------------- */
customerDi.register(fetchCustomersUsecase.name, {
useValue: fetchCustomersUsecase
})
useValue: fetchCustomersUsecase,
});
customerDi.register(customerRepoKey, {
useValue: MockedRepo.object()
})
useValue: MockedRepo.object(),
});
/* -------------------------------------------------------------------------- */
/* Testing */
/* -------------------------------------------------------------------------- */
const usecase = customerDi.resolve<typeof fetchCustomersUsecase>(fetchCustomersUsecase.name)
const usecase = customerDi.resolve<typeof fetchCustomersUsecase>(
fetchCustomersUsecase.name,
);
describe("Fetch customers", () => {
describe("On given query string", () => {
const fakedQuery = faker.person.fullName();
describe("And returning list from repo", () => {
beforeEach(() => {
mockedFetchList.mockResolvedValue(fakedCustomerList)
})
it("Then should return correct list of customers", async () => {
// ! Act
const response = await usecase(fakedQuery)
// ? Assert
expect(response).toEqual(fakedCustomerList)
})
});
describe("On given query string", () => {
const fakedQuery = faker.person.fullName();
describe("And returning list from repo", () => {
beforeEach(() => {
mockedFetchList.mockResolvedValue(right(fakedCustomerList));
});
it("Then should return correct list of customers", async () => {
// ! Act
const response = await usecase(fakedQuery);
// ? Assert
expect(response).toEqual(fakedCustomerList);
});
});
});
});

View File

@ -620,6 +620,11 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@pkgr/core@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
"@radix-ui/react-compose-refs@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
@ -1465,6 +1470,11 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
confusing-browser-globals@^1.0.10:
version "1.0.11"
resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81"
integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==
console-control-strings@^1.0.0, console-control-strings@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
@ -1835,6 +1845,25 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-airbnb-base@^15.0.0:
version "15.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236"
integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==
dependencies:
confusing-browser-globals "^1.0.10"
object.assign "^4.1.2"
object.entries "^1.1.5"
semver "^6.3.0"
eslint-config-airbnb@^19.0.4:
version "19.0.4"
resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz#84d4c3490ad70a0ffa571138ebcdea6ab085fdc3"
integrity sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==
dependencies:
eslint-config-airbnb-base "^15.0.0"
object.assign "^4.1.2"
object.entries "^1.1.5"
eslint-config-next@15.0.1:
version "15.0.1"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.0.1.tgz#5f49a01d312420cdbf1e87299396ef779ae99004"
@ -1851,6 +1880,16 @@ eslint-config-next@15.0.1:
eslint-plugin-react "^7.35.0"
eslint-plugin-react-hooks "^5.0.0"
eslint-config-prettier@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f"
integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==
eslint-import-resolver-alias@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz#297062890e31e4d6651eb5eba9534e1f6e68fc97"
integrity sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==
eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9:
version "0.3.9"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
@ -1860,7 +1899,7 @@ eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9:
is-core-module "^2.13.0"
resolve "^1.22.4"
eslint-import-resolver-typescript@^3.5.2:
eslint-import-resolver-typescript@^3.5.2, eslint-import-resolver-typescript@^3.6.3:
version "3.6.3"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz#bb8e388f6afc0f940ce5d2c5fd4a3d147f038d9e"
integrity sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==
@ -1927,6 +1966,14 @@ eslint-plugin-jsx-a11y@^6.10.0:
safe-regex-test "^1.0.3"
string.prototype.includes "^2.0.1"
eslint-plugin-prettier@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz#d1c8f972d8f60e414c25465c163d16f209411f95"
integrity sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==
dependencies:
prettier-linter-helpers "^1.0.0"
synckit "^0.9.1"
eslint-plugin-react-hooks@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101"
@ -2063,6 +2110,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.1.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
fast-glob@3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
@ -3107,7 +3159,7 @@ object-keys@^1.1.1:
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
object.assign@^4.1.4, object.assign@^4.1.5:
object.assign@^4.1.2, object.assign@^4.1.4, object.assign@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0"
integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==
@ -3117,7 +3169,7 @@ object.assign@^4.1.4, object.assign@^4.1.5:
has-symbols "^1.0.3"
object-keys "^1.1.1"
object.entries@^1.1.8:
object.entries@^1.1.5, object.entries@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41"
integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==
@ -3341,6 +3393,18 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
dependencies:
fast-diff "^1.1.2"
prettier@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
pretty-format@^27.0.2:
version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
@ -3589,7 +3653,7 @@ scheduler@0.25.0-rc-69d4b800-20241021:
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0-rc-69d4b800-20241021.tgz#336e47ef2bd5eddb0ebacfd910b5df1b236d92bd"
integrity sha512-S5AYX/YhMAN6u9AXgKYbZP4U4ZklC6R9Q7HmFSBk7d4DLiHVNxvAvlSvuM4nxFkwOk50MnpfTKQ7UWHXDOc9Eg==
semver@^6.0.0, semver@^6.3.1:
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
@ -3884,6 +3948,14 @@ symbol-tree@^3.2.4:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
synckit@^0.9.1:
version "0.9.2"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62"
integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==
dependencies:
"@pkgr/core" "^0.1.0"
tslib "^2.6.2"
tailwind-merge@^2.5.4:
version "2.5.4"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.4.tgz#4bf574e81fa061adeceba099ae4df56edcee78d1"
@ -4041,7 +4113,7 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@*, tslib@2, tslib@^2.4.0:
tslib@*, tslib@2, tslib@^2.4.0, tslib@^2.6.2:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==