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