develop #3
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start --port 4000",
|
||||
"lint": "next lint",
|
||||
"lint": "next lint --fix",
|
||||
"test": "vitest",
|
||||
"seed": "node -r dotenv/config ./src/bootstrap/boundaries/db/seed.js"
|
||||
},
|
||||
@ -46,10 +46,16 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "15.0.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"jsdom": "^25.0.1",
|
||||
"moq.ts": "^10.0.8",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5",
|
||||
"vitest": "^2.1.4"
|
||||
|
@ -1,13 +1,13 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import Button from "@/app/components/button/button"
|
||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm"
|
||||
import { useDI } from "@/bootstrap/di/di-context"
|
||||
import { useRef } from "react"
|
||||
import Button from "@/app/components/button/button";
|
||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
||||
import { useDI } from "@/bootstrap/di/di-context";
|
||||
import { useRef } from "react";
|
||||
|
||||
export default function CreateRandomInvoiceContainer() {
|
||||
const di = useDI()
|
||||
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM))
|
||||
const di = useDI();
|
||||
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));
|
||||
|
||||
return <Button vm={vm.current}/>
|
||||
return <Button vm={vm.current} />;
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { DocumentIcon } from "@/app/components/icons/document";
|
||||
import HomeIcon from "@/app/components/icons/home";
|
||||
import { UserIcon } from "@/app/components/icons/user";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
type LinkItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: (props: { className?: string }) => JSX.Element;
|
||||
};
|
||||
export default function navLinkPersonalVM() {
|
||||
const pathname = usePathname();
|
||||
// Map of links to display in the side navigation.
|
||||
// Depending on the size of the application, this would be stored in a database.
|
||||
const links: LinkItem[] = [
|
||||
{ name: "Home", href: "/dashboard", icon: HomeIcon },
|
||||
{
|
||||
name: "Invoices",
|
||||
href: "/dashboard/invoices",
|
||||
icon: DocumentIcon,
|
||||
},
|
||||
{ name: "Customers", href: "/dashboard/customers", icon: UserIcon },
|
||||
];
|
||||
return {
|
||||
links,
|
||||
isLinkActive: (link: LinkItem) => pathname === link.href,
|
||||
};
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
import navLinkPersonalVM from '@/app/[lang]/dashboard/components/client/nav-links/nav-link-controller';
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link'
|
||||
"use client";
|
||||
|
||||
import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link-vm";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NavLinks() {
|
||||
const { links, isLinkActive } = navLinkPersonalVM()
|
||||
const { links, isLinkActive } = navLinkPersonalVM();
|
||||
return (
|
||||
<>
|
||||
{links.map((link) => {
|
||||
@ -14,9 +15,9 @@ export default function NavLinks() {
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
className={clsx(
|
||||
'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
|
||||
"flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3",
|
||||
{
|
||||
'bg-sky-100 text-blue-600': isLinkActive(link),
|
||||
"bg-sky-100 text-blue-600": isLinkActive(link),
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
@ -3,17 +3,19 @@ import {
|
||||
ClockIcon,
|
||||
UserGroupIcon,
|
||||
InboxIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
export default function cardController(props: { type: 'invoices' | 'customers' | 'pending' | 'collected'; }) {
|
||||
const { type } = props
|
||||
const iconMap = {
|
||||
collected: BanknotesIcon,
|
||||
customers: UserGroupIcon,
|
||||
pending: ClockIcon,
|
||||
invoices: InboxIcon,
|
||||
};
|
||||
return {
|
||||
Icon: iconMap[type]
|
||||
}
|
||||
export default function cardController(props: {
|
||||
type: "invoices" | "customers" | "pending" | "collected";
|
||||
}) {
|
||||
const { type } = props;
|
||||
const iconMap = {
|
||||
collected: BanknotesIcon,
|
||||
customers: UserGroupIcon,
|
||||
pending: ClockIcon,
|
||||
invoices: InboxIcon,
|
||||
};
|
||||
return {
|
||||
Icon: iconMap[type],
|
||||
};
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
import cardController from "@/app/[lang]/dashboard/components/server/card/card-controller";
|
||||
|
||||
|
||||
|
||||
export function Card({
|
||||
title,
|
||||
value,
|
||||
@ -9,9 +7,9 @@ export function Card({
|
||||
}: {
|
||||
title: string;
|
||||
value: number | string;
|
||||
type: 'invoices' | 'customers' | 'pending' | 'collected';
|
||||
type: "invoices" | "customers" | "pending" | "collected";
|
||||
}) {
|
||||
const { Icon } = cardController({type})
|
||||
const { Icon } = cardController({ type });
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-gray-50 p-2 shadow-sm">
|
||||
@ -19,9 +17,7 @@ export function Card({
|
||||
{Icon ? <Icon className="h-5 w-5 text-gray-700" /> : null}
|
||||
<h3 className="ml-2 text-sm font-medium">{title}</h3>
|
||||
</div>
|
||||
<p
|
||||
className="rounded-xl bg-white px-4 py-8 text-center text-2xl"
|
||||
>
|
||||
<p className="rounded-xl bg-white px-4 py-8 text-center text-2xl">
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info-usecase";
|
||||
|
||||
export default function cardsController() {
|
||||
return fetchSummaryInfoUsecase();
|
||||
return fetchSummaryInfoUsecase();
|
||||
}
|
@ -1,20 +1,16 @@
|
||||
import { Card } from '@/app/[lang]/dashboard/components/server/card/card';
|
||||
import cardsController from '@/app/[lang]/dashboard/components/server/cards/cards-controller';
|
||||
|
||||
import { Card } from "@/app/[lang]/dashboard/components/server/card/card";
|
||||
import cardsController from "@/app/[lang]/dashboard/components/server/cards/cards-controller";
|
||||
|
||||
export default async function CardWrapper() {
|
||||
const {customersNumber, invoicesNumber, invoicesSummary } = await cardsController();
|
||||
const { customersNumber, invoicesNumber, invoicesSummary } =
|
||||
await cardsController();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title="Collected" value={invoicesSummary.paid} type="collected" />
|
||||
<Card title="Pending" value={invoicesSummary.pending} type="pending" />
|
||||
<Card title="Total Invoices" value={invoicesNumber} type="invoices" />
|
||||
<Card
|
||||
title="Total Customers"
|
||||
value={customersNumber}
|
||||
type="customers"
|
||||
/>
|
||||
<Card title="Total Customers" value={customersNumber} type="customers" />
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase";
|
||||
|
||||
export default async function latestInvoicesController() {
|
||||
return await fetchCustomerInvoicesUsecase()
|
||||
export default function latestInvoicesController() {
|
||||
return fetchCustomerInvoicesUsecase();
|
||||
}
|
@ -1,61 +1,49 @@
|
||||
import CreateRandomInvoiceContainer from '@/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice';
|
||||
import latestInvoicesController from '@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller';
|
||||
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||
import clsx from 'clsx';
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import Image from 'next/image';
|
||||
import CreateRandomInvoiceContainer from "@/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice";
|
||||
import latestInvoicesController from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { isLeft } from "fp-ts/lib/Either";
|
||||
import Image from "next/image";
|
||||
|
||||
export default async function LatestInvoices() {
|
||||
const latestInvoices = await latestInvoicesController();
|
||||
|
||||
if (isLeft(latestInvoices)) return <div>Error</div>
|
||||
if (isLeft(latestInvoices)) return <div>Error</div>;
|
||||
|
||||
const invoices = latestInvoices.right.map((invoice, i) => {
|
||||
return (
|
||||
<div
|
||||
key={invoice.id}
|
||||
className={clsx(
|
||||
'flex flex-row items-center justify-between py-4',
|
||||
{
|
||||
'border-t': i !== 0,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={invoice.customerImageUrl}
|
||||
alt={`${invoice.customerName}'s profile picture`}
|
||||
className="mr-4 rounded-full"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold md:text-base">
|
||||
{invoice.customerName}
|
||||
</p>
|
||||
<p className="hidden text-sm text-gray-500 sm:block">
|
||||
{invoice.customerEmail}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className="truncate text-sm font-medium md:text-base"
|
||||
>
|
||||
{invoice.invoicesAmount}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
const invoices = latestInvoices.right.map((invoice, i) => (
|
||||
<div
|
||||
key={invoice.id}
|
||||
className={clsx("flex flex-row items-center justify-between py-4", {
|
||||
"border-t": i !== 0,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={invoice.customerImageUrl}
|
||||
alt={`${invoice.customerName}'s profile picture`}
|
||||
className="mr-4 rounded-full"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold md:text-base">
|
||||
{invoice.customerName}
|
||||
</p>
|
||||
<p className="hidden text-sm text-gray-500 sm:block">
|
||||
{invoice.customerEmail}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="truncate text-sm font-medium md:text-base">
|
||||
{invoice.invoicesAmount}
|
||||
</p>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div className="flex w-full flex-col md:col-span-4">
|
||||
<h2 className="mb-4 text-xl md:text-2xl">
|
||||
Latest Invoices
|
||||
</h2>
|
||||
<h2 className="mb-4 text-xl md:text-2xl">Latest Invoices</h2>
|
||||
<div className="flex grow flex-col max-h-[66.5vh] justify-between rounded-xl bg-gray-50 p-4">
|
||||
|
||||
<div className="bg-white px-6 h-full overflow-y-auto">
|
||||
{invoices}
|
||||
</div>
|
||||
<div className="bg-white px-6 h-full overflow-y-auto">{invoices}</div>
|
||||
<div className="flex items-end mt-auto pb-2 pt-6">
|
||||
<ArrowPathIcon className="h-5 w-5 text-gray-500" />
|
||||
<h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>
|
||||
|
@ -11,8 +11,8 @@ export default async function revenueChartController() {
|
||||
revenue,
|
||||
chartHeight,
|
||||
yAxisLabels,
|
||||
topLabel
|
||||
}
|
||||
topLabel,
|
||||
};
|
||||
}
|
||||
|
||||
function generateYAxis(revenue: Revenue[]) {
|
||||
@ -27,4 +27,4 @@ function generateYAxis(revenue: Revenue[]) {
|
||||
}
|
||||
|
||||
return { yAxisLabels, topLabel };
|
||||
};
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import revenueChartController from '@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller';
|
||||
import { CalendarIcon } from '@heroicons/react/24/outline';
|
||||
import revenueChartController from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller";
|
||||
import { CalendarIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export default async function RevenueChart() {
|
||||
const { chartHeight, revenue, topLabel, yAxisLabels } = await revenueChartController()
|
||||
const { chartHeight, revenue, topLabel, yAxisLabels } =
|
||||
await revenueChartController();
|
||||
|
||||
if (!revenue || revenue.length === 0) {
|
||||
return <p className="mt-4 text-gray-400">No data available.</p>;
|
||||
@ -10,9 +11,7 @@ export default async function RevenueChart() {
|
||||
|
||||
return (
|
||||
<div className="w-full md:col-span-4">
|
||||
<h2 className={` mb-4 text-xl md:text-2xl`}>
|
||||
Recent Revenue
|
||||
</h2>
|
||||
<h2 className={` mb-4 text-xl md:text-2xl`}>Recent Revenue</h2>
|
||||
<div className="rounded-xl bg-gray-50 p-4">
|
||||
<div className="sm:grid-cols-13 mt-0 grid grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4">
|
||||
<div
|
||||
@ -31,7 +30,7 @@ export default async function RevenueChart() {
|
||||
style={{
|
||||
height: `${(chartHeight / topLabel) * month.revenue}px`,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
<p className="-rotate-90 text-sm text-gray-400 sm:rotate-0">
|
||||
{month.month}
|
||||
</p>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import NavLinks from '@/app/[lang]/dashboard/components/client/nav-links/nav-links';
|
||||
import Link from 'next/link';
|
||||
import NavLinks from "@/app/[lang]/dashboard/components/client/nav-links/nav-links";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function SideNav() {
|
||||
return (
|
||||
@ -8,13 +8,11 @@ export default function SideNav() {
|
||||
className="mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40"
|
||||
href="/"
|
||||
>
|
||||
<div className="w-32 text-white md:w-40">
|
||||
Home
|
||||
</div>
|
||||
<div className="w-32 text-white md:w-40">Home</div>
|
||||
</Link>
|
||||
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
|
||||
<NavLinks />
|
||||
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
|
||||
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Loading animation
|
||||
const shimmer =
|
||||
'before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent';
|
||||
"before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent";
|
||||
|
||||
export function CardSkeleton() {
|
||||
return (
|
||||
@ -106,33 +106,36 @@ export function TableRowSkeleton() {
|
||||
return (
|
||||
<tr className="w-full border-b border-gray-100 last-of-type:border-none [&:first-child>td:first-child]:rounded-tl-lg [&:first-child>td:last-child]:rounded-tr-lg [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg">
|
||||
{/* Customer Name and Image */}
|
||||
<td className="relative overflow-hidden whitespace-nowrap py-3 pl-6 pr-3">
|
||||
<td
|
||||
aria-label="Customer name"
|
||||
className="relative overflow-hidden whitespace-nowrap py-3 pl-6 pr-3"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-gray-100"></div>
|
||||
<div className="h-6 w-24 rounded bg-gray-100"></div>
|
||||
<div className="h-8 w-8 rounded-full bg-gray-100" />
|
||||
<div className="h-6 w-24 rounded bg-gray-100" />
|
||||
</div>
|
||||
</td>
|
||||
{/* Email */}
|
||||
<td className="whitespace-nowrap px-3 py-3">
|
||||
<div className="h-6 w-32 rounded bg-gray-100"></div>
|
||||
<td aria-label="Email" className="whitespace-nowrap px-3 py-3">
|
||||
<div className="h-6 w-32 rounded bg-gray-100" />
|
||||
</td>
|
||||
{/* Amount */}
|
||||
<td className="whitespace-nowrap px-3 py-3">
|
||||
<div className="h-6 w-16 rounded bg-gray-100"></div>
|
||||
<td aria-label="Amount" className="whitespace-nowrap px-3 py-3">
|
||||
<div className="h-6 w-16 rounded bg-gray-100" />
|
||||
</td>
|
||||
{/* Date */}
|
||||
<td className="whitespace-nowrap px-3 py-3">
|
||||
<div className="h-6 w-16 rounded bg-gray-100"></div>
|
||||
<td aria-label="Date" className="whitespace-nowrap px-3 py-3">
|
||||
<div className="h-6 w-16 rounded bg-gray-100" />
|
||||
</td>
|
||||
{/* Status */}
|
||||
<td className="whitespace-nowrap px-3 py-3">
|
||||
<div className="h-6 w-16 rounded bg-gray-100"></div>
|
||||
<td aria-label="Status" className="whitespace-nowrap px-3 py-3">
|
||||
<div className="h-6 w-16 rounded bg-gray-100" />
|
||||
</td>
|
||||
{/* Actions */}
|
||||
<td className="whitespace-nowrap py-3 pl-6 pr-3">
|
||||
<td aria-label="Actions" className="whitespace-nowrap py-3 pl-6 pr-3">
|
||||
<div className="flex justify-end gap-3">
|
||||
<div className="h-[38px] w-[38px] rounded bg-gray-100"></div>
|
||||
<div className="h-[38px] w-[38px] rounded bg-gray-100"></div>
|
||||
<div className="h-[38px] w-[38px] rounded bg-gray-100" />
|
||||
<div className="h-[38px] w-[38px] rounded bg-gray-100" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -144,19 +147,19 @@ export function InvoicesMobileSkeleton() {
|
||||
<div className="mb-2 w-full rounded-md bg-white p-4">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-8">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2 h-8 w-8 rounded-full bg-gray-100"></div>
|
||||
<div className="h-6 w-16 rounded bg-gray-100"></div>
|
||||
<div className="mr-2 h-8 w-8 rounded-full bg-gray-100" />
|
||||
<div className="h-6 w-16 rounded bg-gray-100" />
|
||||
</div>
|
||||
<div className="h-6 w-16 rounded bg-gray-100"></div>
|
||||
<div className="h-6 w-16 rounded bg-gray-100" />
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between pt-4">
|
||||
<div>
|
||||
<div className="h-6 w-16 rounded bg-gray-100"></div>
|
||||
<div className="mt-2 h-6 w-24 rounded bg-gray-100"></div>
|
||||
<div className="h-6 w-16 rounded bg-gray-100" />
|
||||
<div className="mt-2 h-6 w-24 rounded bg-gray-100" />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="h-10 w-10 rounded bg-gray-100"></div>
|
||||
<div className="h-10 w-10 rounded bg-gray-100"></div>
|
||||
<div className="h-10 w-10 rounded bg-gray-100" />
|
||||
<div className="h-10 w-10 rounded bg-gray-100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,19 +1,21 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import SideNav from "@/app/[lang]/dashboard/components/server/sidenav";
|
||||
import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard-app-module";
|
||||
import { DiContext } from "@/bootstrap/di/di-context";
|
||||
import { useRef } from "react";
|
||||
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const di = useRef(dashboardAppModule())
|
||||
const di = useRef(dashboardAppModule());
|
||||
return (
|
||||
<DiContext.Provider value={di.current}>
|
||||
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
|
||||
<div className="w-full flex-none md:w-64">
|
||||
<SideNav />
|
||||
</div>
|
||||
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
|
||||
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</DiContext.Provider>
|
||||
);
|
||||
|
@ -1,13 +1,16 @@
|
||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
||||
import di from "@/bootstrap/di/init-di"
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
|
||||
|
||||
export default function dashboardAppModule() {
|
||||
const dashboardDi = di.createChildContainer()
|
||||
const dashboardDi = di.createChildContainer();
|
||||
|
||||
dashboardDi.register(createInvoiceUsecase.name, {
|
||||
useValue: createInvoiceUsecase
|
||||
})
|
||||
dashboardDi.register(CreateRandomInvoiceButtonVM, CreateRandomInvoiceButtonVM)
|
||||
return dashboardDi
|
||||
dashboardDi.register(createInvoiceUsecase.name, {
|
||||
useValue: createInvoiceUsecase,
|
||||
});
|
||||
dashboardDi.register(
|
||||
CreateRandomInvoiceButtonVM,
|
||||
CreateRandomInvoiceButtonVM,
|
||||
);
|
||||
return dashboardDi;
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { LatestInvoicesSkeleton, RevenueChartSkeleton } from "@/app/[lang]/dashboard/components/server/skeletons/skeletons";
|
||||
import {
|
||||
LatestInvoicesSkeleton,
|
||||
RevenueChartSkeleton,
|
||||
} from "@/app/[lang]/dashboard/components/server/skeletons/skeletons";
|
||||
import CardWrapper from "@/app/[lang]/dashboard/components/server/cards/cards";
|
||||
import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices";
|
||||
import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart";
|
||||
@ -6,12 +9,15 @@ import { Suspense } from "react";
|
||||
import { getServerTranslation } from "@/bootstrap/i18n/i18n";
|
||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
||||
|
||||
export default async function Dashboard(props: {params: Promise<{lang: string}>}) {
|
||||
const {lang} = await props.params
|
||||
const { t } = await getServerTranslation(lang)
|
||||
export default async function Dashboard(props: {
|
||||
params: Promise<{ lang: string }>;
|
||||
}) {
|
||||
const { params } = props;
|
||||
const { lang } = await params;
|
||||
const { t } = await getServerTranslation(lang);
|
||||
return (
|
||||
<main>
|
||||
<h1 className={`mb-4 text-xl md:text-2xl`}>
|
||||
<h1 className="mb-4 text-xl md:text-2xl">
|
||||
{t(langKey.global.dashboard)}
|
||||
</h1>
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@ -19,12 +25,12 @@ export default async function Dashboard(props: {params: Promise<{lang: string}>}
|
||||
</div>
|
||||
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
|
||||
<Suspense fallback={<RevenueChartSkeleton />}>
|
||||
<RevenueChart />
|
||||
<RevenueChart />
|
||||
</Suspense>
|
||||
<Suspense fallback={<LatestInvoicesSkeleton />}>
|
||||
<LatestInvoices />
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -8,39 +8,46 @@ import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-i
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> {
|
||||
private createInvoice: typeof createInvoiceUsecase
|
||||
private createInvoice: typeof createInvoiceUsecase;
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.createInvoice = this.di.resolve(createInvoiceUsecase.name)
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.createInvoice = this.di.resolve(createInvoiceUsecase.name);
|
||||
}
|
||||
|
||||
useVM(): ButtonVm {
|
||||
const router = useRouter()
|
||||
const [action, isPending] = useServerAction(() => this.onClickHandler(router.refresh))
|
||||
const throttledOnClick = useThrottle(action, 5000)
|
||||
useVM(): ButtonVm {
|
||||
const router = useRouter();
|
||||
const [action, isPending] = useServerAction(() =>
|
||||
this.onClickHandler(router.refresh),
|
||||
);
|
||||
const throttledOnClick = useThrottle(action, 5000);
|
||||
|
||||
const {t} = useTranslation()
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
props: {
|
||||
title: t(isPending ? langKey.global.loading : langKey.dashboard.invoice.createButton),
|
||||
isDisable: isPending ? true : false
|
||||
},
|
||||
onClick: throttledOnClick.bind(this)
|
||||
}
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
title: t(
|
||||
isPending
|
||||
? langKey.global.loading
|
||||
: langKey.dashboard.invoice.createButton,
|
||||
),
|
||||
isDisable: !!isPending,
|
||||
},
|
||||
onClick: throttledOnClick.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
async onClickHandler(refreshPage: () => void) {
|
||||
const fakedParams: InvoiceParam = {
|
||||
amount: faker.number.int({
|
||||
min: 1,
|
||||
max: 10
|
||||
}),
|
||||
status: "paid"
|
||||
}
|
||||
await this.createInvoice(fakedParams)
|
||||
refreshPage()
|
||||
}
|
||||
async onClickHandler(refreshPage: () => void) {
|
||||
const fakedParams: InvoiceParam = {
|
||||
amount: faker.number.int({
|
||||
min: 1,
|
||||
max: 10,
|
||||
}),
|
||||
status: "paid",
|
||||
};
|
||||
await this.createInvoice(fakedParams);
|
||||
refreshPage();
|
||||
}
|
||||
}
|
@ -2,8 +2,14 @@ import { initI18next } from "@/bootstrap/i18n/i18n";
|
||||
import TranslationsProvider from "@/bootstrap/i18n/i18n-provider";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export default async function layout(props: PropsWithChildren & {params: Promise<{lang: string}>}) {
|
||||
const lang = (await props.params).lang
|
||||
const { resources} = await initI18next({lng: lang})
|
||||
return <TranslationsProvider lng={lang} resources={resources}>{props.children}</TranslationsProvider>
|
||||
export default async function layout(
|
||||
props: PropsWithChildren & { params: Promise<{ lang: string }> },
|
||||
) {
|
||||
const { lang } = await props.params;
|
||||
const { resources } = await initI18next({ lng: lang });
|
||||
return (
|
||||
<TranslationsProvider lng={lang} resources={resources}>
|
||||
{props.children}
|
||||
</TranslationsProvider>
|
||||
);
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col p-6">
|
||||
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
|
||||
<div className="flex flex-col justify-center gap-6 rounded-lg bg-gray-50 px-6 py-10 md:w-2/5 md:px-20">
|
||||
<div />
|
||||
<p className={`text-xl text-gray-800 md:text-3xl md:leading-normal`}>
|
||||
<strong>Welcome to Acme.</strong> This is the example for the{' '}
|
||||
|
||||
, brought to you by Vercel.
|
||||
<div />
|
||||
<p className="text-xl text-gray-800 md:text-3xl md:leading-normal">
|
||||
<strong>Welcome to Acme.</strong> This is the example for the ,
|
||||
brought to you by Vercel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
export default interface ButtonVm {
|
||||
props: {
|
||||
title: string;
|
||||
isDisable: boolean;
|
||||
}
|
||||
onClick(): void
|
||||
props: {
|
||||
title: string;
|
||||
isDisable: boolean;
|
||||
};
|
||||
onClick(): void;
|
||||
}
|
@ -1,21 +1,25 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
|
||||
import ButtonVm from "@/app/components/button/button-vm";
|
||||
import { ReactNode } from "react";
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/bootstrap/helpers/lib/ui-utils";
|
||||
|
||||
export default class Button extends BaseView<ButtonVm> {
|
||||
protected Build(props: BuildProps<ButtonVm>): ReactNode {
|
||||
const {vm} = props
|
||||
protected Build(props: BuildProps<ButtonVm>): ReactNode {
|
||||
const { vm } = props;
|
||||
|
||||
return <ButtonUi disabled={vm.props.isDisable} onClick={vm.onClick} >{vm.props.title}</ButtonUi>
|
||||
}
|
||||
return (
|
||||
<ButtonUi disabled={vm.props.isDisable} onClick={vm.onClick}>
|
||||
{vm.props.title}
|
||||
</ButtonUi>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
@ -43,27 +47,28 @@ const buttonVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const ButtonUi = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
ButtonUi.displayName = "Button"
|
||||
);
|
||||
},
|
||||
);
|
||||
ButtonUi.displayName = "Button";
|
||||
|
||||
export { buttonVariants }
|
||||
export { buttonVariants };
|
||||
|
@ -1,5 +1,20 @@
|
||||
export function DocumentIcon(props: {className?: string}) {
|
||||
return (
|
||||
<svg className={props.className} width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
|
||||
)
|
||||
export function DocumentIcon(props: { className?: string }) {
|
||||
const { className } = props;
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,5 +1,20 @@
|
||||
export default function HomeIcon(props: {className?: string}) {
|
||||
return (
|
||||
<svg className={props.className} width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.07926 0.222253C7.31275 -0.007434 7.6873 -0.007434 7.92079 0.222253L14.6708 6.86227C14.907 7.09465 14.9101 7.47453 14.6778 7.71076C14.4454 7.947 14.0655 7.95012 13.8293 7.71773L13 6.90201V12.5C13 12.7761 12.7762 13 12.5 13H2.50002C2.22388 13 2.00002 12.7761 2.00002 12.5V6.90201L1.17079 7.71773C0.934558 7.95012 0.554672 7.947 0.32229 7.71076C0.0899079 7.47453 0.0930283 7.09465 0.32926 6.86227L7.07926 0.222253ZM7.50002 1.49163L12 5.91831V12H10V8.49999C10 8.22385 9.77617 7.99999 9.50002 7.99999H6.50002C6.22388 7.99999 6.00002 8.22385 6.00002 8.49999V12H3.00002V5.91831L7.50002 1.49163ZM7.00002 12H9.00002V8.99999H7.00002V12Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
|
||||
)
|
||||
export default function HomeIcon(props: { className?: string }) {
|
||||
const { className } = props;
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.07926 0.222253C7.31275 -0.007434 7.6873 -0.007434 7.92079 0.222253L14.6708 6.86227C14.907 7.09465 14.9101 7.47453 14.6778 7.71076C14.4454 7.947 14.0655 7.95012 13.8293 7.71773L13 6.90201V12.5C13 12.7761 12.7762 13 12.5 13H2.50002C2.22388 13 2.00002 12.7761 2.00002 12.5V6.90201L1.17079 7.71773C0.934558 7.95012 0.554672 7.947 0.32229 7.71076C0.0899079 7.47453 0.0930283 7.09465 0.32926 6.86227L7.07926 0.222253ZM7.50002 1.49163L12 5.91831V12H10V8.49999C10 8.22385 9.77617 7.99999 9.50002 7.99999H6.50002C6.22388 7.99999 6.00002 8.22385 6.00002 8.49999V12H3.00002V5.91831L7.50002 1.49163ZM7.00002 12H9.00002V8.99999H7.00002V12Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,7 +1,19 @@
|
||||
export function UserIcon(props: {className?: string}) {
|
||||
return (
|
||||
<svg className={props.className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
)
|
||||
export function UserIcon(props: { className?: string }) {
|
||||
const { className } = props;
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
@ -23,7 +24,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html>
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
|
@ -1,13 +1,12 @@
|
||||
import postgres from "postgres";
|
||||
|
||||
|
||||
const envs = process.env;
|
||||
const dbConfigs = {
|
||||
host: envs.POSTGRES_HOST,
|
||||
port: Number(envs.POSTGRES_PORT),
|
||||
username: envs.POSTGRES_USER,
|
||||
password: envs.POSTGRES_PASS,
|
||||
database: envs.POSTGRES_DB,
|
||||
}
|
||||
host: envs.POSTGRES_HOST,
|
||||
port: Number(envs.POSTGRES_PORT),
|
||||
username: envs.POSTGRES_USER,
|
||||
password: envs.POSTGRES_PASS,
|
||||
database: envs.POSTGRES_DB,
|
||||
};
|
||||
|
||||
export const sql = postgres(dbConfigs);
|
||||
|
@ -2,73 +2,73 @@
|
||||
// https://nextjs.org/learn/dashboard-app/fetching-data
|
||||
const users = [
|
||||
{
|
||||
id: '410544b2-4001-4271-9855-fec4b6a6442a',
|
||||
name: 'User',
|
||||
email: 'user@nextmail.com',
|
||||
password: '123456',
|
||||
id: "410544b2-4001-4271-9855-fec4b6a6442a",
|
||||
name: "User",
|
||||
email: "user@nextmail.com",
|
||||
password: "123456",
|
||||
},
|
||||
];
|
||||
|
||||
const customers = [
|
||||
{
|
||||
id: '3958dc9e-712f-4377-85e9-fec4b6a6442a',
|
||||
name: 'Delba de Oliveira',
|
||||
email: 'delba@oliveira.com',
|
||||
image_url: '/customers/delba-de-oliveira.png',
|
||||
id: "3958dc9e-712f-4377-85e9-fec4b6a6442a",
|
||||
name: "Delba de Oliveira",
|
||||
email: "delba@oliveira.com",
|
||||
image_url: "/customers/delba-de-oliveira.png",
|
||||
},
|
||||
{
|
||||
id: '3958dc9e-742f-4377-85e9-fec4b6a6442a',
|
||||
name: 'Lee Robinson',
|
||||
email: 'lee@robinson.com',
|
||||
image_url: '/customers/lee-robinson.png',
|
||||
id: "3958dc9e-742f-4377-85e9-fec4b6a6442a",
|
||||
name: "Lee Robinson",
|
||||
email: "lee@robinson.com",
|
||||
image_url: "/customers/lee-robinson.png",
|
||||
},
|
||||
{
|
||||
id: '3958dc9e-737f-4377-85e9-fec4b6a6442a',
|
||||
name: 'Hector Simpson',
|
||||
email: 'hector@simpson.com',
|
||||
image_url: '/customers/hector-simpson.png',
|
||||
id: "3958dc9e-737f-4377-85e9-fec4b6a6442a",
|
||||
name: "Hector Simpson",
|
||||
email: "hector@simpson.com",
|
||||
image_url: "/customers/hector-simpson.png",
|
||||
},
|
||||
{
|
||||
id: '50ca3e18-62cd-11ee-8c99-0242ac120002',
|
||||
name: 'Steven Tey',
|
||||
email: 'steven@tey.com',
|
||||
image_url: '/customers/steven-tey.png',
|
||||
id: "50ca3e18-62cd-11ee-8c99-0242ac120002",
|
||||
name: "Steven Tey",
|
||||
email: "steven@tey.com",
|
||||
image_url: "/customers/steven-tey.png",
|
||||
},
|
||||
{
|
||||
id: '3958dc9e-787f-4377-85e9-fec4b6a6442a',
|
||||
name: 'Steph Dietz',
|
||||
email: 'steph@dietz.com',
|
||||
image_url: '/customers/steph-dietz.png',
|
||||
id: "3958dc9e-787f-4377-85e9-fec4b6a6442a",
|
||||
name: "Steph Dietz",
|
||||
email: "steph@dietz.com",
|
||||
image_url: "/customers/steph-dietz.png",
|
||||
},
|
||||
{
|
||||
id: '76d65c26-f784-44a2-ac19-586678f7c2f2',
|
||||
name: 'Michael Novotny',
|
||||
email: 'michael@novotny.com',
|
||||
image_url: '/customers/michael-novotny.png',
|
||||
id: "76d65c26-f784-44a2-ac19-586678f7c2f2",
|
||||
name: "Michael Novotny",
|
||||
email: "michael@novotny.com",
|
||||
image_url: "/customers/michael-novotny.png",
|
||||
},
|
||||
{
|
||||
id: 'd6e15727-9fe1-4961-8c5b-ea44a9bd81aa',
|
||||
name: 'Evil Rabbit',
|
||||
email: 'evil@rabbit.com',
|
||||
image_url: '/customers/evil-rabbit.png',
|
||||
id: "d6e15727-9fe1-4961-8c5b-ea44a9bd81aa",
|
||||
name: "Evil Rabbit",
|
||||
email: "evil@rabbit.com",
|
||||
image_url: "/customers/evil-rabbit.png",
|
||||
},
|
||||
{
|
||||
id: '126eed9c-c90c-4ef6-a4a8-fcf7408d3c66',
|
||||
name: 'Emil Kowalski',
|
||||
email: 'emil@kowalski.com',
|
||||
image_url: '/customers/emil-kowalski.png',
|
||||
id: "126eed9c-c90c-4ef6-a4a8-fcf7408d3c66",
|
||||
name: "Emil Kowalski",
|
||||
email: "emil@kowalski.com",
|
||||
image_url: "/customers/emil-kowalski.png",
|
||||
},
|
||||
{
|
||||
id: 'CC27C14A-0ACF-4F4A-A6C9-D45682C144B9',
|
||||
name: 'Amy Burns',
|
||||
email: 'amy@burns.com',
|
||||
image_url: '/customers/amy-burns.png',
|
||||
id: "CC27C14A-0ACF-4F4A-A6C9-D45682C144B9",
|
||||
name: "Amy Burns",
|
||||
email: "amy@burns.com",
|
||||
image_url: "/customers/amy-burns.png",
|
||||
},
|
||||
{
|
||||
id: '13D07535-C59E-4157-A011-F8D2EF4E0CBB',
|
||||
name: 'Balazs Orban',
|
||||
email: 'balazs@orban.com',
|
||||
image_url: '/customers/balazs-orban.png',
|
||||
id: "13D07535-C59E-4157-A011-F8D2EF4E0CBB",
|
||||
name: "Balazs Orban",
|
||||
email: "balazs@orban.com",
|
||||
image_url: "/customers/balazs-orban.png",
|
||||
},
|
||||
];
|
||||
|
||||
@ -76,108 +76,108 @@ const invoices = [
|
||||
{
|
||||
customer_id: customers[0].id,
|
||||
amount: 15795,
|
||||
status: 'pending',
|
||||
date: '2022-12-06',
|
||||
status: "pending",
|
||||
date: "2022-12-06",
|
||||
},
|
||||
{
|
||||
customer_id: customers[1].id,
|
||||
amount: 20348,
|
||||
status: 'pending',
|
||||
date: '2022-11-14',
|
||||
status: "pending",
|
||||
date: "2022-11-14",
|
||||
},
|
||||
{
|
||||
customer_id: customers[4].id,
|
||||
amount: 3040,
|
||||
status: 'paid',
|
||||
date: '2022-10-29',
|
||||
status: "paid",
|
||||
date: "2022-10-29",
|
||||
},
|
||||
{
|
||||
customer_id: customers[3].id,
|
||||
amount: 44800,
|
||||
status: 'paid',
|
||||
date: '2023-09-10',
|
||||
status: "paid",
|
||||
date: "2023-09-10",
|
||||
},
|
||||
{
|
||||
customer_id: customers[5].id,
|
||||
amount: 34577,
|
||||
status: 'pending',
|
||||
date: '2023-08-05',
|
||||
status: "pending",
|
||||
date: "2023-08-05",
|
||||
},
|
||||
{
|
||||
customer_id: customers[7].id,
|
||||
amount: 54246,
|
||||
status: 'pending',
|
||||
date: '2023-07-16',
|
||||
status: "pending",
|
||||
date: "2023-07-16",
|
||||
},
|
||||
{
|
||||
customer_id: customers[6].id,
|
||||
amount: 666,
|
||||
status: 'pending',
|
||||
date: '2023-06-27',
|
||||
status: "pending",
|
||||
date: "2023-06-27",
|
||||
},
|
||||
{
|
||||
customer_id: customers[3].id,
|
||||
amount: 32545,
|
||||
status: 'paid',
|
||||
date: '2023-06-09',
|
||||
status: "paid",
|
||||
date: "2023-06-09",
|
||||
},
|
||||
{
|
||||
customer_id: customers[4].id,
|
||||
amount: 1250,
|
||||
status: 'paid',
|
||||
date: '2023-06-17',
|
||||
status: "paid",
|
||||
date: "2023-06-17",
|
||||
},
|
||||
{
|
||||
customer_id: customers[5].id,
|
||||
amount: 8546,
|
||||
status: 'paid',
|
||||
date: '2023-06-07',
|
||||
status: "paid",
|
||||
date: "2023-06-07",
|
||||
},
|
||||
{
|
||||
customer_id: customers[1].id,
|
||||
amount: 500,
|
||||
status: 'paid',
|
||||
date: '2023-08-19',
|
||||
status: "paid",
|
||||
date: "2023-08-19",
|
||||
},
|
||||
{
|
||||
customer_id: customers[5].id,
|
||||
amount: 8945,
|
||||
status: 'paid',
|
||||
date: '2023-06-03',
|
||||
status: "paid",
|
||||
date: "2023-06-03",
|
||||
},
|
||||
{
|
||||
customer_id: customers[2].id,
|
||||
amount: 8945,
|
||||
status: 'paid',
|
||||
date: '2023-06-18',
|
||||
status: "paid",
|
||||
date: "2023-06-18",
|
||||
},
|
||||
{
|
||||
customer_id: customers[0].id,
|
||||
amount: 8945,
|
||||
status: 'paid',
|
||||
date: '2023-10-04',
|
||||
status: "paid",
|
||||
date: "2023-10-04",
|
||||
},
|
||||
{
|
||||
customer_id: customers[2].id,
|
||||
amount: 1000,
|
||||
status: 'paid',
|
||||
date: '2022-06-05',
|
||||
status: "paid",
|
||||
date: "2022-06-05",
|
||||
},
|
||||
];
|
||||
|
||||
const revenue = [
|
||||
{ month: 'Jan', revenue: 2000 },
|
||||
{ month: 'Feb', revenue: 1800 },
|
||||
{ month: 'Mar', revenue: 2200 },
|
||||
{ month: 'Apr', revenue: 2500 },
|
||||
{ month: 'May', revenue: 2300 },
|
||||
{ month: 'Jun', revenue: 3200 },
|
||||
{ month: 'Jul', revenue: 3500 },
|
||||
{ month: 'Aug', revenue: 3700 },
|
||||
{ month: 'Sep', revenue: 2500 },
|
||||
{ month: 'Oct', revenue: 2800 },
|
||||
{ month: 'Nov', revenue: 3000 },
|
||||
{ month: 'Dec', revenue: 4800 },
|
||||
{ month: "Jan", revenue: 2000 },
|
||||
{ month: "Feb", revenue: 1800 },
|
||||
{ month: "Mar", revenue: 2200 },
|
||||
{ month: "Apr", revenue: 2500 },
|
||||
{ month: "May", revenue: 2300 },
|
||||
{ month: "Jun", revenue: 3200 },
|
||||
{ month: "Jul", revenue: 3500 },
|
||||
{ month: "Aug", revenue: 3700 },
|
||||
{ month: "Sep", revenue: 2500 },
|
||||
{ month: "Oct", revenue: 2800 },
|
||||
{ month: "Nov", revenue: 3000 },
|
||||
{ month: "Dec", revenue: 4800 },
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
@ -1,12 +1,14 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const bcrypt = require("bcrypt");
|
||||
const postgres = require("postgres");
|
||||
const {
|
||||
invoices,
|
||||
customers,
|
||||
revenue,
|
||||
users,
|
||||
} = require('./placeholder-data.js');
|
||||
const bcrypt = require('bcrypt');
|
||||
const postgres = require('postgres');
|
||||
// eslint-disable-next-line import/extensions
|
||||
} = require("./placeholder-data.js");
|
||||
|
||||
async function seedUsers(sql) {
|
||||
try {
|
||||
@ -42,7 +44,7 @@ async function seedUsers(sql) {
|
||||
users: insertedUsers,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error seeding users:', error);
|
||||
console.error("Error seeding users:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -82,7 +84,7 @@ async function seedInvoices(sql) {
|
||||
invoices: insertedInvoices,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error seeding invoices:', error);
|
||||
console.error("Error seeding invoices:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -121,7 +123,7 @@ async function seedCustomers(sql) {
|
||||
customers: insertedCustomers,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error seeding customers:', error);
|
||||
console.error("Error seeding customers:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -156,7 +158,7 @@ async function seedRevenue(sql) {
|
||||
revenue: insertedRevenue,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error seeding revenue:', error);
|
||||
console.error("Error seeding revenue:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -169,7 +171,7 @@ async function main() {
|
||||
username: envs.POSTGRES_USER,
|
||||
password: envs.POSTGRES_PASS,
|
||||
database: envs.POSTGRES_DB,
|
||||
}
|
||||
};
|
||||
|
||||
const sql = postgres(dbConfigs);
|
||||
|
||||
@ -177,12 +179,11 @@ async function main() {
|
||||
await seedCustomers(sql);
|
||||
await seedInvoices(sql);
|
||||
await seedRevenue(sql);
|
||||
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(
|
||||
'An error occurred while attempting to seed the database:',
|
||||
"An error occurred while attempting to seed the database:",
|
||||
err,
|
||||
);
|
||||
});
|
||||
|
@ -1,21 +1,19 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import { createContext, use } from "react";
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
|
||||
const DiContext = createContext<null | DependencyContainer>(di)
|
||||
const DiContext = createContext<null | DependencyContainer>(di);
|
||||
|
||||
const useDI = () => {
|
||||
const di = use(DiContext)
|
||||
const di = use(DiContext);
|
||||
|
||||
if (!di) {
|
||||
throw new Error("Di has not provided")
|
||||
}
|
||||
if (!di) {
|
||||
throw new Error("Di has not provided");
|
||||
}
|
||||
|
||||
return di
|
||||
}
|
||||
return di;
|
||||
};
|
||||
|
||||
export {
|
||||
DiContext,
|
||||
useDI,
|
||||
}
|
||||
export { DiContext, useDI };
|
||||
|
@ -1,5 +1,5 @@
|
||||
// "use client"
|
||||
import "reflect-metadata"
|
||||
import "reflect-metadata";
|
||||
import { container, DependencyContainer } from "tsyringe";
|
||||
|
||||
/**
|
||||
|
@ -1,2 +1 @@
|
||||
|
||||
export const isServer = typeof window === 'undefined'
|
||||
export const isServer = typeof window === "undefined";
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { useState, useEffect, useTransition, useRef } from 'react';
|
||||
import { useState, useEffect, useTransition, useRef } from "react";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param action Main server action to run
|
||||
* @param onFinished Callback to run after action
|
||||
* @returns transitioned action to run and is pending variable
|
||||
*/
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const useServerAction = <P extends any[], R>(
|
||||
action: (...args: P) => Promise<R>,
|
||||
|
@ -1,18 +1,22 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useRef } from "react";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param callback
|
||||
* @param time In miliseconds
|
||||
*/
|
||||
export default function useThrottle<T extends Function>(callback: T, time: number = 2000) {
|
||||
const lastRun = useRef(Date.now())
|
||||
export default function useThrottle<T extends () => unknown>(
|
||||
callback: T,
|
||||
time: number = 2000,
|
||||
) {
|
||||
const lastRun = useRef(Date.now());
|
||||
|
||||
return function() {
|
||||
if (Date.now() - lastRun.current <= time) return;
|
||||
lastRun.current = Date.now()
|
||||
return callback()
|
||||
}
|
||||
// eslint-disable-next-line func-names
|
||||
return function () {
|
||||
if (Date.now() - lastRun.current <= time) return;
|
||||
lastRun.current = Date.now();
|
||||
callback();
|
||||
};
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
// import gdi from "@/bootstrap/di/init-di";
|
||||
/* eslint-disable react/display-name */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
@ -23,7 +24,7 @@ const VvmConnector = memo(
|
||||
<IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => {
|
||||
const { View, Vm, restProps, children } = props;
|
||||
|
||||
const vm = Vm.useVM()
|
||||
const vm = Vm.useVM();
|
||||
|
||||
const allProps = {
|
||||
restProps,
|
||||
@ -69,16 +70,16 @@ export default abstract class BaseView<
|
||||
IVM extends IVMParent,
|
||||
PROPS extends IPropParent = undefined,
|
||||
> extends Component<BaseProps<IVM, PROPS>> {
|
||||
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
|
||||
|
||||
protected get componentName() {
|
||||
return this.constructor.name
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
|
||||
|
||||
render(): ReactNode {
|
||||
const { vm, restProps, memoizedByVM, children, ...rest } = this.props;
|
||||
|
||||
VvmConnector.displayName = this.componentName
|
||||
VvmConnector.displayName = this.componentName;
|
||||
|
||||
return (
|
||||
<VvmConnector
|
||||
|
@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useDI } from "@/bootstrap/di/di-context";
|
||||
import { NoOverride } from "@/bootstrap/helpers/type-helper";
|
||||
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
||||
@ -31,8 +32,9 @@ export default abstract class BaseVM<
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
protected get di() {
|
||||
return useDI()
|
||||
return useDI();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/**
|
||||
* You can use this hook in your useVm method to get rerender method
|
||||
|
@ -1,16 +1,16 @@
|
||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key"
|
||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
||||
|
||||
const en: typeof langKey = {
|
||||
global: {
|
||||
home: "Home",
|
||||
loading: "Loading",
|
||||
dashboard: "Dashboard"
|
||||
global: {
|
||||
home: "Home",
|
||||
loading: "Loading",
|
||||
dashboard: "Dashboard",
|
||||
},
|
||||
dashboard: {
|
||||
invoice: {
|
||||
createButton: "Create random Invoice",
|
||||
},
|
||||
dashboard: {
|
||||
invoice: {
|
||||
createButton: "Create random Invoice"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default en
|
||||
export default en;
|
||||
|
@ -1,14 +1,14 @@
|
||||
const langKey = {
|
||||
global: {
|
||||
home: "global.home",
|
||||
dashboard: "global.dashboard",
|
||||
loading: "global.loading"
|
||||
global: {
|
||||
home: "global.home",
|
||||
dashboard: "global.dashboard",
|
||||
loading: "global.loading",
|
||||
},
|
||||
dashboard: {
|
||||
invoice: {
|
||||
createButton: "dashboard.invoice.createButton",
|
||||
},
|
||||
dashboard: {
|
||||
invoice: {
|
||||
createButton: "dashboard.invoice.createButton"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default langKey;
|
@ -1,16 +1,16 @@
|
||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key"
|
||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
||||
|
||||
const ru: typeof langKey = {
|
||||
global: {
|
||||
home: "Дом",
|
||||
loading: "Загрузка",
|
||||
dashboard: "Панель приборов"
|
||||
global: {
|
||||
home: "Дом",
|
||||
loading: "Загрузка",
|
||||
dashboard: "Панель приборов",
|
||||
},
|
||||
dashboard: {
|
||||
invoice: {
|
||||
createButton: "Создать случайный счет-фактуру",
|
||||
},
|
||||
dashboard: {
|
||||
invoice: {
|
||||
createButton: "Создать случайный счет-фактуру"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default ru
|
||||
export default ru;
|
||||
|
@ -1,13 +1,18 @@
|
||||
"use client"
|
||||
import { I18nextProvider } from "react-i18next"
|
||||
"use client";
|
||||
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { initI18next } from "@/bootstrap/i18n/i18n";
|
||||
import { createInstance, Resource } from "i18next";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export default function TranslationsProvider({children, lng, resources}: PropsWithChildren & {lng: string; resources: Resource}) {
|
||||
const i18n = createInstance()
|
||||
export default function TranslationsProvider({
|
||||
children,
|
||||
lng,
|
||||
resources,
|
||||
}: PropsWithChildren & { lng: string; resources: Resource }) {
|
||||
const i18n = createInstance();
|
||||
|
||||
initI18next({lng, i18n, resources})
|
||||
initI18next({ lng, i18n, resources });
|
||||
|
||||
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>
|
||||
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
||||
}
|
@ -1,34 +1,51 @@
|
||||
import { getOptions, languages } from '@/bootstrap/i18n/settings'
|
||||
import { createInstance, i18n, Resource } from 'i18next'
|
||||
import resourcesToBackend from 'i18next-resources-to-backend'
|
||||
import { initReactI18next } from 'react-i18next/initReactI18next'
|
||||
import { getOptions, languages } from "@/bootstrap/i18n/settings";
|
||||
import { createInstance, i18n, Resource } from "i18next";
|
||||
import resourcesToBackend from "i18next-resources-to-backend";
|
||||
import { initReactI18next } from "react-i18next/initReactI18next";
|
||||
|
||||
export const initI18next = async (params: {lng: string, i18n?: i18n, resources?: Resource, ns?: string}) => {
|
||||
const { lng, i18n, ns, resources } = params
|
||||
const i18nInstance = i18n ? i18n : createInstance()
|
||||
export const initI18next = async (params: {
|
||||
lng: string;
|
||||
i18n?: i18n;
|
||||
resources?: Resource;
|
||||
ns?: string;
|
||||
}) => {
|
||||
const { lng, i18n, ns, resources } = params;
|
||||
const i18nInstance = i18n || createInstance();
|
||||
await i18nInstance
|
||||
.use(initReactI18next)
|
||||
.use(resourcesToBackend((language: string) => import(`./dictionaries/${language}.ts`)))
|
||||
.use(
|
||||
resourcesToBackend(
|
||||
(language: string) => import(`./dictionaries/${language}.ts`),
|
||||
),
|
||||
)
|
||||
.init({
|
||||
...getOptions(lng, ns),
|
||||
resources,
|
||||
preload: resources ? [] : languages
|
||||
},)
|
||||
...getOptions(lng, ns),
|
||||
resources,
|
||||
preload: resources ? [] : languages,
|
||||
});
|
||||
|
||||
await i18nInstance.init()
|
||||
|
||||
return {
|
||||
i18n: i18nInstance,
|
||||
resources: i18nInstance.services.resourceStore.data,
|
||||
t: i18nInstance.t
|
||||
}
|
||||
}
|
||||
|
||||
export async function getServerTranslation(lng: string, ns?: string, options: {keyPrefix?: string} = {}) {
|
||||
const i18nextInstance = (await initI18next({lng, ns})).i18n
|
||||
await i18nInstance.init();
|
||||
|
||||
return {
|
||||
t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options?.keyPrefix),
|
||||
i18n: i18nextInstance
|
||||
}
|
||||
i18n: i18nInstance,
|
||||
resources: i18nInstance.services.resourceStore.data,
|
||||
t: i18nInstance.t,
|
||||
};
|
||||
};
|
||||
|
||||
export async function getServerTranslation(
|
||||
lng: string,
|
||||
ns?: string,
|
||||
options: { keyPrefix?: string } = {},
|
||||
) {
|
||||
const i18nextInstance = (await initI18next({ lng, ns })).i18n;
|
||||
|
||||
return {
|
||||
t: i18nextInstance.getFixedT(
|
||||
lng,
|
||||
Array.isArray(ns) ? ns[0] : ns,
|
||||
options?.keyPrefix,
|
||||
),
|
||||
i18n: i18nextInstance,
|
||||
};
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
export const fallbackLng = 'en'
|
||||
export const languages = [fallbackLng, 'ru']
|
||||
export const defaultNS = 'translation'
|
||||
export const cookieName = 'i18next'
|
||||
export const fallbackLng = "en";
|
||||
export const languages = [fallbackLng, "ru"];
|
||||
export const defaultNS = "translation";
|
||||
export const cookieName = "i18next";
|
||||
|
||||
export function getOptions (lng = fallbackLng, ns = defaultNS) {
|
||||
export function getOptions(lng = fallbackLng, ns = defaultNS) {
|
||||
return {
|
||||
// debug: true,
|
||||
supportedLngs: languages,
|
||||
@ -11,6 +11,6 @@ export function getOptions (lng = fallbackLng, ns = defaultNS) {
|
||||
lng,
|
||||
fallbackNS: defaultNS,
|
||||
defaultNS,
|
||||
ns
|
||||
}
|
||||
ns,
|
||||
};
|
||||
}
|
@ -2,7 +2,10 @@ import { Either } from "fp-ts/lib/Either";
|
||||
import { TaskEither } from "fp-ts/lib/TaskEither";
|
||||
import BaseFailure from "@/feature/common/failures/base-failure";
|
||||
|
||||
type ApiTask<ResponseType> = TaskEither<BaseFailure, ResponseType>;
|
||||
export type ApiEither<ResponseType> = Either<BaseFailure, ResponseType>;
|
||||
type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
|
||||
export type ApiEither<ResponseType> = Either<
|
||||
BaseFailure<unknown>,
|
||||
ResponseType
|
||||
>;
|
||||
|
||||
export default ApiTask;
|
||||
|
@ -20,7 +20,7 @@ export default abstract class BaseFailure<META_DATA> {
|
||||
/* -------------------------------------------------------------------------- */
|
||||
constructor(key: string, metadata?: META_DATA) {
|
||||
this.message = makeFailureMessage(this.message, key);
|
||||
this.metadata = metadata ?? undefined
|
||||
this.metadata = metadata ?? undefined;
|
||||
}
|
||||
/* -------------------------------------------------------------------------- */
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
|
||||
/**
|
||||
* Failure for needed arguments in a method but sent wrong one
|
||||
*/
|
||||
export default class ArgumentsFailure<META_DATA> extends BaseDevFailure<META_DATA> {
|
||||
export default class ArgumentsFailure<
|
||||
META_DATA,
|
||||
> extends BaseDevFailure<META_DATA> {
|
||||
/* ------------------------------- Constructor ------------------------------ */
|
||||
constructor(metadata?: META_DATA) {
|
||||
super("arguments", metadata);
|
||||
|
@ -1,3 +1,5 @@
|
||||
import BaseFailure from "@/feature/common/failures/base-failure";
|
||||
|
||||
export default abstract class BaseDevFailure<META_DATA> extends BaseFailure<META_DATA> {}
|
||||
export default abstract class BaseDevFailure<
|
||||
META_DATA,
|
||||
> extends BaseFailure<META_DATA> {}
|
||||
|
@ -3,7 +3,9 @@ import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
|
||||
/**
|
||||
* This is a failure of not having specific dependency
|
||||
*/
|
||||
export default class DependencyFailure<META_DATA> extends BaseDevFailure<META_DATA> {
|
||||
export default class DependencyFailure<
|
||||
META_DATA,
|
||||
> extends BaseDevFailure<META_DATA> {
|
||||
constructor(metadata: META_DATA) {
|
||||
super("DependencyFailure", metadata);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
export const formatCurrency = (amount: number) => {
|
||||
return (amount / 100).toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
export const formatCurrency = (amount: number) =>
|
||||
(amount / 100).toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
});
|
||||
};
|
@ -10,21 +10,21 @@ import getSummaryInfoDi from "@/feature/core/summary-info/data/module/summary-in
|
||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
|
||||
import getRevenueDi from "@/feature/core/revenue/data/module/revenue-di";
|
||||
|
||||
const memoizedDis: Record<string, DependencyContainer> = {}
|
||||
const memoizedDis: Record<string, DependencyContainer> = {};
|
||||
|
||||
export default function serverDi(module: string): DependencyContainer {
|
||||
if (memoizedDis[module]) return memoizedDis[module]
|
||||
const getDi = {
|
||||
[customerKey]: getCustomerDi,
|
||||
[customerInvoiceModuleKey]: getCustomerInvoiceDi,
|
||||
[invoiceModuleKey]: getInvoiceDi,
|
||||
[summaryInfoModuleKey]: getSummaryInfoDi,
|
||||
[revenueModuleKey]: getRevenueDi,
|
||||
}[module]
|
||||
if (memoizedDis[module]) return memoizedDis[module];
|
||||
const getDi = {
|
||||
[customerKey]: getCustomerDi,
|
||||
[customerInvoiceModuleKey]: getCustomerInvoiceDi,
|
||||
[invoiceModuleKey]: getInvoiceDi,
|
||||
[summaryInfoModuleKey]: getSummaryInfoDi,
|
||||
[revenueModuleKey]: getRevenueDi,
|
||||
}[module];
|
||||
|
||||
if (!getDi) throw new Error("Server Di didn't found for module: " + module)
|
||||
if (!getDi) throw new Error(`Server Di didn't found for module: ${module}`);
|
||||
|
||||
const di = getDi()
|
||||
memoizedDis[module] = di
|
||||
return di
|
||||
const di = getDi();
|
||||
memoizedDis[module] = di;
|
||||
return di;
|
||||
}
|
@ -4,8 +4,8 @@ import { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
|
||||
export default function getCustomerInvoiceDi(): DependencyContainer {
|
||||
const customerInvoiceDi = di.createChildContainer()
|
||||
const customerInvoiceDi = di.createChildContainer();
|
||||
|
||||
customerInvoiceDi.register(customerInvoiceRepoKey, CustomerInvoiceDbRepo)
|
||||
return customerInvoiceDi
|
||||
customerInvoiceDi.register(customerInvoiceRepoKey, CustomerInvoiceDbRepo);
|
||||
return customerInvoiceDi;
|
||||
}
|
@ -15,39 +15,44 @@ type customerInvoiceDbResponse = {
|
||||
image_url: string;
|
||||
email: string;
|
||||
amount: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default class CustomerInvoiceDbRepo implements CustomerInvoiceRepo {
|
||||
fetchList(): ApiTask<CustomerInvoice[]> {
|
||||
return pipe(
|
||||
tryCatch(
|
||||
async () => {
|
||||
const response = await sql`
|
||||
fetchList(): ApiTask<CustomerInvoice[]> {
|
||||
return pipe(
|
||||
tryCatch(
|
||||
async () => {
|
||||
const response = (await sql`
|
||||
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
ORDER BY invoices.date DESC
|
||||
LIMIT 20 ` as postgres.RowList<customerInvoiceDbResponse[]>;
|
||||
LIMIT 20 `) as postgres.RowList<
|
||||
customerInvoiceDbResponse[]
|
||||
>;
|
||||
|
||||
return this.customerInvoicesDto(response)
|
||||
},
|
||||
(l) => failureOr(l, new NetworkFailure())
|
||||
)
|
||||
)
|
||||
}
|
||||
return this.customerInvoicesDto(response);
|
||||
},
|
||||
(l) => failureOr(l, new NetworkFailure()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private customerInvoicesDto(dbCustomers: customerInvoiceDbResponse[]): CustomerInvoice[] {
|
||||
return dbCustomers.map((customer) => this.customerInvoiceDto(customer));
|
||||
}
|
||||
|
||||
private customerInvoiceDto(dbCustomer: customerInvoiceDbResponse): CustomerInvoice {
|
||||
return new CustomerInvoice({
|
||||
id: dbCustomer.id,
|
||||
customerName: dbCustomer.name,
|
||||
customerEmail: dbCustomer.email,
|
||||
customerImageUrl: dbCustomer.image_url,
|
||||
invoicesAmount: formatCurrency(+dbCustomer.amount),
|
||||
})
|
||||
}
|
||||
private customerInvoicesDto(
|
||||
dbCustomers: customerInvoiceDbResponse[],
|
||||
): CustomerInvoice[] {
|
||||
return dbCustomers.map((customer) => this.customerInvoiceDto(customer));
|
||||
}
|
||||
|
||||
private customerInvoiceDto(
|
||||
dbCustomer: customerInvoiceDbResponse,
|
||||
): CustomerInvoice {
|
||||
return new CustomerInvoice({
|
||||
id: dbCustomer.id,
|
||||
customerName: dbCustomer.name,
|
||||
customerEmail: dbCustomer.email,
|
||||
customerImageUrl: dbCustomer.image_url,
|
||||
invoicesAmount: formatCurrency(+dbCustomer.amount),
|
||||
});
|
||||
}
|
||||
}
|
@ -1,21 +1,25 @@
|
||||
export default class CustomerInvoice {
|
||||
id: string;
|
||||
customerName: string;
|
||||
customerImageUrl: string;
|
||||
customerEmail: string;
|
||||
invoicesAmount: string;
|
||||
id: string;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
customerEmail,
|
||||
customerImageUrl,
|
||||
customerName,
|
||||
invoicesAmount
|
||||
}: CustomerInvoice) {
|
||||
this.id = id;
|
||||
this.customerEmail = customerEmail
|
||||
this.customerImageUrl = customerImageUrl
|
||||
this.customerName = customerName
|
||||
this.invoicesAmount = invoicesAmount
|
||||
}
|
||||
customerName: string;
|
||||
|
||||
customerImageUrl: string;
|
||||
|
||||
customerEmail: string;
|
||||
|
||||
invoicesAmount: string;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
customerEmail,
|
||||
customerImageUrl,
|
||||
customerName,
|
||||
invoicesAmount,
|
||||
}: CustomerInvoice) {
|
||||
this.id = id;
|
||||
this.customerEmail = customerEmail;
|
||||
this.customerImageUrl = customerImageUrl;
|
||||
this.customerName = customerName;
|
||||
this.invoicesAmount = invoicesAmount;
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import ApiTask from "@/feature/common/data/api-task"
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice"
|
||||
import ApiTask from "@/feature/common/data/api-task";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
||||
|
||||
export default interface CustomerInvoiceRepo {
|
||||
fetchList(): ApiTask<CustomerInvoice[]>
|
||||
fetchList(): ApiTask<CustomerInvoice[]>;
|
||||
}
|
||||
|
||||
export const customerInvoiceRepoKey = "customerInvoiceRepoKey"
|
||||
export const customerInvoiceRepoKey = "customerInvoiceRepoKey";
|
||||
|
@ -1,12 +1,18 @@
|
||||
import { ApiEither } from "@/feature/common/data/api-task";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
||||
import CustomerInvoiceRepo, { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||
import CustomerInvoiceRepo, {
|
||||
customerInvoiceRepoKey,
|
||||
} from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
|
||||
import { connection } from "next/server";
|
||||
|
||||
export default async function fetchCustomerInvoicesUsecase(): Promise<ApiEither<CustomerInvoice[]>> {
|
||||
connection()
|
||||
const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(customerInvoiceRepoKey)
|
||||
return repo.fetchList()()
|
||||
export default async function fetchCustomerInvoicesUsecase(): Promise<
|
||||
ApiEither<CustomerInvoice[]>
|
||||
> {
|
||||
connection();
|
||||
const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(
|
||||
customerInvoiceRepoKey,
|
||||
);
|
||||
return repo.fetchList()();
|
||||
}
|
@ -1 +1 @@
|
||||
export const customerInvoiceModuleKey = "customerInvoiceModuleKey"
|
||||
export const customerInvoiceModuleKey = "customerInvoiceModuleKey";
|
||||
|
@ -1 +1 @@
|
||||
export const customerKey = "customerKey"
|
||||
export const customerKey = "customerKey";
|
||||
|
@ -4,8 +4,8 @@ import { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
|
||||
export default function getCustomerDi(): DependencyContainer {
|
||||
const customerDi = di.createChildContainer()
|
||||
const customerDi = di.createChildContainer();
|
||||
|
||||
customerDi.register(customerRepoKey, CustomerDbRepo)
|
||||
return customerDi
|
||||
customerDi.register(customerRepoKey, CustomerDbRepo);
|
||||
return customerDi;
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||
import ApiTask from "@/feature/common/data/api-task";
|
||||
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
||||
import NetworkFailure from "@/feature/common/failures/network-failure";
|
||||
import { formatCurrency } from "@/feature/common/feature-helpers";
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||
import CustomerRepo from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
import { pipe } from "fp-ts/lib/function";
|
||||
import { map, tryCatch } from "fp-ts/lib/TaskEither";
|
||||
import postgres from "postgres";
|
||||
|
||||
type customerDbResponse = {
|
||||
@ -12,57 +17,60 @@ type customerDbResponse = {
|
||||
total_invoices: string;
|
||||
total_pending: string;
|
||||
total_paid: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default class CustomerDbRepo implements CustomerRepo {
|
||||
async fetchList(query: string): Promise<Customer[]> {
|
||||
try {
|
||||
const data = await sql`
|
||||
SELECT
|
||||
customers.id,
|
||||
customers.name,
|
||||
customers.email,
|
||||
customers.image_url,
|
||||
COUNT(invoices.id) AS total_invoices,
|
||||
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
|
||||
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
|
||||
FROM customers
|
||||
LEFT JOIN invoices ON customers.id = invoices.customer_id
|
||||
WHERE
|
||||
customers.name ILIKE ${`%${query}%`} OR
|
||||
customers.email ILIKE ${`%${query}%`}
|
||||
GROUP BY customers.id, customers.name, customers.email, customers.image_url
|
||||
ORDER BY customers.name ASC
|
||||
` as postgres.RowList<customerDbResponse[]>;
|
||||
fetchList(query: string): ApiTask<Customer[]> {
|
||||
return pipe(
|
||||
tryCatch(
|
||||
async () => {
|
||||
const data = (await sql`
|
||||
SELECT
|
||||
customers.id,
|
||||
customers.name,
|
||||
customers.email,
|
||||
customers.image_url,
|
||||
COUNT(invoices.id) AS total_invoices,
|
||||
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
|
||||
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
|
||||
FROM customers
|
||||
LEFT JOIN invoices ON customers.id = invoices.customer_id
|
||||
WHERE
|
||||
customers.name ILIKE ${`%${query}%`} OR
|
||||
customers.email ILIKE ${`%${query}%`}
|
||||
GROUP BY customers.id, customers.name, customers.email, customers.image_url
|
||||
ORDER BY customers.name ASC
|
||||
`) as postgres.RowList<customerDbResponse[]>;
|
||||
|
||||
return data;
|
||||
},
|
||||
(l) => failureOr(l, new NetworkFailure(l as Error)),
|
||||
),
|
||||
map(this.customersDto.bind(this)),
|
||||
) as ApiTask<Customer[]>;
|
||||
}
|
||||
|
||||
return this.customersDto(data);
|
||||
} catch (err) {
|
||||
console.error('Database Error:', err);
|
||||
throw new Error('Failed to fetch customer table.');
|
||||
}
|
||||
}
|
||||
async fetchCustomersAmount(): Promise<number> {
|
||||
const data =
|
||||
(await sql`SELECT COUNT(*) FROM customers`) as postgres.RowList<
|
||||
unknown[]
|
||||
>;
|
||||
return Number(data.count ?? "0");
|
||||
}
|
||||
|
||||
async fetchCustomersAmount(): Promise<number> {
|
||||
const data = await sql`SELECT COUNT(*) FROM customers`as postgres.RowList<unknown[]>;
|
||||
return Number(data.count ?? '0');
|
||||
}
|
||||
|
||||
|
||||
private customersDto(dbCustomers: customerDbResponse[]): Customer[] {
|
||||
return dbCustomers.map((customer) => this.customerDto(customer));
|
||||
}
|
||||
|
||||
private customerDto(dbCustomer: customerDbResponse): Customer {
|
||||
return new Customer({
|
||||
id: dbCustomer.id,
|
||||
name: dbCustomer.name,
|
||||
email: dbCustomer.email,
|
||||
imageUrl: dbCustomer.image_url,
|
||||
totalInvoices: dbCustomer.total_invoices,
|
||||
totalPending: formatCurrency(Number(dbCustomer.total_pending)),
|
||||
totalPaid: formatCurrency(Number(dbCustomer.total_paid)),
|
||||
})
|
||||
}
|
||||
private customersDto(dbCustomers: customerDbResponse[]): Customer[] {
|
||||
return dbCustomers.map((customer) => this.customerDto(customer));
|
||||
}
|
||||
|
||||
private customerDto(dbCustomer: customerDbResponse): Customer {
|
||||
return new Customer({
|
||||
id: dbCustomer.id,
|
||||
name: dbCustomer.name,
|
||||
email: dbCustomer.email,
|
||||
imageUrl: dbCustomer.image_url,
|
||||
totalInvoices: dbCustomer.total_invoices,
|
||||
totalPending: formatCurrency(Number(dbCustomer.total_pending)),
|
||||
totalPaid: formatCurrency(Number(dbCustomer.total_paid)),
|
||||
});
|
||||
}
|
||||
}
|
@ -1,27 +1,33 @@
|
||||
export default class Customer {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
imageUrl: string;
|
||||
totalInvoices: string;
|
||||
totalPending: string;
|
||||
totalPaid: string;
|
||||
id: string;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
email,
|
||||
imageUrl,
|
||||
name,
|
||||
totalInvoices,
|
||||
totalPaid,
|
||||
totalPending
|
||||
}: Customer) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
this.imageUrl = imageUrl;
|
||||
this.totalInvoices = totalInvoices;
|
||||
this.totalPaid = totalPaid;
|
||||
this.totalPending = totalPending;
|
||||
}
|
||||
name: string;
|
||||
|
||||
email: string;
|
||||
|
||||
imageUrl: string;
|
||||
|
||||
totalInvoices: string;
|
||||
|
||||
totalPending: string;
|
||||
|
||||
totalPaid: string;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
email,
|
||||
imageUrl,
|
||||
name,
|
||||
totalInvoices,
|
||||
totalPaid,
|
||||
totalPending,
|
||||
}: Customer) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
this.imageUrl = imageUrl;
|
||||
this.totalInvoices = totalInvoices;
|
||||
this.totalPaid = totalPaid;
|
||||
this.totalPending = totalPending;
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import ApiTask from "@/feature/common/data/api-task"
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer"
|
||||
import ApiTask from "@/feature/common/data/api-task";
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||
|
||||
export default interface CustomerRepo {
|
||||
fetchList(query: string): ApiTask<Customer[]>
|
||||
fetchCustomersAmount(): Promise<number>
|
||||
fetchList(query: string): ApiTask<Customer[]>;
|
||||
fetchCustomersAmount(): Promise<number>;
|
||||
}
|
||||
|
||||
export const customerRepoKey = "customerRepoKey"
|
||||
export const customerRepoKey = "customerRepoKey";
|
||||
|
@ -1,8 +1,10 @@
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||
import CustomerRepo, { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
import CustomerRepo, {
|
||||
customerRepoKey,
|
||||
} from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
|
||||
export default async function fetchCustomersAmountUsecase(): Promise<number> {
|
||||
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey)
|
||||
return repo.fetchCustomersAmount()
|
||||
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey);
|
||||
return repo.fetchCustomersAmount();
|
||||
}
|
@ -1,14 +1,19 @@
|
||||
"use server"
|
||||
"use server";
|
||||
|
||||
import { ApiEither } from "@/feature/common/data/api-task";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||
import CustomerRepo, { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
import CustomerRepo, {
|
||||
customerRepoKey,
|
||||
} from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
import { connection } from "next/server";
|
||||
|
||||
export default async function fetchCustomersUsecase(query: string): Promise<Customer[]> {
|
||||
connection()
|
||||
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey)
|
||||
export default function fetchCustomersUsecase(
|
||||
query: string,
|
||||
): Promise<ApiEither<Customer[]>> {
|
||||
connection();
|
||||
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey);
|
||||
|
||||
return repo.fetchList(query)
|
||||
return repo.fetchList(query)();
|
||||
}
|
@ -4,8 +4,8 @@ import { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-rep
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
|
||||
export default function getInvoiceDi(): DependencyContainer {
|
||||
const invoiceDi = di.createChildContainer()
|
||||
const invoiceDi = di.createChildContainer();
|
||||
|
||||
invoiceDi.register(invoiceRepoKey, invoiceDbRepo)
|
||||
return invoiceDi
|
||||
invoiceDi.register(invoiceRepoKey, invoiceDbRepo);
|
||||
return invoiceDi;
|
||||
}
|
@ -10,57 +10,60 @@ import { pipe } from "fp-ts/lib/function";
|
||||
import { tryCatch } from "fp-ts/lib/TaskEither";
|
||||
import postgres from "postgres";
|
||||
|
||||
type InvoiceSummaryDbResponse = {paid: string, pending: string}
|
||||
type InvoiceSummaryDbResponse = { paid: string; pending: string };
|
||||
export default class InvoiceDbRepo implements InvoiceRepo {
|
||||
async fetchAllInvoicesAmount(): Promise<number> {
|
||||
const data = await sql`SELECT COUNT(*) FROM invoices` as postgres.RowList<unknown[]>;
|
||||
async fetchAllInvoicesAmount(): Promise<number> {
|
||||
const data = (await sql`SELECT COUNT(*) FROM invoices`) as postgres.RowList<
|
||||
unknown[]
|
||||
>;
|
||||
|
||||
return data.count ?? 0
|
||||
}
|
||||
return data.count ?? 0;
|
||||
}
|
||||
|
||||
createInvoice(params: InvoiceParam): ApiTask<string> {
|
||||
return pipe(
|
||||
tryCatch(
|
||||
async () => {
|
||||
const firstCustomerIdDb = await sql`SELECT
|
||||
createInvoice(params: InvoiceParam): ApiTask<string> {
|
||||
return pipe(
|
||||
tryCatch(
|
||||
async () => {
|
||||
const firstCustomerIdDb = await sql`SELECT
|
||||
id FROM customers
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`
|
||||
const customerId = firstCustomerIdDb.at(0)?.id
|
||||
if (!customerId) throw new Error("There is no customer")
|
||||
`;
|
||||
const customerId = firstCustomerIdDb.at(0)?.id;
|
||||
if (!customerId) throw new Error("There is no customer");
|
||||
|
||||
const { amount, status } = params;
|
||||
const amountInCents = amount * 100;
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
const { amount, status } = params;
|
||||
const amountInCents = amount * 100;
|
||||
const date = new Date().toISOString().split("T")[0];
|
||||
|
||||
// Insert data into the database
|
||||
const result = await sql`
|
||||
// Insert data into the database
|
||||
const result = await sql`
|
||||
INSERT INTO invoices (customer_id, amount, status, date)
|
||||
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
|
||||
RETURNING id
|
||||
`;
|
||||
return result.at(0)?.id ?? ""
|
||||
},
|
||||
(l) => failureOr(l, new NetworkFailure(l as Error))
|
||||
),
|
||||
)
|
||||
}
|
||||
return result.at(0)?.id ?? "";
|
||||
},
|
||||
(l) => failureOr(l, new NetworkFailure(l as Error)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
||||
const invoiceStatusPromise = await sql`SELECT
|
||||
async fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
||||
const invoiceStatusPromise = (await sql`SELECT
|
||||
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
|
||||
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
|
||||
FROM invoices` as postgres.RowList<InvoiceSummaryDbResponse[]>;
|
||||
FROM invoices`) as postgres.RowList<InvoiceSummaryDbResponse[]>;
|
||||
|
||||
return this.invoiceSummaryDto(invoiceStatusPromise.at(0))
|
||||
return this.invoiceSummaryDto(invoiceStatusPromise.at(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private invoiceSummaryDto(dbResponse?: InvoiceSummaryDbResponse): InvoiceStatusSummary {
|
||||
return new InvoiceStatusSummary({
|
||||
paid: formatCurrency(Number(dbResponse?.paid ?? '0')),
|
||||
pending: formatCurrency(Number(dbResponse?.pending ?? '0'))
|
||||
})
|
||||
}
|
||||
private invoiceSummaryDto(
|
||||
dbResponse?: InvoiceSummaryDbResponse,
|
||||
): InvoiceStatusSummary {
|
||||
return new InvoiceStatusSummary({
|
||||
paid: formatCurrency(Number(dbResponse?.paid ?? "0")),
|
||||
pending: formatCurrency(Number(dbResponse?.pending ?? "0")),
|
||||
});
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import ApiTask from "@/feature/common/data/api-task"
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param"
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status"
|
||||
import ApiTask from "@/feature/common/data/api-task";
|
||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
|
||||
export default interface InvoiceRepo {
|
||||
fetchAllInvoicesAmount(): Promise<number>
|
||||
fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary>
|
||||
createInvoice(params: InvoiceParam): ApiTask<string>
|
||||
fetchAllInvoicesAmount(): Promise<number>;
|
||||
fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary>;
|
||||
createInvoice(params: InvoiceParam): ApiTask<string>;
|
||||
}
|
||||
|
||||
export const invoiceRepoKey = "invoiceRepoKey"
|
||||
export const invoiceRepoKey = "invoiceRepoKey";
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const invoiceSchema = z.object({
|
||||
amount: z.coerce.number().gt(0, { message: 'Please enter an amount greater than $0.' }),
|
||||
status: z.enum(['pending', 'paid'], {
|
||||
invalid_type_error: 'Please select an invoice status.',
|
||||
amount: z.coerce
|
||||
.number()
|
||||
.gt(0, { message: "Please enter an amount greater than $0." }),
|
||||
status: z.enum(["pending", "paid"], {
|
||||
invalid_type_error: "Please select an invoice status.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type InvoiceParam = z.infer<typeof invoiceSchema>
|
||||
export type InvoiceParam = z.infer<typeof invoiceSchema>;
|
||||
|
@ -1,24 +1,32 @@
|
||||
"use server"
|
||||
"use server";
|
||||
|
||||
import { ApiEither } from "@/feature/common/data/api-task";
|
||||
import ParamsFailure from "@/feature/common/failures/params-failure";
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import { InvoiceParam, invoiceSchema } from "@/feature/core/invoice/domain/param/invoice-param";
|
||||
import InvoiceRepo, {
|
||||
invoiceRepoKey,
|
||||
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import {
|
||||
InvoiceParam,
|
||||
invoiceSchema,
|
||||
} from "@/feature/core/invoice/domain/param/invoice-param";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
import { pipe } from "fp-ts/lib/function";
|
||||
import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither";
|
||||
|
||||
export default async function createInvoiceUsecase(params: InvoiceParam): Promise<ApiEither<string>> {
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
|
||||
export default async function createInvoiceUsecase(
|
||||
params: InvoiceParam,
|
||||
): Promise<ApiEither<string>> {
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
||||
|
||||
return pipe(
|
||||
fromNullable(new ParamsFailure())(params),
|
||||
map((params) => invoiceSchema.safeParse(params)),
|
||||
chain((params) => {
|
||||
const isParamsValid = invoiceSchema.safeParse(params)
|
||||
if (!isParamsValid.success) left(new ParamsFailure())
|
||||
return right(params.data as InvoiceParam)
|
||||
}),
|
||||
chain((params) => repo.createInvoice(params))
|
||||
)()
|
||||
return pipe(
|
||||
fromNullable(new ParamsFailure())(params),
|
||||
map((params) => invoiceSchema.safeParse(params)),
|
||||
chain((params) => {
|
||||
const isParamsValid = invoiceSchema.safeParse(params);
|
||||
if (!isParamsValid.success) left(new ParamsFailure());
|
||||
return right(params.data as InvoiceParam);
|
||||
}),
|
||||
chain((params) => repo.createInvoice(params)),
|
||||
)();
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
"use server"
|
||||
"use server";
|
||||
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import InvoiceRepo, {
|
||||
invoiceRepoKey,
|
||||
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
|
||||
export default async function fetchAllInvoicesAmountUsecase(): Promise<number> {
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
||||
|
||||
return repo.fetchAllInvoicesAmount()
|
||||
return repo.fetchAllInvoicesAmount();
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import InvoiceRepo, {
|
||||
invoiceRepoKey,
|
||||
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
|
||||
export default async function fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
|
||||
return repo.fetchInvoicesStatusSummary()
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
||||
return repo.fetchInvoicesStatusSummary();
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
export default class InvoiceStatusSummary {
|
||||
paid: string;
|
||||
pending: string;
|
||||
paid: string;
|
||||
|
||||
constructor({
|
||||
paid,
|
||||
pending
|
||||
}: InvoiceStatusSummary) {
|
||||
this.paid = paid;
|
||||
this.pending = pending;
|
||||
}
|
||||
pending: string;
|
||||
|
||||
constructor({ paid, pending }: InvoiceStatusSummary) {
|
||||
this.paid = paid;
|
||||
this.pending = pending;
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
export const invoiceModuleKey = "invoiceModuleKey"
|
||||
export const invoiceModuleKey = "invoiceModuleKey";
|
||||
|
@ -1,10 +1,10 @@
|
||||
import di from "@/bootstrap/di/init-di"
|
||||
import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db-repo"
|
||||
import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue-repo"
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db-repo";
|
||||
import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
||||
|
||||
export default function getRevenueDi() {
|
||||
const revenueDi = di.createChildContainer()
|
||||
const revenueDi = di.createChildContainer();
|
||||
|
||||
revenueDi.register(revenueRepoKey, RevenueDbRepo)
|
||||
return revenueDi
|
||||
revenueDi.register(revenueRepoKey, RevenueDbRepo);
|
||||
return revenueDi;
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
import RevenueRepo from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
||||
import { connection } from "next/server";
|
||||
import postgres from "postgres";
|
||||
|
||||
export type RevenueDbResponse = {
|
||||
@ -9,33 +8,30 @@ export type RevenueDbResponse = {
|
||||
revenue: number;
|
||||
};
|
||||
export default class RevenueDbRepo implements RevenueRepo {
|
||||
async fetchRevenues(): Promise<Revenue[]> {
|
||||
try {
|
||||
// Artificially delay a response for demo purposes.
|
||||
// Don't do this in production :)
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
async fetchRevenues(): Promise<Revenue[]> {
|
||||
try {
|
||||
// Artificially delay a response for demo purposes.
|
||||
// Don't do this in production :)
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
const data = await sql`SELECT * FROM revenue` as postgres.RowList<RevenueDbResponse[]>;
|
||||
const data = (await sql`SELECT * FROM revenue`) as postgres.RowList<
|
||||
RevenueDbResponse[]
|
||||
>;
|
||||
|
||||
console.log('Data fetch completed after 3 seconds.');
|
||||
|
||||
return this.revenuesDto(data);
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch revenue data.');
|
||||
}
|
||||
return this.revenuesDto(data);
|
||||
} catch {
|
||||
throw new Error("Failed to fetch revenue data.");
|
||||
}
|
||||
}
|
||||
|
||||
private revenuesDto(dbResponse: RevenueDbResponse[]): Revenue[] {
|
||||
return dbResponse.map((dbRevenue) => this.revenueDto(dbRevenue));
|
||||
}
|
||||
|
||||
private revenuesDto(dbResponse: RevenueDbResponse[]): Revenue[] {
|
||||
return dbResponse.map((dbRevenue) => this.revenueDto(dbRevenue))
|
||||
}
|
||||
|
||||
private revenueDto(dbResponse: RevenueDbResponse): Revenue {
|
||||
return new Revenue({
|
||||
month: dbResponse.month,
|
||||
revenue: dbResponse.revenue
|
||||
})
|
||||
}
|
||||
|
||||
private revenueDto(dbResponse: RevenueDbResponse): Revenue {
|
||||
return new Revenue({
|
||||
month: dbResponse.month,
|
||||
revenue: dbResponse.revenue,
|
||||
});
|
||||
}
|
||||
}
|
@ -1,14 +1,10 @@
|
||||
export default class Revenue {
|
||||
month: string;
|
||||
revenue: number;
|
||||
month: string;
|
||||
|
||||
constructor(
|
||||
{
|
||||
month,
|
||||
revenue
|
||||
}: Revenue
|
||||
) {
|
||||
this.month = month
|
||||
this.revenue = revenue
|
||||
}
|
||||
revenue: number;
|
||||
|
||||
constructor({ month, revenue }: Revenue) {
|
||||
this.month = month;
|
||||
this.revenue = revenue;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
|
||||
export default interface RevenueRepo {
|
||||
fetchRevenues(): Promise<Revenue[]>
|
||||
fetchRevenues(): Promise<Revenue[]>;
|
||||
}
|
||||
|
||||
export const revenueRepoKey = "revenueRepoKey"
|
||||
export const revenueRepoKey = "revenueRepoKey";
|
||||
|
@ -1 +1 @@
|
||||
export const revenueModuleKey = "RevenueModuleKey"
|
||||
export const revenueModuleKey = "RevenueModuleKey";
|
||||
|
@ -1,9 +1,11 @@
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
import RevenueRepo, { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
||||
import RevenueRepo, {
|
||||
revenueRepoKey,
|
||||
} from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
|
||||
|
||||
export default async function fetchRevenuesUsecase(): Promise<Revenue[]> {
|
||||
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey)
|
||||
return repo.fetchRevenues()
|
||||
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey);
|
||||
return repo.fetchRevenues();
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
import di from "@/bootstrap/di/init-di"
|
||||
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase"
|
||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount-usecase"
|
||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary"
|
||||
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase";
|
||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount-usecase";
|
||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary";
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
|
||||
export default function getSummaryInfoDi() {
|
||||
const summaryInfoDi = di.createChildContainer()
|
||||
const summaryInfoDi = di.createChildContainer();
|
||||
|
||||
summaryInfoDi.register(fetchAllInvoicesAmountUsecase.name, {
|
||||
useValue: fetchAllInvoicesAmountUsecase
|
||||
})
|
||||
summaryInfoDi.register(fetchCustomersAmountUsecase.name, {
|
||||
useValue: fetchCustomersAmountUsecase
|
||||
})
|
||||
summaryInfoDi.register(fetchInvoicesStatusSummary.name, {
|
||||
useValue: fetchInvoicesStatusSummary
|
||||
})
|
||||
return summaryInfoDi
|
||||
summaryInfoDi.register(fetchAllInvoicesAmountUsecase.name, {
|
||||
useValue: fetchAllInvoicesAmountUsecase,
|
||||
});
|
||||
summaryInfoDi.register(fetchCustomersAmountUsecase.name, {
|
||||
useValue: fetchCustomersAmountUsecase,
|
||||
});
|
||||
summaryInfoDi.register(fetchInvoicesStatusSummary.name, {
|
||||
useValue: fetchInvoicesStatusSummary,
|
||||
});
|
||||
return summaryInfoDi;
|
||||
}
|
@ -1 +1 @@
|
||||
export const summaryInfoModuleKey = "summaryInfoModuleKey"
|
||||
export const summaryInfoModuleKey = "summaryInfoModuleKey";
|
||||
|
@ -7,28 +7,33 @@ import SummaryInfo from "@/feature/core/summary-info/domain/value-object/summary
|
||||
import { connection } from "next/server";
|
||||
|
||||
export default async function fetchSummaryInfoUsecase(): Promise<SummaryInfo> {
|
||||
connection()
|
||||
connection();
|
||||
|
||||
try{
|
||||
const summaryInfoDi = serverDi(summaryInfoModuleKey)
|
||||
const invoicesAmountPromise = summaryInfoDi.resolve<typeof fetchAllInvoicesAmountUsecase>(fetchAllInvoicesAmountUsecase.name)()
|
||||
const customersAmountPromise = summaryInfoDi.resolve<typeof fetchCustomersAmountUsecase>(fetchCustomersAmountUsecase.name)()
|
||||
const invoiceSummaryPomise = summaryInfoDi.resolve<typeof fetchInvoicesStatusSummary>(fetchInvoicesStatusSummary.name)()
|
||||
try {
|
||||
const summaryInfoDi = serverDi(summaryInfoModuleKey);
|
||||
const invoicesAmountPromise = summaryInfoDi.resolve<
|
||||
typeof fetchAllInvoicesAmountUsecase
|
||||
>(fetchAllInvoicesAmountUsecase.name)();
|
||||
const customersAmountPromise = summaryInfoDi.resolve<
|
||||
typeof fetchCustomersAmountUsecase
|
||||
>(fetchCustomersAmountUsecase.name)();
|
||||
const invoiceSummaryPomise = summaryInfoDi.resolve<
|
||||
typeof fetchInvoicesStatusSummary
|
||||
>(fetchInvoicesStatusSummary.name)();
|
||||
|
||||
const [invoicesAmount, customersAmount, invoicesSummary] = await Promise.all([
|
||||
invoicesAmountPromise,
|
||||
customersAmountPromise,
|
||||
invoiceSummaryPomise,
|
||||
]);
|
||||
const [invoicesAmount, customersAmount, invoicesSummary] =
|
||||
await Promise.all([
|
||||
invoicesAmountPromise,
|
||||
customersAmountPromise,
|
||||
invoiceSummaryPomise,
|
||||
]);
|
||||
|
||||
|
||||
return new SummaryInfo({
|
||||
invoicesNumber: invoicesAmount,
|
||||
customersNumber: customersAmount,
|
||||
invoicesSummary: invoicesSummary
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch card data.');
|
||||
}
|
||||
return new SummaryInfo({
|
||||
invoicesNumber: invoicesAmount,
|
||||
customersNumber: customersAmount,
|
||||
invoicesSummary,
|
||||
});
|
||||
} catch {
|
||||
throw new Error("Failed to fetch card data.");
|
||||
}
|
||||
}
|
@ -1,17 +1,19 @@
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
|
||||
export default class SummaryInfo {
|
||||
customersNumber: number;
|
||||
invoicesNumber: number;
|
||||
invoicesSummary: InvoiceStatusSummary
|
||||
customersNumber: number;
|
||||
|
||||
constructor({
|
||||
customersNumber,
|
||||
invoicesNumber,
|
||||
invoicesSummary
|
||||
}: SummaryInfo) {
|
||||
this.customersNumber = customersNumber
|
||||
this.invoicesNumber = invoicesNumber
|
||||
this.invoicesSummary = invoicesSummary
|
||||
}
|
||||
invoicesNumber: number;
|
||||
|
||||
invoicesSummary: InvoiceStatusSummary;
|
||||
|
||||
constructor({
|
||||
customersNumber,
|
||||
invoicesNumber,
|
||||
invoicesSummary,
|
||||
}: SummaryInfo) {
|
||||
this.customersNumber = customersNumber;
|
||||
this.invoicesNumber = invoicesNumber;
|
||||
this.invoicesSummary = invoicesSummary;
|
||||
}
|
||||
}
|
@ -1,36 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import acceptLanguage from 'accept-language'
|
||||
import { cookieName, fallbackLng, languages } from '@/bootstrap/i18n/settings'
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import acceptLanguage from "accept-language";
|
||||
import { cookieName, fallbackLng, languages } from "@/bootstrap/i18n/settings";
|
||||
|
||||
acceptLanguage.languages(languages)
|
||||
acceptLanguage.languages(languages);
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!api|static|.*\\..*|_next).*)"]
|
||||
}
|
||||
matcher: ["/((?!api|static|.*\\..*|_next).*)"],
|
||||
};
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
let lng
|
||||
if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req?.cookies?.get(cookieName)?.value)
|
||||
if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'))
|
||||
if (!lng) lng = fallbackLng
|
||||
let lng;
|
||||
if (req.cookies.has(cookieName))
|
||||
lng = acceptLanguage.get(req?.cookies?.get(cookieName)?.value);
|
||||
if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"));
|
||||
if (!lng) lng = fallbackLng;
|
||||
|
||||
// Redirect if lng in path is not supported
|
||||
if (
|
||||
!languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
|
||||
!req.nextUrl.pathname.startsWith('/_next')
|
||||
!languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
|
||||
!req.nextUrl.pathname.startsWith("/_next")
|
||||
) {
|
||||
return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
|
||||
return NextResponse.redirect(
|
||||
new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
|
||||
);
|
||||
}
|
||||
|
||||
if (req.headers.has('referer')) {
|
||||
const refererUrl = new URL(req?.headers?.get('referer') ?? "")
|
||||
const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`))
|
||||
const response = NextResponse.next()
|
||||
if (req.headers.has("referer")) {
|
||||
const refererUrl = new URL(req?.headers?.get("referer") ?? "");
|
||||
const lngInReferer = languages.find((l) =>
|
||||
refererUrl.pathname.startsWith(`/${l}`),
|
||||
);
|
||||
const response = NextResponse.next();
|
||||
if (lngInReferer) {
|
||||
response.cookies.set(cookieName, lngInReferer)
|
||||
response.cookies.set(cookieName, lngInReferer);
|
||||
}
|
||||
return response
|
||||
return response;
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
return NextResponse.next();
|
||||
}
|
@ -2,20 +2,21 @@ import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export default class CustomerFakeFactory {
|
||||
static getFakeCustomer(): Customer {
|
||||
return new Customer({
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
imageUrl: faker.image.url(),
|
||||
totalInvoices: faker.number.int().toLocaleString(),
|
||||
totalPaid: faker.finance.amount(),
|
||||
totalPending: faker.number.int().toLocaleString(),
|
||||
})
|
||||
}
|
||||
static getFakeCustomer(): Customer {
|
||||
return new Customer({
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
imageUrl: faker.image.url(),
|
||||
totalInvoices: faker.number.int().toLocaleString(),
|
||||
totalPaid: faker.finance.amount(),
|
||||
totalPending: faker.number.int().toLocaleString(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
static getFakeCustomerList(length: number = 10): Customer[] {
|
||||
return Array.from({length}).map(() => CustomerFakeFactory.getFakeCustomer())
|
||||
}
|
||||
static getFakeCustomerList(length: number = 10): Customer[] {
|
||||
return Array.from({ length }).map(() =>
|
||||
CustomerFakeFactory.getFakeCustomer(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import di from "@/bootstrap/di/init-di"
|
||||
import * as serverDi from "@/feature/common/server-di"
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import * as serverDi from "@/feature/common/server-di";
|
||||
|
||||
export default function mockDi() {
|
||||
vi.spyOn(serverDi, "default").mockReturnValue(di)
|
||||
return di
|
||||
vi.spyOn(serverDi, "default").mockReturnValue(di);
|
||||
return di;
|
||||
}
|
@ -1,48 +1,53 @@
|
||||
import CustomerRepo, { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
import CustomerRepo, {
|
||||
customerRepoKey,
|
||||
} from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
import { getMock } from "@/test/common/mock/mock-factory";
|
||||
import { describe } from "vitest";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import CustomerFakeFactory from "@/test/common/fake-factory/customer/customer-fake-factory";
|
||||
import fetchCustomersUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-usecase";
|
||||
import mockDi from "@/test/common/mock/mock-di";
|
||||
import { right } from "fp-ts/lib/TaskEither";
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Faking */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
const fakedCustomerList = CustomerFakeFactory.getFakeCustomerList()
|
||||
const fakedCustomerList = CustomerFakeFactory.getFakeCustomerList();
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Mocking */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
const customerDi = mockDi()
|
||||
const customerDi = mockDi();
|
||||
|
||||
const mockedFetchList = vi.fn<CustomerRepo['fetchList']>()
|
||||
const MockedRepo = getMock<CustomerRepo>()
|
||||
MockedRepo.setup((instance) => instance.fetchList).returns(mockedFetchList)
|
||||
const mockedFetchList = vi.fn<CustomerRepo["fetchList"]>();
|
||||
const MockedRepo = getMock<CustomerRepo>();
|
||||
MockedRepo.setup((instance) => instance.fetchList).returns(mockedFetchList);
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* DI */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
customerDi.register(fetchCustomersUsecase.name, {
|
||||
useValue: fetchCustomersUsecase
|
||||
})
|
||||
useValue: fetchCustomersUsecase,
|
||||
});
|
||||
customerDi.register(customerRepoKey, {
|
||||
useValue: MockedRepo.object()
|
||||
})
|
||||
useValue: MockedRepo.object(),
|
||||
});
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Testing */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
const usecase = customerDi.resolve<typeof fetchCustomersUsecase>(fetchCustomersUsecase.name)
|
||||
const usecase = customerDi.resolve<typeof fetchCustomersUsecase>(
|
||||
fetchCustomersUsecase.name,
|
||||
);
|
||||
describe("Fetch customers", () => {
|
||||
describe("On given query string", () => {
|
||||
const fakedQuery = faker.person.fullName();
|
||||
describe("And returning list from repo", () => {
|
||||
beforeEach(() => {
|
||||
mockedFetchList.mockResolvedValue(fakedCustomerList)
|
||||
})
|
||||
it("Then should return correct list of customers", async () => {
|
||||
// ! Act
|
||||
const response = await usecase(fakedQuery)
|
||||
// ? Assert
|
||||
expect(response).toEqual(fakedCustomerList)
|
||||
})
|
||||
});
|
||||
describe("On given query string", () => {
|
||||
const fakedQuery = faker.person.fullName();
|
||||
describe("And returning list from repo", () => {
|
||||
beforeEach(() => {
|
||||
mockedFetchList.mockResolvedValue(right(fakedCustomerList));
|
||||
});
|
||||
it("Then should return correct list of customers", async () => {
|
||||
// ! Act
|
||||
const response = await usecase(fakedQuery);
|
||||
// ? Assert
|
||||
expect(response).toEqual(fakedCustomerList);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
82
yarn.lock
82
yarn.lock
@ -620,6 +620,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@pkgr/core@^0.1.0":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
|
||||
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
|
||||
|
||||
"@radix-ui/react-compose-refs@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
|
||||
@ -1465,6 +1470,11 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
confusing-browser-globals@^1.0.10:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81"
|
||||
integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==
|
||||
|
||||
console-control-strings@^1.0.0, console-control-strings@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
@ -1835,6 +1845,25 @@ escape-string-regexp@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
||||
|
||||
eslint-config-airbnb-base@^15.0.0:
|
||||
version "15.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236"
|
||||
integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==
|
||||
dependencies:
|
||||
confusing-browser-globals "^1.0.10"
|
||||
object.assign "^4.1.2"
|
||||
object.entries "^1.1.5"
|
||||
semver "^6.3.0"
|
||||
|
||||
eslint-config-airbnb@^19.0.4:
|
||||
version "19.0.4"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz#84d4c3490ad70a0ffa571138ebcdea6ab085fdc3"
|
||||
integrity sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==
|
||||
dependencies:
|
||||
eslint-config-airbnb-base "^15.0.0"
|
||||
object.assign "^4.1.2"
|
||||
object.entries "^1.1.5"
|
||||
|
||||
eslint-config-next@15.0.1:
|
||||
version "15.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.0.1.tgz#5f49a01d312420cdbf1e87299396ef779ae99004"
|
||||
@ -1851,6 +1880,16 @@ eslint-config-next@15.0.1:
|
||||
eslint-plugin-react "^7.35.0"
|
||||
eslint-plugin-react-hooks "^5.0.0"
|
||||
|
||||
eslint-config-prettier@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f"
|
||||
integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==
|
||||
|
||||
eslint-import-resolver-alias@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz#297062890e31e4d6651eb5eba9534e1f6e68fc97"
|
||||
integrity sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==
|
||||
|
||||
eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9:
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
|
||||
@ -1860,7 +1899,7 @@ eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9:
|
||||
is-core-module "^2.13.0"
|
||||
resolve "^1.22.4"
|
||||
|
||||
eslint-import-resolver-typescript@^3.5.2:
|
||||
eslint-import-resolver-typescript@^3.5.2, eslint-import-resolver-typescript@^3.6.3:
|
||||
version "3.6.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz#bb8e388f6afc0f940ce5d2c5fd4a3d147f038d9e"
|
||||
integrity sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==
|
||||
@ -1927,6 +1966,14 @@ eslint-plugin-jsx-a11y@^6.10.0:
|
||||
safe-regex-test "^1.0.3"
|
||||
string.prototype.includes "^2.0.1"
|
||||
|
||||
eslint-plugin-prettier@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz#d1c8f972d8f60e414c25465c163d16f209411f95"
|
||||
integrity sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
synckit "^0.9.1"
|
||||
|
||||
eslint-plugin-react-hooks@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101"
|
||||
@ -2063,6 +2110,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-diff@^1.1.2:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
|
||||
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
|
||||
|
||||
fast-glob@3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
|
||||
@ -3107,7 +3159,7 @@ object-keys@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||
|
||||
object.assign@^4.1.4, object.assign@^4.1.5:
|
||||
object.assign@^4.1.2, object.assign@^4.1.4, object.assign@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0"
|
||||
integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==
|
||||
@ -3117,7 +3169,7 @@ object.assign@^4.1.4, object.assign@^4.1.5:
|
||||
has-symbols "^1.0.3"
|
||||
object-keys "^1.1.1"
|
||||
|
||||
object.entries@^1.1.8:
|
||||
object.entries@^1.1.5, object.entries@^1.1.8:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41"
|
||||
integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==
|
||||
@ -3341,6 +3393,18 @@ prelude-ls@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier-linter-helpers@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
|
||||
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
|
||||
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
|
||||
|
||||
pretty-format@^27.0.2:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||
@ -3589,7 +3653,7 @@ scheduler@0.25.0-rc-69d4b800-20241021:
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0-rc-69d4b800-20241021.tgz#336e47ef2bd5eddb0ebacfd910b5df1b236d92bd"
|
||||
integrity sha512-S5AYX/YhMAN6u9AXgKYbZP4U4ZklC6R9Q7HmFSBk7d4DLiHVNxvAvlSvuM4nxFkwOk50MnpfTKQ7UWHXDOc9Eg==
|
||||
|
||||
semver@^6.0.0, semver@^6.3.1:
|
||||
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
@ -3884,6 +3948,14 @@ symbol-tree@^3.2.4:
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
|
||||
synckit@^0.9.1:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62"
|
||||
integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.1.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
tailwind-merge@^2.5.4:
|
||||
version "2.5.4"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.4.tgz#4bf574e81fa061adeceba099ae4df56edcee78d1"
|
||||
@ -4041,7 +4113,7 @@ tsconfig-paths@^3.15.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@*, tslib@2, tslib@^2.4.0:
|
||||
tslib@*, tslib@2, tslib@^2.4.0, tslib@^2.6.2:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
Loading…
x
Reference in New Issue
Block a user