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", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start --port 4000", "start": "next start --port 4000",
"lint": "next lint", "lint": "next lint --fix",
"test": "vitest", "test": "vitest",
"seed": "node -r dotenv/config ./src/bootstrap/boundaries/db/seed.js" "seed": "node -r dotenv/config ./src/bootstrap/boundaries/db/seed.js"
}, },
@ -46,10 +46,16 @@
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"eslint": "^8", "eslint": "^8",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-next": "15.0.1", "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", "jsdom": "^25.0.1",
"moq.ts": "^10.0.8", "moq.ts": "^10.0.8",
"postcss": "^8", "postcss": "^8",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5", "typescript": "^5",
"vitest": "^2.1.4" "vitest": "^2.1.4"

View File

@ -1,13 +1,13 @@
"use client" "use client";
import Button from "@/app/components/button/button" import Button from "@/app/components/button/button";
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm" import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
import { useDI } from "@/bootstrap/di/di-context" import { useDI } from "@/bootstrap/di/di-context";
import { useRef } from "react" import { useRef } from "react";
export default function CreateRandomInvoiceContainer() { export default function CreateRandomInvoiceContainer() {
const di = useDI() const di = useDI();
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM)) 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' "use client";
import navLinkPersonalVM from '@/app/[lang]/dashboard/components/client/nav-links/nav-link-controller';
import clsx from 'clsx'; import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link-vm";
import Link from 'next/link' import clsx from "clsx";
import Link from "next/link";
export default function NavLinks() { export default function NavLinks() {
const { links, isLinkActive } = navLinkPersonalVM() const { links, isLinkActive } = navLinkPersonalVM();
return ( return (
<> <>
{links.map((link) => { {links.map((link) => {
@ -14,9 +15,9 @@ export default function NavLinks() {
key={link.name} key={link.name}
href={link.href} href={link.href}
className={clsx( 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, ClockIcon,
UserGroupIcon, UserGroupIcon,
InboxIcon, InboxIcon,
} from '@heroicons/react/24/outline'; } from "@heroicons/react/24/outline";
export default function cardController(props: { type: 'invoices' | 'customers' | 'pending' | 'collected'; }) { export default function cardController(props: {
const { type } = props type: "invoices" | "customers" | "pending" | "collected";
const iconMap = { }) {
collected: BanknotesIcon, const { type } = props;
customers: UserGroupIcon, const iconMap = {
pending: ClockIcon, collected: BanknotesIcon,
invoices: InboxIcon, customers: UserGroupIcon,
}; pending: ClockIcon,
return { invoices: InboxIcon,
Icon: iconMap[type] };
} return {
} Icon: iconMap[type],
};
}

View File

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

View File

@ -1,5 +1,5 @@
import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info-usecase"; import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info-usecase";
export default function cardsController() { 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 { Card } from "@/app/[lang]/dashboard/components/server/card/card";
import cardsController from '@/app/[lang]/dashboard/components/server/cards/cards-controller'; import cardsController from "@/app/[lang]/dashboard/components/server/cards/cards-controller";
export default async function CardWrapper() { export default async function CardWrapper() {
const {customersNumber, invoicesNumber, invoicesSummary } = await cardsController(); const { customersNumber, invoicesNumber, invoicesSummary } =
await cardsController();
return ( return (
<> <>
<Card title="Collected" value={invoicesSummary.paid} type="collected" /> <Card title="Collected" value={invoicesSummary.paid} type="collected" />
<Card title="Pending" value={invoicesSummary.pending} type="pending" /> <Card title="Pending" value={invoicesSummary.pending} type="pending" />
<Card title="Total Invoices" value={invoicesNumber} type="invoices" /> <Card title="Total Invoices" value={invoicesNumber} type="invoices" />
<Card <Card title="Total Customers" value={customersNumber} type="customers" />
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"; import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase";
export default async function latestInvoicesController() { export default function latestInvoicesController() {
return await fetchCustomerInvoicesUsecase() return fetchCustomerInvoicesUsecase();
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
// Loading animation // Loading animation
const shimmer = 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() { export function CardSkeleton() {
return ( return (
@ -106,33 +106,36 @@ export function TableRowSkeleton() {
return ( 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"> <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 */} {/* 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="flex items-center gap-3">
<div className="h-8 w-8 rounded-full 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> <div className="h-6 w-24 rounded bg-gray-100" />
</div> </div>
</td> </td>
{/* Email */} {/* Email */}
<td className="whitespace-nowrap px-3 py-3"> <td aria-label="Email" className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-32 rounded bg-gray-100"></div> <div className="h-6 w-32 rounded bg-gray-100" />
</td> </td>
{/* Amount */} {/* Amount */}
<td className="whitespace-nowrap px-3 py-3"> <td aria-label="Amount" className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100"></div> <div className="h-6 w-16 rounded bg-gray-100" />
</td> </td>
{/* Date */} {/* Date */}
<td className="whitespace-nowrap px-3 py-3"> <td aria-label="Date" className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100"></div> <div className="h-6 w-16 rounded bg-gray-100" />
</td> </td>
{/* Status */} {/* Status */}
<td className="whitespace-nowrap px-3 py-3"> <td aria-label="Status" className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100"></div> <div className="h-6 w-16 rounded bg-gray-100" />
</td> </td>
{/* Actions */} {/* 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="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 className="h-[38px] w-[38px] rounded bg-gray-100"></div> <div className="h-[38px] w-[38px] rounded bg-gray-100" />
</div> </div>
</td> </td>
</tr> </tr>
@ -144,19 +147,19 @@ export function InvoicesMobileSkeleton() {
<div className="mb-2 w-full rounded-md bg-white p-4"> <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 justify-between border-b border-gray-100 pb-8">
<div className="flex items-center"> <div className="flex items-center">
<div className="mr-2 h-8 w-8 rounded-full 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>
<div className="h-6 w-16 rounded bg-gray-100"></div> <div className="h-6 w-16 rounded bg-gray-100" />
</div> </div>
<div className="flex w-full items-center justify-between pt-4"> <div className="flex w-full items-center justify-between pt-4">
<div> <div>
<div className="h-6 w-16 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="mt-2 h-6 w-24 rounded bg-gray-100" />
</div> </div>
<div className="flex justify-end gap-2"> <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 className="h-10 w-10 rounded bg-gray-100"></div> <div className="h-10 w-10 rounded bg-gray-100" />
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -2,4 +2,4 @@ import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeleton
export default function Loading() { export default function Loading() {
return <DashboardSkeleton />; return <DashboardSkeleton />;
} }

View File

@ -1,13 +1,16 @@
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm"; import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
import di from "@/bootstrap/di/init-di" import di from "@/bootstrap/di/init-di";
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase"; import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
export default function dashboardAppModule() { export default function dashboardAppModule() {
const dashboardDi = di.createChildContainer() const dashboardDi = di.createChildContainer();
dashboardDi.register(createInvoiceUsecase.name, { dashboardDi.register(createInvoiceUsecase.name, {
useValue: createInvoiceUsecase useValue: createInvoiceUsecase,
}) });
dashboardDi.register(CreateRandomInvoiceButtonVM, CreateRandomInvoiceButtonVM) dashboardDi.register(
return dashboardDi 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 CardWrapper from "@/app/[lang]/dashboard/components/server/cards/cards";
import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices"; import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices";
import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart"; import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart";
@ -6,25 +9,28 @@ import { Suspense } from "react";
import { getServerTranslation } from "@/bootstrap/i18n/i18n"; import { getServerTranslation } from "@/bootstrap/i18n/i18n";
import langKey from "@/bootstrap/i18n/dictionaries/lang-key"; import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
export default async function Dashboard(props: {params: Promise<{lang: string}>}) { export default async function Dashboard(props: {
const {lang} = await props.params params: Promise<{ lang: string }>;
const { t } = await getServerTranslation(lang) }) {
const { params } = props;
const { lang } = await params;
const { t } = await getServerTranslation(lang);
return ( return (
<main> <main>
<h1 className={`mb-4 text-xl md:text-2xl`}> <h1 className="mb-4 text-xl md:text-2xl">
{t(langKey.global.dashboard)} {t(langKey.global.dashboard)}
</h1> </h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<CardWrapper /> <CardWrapper />
</div> </div>
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8"> <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
<Suspense fallback={<RevenueChartSkeleton />}> <Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart /> <RevenueChart />
</Suspense> </Suspense>
<Suspense fallback={<LatestInvoicesSkeleton />}> <Suspense fallback={<LatestInvoicesSkeleton />}>
<LatestInvoices /> <LatestInvoices />
</Suspense> </Suspense>
</div> </div>
</main> </main>
) );
} }

View File

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

View File

@ -2,8 +2,14 @@ import { initI18next } from "@/bootstrap/i18n/i18n";
import TranslationsProvider from "@/bootstrap/i18n/i18n-provider"; import TranslationsProvider from "@/bootstrap/i18n/i18n-provider";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
export default async function layout(props: PropsWithChildren & {params: Promise<{lang: string}>}) { export default async function layout(
const lang = (await props.params).lang props: PropsWithChildren & { params: Promise<{ lang: string }> },
const { resources} = await initI18next({lng: lang}) ) {
return <TranslationsProvider lng={lang} resources={resources}>{props.children}</TranslationsProvider> 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() { export default function Home() {
return ( return (
<main className="flex min-h-screen flex-col p-6"> <main className="flex min-h-screen flex-col p-6">
<div className="mt-4 flex grow flex-col gap-4 md:flex-row"> <div className="mt-4 flex grow flex-col gap-4 md:flex-row">
<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 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 /> <div />
<p className={`text-xl text-gray-800 md:text-3xl md:leading-normal`}> <p className="text-xl text-gray-800 md:text-3xl md:leading-normal">
<strong>Welcome to Acme.</strong> This is the example for the{' '} <strong>Welcome to Acme.</strong> This is the example for the ,
brought to you by Vercel.
, brought to you by Vercel.
</p> </p>
</div> </div>
</div> </div>

View File

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

View File

@ -1,20 +1,24 @@
"use client" "use client";
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view"; import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
import ButtonVm from "@/app/components/button/button-vm"; import ButtonVm from "@/app/components/button/button-vm";
import { ReactNode } from "react"; import { ReactNode } from "react";
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/bootstrap/helpers/lib/ui-utils"; import { cn } from "@/bootstrap/helpers/lib/ui-utils";
export default class Button extends BaseView<ButtonVm> { export default class Button extends BaseView<ButtonVm> {
protected Build(props: BuildProps<ButtonVm>): ReactNode { protected Build(props: BuildProps<ButtonVm>): ReactNode {
const {vm} = props 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( 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", "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", variant: "default",
size: "default", size: "default",
}, },
} },
) );
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean;
} }
const ButtonUi = React.forwardRef<HTMLButtonElement, ButtonProps>( const ButtonUi = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props} {...props}
/> />
) );
} },
) );
ButtonUi.displayName = "Button" ButtonUi.displayName = "Button";
export { buttonVariants } export { buttonVariants };

View File

@ -1,5 +1,20 @@
export function DocumentIcon(props: {className?: string}) { export function DocumentIcon(props: { className?: string }) {
return ( const { className } = props;
<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> 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}) { export default function HomeIcon(props: { className?: string }) {
return ( const { className } = props;
<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> 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}) { export function UserIcon(props: { className?: string }) {
return ( const { className } = props;
<svg className={props.className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor"> return (
<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
</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 type { Metadata } from "next";
import localFont from "next/font/local"; import localFont from "next/font/local";
import "./globals.css"; import "./globals.css";
const geistSans = localFont({ const geistSans = localFont({
src: "./fonts/GeistVF.woff", src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -23,7 +24,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html> <html lang="en">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
// "use client" // "use client"
import "reflect-metadata" import "reflect-metadata";
import { container, DependencyContainer } from "tsyringe"; import { container, DependencyContainer } from "tsyringe";
/** /**
@ -15,4 +15,4 @@ const InitDI = (): DependencyContainer => {
const di = InitDI(); const di = InitDI();
export default di; export default di;

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 action Main server action to run
* @param onFinished Callback to run after action * @param onFinished Callback to run after action
* @returns transitioned action to run and is pending variable * @returns transitioned action to run and is pending variable
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useServerAction = <P extends any[], R>( export const useServerAction = <P extends any[], R>(
action: (...args: P) => Promise<R>, action: (...args: P) => Promise<R>,
@ -37,4 +37,4 @@ export const useServerAction = <P extends any[], R>(
}; };
return [runAction, isPending]; return [runAction, isPending];
}; };

View File

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

View File

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

View File

@ -5,4 +5,4 @@ type Forbidden = { [_]: typeof _ };
/** /**
* You can use this type to make your parent class method forbidden to overwrite * You can use this type to make your parent class method forbidden to overwrite
*/ */
export type NoOverride<T = void> = T & Forbidden; export type NoOverride<T = void> = T & Forbidden;

View File

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

View File

@ -1,4 +1,5 @@
"use client" "use client";
import { useDI } from "@/bootstrap/di/di-context"; import { useDI } from "@/bootstrap/di/di-context";
import { NoOverride } from "@/bootstrap/helpers/type-helper"; import { NoOverride } from "@/bootstrap/helpers/type-helper";
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm"; import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
@ -31,8 +32,9 @@ export default abstract class BaseVM<
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
protected get di() { protected get di() {
return useDI() return useDI();
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/** /**
* You can use this hook in your useVm method to get rerender method * 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 = { const en: typeof langKey = {
global: { global: {
home: "Home", home: "Home",
loading: "Loading", loading: "Loading",
dashboard: "Dashboard" 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 = { const langKey = {
global: { global: {
home: "global.home", home: "global.home",
dashboard: "global.dashboard", dashboard: "global.dashboard",
loading: "global.loading" loading: "global.loading",
},
dashboard: {
invoice: {
createButton: "dashboard.invoice.createButton",
}, },
dashboard: { },
invoice: { };
createButton: "dashboard.invoice.createButton"
}
}
}
export default langKey; 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 = { const ru: typeof langKey = {
global: { global: {
home: "Дом", home: "Дом",
loading: "Загрузка", loading: "Загрузка",
dashboard: "Панель приборов" dashboard: "Панель приборов",
},
dashboard: {
invoice: {
createButton: "Создать случайный счет-фактуру",
}, },
dashboard: { },
invoice: { };
createButton: "Создать случайный счет-фактуру"
}
}
}
export default ru export default ru;

View File

@ -1,13 +1,18 @@
"use client" "use client";
import { I18nextProvider } from "react-i18next"
import { I18nextProvider } from "react-i18next";
import { initI18next } from "@/bootstrap/i18n/i18n"; import { initI18next } from "@/bootstrap/i18n/i18n";
import { createInstance, Resource } from "i18next"; import { createInstance, Resource } from "i18next";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
export default function TranslationsProvider({children, lng, resources}: PropsWithChildren & {lng: string; resources: Resource}) { export default function TranslationsProvider({
const i18n = createInstance() 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 { getOptions, languages } from "@/bootstrap/i18n/settings";
import { createInstance, i18n, Resource } from 'i18next' import { createInstance, i18n, Resource } from "i18next";
import resourcesToBackend from 'i18next-resources-to-backend' import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from 'react-i18next/initReactI18next' import { initReactI18next } from "react-i18next/initReactI18next";
export const initI18next = async (params: {lng: string, i18n?: i18n, resources?: Resource, ns?: string}) => { export const initI18next = async (params: {
const { lng, i18n, ns, resources } = params lng: string;
const i18nInstance = i18n ? i18n : createInstance() i18n?: i18n;
resources?: Resource;
ns?: string;
}) => {
const { lng, i18n, ns, resources } = params;
const i18nInstance = i18n || createInstance();
await i18nInstance await i18nInstance
.use(initReactI18next) .use(initReactI18next)
.use(resourcesToBackend((language: string) => import(`./dictionaries/${language}.ts`))) .use(
resourcesToBackend(
(language: string) => import(`./dictionaries/${language}.ts`),
),
)
.init({ .init({
...getOptions(lng, ns), ...getOptions(lng, ns),
resources, resources,
preload: resources ? [] : languages preload: resources ? [] : languages,
},) });
await i18nInstance.init() 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
return { return {
t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options?.keyPrefix), i18n: i18nInstance,
i18n: i18nextInstance 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 fallbackLng = "en";
export const languages = [fallbackLng, 'ru'] export const languages = [fallbackLng, "ru"];
export const defaultNS = 'translation' export const defaultNS = "translation";
export const cookieName = 'i18next' export const cookieName = "i18next";
export function getOptions (lng = fallbackLng, ns = defaultNS) { export function getOptions(lng = fallbackLng, ns = defaultNS) {
return { return {
// debug: true, // debug: true,
supportedLngs: languages, supportedLngs: languages,
@ -11,6 +11,6 @@ export function getOptions (lng = fallbackLng, ns = defaultNS) {
lng, lng,
fallbackNS: defaultNS, fallbackNS: defaultNS,
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 { TaskEither } from "fp-ts/lib/TaskEither";
import BaseFailure from "@/feature/common/failures/base-failure"; import BaseFailure from "@/feature/common/failures/base-failure";
type ApiTask<ResponseType> = TaskEither<BaseFailure, ResponseType>; type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
export type ApiEither<ResponseType> = Either<BaseFailure, ResponseType>; export type ApiEither<ResponseType> = Either<
BaseFailure<unknown>,
ResponseType
>;
export default ApiTask; export default ApiTask;

View File

@ -15,12 +15,12 @@ export default abstract class BaseFailure<META_DATA> {
message = this.BASE_FAILURE_MESSAGE; message = this.BASE_FAILURE_MESSAGE;
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
metadata: META_DATA | undefined; metadata: META_DATA | undefined;
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
constructor(key: string, metadata?: META_DATA) { constructor(key: string, metadata?: META_DATA) {
this.message = makeFailureMessage(this.message, key); 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 * 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 ------------------------------ */
constructor(metadata?: META_DATA) { constructor(metadata?: META_DATA) {
super("arguments", metadata); super("arguments", metadata);

View File

@ -1,3 +1,5 @@
import BaseFailure from "@/feature/common/failures/base-failure"; 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 * 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) { constructor(metadata: META_DATA) {
super("DependencyFailure", metadata); super("DependencyFailure", metadata);
} }

View File

@ -1,7 +1,7 @@
import BaseFailure from "./base-failure"; import BaseFailure from "./base-failure";
/** /**
* Failure for params failure * Failure for params failure
*/ */
export default class ParamsFailure<META_DATA> extends BaseFailure<META_DATA> { export default class ParamsFailure<META_DATA> extends BaseFailure<META_DATA> {
/* ------------------------------- Constructor ------------------------------ */ /* ------------------------------- Constructor ------------------------------ */

View File

@ -1,6 +1,5 @@
export const formatCurrency = (amount: number) => { export const formatCurrency = (amount: number) =>
return (amount / 100).toLocaleString('en-US', { (amount / 100).toLocaleString("en-US", {
style: 'currency', style: "currency",
currency: 'USD', 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 { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
import getRevenueDi from "@/feature/core/revenue/data/module/revenue-di"; 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 { export default function serverDi(module: string): DependencyContainer {
if (memoizedDis[module]) return memoizedDis[module] if (memoizedDis[module]) return memoizedDis[module];
const getDi = { const getDi = {
[customerKey]: getCustomerDi, [customerKey]: getCustomerDi,
[customerInvoiceModuleKey]: getCustomerInvoiceDi, [customerInvoiceModuleKey]: getCustomerInvoiceDi,
[invoiceModuleKey]: getInvoiceDi, [invoiceModuleKey]: getInvoiceDi,
[summaryInfoModuleKey]: getSummaryInfoDi, [summaryInfoModuleKey]: getSummaryInfoDi,
[revenueModuleKey]: getRevenueDi, [revenueModuleKey]: getRevenueDi,
}[module] }[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() const di = getDi();
memoizedDis[module] = di memoizedDis[module] = di;
return di return di;
} }

View File

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

View File

@ -15,39 +15,44 @@ type customerInvoiceDbResponse = {
image_url: string; image_url: string;
email: string; email: string;
amount: string; amount: string;
} };
export default class CustomerInvoiceDbRepo implements CustomerInvoiceRepo { export default class CustomerInvoiceDbRepo implements CustomerInvoiceRepo {
fetchList(): ApiTask<CustomerInvoice[]> { fetchList(): ApiTask<CustomerInvoice[]> {
return pipe( return pipe(
tryCatch( tryCatch(
async () => { async () => {
const response = await sql` const response = (await sql`
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
FROM invoices FROM invoices
JOIN customers ON invoices.customer_id = customers.id JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC 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())
)
)
}
private customerInvoicesDto(dbCustomers: customerInvoiceDbResponse[]): CustomerInvoice[] { return this.customerInvoicesDto(response);
return dbCustomers.map((customer) => this.customerInvoiceDto(customer)); },
} (l) => failureOr(l, new NetworkFailure()),
),
);
}
private customerInvoiceDto(dbCustomer: customerInvoiceDbResponse): CustomerInvoice { private customerInvoicesDto(
return new CustomerInvoice({ dbCustomers: customerInvoiceDbResponse[],
id: dbCustomer.id, ): CustomerInvoice[] {
customerName: dbCustomer.name, return dbCustomers.map((customer) => this.customerInvoiceDto(customer));
customerEmail: dbCustomer.email, }
customerImageUrl: dbCustomer.image_url,
invoicesAmount: formatCurrency(+dbCustomer.amount),
})
}
} 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 { export default class CustomerInvoice {
id: string; id: string;
customerName: string;
customerImageUrl: string;
customerEmail: string;
invoicesAmount: string;
constructor({ customerName: string;
id,
customerEmail, customerImageUrl: string;
customerImageUrl,
customerName, customerEmail: string;
invoicesAmount
}: CustomerInvoice) { invoicesAmount: string;
this.id = id;
this.customerEmail = customerEmail constructor({
this.customerImageUrl = customerImageUrl id,
this.customerName = customerName customerEmail,
this.invoicesAmount = invoicesAmount 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 ApiTask from "@/feature/common/data/api-task";
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice" import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
export default interface CustomerInvoiceRepo { 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 { ApiEither } from "@/feature/common/data/api-task";
import serverDi from "@/feature/common/server-di"; import serverDi from "@/feature/common/server-di";
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice"; 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 { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
import { connection } from "next/server"; import { connection } from "next/server";
export default async function fetchCustomerInvoicesUsecase(): Promise<ApiEither<CustomerInvoice[]>> { export default async function fetchCustomerInvoicesUsecase(): Promise<
connection() ApiEither<CustomerInvoice[]>
const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(customerInvoiceRepoKey) > {
return repo.fetchList()() 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"; import { DependencyContainer } from "tsyringe";
export default function getCustomerDi(): DependencyContainer { export default function getCustomerDi(): DependencyContainer {
const customerDi = di.createChildContainer() const customerDi = di.createChildContainer();
customerDi.register(customerRepoKey, CustomerDbRepo) customerDi.register(customerRepoKey, CustomerDbRepo);
return customerDi return customerDi;
} }

View File

@ -1,7 +1,12 @@
import { sql } from "@/bootstrap/boundaries/db/db"; 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 { formatCurrency } from "@/feature/common/feature-helpers";
import Customer from "@/feature/core/customer/domain/entity/customer"; import Customer from "@/feature/core/customer/domain/entity/customer";
import CustomerRepo from "@/feature/core/customer/domain/i-repo/customer-repo"; 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"; import postgres from "postgres";
type customerDbResponse = { type customerDbResponse = {
@ -12,57 +17,60 @@ type customerDbResponse = {
total_invoices: string; total_invoices: string;
total_pending: string; total_pending: string;
total_paid: string; total_paid: string;
} };
export default class CustomerDbRepo implements CustomerRepo { export default class CustomerDbRepo implements CustomerRepo {
async fetchList(query: string): Promise<Customer[]> { fetchList(query: string): ApiTask<Customer[]> {
try { return pipe(
const data = await sql` tryCatch(
SELECT async () => {
customers.id, const data = (await sql`
customers.name, SELECT
customers.email, customers.id,
customers.image_url, customers.name,
COUNT(invoices.id) AS total_invoices, customers.email,
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending, customers.image_url,
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid COUNT(invoices.id) AS total_invoices,
FROM customers SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
LEFT JOIN invoices ON customers.id = invoices.customer_id SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
WHERE FROM customers
customers.name ILIKE ${`%${query}%`} OR LEFT JOIN invoices ON customers.id = invoices.customer_id
customers.email ILIKE ${`%${query}%`} WHERE
GROUP BY customers.id, customers.name, customers.email, customers.image_url customers.name ILIKE ${`%${query}%`} OR
ORDER BY customers.name ASC customers.email ILIKE ${`%${query}%`}
` as postgres.RowList<customerDbResponse[]>; 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); async fetchCustomersAmount(): Promise<number> {
} catch (err) { const data =
console.error('Database Error:', err); (await sql`SELECT COUNT(*) FROM customers`) as postgres.RowList<
throw new Error('Failed to fetch customer table.'); unknown[]
} >;
} return Number(data.count ?? "0");
}
async fetchCustomersAmount(): Promise<number> { private customersDto(dbCustomers: customerDbResponse[]): Customer[] {
const data = await sql`SELECT COUNT(*) FROM customers`as postgres.RowList<unknown[]>; return dbCustomers.map((customer) => this.customerDto(customer));
return Number(data.count ?? '0'); }
}
private customerDto(dbCustomer: customerDbResponse): Customer {
private customersDto(dbCustomers: customerDbResponse[]): Customer[] { return new Customer({
return dbCustomers.map((customer) => this.customerDto(customer)); id: dbCustomer.id,
} name: dbCustomer.name,
email: dbCustomer.email,
private customerDto(dbCustomer: customerDbResponse): Customer { imageUrl: dbCustomer.image_url,
return new Customer({ totalInvoices: dbCustomer.total_invoices,
id: dbCustomer.id, totalPending: formatCurrency(Number(dbCustomer.total_pending)),
name: dbCustomer.name, totalPaid: formatCurrency(Number(dbCustomer.total_paid)),
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 { export default class Customer {
id: string; id: string;
name: string;
email: string;
imageUrl: string;
totalInvoices: string;
totalPending: string;
totalPaid: string;
constructor({ name: string;
id,
email, email: string;
imageUrl,
name, imageUrl: string;
totalInvoices,
totalPaid, totalInvoices: string;
totalPending
}: Customer) { totalPending: string;
this.id = id;
this.name = name; totalPaid: string;
this.email = email;
this.imageUrl = imageUrl; constructor({
this.totalInvoices = totalInvoices; id,
this.totalPaid = totalPaid; email,
this.totalPending = totalPending; 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 ApiTask from "@/feature/common/data/api-task";
import Customer from "@/feature/core/customer/domain/entity/customer" import Customer from "@/feature/core/customer/domain/entity/customer";
export default interface CustomerRepo { export default interface CustomerRepo {
fetchList(query: string): ApiTask<Customer[]> fetchList(query: string): ApiTask<Customer[]>;
fetchCustomersAmount(): Promise<number> 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 serverDi from "@/feature/common/server-di";
import { customerKey } from "@/feature/core/customer/customer-key"; 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> { export default async function fetchCustomersAmountUsecase(): Promise<number> {
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey) const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey);
return repo.fetchCustomersAmount() 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 serverDi from "@/feature/common/server-di";
import { customerKey } from "@/feature/core/customer/customer-key"; import { customerKey } from "@/feature/core/customer/customer-key";
import Customer from "@/feature/core/customer/domain/entity/customer"; 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"; import { connection } from "next/server";
export default async function fetchCustomersUsecase(query: string): Promise<Customer[]> { export default function fetchCustomersUsecase(
connection() query: string,
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey) ): 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"; import { DependencyContainer } from "tsyringe";
export default function getInvoiceDi(): DependencyContainer { export default function getInvoiceDi(): DependencyContainer {
const invoiceDi = di.createChildContainer() const invoiceDi = di.createChildContainer();
invoiceDi.register(invoiceRepoKey, invoiceDbRepo) invoiceDi.register(invoiceRepoKey, invoiceDbRepo);
return invoiceDi return invoiceDi;
} }

View File

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

View File

@ -1,10 +1,12 @@
import { z } from "zod"; import { z } from "zod";
export const invoiceSchema = z.object({ export const invoiceSchema = z.object({
amount: z.coerce.number().gt(0, { message: 'Please enter an amount greater than $0.' }), amount: z.coerce
status: z.enum(['pending', 'paid'], { .number()
invalid_type_error: 'Please select an invoice status.', .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 { ApiEither } from "@/feature/common/data/api-task";
import ParamsFailure from "@/feature/common/failures/params-failure"; import ParamsFailure from "@/feature/common/failures/params-failure";
import serverDi from "@/feature/common/server-di"; import serverDi from "@/feature/common/server-di";
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo"; import InvoiceRepo, {
import { InvoiceParam, invoiceSchema } from "@/feature/core/invoice/domain/param/invoice-param"; 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 { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
import { pipe } from "fp-ts/lib/function"; import { pipe } from "fp-ts/lib/function";
import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither"; import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither";
export default async function createInvoiceUsecase(params: InvoiceParam): Promise<ApiEither<string>> { export default async function createInvoiceUsecase(
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey) params: InvoiceParam,
): Promise<ApiEither<string>> {
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
return pipe( return pipe(
fromNullable(new ParamsFailure())(params), fromNullable(new ParamsFailure())(params),
map((params) => invoiceSchema.safeParse(params)), map((params) => invoiceSchema.safeParse(params)),
chain((params) => { chain((params) => {
const isParamsValid = invoiceSchema.safeParse(params) const isParamsValid = invoiceSchema.safeParse(params);
if (!isParamsValid.success) left(new ParamsFailure()) if (!isParamsValid.success) left(new ParamsFailure());
return right(params.data as InvoiceParam) return right(params.data as InvoiceParam);
}), }),
chain((params) => repo.createInvoice(params)) chain((params) => repo.createInvoice(params)),
)() )();
} }

View File

@ -1,10 +1,13 @@
"use server" "use server";
import serverDi from "@/feature/common/server-di"; 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"; import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
export default async function fetchAllInvoicesAmountUsecase(): Promise<number> { 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 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 InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key"; import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
export default async function fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> { export default async function fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey) const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
return repo.fetchInvoicesStatusSummary() return repo.fetchInvoicesStatusSummary();
} }

View File

@ -1,12 +1,10 @@
export default class InvoiceStatusSummary { export default class InvoiceStatusSummary {
paid: string; paid: string;
pending: string;
constructor({ pending: string;
paid,
pending constructor({ paid, pending }: InvoiceStatusSummary) {
}: InvoiceStatusSummary) { this.paid = paid;
this.paid = paid; this.pending = pending;
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 di from "@/bootstrap/di/init-di";
import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db-repo" import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db-repo";
import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue-repo" import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue-repo";
export default function getRevenueDi() { export default function getRevenueDi() {
const revenueDi = di.createChildContainer() const revenueDi = di.createChildContainer();
revenueDi.register(revenueRepoKey, RevenueDbRepo) revenueDi.register(revenueRepoKey, RevenueDbRepo);
return revenueDi return revenueDi;
} }

View File

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

View File

@ -1,7 +1,7 @@
import Revenue from "@/feature/core/revenue/domain/entity/revenue"; import Revenue from "@/feature/core/revenue/domain/entity/revenue";
export default interface RevenueRepo { 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 serverDi from "@/feature/common/server-di";
import Revenue from "@/feature/core/revenue/domain/entity/revenue"; 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"; import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
export default async function fetchRevenuesUsecase(): Promise<Revenue[]> { export default async function fetchRevenuesUsecase(): Promise<Revenue[]> {
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey) const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey);
return repo.fetchRevenues() 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 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 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 fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary" import di from "@/bootstrap/di/init-di";
export default function getSummaryInfoDi() { export default function getSummaryInfoDi() {
const summaryInfoDi = di.createChildContainer() const summaryInfoDi = di.createChildContainer();
summaryInfoDi.register(fetchAllInvoicesAmountUsecase.name, { summaryInfoDi.register(fetchAllInvoicesAmountUsecase.name, {
useValue: fetchAllInvoicesAmountUsecase useValue: fetchAllInvoicesAmountUsecase,
}) });
summaryInfoDi.register(fetchCustomersAmountUsecase.name, { summaryInfoDi.register(fetchCustomersAmountUsecase.name, {
useValue: fetchCustomersAmountUsecase useValue: fetchCustomersAmountUsecase,
}) });
summaryInfoDi.register(fetchInvoicesStatusSummary.name, { summaryInfoDi.register(fetchInvoicesStatusSummary.name, {
useValue: fetchInvoicesStatusSummary useValue: fetchInvoicesStatusSummary,
}) });
return summaryInfoDi 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"; import { connection } from "next/server";
export default async function fetchSummaryInfoUsecase(): Promise<SummaryInfo> { export default async function fetchSummaryInfoUsecase(): Promise<SummaryInfo> {
connection() connection();
try{ try {
const summaryInfoDi = serverDi(summaryInfoModuleKey) const summaryInfoDi = serverDi(summaryInfoModuleKey);
const invoicesAmountPromise = summaryInfoDi.resolve<typeof fetchAllInvoicesAmountUsecase>(fetchAllInvoicesAmountUsecase.name)() const invoicesAmountPromise = summaryInfoDi.resolve<
const customersAmountPromise = summaryInfoDi.resolve<typeof fetchCustomersAmountUsecase>(fetchCustomersAmountUsecase.name)() typeof fetchAllInvoicesAmountUsecase
const invoiceSummaryPomise = summaryInfoDi.resolve<typeof fetchInvoicesStatusSummary>(fetchInvoicesStatusSummary.name)() >(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([ const [invoicesAmount, customersAmount, invoicesSummary] =
invoicesAmountPromise, await Promise.all([
customersAmountPromise, invoicesAmountPromise,
invoiceSummaryPomise, customersAmountPromise,
]); invoiceSummaryPomise,
]);
return new SummaryInfo({
return new SummaryInfo({ invoicesNumber: invoicesAmount,
invoicesNumber: invoicesAmount, customersNumber: customersAmount,
customersNumber: customersAmount, invoicesSummary,
invoicesSummary: invoicesSummary });
}) } catch {
} catch (error) { throw new Error("Failed to fetch card data.");
console.error('Database Error:', error); }
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"; import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
export default class SummaryInfo { export default class SummaryInfo {
customersNumber: number; customersNumber: number;
invoicesNumber: number;
invoicesSummary: InvoiceStatusSummary
constructor({ invoicesNumber: number;
customersNumber,
invoicesNumber, invoicesSummary: InvoiceStatusSummary;
invoicesSummary
}: SummaryInfo) { constructor({
this.customersNumber = customersNumber customersNumber,
this.invoicesNumber = invoicesNumber invoicesNumber,
this.invoicesSummary = invoicesSummary invoicesSummary,
} }: SummaryInfo) {
} this.customersNumber = customersNumber;
this.invoicesNumber = invoicesNumber;
this.invoicesSummary = invoicesSummary;
}
}

View File

@ -1,36 +1,41 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from "next/server";
import acceptLanguage from 'accept-language' import acceptLanguage from "accept-language";
import { cookieName, fallbackLng, languages } from '@/bootstrap/i18n/settings' import { cookieName, fallbackLng, languages } from "@/bootstrap/i18n/settings";
acceptLanguage.languages(languages) acceptLanguage.languages(languages);
export const config = { export const config = {
matcher: ["/((?!api|static|.*\\..*|_next).*)"] matcher: ["/((?!api|static|.*\\..*|_next).*)"],
} };
export function middleware(req: NextRequest) { export function middleware(req: NextRequest) {
let lng let lng;
if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req?.cookies?.get(cookieName)?.value) if (req.cookies.has(cookieName))
if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language')) lng = acceptLanguage.get(req?.cookies?.get(cookieName)?.value);
if (!lng) lng = fallbackLng if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"));
if (!lng) lng = fallbackLng;
// Redirect if lng in path is not supported // Redirect if lng in path is not supported
if ( if (
!languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) && !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
!req.nextUrl.pathname.startsWith('/_next') !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')) { if (req.headers.has("referer")) {
const refererUrl = new URL(req?.headers?.get('referer') ?? "") const refererUrl = new URL(req?.headers?.get("referer") ?? "");
const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`)) const lngInReferer = languages.find((l) =>
const response = NextResponse.next() refererUrl.pathname.startsWith(`/${l}`),
);
const response = NextResponse.next();
if (lngInReferer) { 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"; import { faker } from "@faker-js/faker";
export default class CustomerFakeFactory { export default class CustomerFakeFactory {
static getFakeCustomer(): Customer { static getFakeCustomer(): Customer {
return new Customer({ return new Customer({
id: faker.string.uuid(), id: faker.string.uuid(),
name: faker.person.fullName(), name: faker.person.fullName(),
email: faker.internet.email(), email: faker.internet.email(),
imageUrl: faker.image.url(), imageUrl: faker.image.url(),
totalInvoices: faker.number.int().toLocaleString(), totalInvoices: faker.number.int().toLocaleString(),
totalPaid: faker.finance.amount(), totalPaid: faker.finance.amount(),
totalPending: faker.number.int().toLocaleString(), totalPending: faker.number.int().toLocaleString(),
}) });
} }
static getFakeCustomerList(length: number = 10): Customer[] {
static getFakeCustomerList(length: number = 10): Customer[] { return Array.from({ length }).map(() =>
return Array.from({length}).map(() => CustomerFakeFactory.getFakeCustomer()) CustomerFakeFactory.getFakeCustomer(),
} );
} }
}

View File

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

View File

@ -1 +1 @@
import "reflect-metadata"; import "reflect-metadata";

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

View File

@ -620,6 +620,11 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== 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": "@radix-ui/react-compose-refs@1.1.0":
version "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" 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" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 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: console-control-strings@^1.0.0, console-control-strings@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" 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" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 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: eslint-config-next@15.0.1:
version "15.0.1" version "15.0.1"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.0.1.tgz#5f49a01d312420cdbf1e87299396ef779ae99004" 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 "^7.35.0"
eslint-plugin-react-hooks "^5.0.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: eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9:
version "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" 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" is-core-module "^2.13.0"
resolve "^1.22.4" 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" version "3.6.3"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz#bb8e388f6afc0f940ce5d2c5fd4a3d147f038d9e" 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== integrity sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==
@ -1927,6 +1966,14 @@ eslint-plugin-jsx-a11y@^6.10.0:
safe-regex-test "^1.0.3" safe-regex-test "^1.0.3"
string.prototype.includes "^2.0.1" 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: eslint-plugin-react-hooks@^5.0.0:
version "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" 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" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 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: fast-glob@3.3.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" 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" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== 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" version "4.1.5"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0"
integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== 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" has-symbols "^1.0.3"
object-keys "^1.1.1" object-keys "^1.1.1"
object.entries@^1.1.8: object.entries@^1.1.5, object.entries@^1.1.8:
version "1.1.8" version "1.1.8"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41"
integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== 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" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== 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: pretty-format@^27.0.2:
version "27.5.1" version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" 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" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0-rc-69d4b800-20241021.tgz#336e47ef2bd5eddb0ebacfd910b5df1b236d92bd"
integrity sha512-S5AYX/YhMAN6u9AXgKYbZP4U4ZklC6R9Q7HmFSBk7d4DLiHVNxvAvlSvuM4nxFkwOk50MnpfTKQ7UWHXDOc9Eg== 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" version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== 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" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== 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: tailwind-merge@^2.5.4:
version "2.5.4" version "2.5.4"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.4.tgz#4bf574e81fa061adeceba099ae4df56edcee78d1" 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" minimist "^1.2.6"
strip-bom "^3.0.0" 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" version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==