```
├── .github/
├── ISSUE_TEMPLATE/
├── bug_report.md
├── feature_request.md
├── dependabot.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app/
├── [locale]/
├── [...rest]/
├── page.tsx
├── layout.tsx
├── page.tsx
├── template/
├── [id]/
├── page.tsx
├── api/
├── invoice/
├── export/
├── route.ts
├── generate/
├── route.ts
├── send/
├── route.ts
├── components/
├── dev/
├── DevDebug.tsx
├── index.ts
├── invoice/
├── InvoiceActions.tsx
├── InvoiceForm.tsx
├── InvoiceMain.tsx
├── actions/
├── FinalPdf.tsx
├── LivePreview.tsx
├── PdfViewer.tsx
├── form/
├── Charges.tsx
├── SingleItem.tsx
├── TemplateSelector.tsx
├── sections/
├── BillFromSection.tsx
├── BillToSection.tsx
├── ImportJsonButton.tsx
├── InvoiceDetails.tsx
├── InvoiceSummary.tsx
├── Items.tsx
├── PaymentInformation.tsx
├── wizard/
├── WizardNavigation.tsx
├── WizardProgress.tsx
├── WizardStep.tsx
├── layout/
├── BaseFooter.tsx
├── BaseNavbar.tsx
├── modals/
├── alerts/
├── NewInvoiceAlert.tsx
├── email/
├── SendPdfToEmailModal.tsx
├── invoice/
├── InvoiceExportModal.tsx
├── InvoiceLoaderModal.tsx
├── components/
├── SavedInvoicesList.tsx
├── signature/
├── SignatureModal.tsx
├── components/
├── SignatureColorSelector.tsx
├── SignatureFontSelector.tsx
├── tabs/
├── DrawSignature.tsx
├── TypeSignature.tsx
├── UploadSignature.tsx
├── reusables/
├── BaseButton.tsx
├── LanguageSelector.tsx
├── Subheading.tsx
├── ThemeSwitcher.tsx
├── form-fields/
├── ChargeInput.tsx
├── CurrencySelector.tsx
├── DatePickerFormField.tsx
├── FormCustomInput.tsx
├── FormFile.tsx
├── FormInput.tsx
├── FormTextarea.tsx
├── templates/
├── email/
├── SendPdfEmail.tsx
├── invoice-pdf/
├── DynamicInvoiceTemplate.tsx
├── InvoiceLayout.tsx
├── InvoiceTemplate1.tsx
├── InvoiceTemplate2.tsx
├── globals.css
├── layout.tsx
├── not-found.tsx
├── page.tsx
├── robots.txt
├── components.json
├── components/
├── ui/
├── alert-dialog.tsx
├── aspect-ratio.tsx
├── badge.tsx
├── button.tsx
├── calendar.tsx
├── card.tsx
├── dialog.tsx
├── form.tsx
├── input.tsx
├── label.tsx
├── navigation-menu.tsx
├── popover.tsx
├── scroll-area.tsx
├── select.tsx
├── skeleton.tsx
├── switch.tsx
├── table.tsx
├── tabs.tsx
├── textarea.tsx
├── toast.tsx
├── toaster.tsx
├── tooltip.tsx
├── use-toast.ts
├── contexts/
├── ChargesContext.tsx
├── InvoiceContext.tsx
├── Providers.tsx
├── SignatureContext.tsx
```
## /.github/ISSUE_TEMPLATE/bug_report.md
---
name: Bug Report
about: Create a bug report to help us improve
title: "[BUG] - "
---
#### Issue Summary
[Concise description of the issue]
#### Environment
- **Browser/Platform:** [Insert Browser/Platform Name and Version]
#### Steps to Reproduce
1. [First step to reproduce the issue]
2. [Second step to reproduce the issue]
3. [And so on...]
#### Expected Behavior
[What you expected to happen]
#### Actual Behavior
[What actually happened]
#### Screenshots/Logs
[If applicable, include relevant screenshots or error logs]
#### Additional Information
[Include any additional information that may help in diagnosing the issue]
#### Possible Solutions
[If you have suggestions for how to fix the issue, provide them here]
---
#### Acceptance Criteria
- [ ] The issue is confirmed and reproducible.
- [ ] Relevant details about the environment are provided.
- [ ] Steps to reproduce are clear and concise.
- [ ] Expected and actual behavior are described.
- [ ] Screenshots/logs (if applicable) are attached.
- [ ] Any additional relevant information is included.
---
[Note: Please replace the placeholders in square brackets with specific information related to your bug report.]
## /.github/ISSUE_TEMPLATE/feature_request.md
---
name: Feature Request
about: Suggest an enhancement or a new feature
title: "[FEATURE] - "
---
#### Feature Summary
[Concise description of the feature or enhancement]
#### Use Case
[Explain the specific use case or scenario where this feature would be beneficial]
#### Proposed Solution
[Describe your proposed solution or how you envision implementing this feature]
#### Additional Information
[Include any additional context or details that support your feature request]
---
[Note: Please replace the placeholders in square brackets with specific information related to your feature request.]
## /.github/dependabot.yml
```yml path="/.github/dependabot.yml"
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10
versioning-strategy: "lockfile-only"
allow:
- dependency-type: "all"
ignore:
- dependency-name: "next"
- dependency-name: "react"
- dependency-name: "react-dom"
- dependency-name: "@types/node"
- dependency-name: "@types/react"
- dependency-name: "@types/react-dom"
```
## /.gitignore
```gitignore path="/.gitignore"
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# cache
/.cache
```
## /Dockerfile
``` path="/Dockerfile"
FROM node:22-alpine AS build
WORKDIR /app
COPY package* .
RUN npm install
COPY . .
RUN npm run build
FROM node:22-alpine AS production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=build --chown=nextjs:nodejs /app/.next ./.next
COPY --from=build --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=build --chown=nextjs:nodejs /app/package.json ./package.json
COPY --from=build --chown=nextjs:nodejs /app/public ./public
EXPOSE 3000
CMD npm start
```
## /LICENSE
``` path="/LICENSE"
MIT License
Copyright (c) 2023 Ali Abbasov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## /README.md
[](https://discord.gg/uhXKHbVKHZ)
# Invoify
Invoify is a web-based invoice generator application built with Next.js 13, TypeScript, React, and the Shadcn UI library. It provides an easy way to create and manage professional invoices.

## Table of Contents
- [Invoify](#invoify)
- [Table of Contents](#table-of-contents)
- [Technologies](#technologies)
- [Core Technologies](#core-technologies)
- [Additional Dependencies](#additional-dependencies)
- [Roadmap](#roadmap)
- [Demo](#demo)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [License](#license)
## Technologies
### Core Technologies
- **Next.js:** React framework for SSR and client-side navigation.
- **TypeScript:** JavaScript superset with static typing.
- **Shadcn-UI:** UI library for enhanced visuals.
- **Tailwind:** Utility-first CSS framework.
- **React Hook Form:** Form management for React.
- **Zod:** TypeScript-first schema validation.
- **Puppeteer:** PDF generation with headless browsers.
### Additional Dependencies
- **Nodemailer:** Node.js module for sending emails.
- **Lucide Icons:** Collection of customizable SVG icons.
## Roadmap
- [x] **Easily Create Invoices:** Utilize a simple form to quickly generate invoices.
- [x] **Save for Future Access:** Store your invoices directly in your browser for easy retrieval.
- [x] **Retrieve Invoices Effortlessly:** Load and access invoices seamlessly from your saved list.
- [x] **Flexible Download Options:** Download invoices directly or send them via email in PDF format.
- [x] **Template Variety:** Choose from multiple (currently 2) invoice templates.
- [x] **Live Preview:** Edit the form and see changes in real-time with the live preview feature.
- [x] **Export in Various Formats:** Export invoices in different formats, including JSON, XLSX, CSV, and XML.
- [ ] **I18N Support:** i18n support with multiple languages for UI and templates.
- [ ] **Themeable Templates:** Select a theme color for the invoice
- [ ] **Custom Inputs:** Define your own inputs that are missing from the default invoice builder. (Ex: VAT number)
- [ ] **Individual Tax for Line Items:** Add tax details for a specific line item other than the general tax
## Demo
> [!NOTE]
> Please be advised that there are currently issues when using this application in the Mozilla Firefox browser. For more information, refer to [Issue #11](https://github.com/aliabb01/invoify/issues/11).
Visit the [live demo](https://invoify.vercel.app) to see Invoify in action.
## Getting Started
Follow these instructions to get Invoify up and running on your local machine.
### Prerequisites
- Node.js and npm installed on your system.
### Installation
1. Clone the repository:
```bash
git clone https://github.com/al1abb/invoify.git
cd invoify
```
2. Install dependencies
```bash
npm install
```
3. Create an .env.local file with this content (This step is for sending pdf to email feature):
```env
NODEMAILER_EMAIL=your_email@example.com
NODEMAILER_PW=your_email_password
```
4. Start development server
```bash
npm run dev
```
5. Open your web browser and access the application at [http://localhost:3000](http://localhost:3000)
## License
Distributed under the MIT License. See `LICENSE.txt` for more information.
## Discord
Join the Discord server [here](https://discord.gg/uhXKHbVKHZ)
## /app/[locale]/[...rest]/page.tsx
```tsx path="/app/[locale]/[...rest]/page.tsx"
import { notFound } from "next/navigation";
export default function CatchAllPage() {
notFound();
}
```
## /app/[locale]/layout.tsx
```tsx path="/app/[locale]/layout.tsx"
import type { Metadata } from "next";
import { notFound } from "next/navigation";
// Fonts
import {
alexBrush,
dancingScript,
greatVibes,
outfit,
parisienne,
} from "@/lib/fonts";
// Favicon
import Favicon from "@/public/assets/favicon/favicon.ico";
// Vercel Analytics
import { Analytics } from "@vercel/analytics/react";
// Next Intl
import { NextIntlClientProvider } from "next-intl";
// ShadCn
import { Toaster } from "@/components/ui/toaster";
// Components
import { BaseNavbar, BaseFooter } from "@/app/components";
// Contexts
import Providers from "@/contexts/Providers";
// SEO
import { JSONLD, ROOTKEYWORDS } from "@/lib/seo";
// Variables
import { BASE_URL, GOOGLE_SC_VERIFICATION, LOCALES } from "@/lib/variables";
export const metadata: Metadata = {
title: "Invoify | Free Invoice Generator",
description:
"Create invoices effortlessly with Invoify, the free invoice generator. Try it now!",
icons: [{ rel: "icon", url: Favicon.src }],
keywords: ROOTKEYWORDS,
viewport: "width=device-width, initial-scale=1",
robots: {
index: true,
follow: true,
},
alternates: {
canonical: BASE_URL,
},
authors: {
name: "Ali Abbasov",
url: "https://aliabb.vercel.app",
},
verification: {
google: GOOGLE_SC_VERIFICATION,
},
};
export function generateStaticParams() {
const locales = LOCALES.map((locale) => locale.code);
return locales;
}
export default async function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: string };
}) {
let messages;
try {
messages = (await import(`@/i18n/locales/${locale}.json`)).default;
} catch (error) {
notFound();
}
return (
{children}
{/* Toast component */}
{/* Vercel analytics */}
);
}
```
## /app/[locale]/page.tsx
```tsx path="/app/[locale]/page.tsx"
// Components
import { InvoiceMain } from "@/app/components";
export default function Home() {
return (
);
}
```
## /app/[locale]/template/[id]/page.tsx
```tsx path="/app/[locale]/template/[id]/page.tsx"
"use client";
// Next
import dynamic from "next/dynamic";
// RHF
import { useFormContext } from "react-hook-form";
// Types
import { InvoiceType } from "@/types";
type ViewTemplatePageProps = {
params: { id: string };
};
const ViewTemplate = ({ params }: ViewTemplatePageProps) => {
const templateNumber = params.id;
const DynamicComponent = dynamic(
() =>
import(
`@/app/components/templates/invoice-pdf/InvoiceTemplate${templateNumber}`
)
);
const { getValues } = useFormContext();
const formValues = getValues();
return (
);
};
export default ViewTemplate;
```
## /app/api/invoice/export/route.ts
```ts path="/app/api/invoice/export/route.ts"
import { NextRequest } from "next/server";
// Services
import { exportInvoiceService } from "@/services/invoice/server/exportInvoiceService";
export async function POST(req: NextRequest) {
const result = await exportInvoiceService(req);
return result;
}
```
## /app/api/invoice/generate/route.ts
```ts path="/app/api/invoice/generate/route.ts"
import { NextRequest } from "next/server";
// Services
import { generatePdfService } from "@/services/invoice/server/generatePdfService";
export async function POST(req: NextRequest) {
const result = await generatePdfService(req);
return result;
}
```
## /app/api/invoice/send/route.ts
```ts path="/app/api/invoice/send/route.ts"
import { NextRequest, NextResponse } from "next/server";
// Services
import { sendPdfToEmailService } from "@/services/invoice/server/sendPdfToEmailService";
export async function POST(req: NextRequest) {
try {
const emailSent = await sendPdfToEmailService(req);
if (emailSent) {
return new NextResponse("Email sent successfully", {
status: 200,
});
} else {
return new NextResponse("Failed to send email", {
status: 500,
});
}
} catch (err) {
console.log(err);
return new NextResponse("Failed to send email", { status: 500 });
}
}
```
## /app/components/dev/DevDebug.tsx
```tsx path="/app/components/dev/DevDebug.tsx"
"use client";
// Next
import Link from "next/link";
// RHF
import { useFormContext } from "react-hook-form";
// Component
import { BaseButton } from "@/app/components";
// Variables
import { FORM_FILL_VALUES } from "@/lib/variables";
type DevDebugProps = {};
const DevDebug = ({}: DevDebugProps) => {
const { reset, formState } = useFormContext();
return (
DEV:
Form: {formState.isDirty ? "Dirty" : "Clean"}
reset(FORM_FILL_VALUES)}
>
Fill in the form
Template 1
Template 2
);
};
export default DevDebug;
```
## /app/components/index.ts
```ts path="/app/components/index.ts"
/* =========================
* Navigation
========================= */
import BaseNavbar from "./layout/BaseNavbar";
import BaseFooter from "./layout/BaseFooter";
/* =========================
* Invoice
========================= */
import InvoiceMain from "./invoice/InvoiceMain";
import InvoiceForm from "./invoice/InvoiceForm";
import InvoiceActions from "./invoice/InvoiceActions";
/* =========================
* Invoice components
========================= */
// * Form
// Form components
import SingleItem from "./invoice/form/SingleItem";
import Charges from "./invoice/form/Charges";
import TemplateSelector from "./invoice/form/TemplateSelector";
// Form / Wizard
import WizardNavigation from "./invoice/form/wizard/WizardNavigation";
import WizardStep from "./invoice/form/wizard/WizardStep";
import WizardProgress from "./invoice/form/wizard/WizardProgress";
// Form / Sections
import BillFromSection from "./invoice/form/sections/BillFromSection";
import BillToSection from "./invoice/form/sections/BillToSection";
import InvoiceDetails from "./invoice/form/sections/InvoiceDetails";
import Items from "./invoice/form/sections/Items";
import PaymentInformation from "./invoice/form/sections/PaymentInformation";
import InvoiceSummary from "./invoice/form/sections/InvoiceSummary";
import ImportJsonButton from "./invoice/form/sections/ImportJsonButton";
// * Actions
import PdfViewer from "./invoice/actions/PdfViewer";
import LivePreview from "./invoice/actions/LivePreview";
import FinalPdf from "./invoice/actions/FinalPdf";
// * Reusable components
// Form fields
import CurrencySelector from "./reusables/form-fields/CurrencySelector";
import FormInput from "./reusables/form-fields/FormInput";
import FormTextarea from "./reusables/form-fields/FormTextarea";
import DatePickerFormField from "./reusables/form-fields/DatePickerFormField";
import FormFile from "./reusables/form-fields/FormFile";
import ChargeInput from "./reusables/form-fields/ChargeInput";
import FormCustomInput from "./reusables/form-fields/FormCustomInput";
import BaseButton from "./reusables/BaseButton";
import ThemeSwitcher from "./reusables/ThemeSwitcher";
import LanguageSelector from "./reusables/LanguageSelector";
import Subheading from "./reusables/Subheading";
/* =========================
* Modals & Alerts
========================= */
import SendPdfToEmailModal from "./modals/email/SendPdfToEmailModal";
// Import/Export
import InvoiceLoaderModal from "./modals/invoice/InvoiceLoaderModal";
import InvoiceExportModal from "./modals/invoice/InvoiceExportModal";
// Custom Selectors
import SavedInvoicesList from "./modals/invoice/components/SavedInvoicesList";
// Signature
import SignatureModal from "./modals/signature/SignatureModal";
// Signature / Tabs
import DrawSignature from "./modals/signature/tabs/DrawSignature";
import TypeSignature from "./modals/signature/tabs/TypeSignature";
import UploadSignature from "./modals/signature/tabs/UploadSignature";
// Signature / Components
import SignatureColorSelector from "./modals/signature/components/SignatureColorSelector";
import SignatureFontSelector from "./modals/signature/components/SignatureFontSelector";
// Alerts
import NewInvoiceAlert from "./modals/alerts/NewInvoiceAlert";
/* =========================
* Templates
========================= */
// Invoice templates
import DynamicInvoiceTemplate from "./templates/invoice-pdf/DynamicInvoiceTemplate";
import InvoiceLayout from "./templates/invoice-pdf/InvoiceLayout";
import InvoiceTemplate1 from "./templates/invoice-pdf/InvoiceTemplate1";
import InvoiceTemplate2 from "./templates/invoice-pdf/InvoiceTemplate2";
// Email templates
import SendPdfEmail from "./templates/email/SendPdfEmail";
/* =========================
? DEV ONLY
========================= */
import DevDebug from "./dev/DevDebug";
export {
BaseNavbar,
BaseFooter,
InvoiceMain,
InvoiceForm,
InvoiceActions,
BillFromSection,
BillToSection,
InvoiceDetails,
Items,
SingleItem,
Charges,
TemplateSelector,
WizardNavigation,
WizardStep,
WizardProgress,
PaymentInformation,
InvoiceSummary,
CurrencySelector,
SavedInvoicesList,
PdfViewer,
LivePreview,
FinalPdf,
FormInput,
FormTextarea,
DatePickerFormField,
FormFile,
ChargeInput,
FormCustomInput,
BaseButton,
ThemeSwitcher,
LanguageSelector,
Subheading,
SendPdfToEmailModal,
InvoiceLoaderModal,
InvoiceExportModal,
ImportJsonButton,
SignatureModal,
DrawSignature,
TypeSignature,
UploadSignature,
SignatureColorSelector,
SignatureFontSelector,
NewInvoiceAlert,
DynamicInvoiceTemplate,
InvoiceLayout,
InvoiceTemplate1,
InvoiceTemplate2,
SendPdfEmail,
DevDebug,
};
```
## /app/components/invoice/InvoiceActions.tsx
```tsx path="/app/components/invoice/InvoiceActions.tsx"
"use client";
// ShadCn
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
// Components
import {
PdfViewer,
BaseButton,
NewInvoiceAlert,
InvoiceLoaderModal,
InvoiceExportModal,
} from "@/app/components";
// Contexts
import { useInvoiceContext } from "@/contexts/InvoiceContext";
import { useTranslationContext } from "@/contexts/TranslationContext";
// Icons
import { FileInput, FolderUp, Import, Plus } from "lucide-react";
const InvoiceActions = () => {
const { invoicePdfLoading } = useInvoiceContext();
const { _t } = useTranslationContext();
return (
{_t("actions.title")}
{_t("actions.description")}
{/* Load modal button */}
{_t("actions.loadInvoice")}
{/* Export modal button */}
{_t("actions.exportInvoice")}
{/* New invoice button */}
{_t("actions.newInvoice")}
{/* Generate pdf button */}
{_t("actions.generatePdf")}
{/* Live preview and Final pdf */}
);
};
export default InvoiceActions;
```
## /app/components/invoice/InvoiceForm.tsx
```tsx path="/app/components/invoice/InvoiceForm.tsx"
"use client";
import { useMemo } from "react";
// RHF
import { useFormContext, useWatch } from "react-hook-form";
// ShadCn
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
// React Wizard
import { Wizard } from "react-use-wizard";
// Components
import {
WizardStep,
BillFromSection,
BillToSection,
InvoiceDetails,
Items,
PaymentInformation,
InvoiceSummary,
} from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
const InvoiceForm = () => {
const { _t } = useTranslationContext();
const { control } = useFormContext();
// Get invoice number variable
const invoiceNumber = useWatch({
name: "details.invoiceNumber",
control,
});
const invoiceNumberLabel = useMemo(() => {
if (invoiceNumber) {
return `#${invoiceNumber}`;
} else {
return _t("form.newInvBadge");
}
}, [invoiceNumber]);
return (
{_t("form.title")}
{invoiceNumberLabel}
{_t("form.description")}
);
};
export default InvoiceForm;
```
## /app/components/invoice/InvoiceMain.tsx
```tsx path="/app/components/invoice/InvoiceMain.tsx"
"use client";
// RHF
import { useFormContext } from "react-hook-form";
// ShadCn
import { Form } from "@/components/ui/form";
// Components
import { InvoiceActions, InvoiceForm } from "@/app/components";
// Context
import { useInvoiceContext } from "@/contexts/InvoiceContext";
// Types
import { InvoiceType } from "@/types";
const InvoiceMain = () => {
const { handleSubmit } = useFormContext();
// Get the needed values from invoice context
const { onFormSubmit } = useInvoiceContext();
return (
<>
>
);
};
export default InvoiceMain;
```
## /app/components/invoice/actions/FinalPdf.tsx
```tsx path="/app/components/invoice/actions/FinalPdf.tsx"
"use client";
// ShadCn
import { AspectRatio } from "@/components/ui/aspect-ratio";
// Components
import { BaseButton, SendPdfToEmailModal, Subheading } from "@/app/components";
// Contexts
import { useInvoiceContext } from "@/contexts/InvoiceContext";
// Icons
import {
BookmarkIcon,
DownloadCloudIcon,
Eye,
Mail,
MoveLeft,
Printer,
} from "lucide-react";
export default function FinalPdf() {
const {
pdfUrl,
removeFinalPdf,
previewPdfInTab,
downloadPdf,
printPdf,
saveInvoice,
sendPdfToMail,
} = useInvoiceContext();
return (
<>
Final PDF:
Back to Live Preview
{/* Buttons */}
Preview
Download
Print
Save
Send to mail
>
);
}
```
## /app/components/invoice/actions/LivePreview.tsx
```tsx path="/app/components/invoice/actions/LivePreview.tsx"
// Components
import { DynamicInvoiceTemplate, Subheading } from "@/app/components";
// Types
import { InvoiceType } from "@/types";
type LivePreviewProps = {
data: InvoiceType;
};
export default function LivePreview({ data }: LivePreviewProps) {
return (
<>
Live Preview:
>
);
}
```
## /app/components/invoice/actions/PdfViewer.tsx
```tsx path="/app/components/invoice/actions/PdfViewer.tsx"
"use client";
// Debounce
import { useDebounce } from "use-debounce";
// RHF
import { useFormContext } from "react-hook-form";
// Components
import { FinalPdf, LivePreview } from "@/app/components";
// Contexts
import { useInvoiceContext } from "@/contexts/InvoiceContext";
// Types
import { InvoiceType } from "@/types";
const PdfViewer = () => {
const { invoicePdf } = useInvoiceContext();
const { watch } = useFormContext();
const [debouncedWatch] = useDebounce(watch, 1000);
const formValues = debouncedWatch();
return (
{invoicePdf.size == 0 ? (
) : (
)}
);
};
export default PdfViewer;
```
## /app/components/invoice/form/Charges.tsx
```tsx path="/app/components/invoice/form/Charges.tsx"
"use client";
// RHF
import { useFormContext } from "react-hook-form";
// ShadCn
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
// Components
import { ChargeInput } from "@/app/components";
// Contexts
import { useChargesContext } from "@/contexts/ChargesContext";
import { useTranslationContext } from "@/contexts/TranslationContext";
// Helpers
import { formatNumberWithCommas } from "@/lib/helpers";
// Types
import { InvoiceType } from "@/types";
const Charges = () => {
const {
formState: { errors },
} = useFormContext();
const { _t } = useTranslationContext();
const {
discountSwitch,
setDiscountSwitch,
taxSwitch,
setTaxSwitch,
shippingSwitch,
setShippingSwitch,
discountType,
setDiscountType,
taxType,
setTaxType,
shippingType,
setShippingType,
totalInWordsSwitch,
setTotalInWordsSwitch,
currency,
subTotal,
totalAmount,
} = useChargesContext();
const switchAmountType = (
type: string,
setType: (type: string) => void
) => {
if (type == "amount") {
setType("percentage");
} else {
setType("amount");
}
};
return (
<>
{/* Charges */}
{/* Switches */}
{_t("form.steps.summary.discount")}
{
setDiscountSwitch(value);
}}
/>
{_t("form.steps.summary.tax")}
{
setTaxSwitch(value);
}}
/>
{_t("form.steps.summary.shipping")}
{
setShippingSwitch(value);
}}
/>
{_t("form.steps.summary.subTotal")}
{formatNumberWithCommas(subTotal)} {currency}
{discountSwitch && (
)}
{taxSwitch && (
)}
{shippingSwitch && (
)}
{_t("form.steps.summary.totalAmount")}
{formatNumberWithCommas(totalAmount)} {currency}
{errors.details?.totalAmount?.message}
{_t("form.steps.summary.includeTotalInWords")}
{" "}
{totalInWordsSwitch
? _t("form.steps.summary.yes")
: _t("form.steps.summary.no")}
{
setTotalInWordsSwitch(value);
}}
/>
>
);
};
export default Charges;
```
## /app/components/invoice/form/SingleItem.tsx
```tsx path="/app/components/invoice/form/SingleItem.tsx"
"use client";
import { useEffect } from "react";
// RHF
import { FieldArrayWithId, useFormContext, useWatch } from "react-hook-form";
// DnD
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
// ShadCn
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
// Components
import { BaseButton, FormInput, FormTextarea } from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
// Icons
import { ChevronDown, ChevronUp, GripVertical, Trash2 } from "lucide-react";
// Types
import { ItemType, NameType } from "@/types";
type SingleItemProps = {
name: NameType;
index: number;
fields: ItemType[];
field: FieldArrayWithId;
moveFieldUp: (index: number) => void;
moveFieldDown: (index: number) => void;
removeField: (index: number) => void;
};
const SingleItem = ({
name,
index,
fields,
field,
moveFieldUp,
moveFieldDown,
removeField,
}: SingleItemProps) => {
const { control, setValue } = useFormContext();
const { _t } = useTranslationContext();
// Items
const itemName = useWatch({
name: `${name}[${index}].name`,
control,
});
const rate = useWatch({
name: `${name}[${index}].unitPrice`,
control,
});
const quantity = useWatch({
name: `${name}[${index}].quantity`,
control,
});
const total = useWatch({
name: `${name}[${index}].total`,
control,
});
// Currency
const currency = useWatch({
name: `details.currency`,
control,
});
useEffect(() => {
// Calculate total when rate or quantity changes
if (rate != undefined && quantity != undefined) {
const calculatedTotal = (rate * quantity).toFixed(2);
setValue(`${name}[${index}].total`, calculatedTotal);
}
}, [rate, quantity]);
// DnD
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: field.id });
const style = {
transition,
transform: CSS.Transform.toString(transform),
};
const boxDragClasses = isDragging
? "border-2 bg-gray-200 border-blue-600 dark:bg-slate-900 z-10"
: "border";
const gripDragClasses = isDragging
? "opacity-0 group-hover:opacity-100 transition-opacity cursor-grabbing"
: "cursor-grab";
return (
{/* {isDragging &&
} */}
{itemName != "" ? (
#{index + 1} - {itemName}
) : (
#{index + 1} - Empty name
)}
{/* Drag and Drop Button */}
{/* Up Button */}
moveFieldUp(index)}
disabled={index === 0}
>
{/* Down Button */}
moveFieldDown(index)}
disabled={index === fields.length - 1}
>
{/* Not allowing deletion for first item when there is only 1 item */}
{fields.length > 1 && (
removeField(index)}
>
{_t("form.steps.lineItems.removeItem")}
)}
);
};
export default SingleItem;
```
## /app/components/invoice/form/TemplateSelector.tsx
```tsx path="/app/components/invoice/form/TemplateSelector.tsx"
"use client";
import Image from "next/image";
// RHF
import { useFormContext } from "react-hook-form";
// ShadCn
import {
Card,
CardContent,
CardDescription,
CardHeader,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
// Components
import {
BaseButton,
InvoiceTemplate1,
InvoiceTemplate2,
} from "@/app/components";
// Template images
import template1 from "@/public/assets/img/invoice-1-example.png";
import template2 from "@/public/assets/img/invoice-2-example.png";
// Icons
import { Check } from "lucide-react";
// Types
import { InvoiceType } from "@/types";
const TemplateSelector = () => {
const { watch, setValue } = useFormContext();
const formValues = watch();
const templates = [
{
id: 1,
name: "Template 1",
description: "Template 1 description",
img: template1,
component: ,
},
{
id: 2,
name: "Template 2",
description: "Second template",
img: template2,
component: ,
},
];
return (
<>
Choose Invoice Template:
Templates
Select one of the predefined templates
{templates.map((template, idx) => (
{template.name}
{formValues.details.pdfTemplate ===
template.id && (
)}
setValue(
"details.pdfTemplate",
template.id
)
}
/>
{/* {template.component} */}
setValue(
"details.pdfTemplate",
template.id
)
}
>
Select
))}
>
);
};
export default TemplateSelector;
```
## /app/components/invoice/form/sections/BillFromSection.tsx
```tsx path="/app/components/invoice/form/sections/BillFromSection.tsx"
"use client";
// RHF
import { useFieldArray, useFormContext } from "react-hook-form";
// Components
import {
BaseButton,
FormCustomInput,
FormInput,
Subheading,
} from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
// Icons
import { Plus } from "lucide-react";
const BillFromSection = () => {
const { control } = useFormContext();
const { _t } = useTranslationContext();
const CUSTOM_INPUT_NAME = "sender.customInputs";
const { fields, append, remove } = useFieldArray({
control: control,
name: CUSTOM_INPUT_NAME,
});
const addNewCustomInput = () => {
append({
key: "",
value: "",
});
};
const removeCustomInput = (index: number) => {
remove(index);
};
return (
{_t("form.steps.fromAndTo.billFrom")}:
{
const target = e.target as HTMLInputElement;
target.value = target.value.replace(/[^\d\+\-\(\)\s]/g, "");
}}
/>
{/* //? key = field.id fixes a bug where wrong field gets deleted */}
{fields?.map((field, index) => (
))}
{_t("form.steps.fromAndTo.addCustomInput")}
);
};
export default BillFromSection;
```
## /app/components/invoice/form/sections/BillToSection.tsx
```tsx path="/app/components/invoice/form/sections/BillToSection.tsx"
"use client";
// RHF
import { useFieldArray, useFormContext } from "react-hook-form";
// Components
import {
BaseButton,
FormCustomInput,
FormInput,
Subheading,
} from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
// Icons
import { Plus } from "lucide-react";
const BillToSection = () => {
const { control } = useFormContext();
const { _t } = useTranslationContext();
const CUSTOM_INPUT_NAME = "receiver.customInputs";
const { fields, append, remove } = useFieldArray({
control: control,
name: CUSTOM_INPUT_NAME,
});
const addNewCustomInput = () => {
append({
key: "",
value: "",
});
};
const removeCustomInput = (index: number) => {
remove(index);
};
return (
{_t("form.steps.fromAndTo.billTo")}:
{
const target = e.target as HTMLInputElement;
target.value = target.value.replace(/[^\d\+\-\(\)\s]/g, "");
}}
/>
{/* //? key = field.id fixes a bug where wrong field gets deleted */}
{fields?.map((field, index) => (
))}
{_t("form.steps.fromAndTo.addCustomInput")}
);
};
export default BillToSection;
```
## /app/components/invoice/form/sections/ImportJsonButton.tsx
```tsx path="/app/components/invoice/form/sections/ImportJsonButton.tsx"
"use client"
import { useRef } from 'react';
import { BaseButton } from '@/app/components';
import { useInvoiceContext } from '@/contexts/InvoiceContext';
import { Import } from 'lucide-react';
type ImportJsonButtonType = {
setOpen: (open: boolean) => void;
}
const ImportJsonButton = ({ setOpen }: ImportJsonButtonType) => {
const fileInputRef = useRef(null);
const { importInvoice, invoicePdfLoading } = useInvoiceContext();
const handleClick = () => {
fileInputRef.current?.click();
};
const handleFileChange = (event: React.ChangeEvent) => {
const file = event.target.files?.[0];
if (file && file.type === 'application/json') {
importInvoice(file);
setOpen(false);
}
// Reset input value to allow selecting the same file again
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<>
Import JSON
>
);
};
export default ImportJsonButton;
```
## /app/components/invoice/form/sections/InvoiceDetails.tsx
```tsx path="/app/components/invoice/form/sections/InvoiceDetails.tsx"
"use client";
// Components
import {
CurrencySelector,
DatePickerFormField,
FormInput,
FormFile,
Subheading,
TemplateSelector,
} from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
const InvoiceDetails = () => {
const { _t } = useTranslationContext();
return (
{_t("form.steps.invoiceDetails.heading")}:
);
};
export default InvoiceDetails;
```
## /app/components/invoice/form/sections/InvoiceSummary.tsx
```tsx path="/app/components/invoice/form/sections/InvoiceSummary.tsx"
"use client";
// Components
import {
Charges,
FormTextarea,
SignatureModal,
Subheading,
} from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
import { SignatureContextProvider } from "@/contexts/SignatureContext";
const InvoiceSummary = () => {
const { _t } = useTranslationContext();
return (
{_t("form.steps.summary.heading")}:
{/* Signature dialog */}
{/* Additional notes & Payment terms */}
{/* Final charges */}
);
};
export default InvoiceSummary;
```
## /app/components/invoice/form/sections/Items.tsx
```tsx path="/app/components/invoice/form/sections/Items.tsx"
"use client";
import React, { useCallback, useState } from "react";
// RHF
import { useFieldArray, useFormContext } from "react-hook-form";
// DnD
import {
DndContext,
closestCenter,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
DragEndEvent,
DragOverlay,
UniqueIdentifier,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
// Components
import { BaseButton, SingleItem, Subheading } from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
// Icons
import { Plus } from "lucide-react";
// Types
import { InvoiceType } from "@/types";
const Items = () => {
const { control, setValue } = useFormContext();
const { _t } = useTranslationContext();
const ITEMS_NAME = "details.items";
const { fields, append, remove, move } = useFieldArray({
control: control,
name: ITEMS_NAME,
});
const addNewField = () => {
append({
name: "",
description: "",
quantity: 0,
unitPrice: 0,
total: 0,
});
};
const removeField = (index: number) => {
remove(index);
};
const moveFieldUp = (index: number) => {
if (index > 0) {
move(index, index - 1);
}
};
const moveFieldDown = (index: number) => {
if (index < fields.length - 1) {
move(index, index + 1);
}
};
// DnD
const [activeId, setActiveId] = useState();
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragEnd = useCallback(
async (event: DragEndEvent) => {
const { active, over } = event;
setActiveId(active.id);
if (active.id !== over?.id) {
const oldIndex = fields.findIndex(
(item) => item.id === active.id
);
const newIndex = fields.findIndex(
(item) => item.id === over?.id
);
move(oldIndex, newIndex);
}
},
[fields, setValue]
);
return (
{_t("form.steps.lineItems.heading")}:
{
const { active } = event;
setActiveId(active.id);
}}
onDragEnd={handleDragEnd}
>
{fields.map((field, index) => (
))}
{/*
*/}
{_t("form.steps.lineItems.addNewItem")}
);
};
export default Items;
```
## /app/components/invoice/form/sections/PaymentInformation.tsx
```tsx path="/app/components/invoice/form/sections/PaymentInformation.tsx"
"use client";
// Components
import { FormInput, Subheading } from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
const PaymentInformation = () => {
const { _t } = useTranslationContext();
return (
{_t("form.steps.paymentInfo.heading")}:
);
};
export default PaymentInformation;
```
## /app/components/invoice/form/wizard/WizardNavigation.tsx
```tsx path="/app/components/invoice/form/wizard/WizardNavigation.tsx"
"use client";
// React Wizard
import { useWizard } from "react-use-wizard";
// Components
import { BaseButton } from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
// Icons
import { ArrowLeft, ArrowRight } from "lucide-react";
const WizardNavigation = () => {
const { isFirstStep, isLastStep, handleStep, previousStep, nextStep } =
useWizard();
const { _t } = useTranslationContext();
return (
{!isFirstStep && (
{_t("form.wizard.back")}
)}
{_t("form.wizard.next")}
);
};
export default WizardNavigation;
```
## /app/components/invoice/form/wizard/WizardProgress.tsx
```tsx path="/app/components/invoice/form/wizard/WizardProgress.tsx"
"use client";
// RHF
import { useFormContext } from "react-hook-form";
// React Wizard
import { WizardValues } from "react-use-wizard";
// Components
import { BaseButton } from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
// Types
import { InvoiceType, WizardStepType } from "@/types";
type WizardProgressProps = {
wizard: WizardValues;
};
const WizardProgress = ({ wizard }: WizardProgressProps) => {
const { activeStep, stepCount } = wizard;
const {
formState: { errors },
} = useFormContext();
const { _t } = useTranslationContext();
const step1Valid = !errors.sender && !errors.receiver;
const step2Valid =
!errors.details?.invoiceNumber &&
!errors.details?.dueDate &&
!errors.details?.invoiceDate &&
!errors.details?.currency;
const step3Valid = !errors.details?.items;
const step4Valid = !errors.details?.paymentInformation;
const step5Valid =
!errors.details?.paymentTerms &&
!errors.details?.subTotal &&
!errors.details?.totalAmount &&
!errors.details?.discountDetails?.amount &&
!errors.details?.taxDetails?.amount &&
!errors.details?.shippingDetails?.cost;
/**
* Determines the button variant based on the given WizardStepType.
*
* @param {WizardStepType} step - The wizard step object
* @returns The button variant ("destructive", "default", or "outline") based on the step's validity and active status.
*/
const returnButtonVariant = (step: WizardStepType) => {
if (!step.isValid) {
return "destructive";
}
if (step.id === activeStep) {
return "default";
} else {
return "outline";
}
};
/**
* Checks whether the given WizardStepType has been passed or not.
*
* @param {WizardStepType} currentStep - The WizardStepType object
* @returns `true` if the step has been passed, `false` if it hasn't, or `undefined` if the step is not valid.
*/
const stepPassed = (currentStep: WizardStepType) => {
if (currentStep.isValid) {
return activeStep > currentStep.id ? true : false;
}
};
const steps: WizardStepType[] = [
{
id: 0,
label: _t("form.wizard.fromAndTo"),
isValid: step1Valid,
},
{
id: 1,
label: _t("form.wizard.invoiceDetails"),
isValid: step2Valid,
},
{
id: 2,
label: _t("form.wizard.lineItems"),
isValid: step3Valid,
},
{
id: 3,
label: _t("form.wizard.paymentInfo"),
isValid: step4Valid,
},
{
id: 4,
label: _t("form.wizard.summary"),
isValid: step5Valid,
},
];
return (
{steps.map((step, idx) => (
{
wizard.goToStep(step.id);
}}
>
{step.id + 1}. {step.label}
{/* {step.id != stepCount - 1 && (
)} */}
))}
);
};
export default WizardProgress;
```
## /app/components/invoice/form/wizard/WizardStep.tsx
```tsx path="/app/components/invoice/form/wizard/WizardStep.tsx"
"use client";
import React from "react";
// React Wizard
import { useWizard } from "react-use-wizard";
// Components
import { WizardNavigation, WizardProgress } from "@/app/components";
type WizardStepProps = {
children: React.ReactNode;
};
const WizardStep = ({ children }: WizardStepProps) => {
const wizard = useWizard();
return (
);
};
export default WizardStep;
```
## /app/components/layout/BaseFooter.tsx
```tsx path="/app/components/layout/BaseFooter.tsx"
"use client";
import { useTranslationContext } from "@/contexts/TranslationContext";
// Variables
import { AUTHOR_GITHUB } from "@/lib/variables";
const BaseFooter = () => {
const { _t } = useTranslationContext();
return (
);
};
export default BaseFooter;
```
## /app/components/layout/BaseNavbar.tsx
```tsx path="/app/components/layout/BaseNavbar.tsx"
import { useMemo } from "react";
// Next
import Link from "next/link";
import Image from "next/image";
// Assets
import Logo from "@/public/assets/img/invoify-logo.svg";
// ShadCn
import { Card } from "@/components/ui/card";
// Components
import { DevDebug, LanguageSelector, ThemeSwitcher } from "@/app/components";
const BaseNavbar = () => {
const devEnv = useMemo(() => {
return process.env.NODE_ENV === "development";
}, []);
return (
{/* ? DEV Only */}
{devEnv && }
);
};
export default BaseNavbar;
```
## /app/components/modals/alerts/NewInvoiceAlert.tsx
```tsx path="/app/components/modals/alerts/NewInvoiceAlert.tsx"
"use client";
import React, { useState } from "react";
// RHF
import { useFormContext } from "react-hook-form";
// Context
import { useInvoiceContext } from "@/contexts/InvoiceContext";
// ShadCn
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
type NewInvoiceAlertProps = {
children: React.ReactNode;
};
const NewInvoiceAlert = ({ children }: NewInvoiceAlertProps) => {
// Invoice context
const { newInvoice } = useInvoiceContext();
const {
formState: { isDirty },
} = useFormContext();
const [open, setOpen] = useState(false);
const handleNewInvoice = () => {
if (isDirty) {
// If the form is dirty, show the alert dialog
setOpen(true);
} else {
// If the form is not dirty, call the newInvoice function from context
newInvoice();
}
};
const handleCancel = () => {
setOpen(false);
};
return (
<>
Are you absolutely sure?
This action cannot be undone. You might lose your
data if you have unsaved changes
Cancel
Create new invoice
{/* Not for showing div and instead showing the whole button */}
{React.cloneElement(children as React.ReactElement, {
onClick: handleNewInvoice,
})}
>
);
};
export default NewInvoiceAlert;
```
## /app/components/modals/email/SendPdfToEmailModal.tsx
```tsx path="/app/components/modals/email/SendPdfToEmailModal.tsx"
"use client";
import React, { useState } from "react";
// ShadCn
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
// Components
import { BaseButton } from "@/app/components";
// Helpers
import { isValidEmail } from "@/lib/helpers";
type SendPdfToEmailModalProps = {
sendPdfToMail: (email: string) => Promise;
children: React.ReactNode;
};
const SendPdfToEmailModal = ({
sendPdfToMail,
children,
}: SendPdfToEmailModalProps) => {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const errorMessage = "Please enter a valid email address";
const handleSendPdf = () => {
setLoading(true);
if (isValidEmail(email)) {
sendPdfToMail(email).finally(() => {
setError("");
setLoading(false);
setEmail("");
setOpen(false);
});
} else {
setLoading(false);
setError(errorMessage);
}
};
return (
{children}
Send to email
Please specify the email address for invoice delivery.
Email
setEmail(e.target.value)}
>
{!loading && error && (
{error}
)}
Send PDF
);
};
export default SendPdfToEmailModal;
```
## /app/components/modals/invoice/InvoiceExportModal.tsx
```tsx path="/app/components/modals/invoice/InvoiceExportModal.tsx"
"use client";
import { useState } from "react";
// ShadCn
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
// Components
import { BaseButton } from "@/app/components";
// Context
import { useInvoiceContext } from "@/contexts/InvoiceContext";
// Types
import { ExportTypes } from "@/types";
type InvoiceExportModalType = {
children: React.ReactNode;
};
const InvoiceExportModal = ({ children }: InvoiceExportModalType) => {
const [open, setOpen] = useState(false);
const { invoicePdfLoading, exportInvoiceAs } = useInvoiceContext();
return (
{children}
Export the invoice
Please select export option for your invoice
{/* Export options here */}
exportInvoiceAs(ExportTypes.JSON)}
>
Export as JSON
exportInvoiceAs(ExportTypes.CSV)}
>
Export as CSV
exportInvoiceAs(ExportTypes.XML)}
>
Export as XML
exportInvoiceAs(ExportTypes.XLSX)}
>
Export as XLSX
);
};
export default InvoiceExportModal;
```
## /app/components/modals/invoice/InvoiceLoaderModal.tsx
```tsx path="/app/components/modals/invoice/InvoiceLoaderModal.tsx"
"use client";
import { useState } from "react";
// ShadCn
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
// Components
import { SavedInvoicesList } from "@/app/components";
import { ImportJsonButton } from "@/app/components";
// Context
import { useInvoiceContext } from "@/contexts/InvoiceContext";
type InvoiceLoaderModalType = {
children: React.ReactNode;
};
const InvoiceLoaderModal = ({ children }: InvoiceLoaderModalType) => {
const [open, setOpen] = useState(false);
const { savedInvoices } = useInvoiceContext();
return (
{children}
Saved Invoices
You have {savedInvoices.length} saved invoices
);
};
export default InvoiceLoaderModal;
```
## /app/components/modals/invoice/components/SavedInvoicesList.tsx
```tsx path="/app/components/modals/invoice/components/SavedInvoicesList.tsx"
"use client";
import React from "react";
// RHF
import { useFormContext } from "react-hook-form";
// ShadCn
import { Card, CardContent } from "@/components/ui/card";
// Components
import { BaseButton } from "@/app/components";
// Contexts
import { useInvoiceContext } from "@/contexts/InvoiceContext";
// Helpers
import { formatNumberWithCommas } from "@/lib/helpers";
// Variables
import { DATE_OPTIONS } from "@/lib/variables";
// Types
import { InvoiceType } from "@/types";
type SavedInvoicesListProps = {
setModalState: React.Dispatch>;
};
const SavedInvoicesList = ({ setModalState }: SavedInvoicesListProps) => {
const { savedInvoices, onFormSubmit, deleteInvoice } = useInvoiceContext();
const { reset } = useFormContext();
// TODO: Remove "any" from the function below
// Update fields when selected invoice is changed.
// ? Reason: The fields don't go through validation when invoice loads
const updateFields = (selected: any) => {
// Next 2 lines are so that when invoice loads,
// the dates won't be in the wrong format
// ? Selected cannot be of type InvoiceType because of these 2 variables
selected.details.dueDate = new Date(selected.details.dueDate);
selected.details.invoiceDate = new Date(selected.details.invoiceDate);
selected.details.invoiceLogo = "";
selected.details.signature = {
data: "",
};
};
/**
* Transform date values for next submission
*
* @param {InvoiceType} selected - The selected invoice
*/
const transformDates = (selected: InvoiceType) => {
selected.details.dueDate = new Date(
selected.details.dueDate
).toLocaleDateString("en-US", DATE_OPTIONS);
selected.details.invoiceDate = new Date(
selected.details.invoiceDate
).toLocaleDateString("en-US", DATE_OPTIONS);
};
/**
* Loads a given invoice into the form.
*
* @param {InvoiceType} selectedInvoice - The selected invoice
*/
const load = (selectedInvoice: InvoiceType) => {
if (selectedInvoice) {
updateFields(selectedInvoice);
reset(selectedInvoice);
transformDates(selectedInvoice);
// Close modal
setModalState(false);
}
};
/**
* Loads a given invoice into the form and generates a pdf by submitting the form.
*
* @param {InvoiceType} selectedInvoice - The selected invoice
*/
const loadAndGeneratePdf = (selectedInvoice: InvoiceType) => {
load(selectedInvoice);
// Submit form
onFormSubmit(selectedInvoice);
};
return (
<>
{savedInvoices.map((invoice, idx) => (
handleSelect(invoice)}
>
{/*
*/}
Invoice #{invoice.details.invoiceNumber}{" "}
Updated at: {invoice.details.updatedAt}
Sender: {invoice.sender.name}
Receiver: {invoice.receiver.name}
Total:{" "}
{formatNumberWithCommas(
Number(
invoice.details.totalAmount
)
)}{" "}
{invoice.details.currency}
load(invoice)}
>
Load
loadAndGeneratePdf(invoice)}
>
Load & Generate
{/* Remove Invoice Button */}
{
e.stopPropagation();
deleteInvoice(idx);
}}
>
Delete
))}
{savedInvoices.length == 0 && (
)}
>
);
};
export default SavedInvoicesList;
```
## /app/components/modals/signature/SignatureModal.tsx
```tsx path="/app/components/modals/signature/SignatureModal.tsx"
"use client";
import { useEffect, useState } from "react";
// RHF
import { useFormContext, useWatch } from "react-hook-form";
// ShadCn
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Label } from "@/components/ui/label";
// Components
import {
DrawSignature,
TypeSignature,
UploadSignature,
} from "@/app/components";
// Contexts
import { useTranslationContext } from "@/contexts/TranslationContext";
import { useSignatureContext } from "@/contexts/SignatureContext";
// Icons
import { FileSignature } from "lucide-react";
// Helpers
import { isDataUrl } from "@/lib/helpers";
// Types
import { SignatureTabs } from "@/types";
type SignatureModalProps = {};
const SignatureModal = ({}: SignatureModalProps) => {
const { setValue } = useFormContext();
const {
handleCanvasEnd,
signatureData,
typedSignature,
selectedFont,
uploadSignatureImg,
signatureRef,
} = useSignatureContext();
const { _t } = useTranslationContext();
// Modal state
const [open, setOpen] = useState(false);
// Modal tabs
const [tab, setTab] = useState(SignatureTabs.DRAW);
const onTabChange = (value: string) => {
setTab(value as string);
};
const signature = useWatch({
name: "details.signature.data",
});
/**
* Function that handles signature save logic for all tabs (draw, type, upload)
*/
const handleSaveSignature = () => {
if (tab == SignatureTabs.DRAW) {
handleCanvasEnd();
// This setValue was removed from handleCanvasEnd and put here to prevent
// the signature from showing updated drawing every time drawing stops
setValue("details.signature.data", signatureData, {
shouldDirty: true,
});
setOpen(false);
}
if (tab == SignatureTabs.TYPE) {
setValue(
"details.signature",
{
data: typedSignature,
fontFamily: selectedFont.name,
},
{
shouldDirty: true,
}
);
setOpen(false);
}
if (tab == SignatureTabs.UPLOAD) {
setValue("details.signature.data", uploadSignatureImg, {
shouldDirty: true,
});
setOpen(false);
}
};
// When opening modal or switching tabs, apply signatureData to the canvas when it's available
// Persists the signature
useEffect(() => {
if (open && signatureData) {
// Access the canvas element and draw the signature
setTimeout(() => {
const canvas = signatureRef?.current;
if (canvas) {
canvas.fromDataURL(signatureData);
}
}, 50);
}
}, [open, tab]);
return (
<>
{_t("form.steps.summary.signature.heading")}
{signature && isDataUrl(signature) ? (
) : signature && typedSignature ? (
) : (
{_t(
"form.steps.summary.signature.placeholder"
)}
)}
{_t("form.steps.summary.signature.heading")}
{_t("form.steps.summary.signature.draw")}
{_t("form.steps.summary.signature.type")}
{_t("form.steps.summary.signature.upload")}
{/* DRAW */}
{/* TYPE */}
{/* UPLOAD */}
>
);
};
export default SignatureModal;
```
## /app/components/modals/signature/components/SignatureColorSelector.tsx
```tsx path="/app/components/modals/signature/components/SignatureColorSelector.tsx"
// Components
import { BaseButton } from "@/app/components";
// Icons
import { Check } from "lucide-react";
// Types
import { SignatureColor } from "@/types";
type SignatureColorSelectorProps = {
colors: SignatureColor[];
selectedColor: string;
handleColorButtonClick: (color: string) => void;
};
const SignatureColorSelector = ({
colors,
selectedColor,
handleColorButtonClick,
}: SignatureColorSelectorProps) => {
return (
{colors.map((color) => (
handleColorButtonClick(color.color)}
>
{selectedColor === color.color && (
)}
))}
);
};
export default SignatureColorSelector;
```
## /app/components/modals/signature/components/SignatureFontSelector.tsx
```tsx path="/app/components/modals/signature/components/SignatureFontSelector.tsx"
// ShadCn
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// Types
import { SignatureFont } from "@/types";
type SignatureFontSelectorProps = {
typedSignatureFonts: SignatureFont[];
selectedFont: SignatureFont;
setSelectedFont: (value: SignatureFont) => void;
};
const SignatureFontSelector = ({
typedSignatureFonts,
selectedFont,
setSelectedFont,
}: SignatureFontSelectorProps) => {
return (
{/* Font select */}
{
// Find the selected font object based on the variable
const selectedFontObject = typedSignatureFonts.find(
(font) => font.variable === fontVariable
);
if (selectedFontObject) {
setSelectedFont(selectedFontObject);
}
}}
>
{typedSignatureFonts.map((font) => (
{font.name}
))}
);
};
export default SignatureFontSelector;
```
## /app/components/modals/signature/tabs/DrawSignature.tsx
```tsx path="/app/components/modals/signature/tabs/DrawSignature.tsx"
"use client";
// React Signature Canvas
import SignatureCanvas from "react-signature-canvas";
// ShadCn
import { Card, CardContent } from "@/components/ui/card";
import { TabsContent } from "@/components/ui/tabs";
// Components
import { BaseButton, SignatureColorSelector } from "@/app/components";
// Contexts
import { useSignatureContext } from "@/contexts/SignatureContext";
// Icons
import { Check, Eraser } from "lucide-react";
// Types
import { SignatureTabs } from "@/types";
type DrawSignatureProps = {
handleSaveSignature: () => void;
};
const DrawSignature = ({ handleSaveSignature }: DrawSignatureProps) => {
const {
signatureData,
signatureRef,
colors,
selectedColor,
handleColorButtonClick,
clearSignature,
handleCanvasEnd,
} = useSignatureContext();
return (
{/* Signature Canvas to draw signature */}
{/* Color selector */}
{signatureData && (
Erase
)}
Done
);
};
export default DrawSignature;
```
## /app/components/modals/signature/tabs/TypeSignature.tsx
```tsx path="/app/components/modals/signature/tabs/TypeSignature.tsx"
import React from "react";
// ShadCn
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
// Components
import { BaseButton, SignatureFontSelector } from "@/app/components";
// Contexts
import { useSignatureContext } from "@/contexts/SignatureContext";
// Icons
import { Check, Eraser } from "lucide-react";
// Types
import { SignatureTabs } from "@/types";
type TypeSignatureProps = {
handleSaveSignature: () => void;
};
const TypeSignature = ({ handleSaveSignature }: TypeSignatureProps) => {
const {
typedSignatureFontSize,
selectedFont,
setSelectedFont,
typedSignature,
setTypedSignature,
typedSignatureRef,
typedSignatureFonts,
clearTypedSignature,
} = useSignatureContext();
return (
) => {
setTypedSignature(e.target.value);
}}
/>
{typedSignature && (
Clear
)}
Done
);
};
export default TypeSignature;
```
## /app/components/modals/signature/tabs/UploadSignature.tsx
```tsx path="/app/components/modals/signature/tabs/UploadSignature.tsx"
// ShadCn
import { Card, CardContent } from "@/components/ui/card";
import { TabsContent } from "@/components/ui/tabs";
// Components
import { BaseButton } from "@/app/components";
// Contexts
import { useSignatureContext } from "@/contexts/SignatureContext";
// Icons
import { Check, Trash2 } from "lucide-react";
// Types
import { SignatureTabs } from "@/types";
type UploadSignatureProps = {
handleSaveSignature: () => void;
};
const UploadSignature = ({ handleSaveSignature }: UploadSignatureProps) => {
const {
uploadSignatureRef,
uploadSignatureImg,
handleUploadSignatureChange,
handleRemoveUploadedSignature,
} = useSignatureContext();
return (
{uploadSignatureImg ? (
) : (
Upload image
)}
{/* Upload file here */}
{/* Buttons and operations */}
{uploadSignatureImg && (
Remove
)}
Done
);
};
export default UploadSignature;
```
## /app/components/reusables/BaseButton.tsx
```tsx path="/app/components/reusables/BaseButton.tsx"
"use client";
import React from "react";
// ShadCn
import { Button, ButtonProps } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
// Icons
import { Loader2 } from "lucide-react";
type BaseButtonProps = {
tooltipLabel?: string;
type?: "button" | "submit" | "reset";
loading?: boolean;
loadingText?: string;
children?: React.ReactNode;
} & ButtonProps;
const BaseButton = ({
tooltipLabel,
type = "button",
loading,
loadingText = "Loading",
children,
...props
}: BaseButtonProps) => {
const withoutTooltip = (
<>
{!loading ? (
{children}
) : (
{loadingText}
)}
>
);
if (!tooltipLabel) return withoutTooltip;
return (
{!loading ? (
{children}
) : (
{loadingText}
)}
{tooltipLabel}
);
};
export default BaseButton;
```
## /app/components/reusables/LanguageSelector.tsx
```tsx path="/app/components/reusables/LanguageSelector.tsx"
"use client";
import { useParams } from "next/navigation";
// Next Intl
import { useRouter } from "next-intl/client"; // This useRouter is wrapped with next/navigation useRouter
// ShadCn
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
// Variables
import { LOCALES } from "@/lib/variables";
const LanguageSelector = () => {
const router = useRouter();
const params = useParams();
const handleLanguageChange = (lang: string) => {
console.log(lang);
router.push("/", { locale: lang });
};
return (
handleLanguageChange(lang)}
>
BETA
Languages
{LOCALES.map((locale) => (
{locale.name}
))}
);
};
export default LanguageSelector;
```
## /app/components/reusables/Subheading.tsx
```tsx path="/app/components/reusables/Subheading.tsx"
import React from "react";
type SubheadingProps = {
children: React.ReactNode;
};
export default function Subheading({ children }: SubheadingProps) {
return {children} ;
}
```
## /app/components/reusables/ThemeSwitcher.tsx
```tsx path="/app/components/reusables/ThemeSwitcher.tsx"
"use client";
import { useTheme } from "next-themes";
// Components
import { BaseButton } from "@/app/components";
// Icons
import { Moon, Sun } from "lucide-react";
const ThemeSwitcher = () => {
const { theme, setTheme } = useTheme();
return (
<>
setTheme(theme === "dark" ? "light" : "dark")}
>
Toggle theme
>
);
};
export default ThemeSwitcher;
```
## /app/components/reusables/form-fields/ChargeInput.tsx
```tsx path="/app/components/reusables/form-fields/ChargeInput.tsx"
"use client";
import React from "react";
// RHF
import { useFormContext } from "react-hook-form";
// ShadCn
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
// Components
import { BaseButton } from "@/app/components";
// Icons
import { Percent, RefreshCw } from "lucide-react";
// Types
import { NameType } from "@/types";
type ChargeInputProps = {
label: string;
name: NameType;
switchAmountType: (
type: string,
setType: React.Dispatch>
) => void;
type: string;
setType: React.Dispatch>;
currency: string;
};
const ChargeInput = ({
label,
name,
switchAmountType,
type,
setType,
currency,
}: ChargeInputProps) => {
const { control } = useFormContext();
return (
<>
>
);
};
export default ChargeInput;
```
## /app/components/reusables/form-fields/CurrencySelector.tsx
```tsx path="/app/components/reusables/form-fields/CurrencySelector.tsx"
"use client";
// RHF
import { useFormContext } from "react-hook-form";
// ShadCn
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// Hooks
import useCurrencies from "@/hooks/useCurrencies";
// Types
import { CurrencyType, NameType } from "@/types";
type CurrencySelectorProps = {
name: NameType;
label?: string;
placeholder?: string;
};
const CurrencySelector = ({
name,
label,
placeholder,
}: CurrencySelectorProps) => {
const { control } = useFormContext();
const { currencies, currenciesLoading } = useCurrencies();
return (
(
{label}:
Currencies
{!currenciesLoading &&
currencies.map(
(
currency: CurrencyType,
idx: number
) => (
{currency.name}{" "}
{`(${currency.code})`}
)
)}
)}
/>
);
};
export default CurrencySelector;
```
## /app/components/reusables/form-fields/DatePickerFormField.tsx
```tsx path="/app/components/reusables/form-fields/DatePickerFormField.tsx"
"use client";
import { useState } from "react";
// RHF
import { useFormContext } from "react-hook-form";
// ShadCn
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
// Utils
import { cn } from "@/lib/utils";
// Variables
import { DATE_OPTIONS } from "@/lib/variables";
// Icons
import { CalendarIcon } from "lucide-react";
// Types
import { NameType } from "@/types";
type DatePickerFormFieldProps = {
name: NameType;
label?: string;
};
const DatePickerFormField = ({ name, label }: DatePickerFormFieldProps) => {
const { control } = useFormContext();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
return (
<>
(
{label}:
{field.value ? (
new Date(
field.value
).toLocaleDateString(
"en-US",
DATE_OPTIONS
)
) : (
Pick a date
)}
{
field.onChange(e);
setIsPopoverOpen(false);
}}
disabled={(date) =>
date < new Date("1900-01-01")
}
fromYear={1960}
toYear={
new Date().getFullYear() + 30
}
initialFocus
/>
)}
/>
>
);
};
export default DatePickerFormField;
```
## /app/components/reusables/form-fields/FormCustomInput.tsx
```tsx path="/app/components/reusables/form-fields/FormCustomInput.tsx"
// Components
import { BaseButton, FormInput } from "@/app/components";
// Icons
import { Trash2 } from "lucide-react";
type FormCustomInputProps = {
index: number;
location: string;
removeField: (index: number) => void;
};
const FormCustomInput = ({
index,
location,
removeField,
}: FormCustomInputProps) => {
const nameKey = `${location}[${index}].key`;
const nameValue = `${location}[${index}].value`;
return (
<>
removeField(index)}
>
>
);
};
export default FormCustomInput;
```
## /app/components/reusables/form-fields/FormFile.tsx
```tsx path="/app/components/reusables/form-fields/FormFile.tsx"
"use client";
import { ChangeEvent, useRef, useState } from "react";
// RHF
import { useFormContext, useWatch } from "react-hook-form";
// ShadCn components
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Label } from "@/components/ui/label";
// Components
import { BaseButton } from "@/app/components";
// Icons
import { ImageMinus, Image } from "lucide-react";
// Types
import { NameType } from "@/types";
type FormFileProps = {
name: NameType;
label?: string;
placeholder?: string;
};
const FormFile = ({ name, label, placeholder }: FormFileProps) => {
const { control, setValue } = useFormContext();
const logoImage = useWatch({
name: name,
control,
});
const [base64Image, setBase64Image] = useState(logoImage ?? "");
const fileInputRef = useRef(null);
const handleFileChange = (e: ChangeEvent) => {
const file = e.target.files![0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const base64String = event.target!.result as string;
setBase64Image(base64String);
setValue(name, base64String); // Set the value for form submission
};
reader.readAsDataURL(file);
}
};
const removeLogo = () => {
setBase64Image("");
setValue(name, "");
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
return (
<>
(
{label}:
{base64Image ? (
) : (
)}
)}
/>
{base64Image && (
Remove logo
)}
>
);
};
export default FormFile;
```
## /app/components/reusables/form-fields/FormInput.tsx
```tsx path="/app/components/reusables/form-fields/FormInput.tsx"
"use client";
// RHF
import { useFormContext } from "react-hook-form";
// ShadCn
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input, InputProps } from "@/components/ui/input";
type FormInputProps = {
name: string;
label?: string;
labelHelper?: string;
placeholder?: string;
vertical?: boolean;
} & InputProps;
const FormInput = ({
name,
label,
labelHelper,
placeholder,
vertical = false,
...props
}: FormInputProps) => {
const { control } = useFormContext();
const verticalInput = (
(
{label && {`${label}:`} }
{labelHelper && (
{labelHelper}
)}
)}
/>
);
const horizontalInput = (
(
{label &&
{`${label}:`} }
{labelHelper && (
{labelHelper}
)}
)}
/>
);
return vertical ? verticalInput : horizontalInput;
};
export default FormInput;
```
## /app/components/reusables/form-fields/FormTextarea.tsx
```tsx path="/app/components/reusables/form-fields/FormTextarea.tsx"
"use client";
// RHF
import { useFormContext } from "react-hook-form";
// ShadCn
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Textarea, TextareaProps } from "@/components/ui/textarea";
type FormTextareaProps = {
name: string;
label?: string;
labelHelper?: string;
placeholder?: string;
} & TextareaProps;
const FormTextarea = ({
name,
label,
labelHelper,
placeholder,
...props
}: FormTextareaProps) => {
const { control } = useFormContext();
return (
(
{label && {`${label}:`} }
{labelHelper && (
{labelHelper}
)}
)}
/>
);
};
export default FormTextarea;
```
## /app/components/templates/email/SendPdfEmail.tsx
```tsx path="/app/components/templates/email/SendPdfEmail.tsx"
// React-email
import {
Html,
Body,
Head,
Heading,
Hr,
Container,
Preview,
Section,
Text,
Img,
} from "@react-email/components";
import { Tailwind } from "@react-email/tailwind";
// Variables
import { BASE_URL } from "@/lib/variables";
type SendPdfEmailProps = {
invoiceNumber: string;
};
export default function SendPdfEmail({ invoiceNumber }: SendPdfEmailProps) {
const logo = `${BASE_URL}/assets/img/invoify-logo.png`;
return (
Your invoice #{invoiceNumber} is ready for download
Thanks for using Invoify!
We're pleased to inform you that your invoice{" "}
#{invoiceNumber} is ready for download.
Please find the attached PDF document.
Best Regards,
Invoify Team
);
}
```
## /app/components/templates/invoice-pdf/DynamicInvoiceTemplate.tsx
```tsx path="/app/components/templates/invoice-pdf/DynamicInvoiceTemplate.tsx"
import React, { useMemo } from "react";
import dynamic from "next/dynamic";
// ShadCn
import { Skeleton } from "@/components/ui/skeleton";
// Types
import { InvoiceType } from "@/types";
const DynamicInvoiceTemplateSkeleton = () => {
return ;
};
const DynamicInvoiceTemplate = (props: InvoiceType) => {
// Dynamic template component name
const templateName = `InvoiceTemplate${props.details.pdfTemplate}`;
const DynamicInvoice = useMemo(
() =>
dynamic(
() =>
import(
`@/app/components/templates/invoice-pdf/${templateName}`
),
{
loading: () => ,
ssr: false,
}
),
[templateName]
);
return ;
};
export default DynamicInvoiceTemplate;
```
## /app/components/templates/invoice-pdf/InvoiceLayout.tsx
```tsx path="/app/components/templates/invoice-pdf/InvoiceLayout.tsx"
import { ReactNode } from "react";
// Types
import { InvoiceType } from "@/types";
type InvoiceLayoutProps = {
data: InvoiceType;
children: ReactNode;
};
export default function InvoiceLayout({ data, children }: InvoiceLayoutProps) {
const { sender, receiver, details } = data;
// Instead of fetching all signature fonts, get the specific one user selected.
const fontHref = details.signature?.fontFamily
? `https://fonts.googleapis.com/css2?family=${details?.signature?.fontFamily}&display=swap`
: "";
const head = (
<>
{details.signature?.fontFamily && (
<>
>
)}
>
);
return (
<>
{head}
>
);
}
```
## /app/components/templates/invoice-pdf/InvoiceTemplate1.tsx
```tsx path="/app/components/templates/invoice-pdf/InvoiceTemplate1.tsx"
import React from "react";
// Components
import { InvoiceLayout } from "@/app/components";
// Helpers
import { formatNumberWithCommas, isDataUrl } from "@/lib/helpers";
// Variables
import { DATE_OPTIONS } from "@/lib/variables";
// Types
import { InvoiceType } from "@/types";
const InvoiceTemplate = (data: InvoiceType) => {
const { sender, receiver, details } = data;
return (
{details.invoiceLogo && (
)}
{sender.name}
Invoice #
{details.invoiceNumber}
{sender.address}
{sender.zipCode}, {sender.city}
{sender.country}
Bill to:
{receiver.name}
{}
{receiver.address && receiver.address.length > 0 ? receiver.address : null}
{receiver.zipCode && receiver.zipCode.length > 0 ? `, ${receiver.zipCode}` : null}
{receiver.city}, {receiver.country}
Invoice date:
{new Date(details.invoiceDate).toLocaleDateString("en-US", DATE_OPTIONS)}
Due date:
{new Date(details.dueDate).toLocaleDateString("en-US", DATE_OPTIONS)}
{details.items.map((item, index) => (
{item.name}
{item.description}
{item.unitPrice} {details.currency}
{item.total} {details.currency}
))}
Subtotal:
{formatNumberWithCommas(Number(details.subTotal))} {details.currency}
{details.discountDetails?.amount != undefined &&
details.discountDetails?.amount > 0 && (
Discount:
{details.discountDetails.amountType === "amount"
? `- ${details.discountDetails.amount} ${details.currency}`
: `- ${details.discountDetails.amount}%`}
)}
{details.taxDetails?.amount != undefined && details.taxDetails?.amount > 0 && (
Tax:
{details.taxDetails.amountType === "amount"
? `+ ${details.taxDetails.amount} ${details.currency}`
: `+ ${details.taxDetails.amount}%`}
)}
{details.shippingDetails?.cost != undefined && details.shippingDetails?.cost > 0 && (
Shipping:
{details.shippingDetails.costType === "amount"
? `+ ${details.shippingDetails.cost} ${details.currency}`
: `+ ${details.shippingDetails.cost}%`}
)}
Total:
{formatNumberWithCommas(Number(details.totalAmount))} {details.currency}
{details.totalAmountInWords && (
Total in words:
{details.totalAmountInWords} {details.currency}
)}
Additional notes:
{details.additionalNotes}
Payment terms:
{details.paymentTerms}
Please send the payment to this address
Bank: {details.paymentInformation?.bankName}
Account name: {details.paymentInformation?.accountName}
Account no: {details.paymentInformation?.accountNumber}
If you have any questions concerning this invoice, use the following contact information:
{sender.email}
{sender.phone}
{/* Signature */}
{details?.signature?.data && isDataUrl(details?.signature?.data) ? (
Signature:
) : details.signature?.data ? (
Signature:
{details.signature.data}
) : null}
);
};
export default InvoiceTemplate;
```
## /app/components/templates/invoice-pdf/InvoiceTemplate2.tsx
```tsx path="/app/components/templates/invoice-pdf/InvoiceTemplate2.tsx"
import React from "react";
// Components
import { InvoiceLayout } from "@/app/components";
// Helpers
import { formatNumberWithCommas, isDataUrl } from "@/lib/helpers";
// Variables
import { DATE_OPTIONS } from "@/lib/variables";
// Types
import { InvoiceType } from "@/types";
const InvoiceTemplate2 = (data: InvoiceType) => {
const { sender, receiver, details } = data;
return (
Invoice #
{details.invoiceNumber}
{details.invoiceLogo && (
)}
{sender.name}
{sender.address}
{sender.zipCode}, {sender.city}
{sender.country}
Bill to:
{receiver.name}
{receiver.address}, {receiver.zipCode}
{receiver.city}, {receiver.country}
Invoice date:
{new Date(
details.invoiceDate
).toLocaleDateString("en-US", DATE_OPTIONS)}
Due date:
{new Date(details.dueDate).toLocaleDateString(
"en-US",
DATE_OPTIONS
)}
{details.items.map((item, index) => (
{item.name}
{item.description}
{item.unitPrice} {details.currency}
{item.total} {details.currency}
))}
Subtotal:
{formatNumberWithCommas(
Number(details.subTotal)
)}{" "}
{details.currency}
{details.discountDetails?.amount != undefined &&
details.discountDetails?.amount > 0 && (
Discount:
{details.discountDetails.amountType ===
"amount"
? `- ${details.discountDetails.amount} ${details.currency}`
: `- ${details.discountDetails.amount}%`}
)}
{details.taxDetails?.amount != undefined &&
details.taxDetails?.amount > 0 && (
Tax:
{details.taxDetails.amountType ===
"amount"
? `+ ${details.taxDetails.amount} ${details.currency}`
: `+ ${details.taxDetails.amount}%`}
)}
{details.shippingDetails?.cost != undefined &&
details.shippingDetails?.cost > 0 && (
Shipping:
{details.shippingDetails.costType ===
"amount"
? `+ ${details.shippingDetails.cost} ${details.currency}`
: `+ ${details.shippingDetails.cost}%`}
)}
Total:
{formatNumberWithCommas(
Number(details.totalAmount)
)}{" "}
{details.currency}
{details.totalAmountInWords && (
Total in words:
{details.totalAmountInWords}{" "}
{details.currency}
)}
Additional notes:
{details.additionalNotes}
Payment terms:
{details.paymentTerms}
Please send the payment to this address
Bank: {details.paymentInformation?.bankName}
Account name:{" "}
{details.paymentInformation?.accountName}
Account no:{" "}
{details.paymentInformation?.accountNumber}
If you have any questions concerning this invoice, use the
following contact information:
{sender.email}
{sender.phone}
{/* Signature */}
{details?.signature?.data && isDataUrl(details?.signature?.data) ? (
Signature:
) : details.signature?.data ? (
Signature:
{details.signature.data}
) : null}
);
};
export default InvoiceTemplate2;
```
## /app/globals.css
```css path="/app/globals.css"
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
body {
font-family: "Outfit" sans-serif;
}
:focus {
outline: none;
}
/* Remove chrome input focus */
input,
textarea,
select,
a,
svg,
button {
--tw-ring-offset-color: "transparent" !important;
}
/* For Customized Calendar */
.rdp-vhidden {
@apply hidden;
}
```
## /app/layout.tsx
```tsx path="/app/layout.tsx"
import { ReactNode } from "react";
import "@/app/globals.css";
type Props = {
children: ReactNode;
};
// Since we have a `not-found.tsx` page on the root, a layout file
// is required, even if it's just passing children through.
export default function RootLayout({ children }: Props) {
return children;
}
```
## /app/not-found.tsx
```tsx path="/app/not-found.tsx"
"use client";
import Error from "next/error";
// Render the default Next.js 404 page when a route
// is requested that doesn't match the middleware and
// therefore doesn't have a locale associated with it.
export default function NotFound() {
return (
);
}
```
## /app/page.tsx
```tsx path="/app/page.tsx"
import { redirect } from "next/navigation";
// This page only renders when the app is built statically (output: 'export')
export default function RootPage() {
redirect("/en");
}
```
## /app/robots.txt
User-Agent: *
Disallow:
## /components.json
```json path="/components.json"
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
```
## /components/ui/alert-dialog.tsx
```tsx path="/components/ui/alert-dialog.tsx"
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes) => (
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes) => (
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
```
## /components/ui/aspect-ratio.tsx
```tsx path="/components/ui/aspect-ratio.tsx"
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }
```
## /components/ui/badge.tsx
```tsx path="/components/ui/badge.tsx"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes,
VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
)
}
export { Badge, badgeVariants }
```
## /components/ui/button.tsx
```tsx path="/components/ui/button.tsx"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean
}
const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
```
## /components/ui/calendar.tsx
```tsx path="/components/ui/calendar.tsx"
"use client";
import * as React from "react";
import { buttonVariants } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker, DropdownProps } from "react-day-picker";
export type CalendarProps = React.ComponentProps;
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
{
const options = React.Children.toArray(
children
) as React.ReactElement<
React.HTMLProps
>[];
const selected = options.find(
(child) => child.props.value === value
);
const handleChange = (value: string) => {
const changeEvent = {
target: { value },
} as React.ChangeEvent;
onChange?.(changeEvent);
};
return (
{
handleChange(value);
}}
>
{selected?.props?.children}
{options.map((option, id: number) => (
{option.props.children}
))}
);
},
IconLeft: ({ ...props }) => ,
IconRight: ({ ...props }) => (
),
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };
```
## /components/ui/card.tsx
```tsx path="/components/ui/card.tsx"
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
```
## /components/ui/dialog.tsx
```tsx path="/components/ui/dialog.tsx"
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
Close
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes) => (
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes) => (
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
```
## /components/ui/form.tsx
```tsx path="/components/ui/form.tsx"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath = FieldPath
> = {
name: TName
}
const FormFieldContext = React.createContext(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath = FieldPath
>({
...props
}: ControllerProps) => {
return (
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within ")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
{body}
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
```
## /components/ui/input.tsx
```tsx path="/components/ui/input.tsx"
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes {}
const Input = React.forwardRef(
({ className, type, ...props }, ref) => {
return (
)
}
)
Input.displayName = "Input"
export { Input }
```
## /components/ui/label.tsx
```tsx path="/components/ui/label.tsx"
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, ...props }, ref) => (
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
```
## /components/ui/navigation-menu.tsx
```tsx path="/components/ui/navigation-menu.tsx"
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}{" "}
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}
```
## /components/ui/popover.tsx
```tsx path="/components/ui/popover.tsx"
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }
```
## /components/ui/scroll-area.tsx
```tsx path="/components/ui/scroll-area.tsx"
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, orientation = "vertical", ...props }, ref) => (
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }
```
## /components/ui/select.tsx
```tsx path="/components/ui/select.tsx"
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, position = "popper", ...props }, ref) => (
{children}
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
}
```
## /components/ui/skeleton.tsx
```tsx path="/components/ui/skeleton.tsx"
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes) {
return (
)
}
export { Skeleton }
```
## /components/ui/switch.tsx
```tsx path="/components/ui/switch.tsx"
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
```
## /components/ui/table.tsx
```tsx path="/components/ui/table.tsx"
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes
>(({ className, ...props }, ref) => (
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes
>(({ className, ...props }, ref) => (
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
```
## /components/ui/tabs.tsx
```tsx path="/components/ui/tabs.tsx"
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
```
## /components/ui/textarea.tsx
```tsx path="/components/ui/textarea.tsx"
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes {}
const Textarea = React.forwardRef(
({ className, ...props }, ref) => {
return (
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }
```
## /components/ui/toast.tsx
```tsx path="/components/ui/toast.tsx"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, variant, ...props }, ref) => {
return (
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef
type ToastActionElement = React.ReactElement
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
```
## /components/ui/toaster.tsx
```tsx path="/components/ui/toaster.tsx"
"use client"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
{title && {title} }
{description && (
{description}
)}
{action}
)
})}
)
}
```
## /components/ui/tooltip.tsx
```tsx path="/components/ui/tooltip.tsx"
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, sideOffset = 4, ...props }, ref) => (
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
```
## /components/ui/use-toast.ts
```ts path="/components/ui/use-toast.ts"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
```
## /contexts/ChargesContext.tsx
```tsx path="/contexts/ChargesContext.tsx"
"use client";
import React, {
SetStateAction,
createContext,
useContext,
useEffect,
useState,
} from "react";
// RHF
import { useFormContext, useWatch } from "react-hook-form";
// Helpers
import { formatPriceToString } from "@/lib/helpers";
// Types
import { InvoiceType, ItemType } from "@/types";
const defaultChargesContext = {
discountSwitch: false,
setDiscountSwitch: (newValue: boolean) => {},
taxSwitch: false,
setTaxSwitch: (newValue: boolean) => {},
shippingSwitch: false,
setShippingSwitch: (newValue: boolean) => {},
discountType: "amount",
setDiscountType: (newValue: SetStateAction) => {},
taxType: "amount",
setTaxType: (newValue: SetStateAction) => {},
shippingType: "amount",
setShippingType: (newValue: SetStateAction) => {},
totalInWordsSwitch: true,
setTotalInWordsSwitch: (newValue: boolean) => {},
currency: "USD",
subTotal: 0,
totalAmount: 0,
calculateTotal: () => {},
};
export const ChargesContext = createContext(defaultChargesContext);
export const useChargesContext = () => {
return useContext(ChargesContext);
};
type ChargesContextProps = {
children: React.ReactNode;
};
export const ChargesContextProvider = ({ children }: ChargesContextProps) => {
const { control, setValue, getValues } = useFormContext();
// Form Fields
const itemsArray = useWatch({
name: `details.items`,
control,
});
const currency = useWatch({
name: `details.currency`,
control,
});
// Charges
const charges = {
discount: useWatch({ name: `details.discountDetails`, control }) || {
amount: 0,
amountType: "amount",
},
tax: useWatch({ name: `details.taxDetails`, control }) || {
amount: 0,
amountType: "amount",
},
shipping: useWatch({ name: `details.shippingDetails`, control }) || {
cost: 0,
costType: "amount",
},
};
const { discount, tax, shipping } = charges;
// Switch states. On/Off
const [discountSwitch, setDiscountSwitch] = useState(
discount?.amount ? true : false
);
const [taxSwitch, setTaxSwitch] = useState(
tax?.amount ? true : false
);
const [shippingSwitch, setShippingSwitch] = useState(
shipping?.cost ? true : false
);
// ? Old approach of using totalInWords variable
// totalInWords ? true : false
const [totalInWordsSwitch, setTotalInWordsSwitch] = useState(true);
// Initial subtotal and total
const [subTotal, setSubTotal] = useState(0);
const [totalAmount, setTotalAmount] = useState(0);
// Types for discount, tax, and shipping. Amount | Percentage
const [discountType, setDiscountType] = useState("amount");
const [taxType, setTaxType] = useState("amount");
const [shippingType, setShippingType] = useState("amount");
// When loading invoice, if received values, turn on the switches
useEffect(() => {
if (discount?.amount) {
setDiscountSwitch(true);
}
if (tax?.amount) {
setTaxSwitch(true);
}
if (shipping?.cost) {
setShippingSwitch(true);
}
if (discount?.amountType == "amount") {
setDiscountType("amount");
} else {
setDiscountType("percentage");
}
if (tax?.amountType == "amount") {
setTaxType("amount");
} else {
setTaxType("percentage");
}
if (shipping?.costType == "amount") {
setShippingType("amount");
} else {
setShippingType("percentage");
}
}, [discount?.amount, tax?.amount, shipping?.cost]);
// Check switches, if off set values to zero
useEffect(() => {
if (!discountSwitch) {
setValue("details.discountDetails.amount", 0);
}
if (!taxSwitch) {
setValue("details.taxDetails.amount", 0);
}
if (!shippingSwitch) {
setValue("details.shippingDetails.cost", 0);
}
}, [discountSwitch, taxSwitch, shippingSwitch]);
// Calculate total when values change
useEffect(() => {
calculateTotal();
}, [
itemsArray,
totalInWordsSwitch,
discountType,
discount?.amount,
taxType,
tax?.amount,
shippingType,
shipping?.cost,
currency,
]);
/**
* Calculates the subtotal, total, and the total amount in words on the invoice.
*/
const calculateTotal = () => {
// Here Number(item.total) fixes a bug where an extra zero appears
// at the beginning of subTotal caused by toFixed(2) in item.total in single item
// Reason: toFixed(2) returns string, not a number instance
const totalSum: number = itemsArray.reduce(
(sum: number, item: ItemType) => sum + Number(item.total),
0
);
setValue("details.subTotal", totalSum);
setSubTotal(totalSum);
let discountAmount: number =
parseFloat(discount!.amount.toString()) ?? 0;
let taxAmount: number = parseFloat(tax!.amount.toString()) ?? 0;
let shippingCost: number = parseFloat(shipping!.cost.toString()) ?? 0;
let discountAmountType: string = "amount";
let taxAmountType: string = "amount";
let shippingCostType: string = "amount";
let total: number = totalSum;
if (!isNaN(discountAmount)) {
if (discountType == "amount") {
total -= discountAmount;
discountAmountType = "amount";
} else {
total -= total * (discountAmount / 100);
discountAmountType = "percentage";
}
setValue("details.discountDetails.amount", discountAmount);
}
if (!isNaN(taxAmount)) {
if (taxType == "amount") {
total += taxAmount;
taxAmountType = "amount";
} else {
total += total * (taxAmount / 100);
taxAmountType = "percentage";
}
setValue("details.taxDetails.amount", taxAmount);
}
if (!isNaN(shippingCost)) {
if (shippingType == "amount") {
total += shippingCost;
shippingCostType = "amount";
} else {
total += total * (shippingCost / 100);
shippingCostType = "percentage";
}
setValue("details.shippingDetails.cost", shippingCost);
}
setTotalAmount(total);
setValue("details.discountDetails.amountType", discountAmountType);
setValue("details.taxDetails.amountType", taxAmountType);
setValue("details.shippingDetails.costType", shippingCostType);
setValue("details.totalAmount", total);
if (totalInWordsSwitch) {
setValue("details.totalAmountInWords", formatPriceToString(total, getValues("details.currency")));
} else {
setValue("details.totalAmountInWords", "");
}
};
return (
{children}
);
};
```
## /contexts/InvoiceContext.tsx
```tsx path="/contexts/InvoiceContext.tsx"
"use client";
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { useRouter } from "next/navigation";
// RHF
import { useFormContext } from "react-hook-form";
// Hooks
import useToasts from "@/hooks/useToasts";
// Services
import { exportInvoice } from "@/services/invoice/client/exportInvoice";
// Variables
import {
FORM_DEFAULT_VALUES,
GENERATE_PDF_API,
SEND_PDF_API,
SHORT_DATE_OPTIONS,
} from "@/lib/variables";
// Types
import { ExportTypes, InvoiceType } from "@/types";
const defaultInvoiceContext = {
invoicePdf: new Blob(),
invoicePdfLoading: false,
savedInvoices: [] as InvoiceType[],
pdfUrl: null as string | null,
onFormSubmit: (values: InvoiceType) => {},
newInvoice: () => {},
generatePdf: async (data: InvoiceType) => {},
removeFinalPdf: () => {},
downloadPdf: () => {},
printPdf: () => {},
previewPdfInTab: () => {},
saveInvoice: () => {},
deleteInvoice: (index: number) => {},
sendPdfToMail: (email: string): Promise => Promise.resolve(),
exportInvoiceAs: (exportAs: ExportTypes) => {},
importInvoice: (file: File) => {},
};
export const InvoiceContext = createContext(defaultInvoiceContext);
export const useInvoiceContext = () => {
return useContext(InvoiceContext);
};
type InvoiceContextProviderProps = {
children: React.ReactNode;
};
export const InvoiceContextProvider = ({
children,
}: InvoiceContextProviderProps) => {
const router = useRouter();
// Toasts
const {
newInvoiceSuccess,
pdfGenerationSuccess,
saveInvoiceSuccess,
modifiedInvoiceSuccess,
sendPdfSuccess,
sendPdfError,
importInvoiceError,
} = useToasts();
// Get form values and methods from form context
const { getValues, reset } = useFormContext();
// Variables
const [invoicePdf, setInvoicePdf] = useState(new Blob());
const [invoicePdfLoading, setInvoicePdfLoading] = useState(false);
// Saved invoices
const [savedInvoices, setSavedInvoices] = useState([]);
useEffect(() => {
let savedInvoicesDefault;
if (typeof window !== undefined) {
// Saved invoices variables
const savedInvoicesJSON =
window.localStorage.getItem("savedInvoices");
savedInvoicesDefault = savedInvoicesJSON
? JSON.parse(savedInvoicesJSON)
: [];
setSavedInvoices(savedInvoicesDefault);
}
}, []);
// Get pdf url from blob
const pdfUrl = useMemo(() => {
if (invoicePdf.size > 0) {
return window.URL.createObjectURL(invoicePdf);
}
return null;
}, [invoicePdf]);
/**
* Handles form submission.
*
* @param {InvoiceType} data - The form values used to generate the PDF.
*/
const onFormSubmit = (data: InvoiceType) => {
console.log("VALUE");
console.log(data);
// Call generate pdf method
generatePdf(data);
};
/**
* Generates a new invoice.
*/
const newInvoice = () => {
reset(FORM_DEFAULT_VALUES);
setInvoicePdf(new Blob());
router.refresh();
// Toast
newInvoiceSuccess();
};
/**
* Generate a PDF document based on the provided data.
*
* @param {InvoiceType} data - The data used to generate the PDF.
* @returns {Promise} - A promise that resolves when the PDF is successfully generated.
* @throws {Error} - If an error occurs during the PDF generation process.
*/
const generatePdf = useCallback(async (data: InvoiceType) => {
setInvoicePdfLoading(true);
try {
const response = await fetch(GENERATE_PDF_API, {
method: "POST",
body: JSON.stringify(data),
});
const result = await response.blob();
setInvoicePdf(result);
if (result.size > 0) {
// Toast
pdfGenerationSuccess();
}
} catch (err) {
console.log(err);
} finally {
setInvoicePdfLoading(false);
}
}, []);
/**
* Removes the final PDF file and switches to Live Preview
*/
const removeFinalPdf = () => {
setInvoicePdf(new Blob());
};
/**
* Generates a preview of a PDF file and opens it in a new browser tab.
*/
const previewPdfInTab = () => {
if (invoicePdf) {
const url = window.URL.createObjectURL(invoicePdf);
window.open(url, "_blank");
}
};
/**
* Downloads a PDF file.
*/
const downloadPdf = () => {
// Only download if there is an invoice
if (invoicePdf instanceof Blob && invoicePdf.size > 0) {
// Create a blob URL to trigger the download
const url = window.URL.createObjectURL(invoicePdf);
// Create an anchor element to initiate the download
const a = document.createElement("a");
a.href = url;
a.download = "invoice.pdf";
document.body.appendChild(a);
// Trigger the download
a.click();
// Clean up the URL object
window.URL.revokeObjectURL(url);
}
};
/**
* Prints a PDF file.
*/
const printPdf = () => {
if (invoicePdf) {
const pdfUrl = URL.createObjectURL(invoicePdf);
const printWindow = window.open(pdfUrl, "_blank");
if (printWindow) {
printWindow.onload = () => {
printWindow.print();
};
}
}
};
// TODO: Change function name. (saveInvoiceData maybe?)
/**
* Saves the invoice data to local storage.
*/
const saveInvoice = () => {
if (invoicePdf) {
// If get values function is provided, allow to save the invoice
if (getValues) {
// Retrieve the existing array from local storage or initialize an empty array
const savedInvoicesJSON = localStorage.getItem("savedInvoices");
const savedInvoices = savedInvoicesJSON
? JSON.parse(savedInvoicesJSON)
: [];
const updatedDate = new Date().toLocaleDateString(
"en-US",
SHORT_DATE_OPTIONS
);
const formValues = getValues();
formValues.details.updatedAt = updatedDate;
const existingInvoiceIndex = savedInvoices.findIndex(
(invoice: InvoiceType) => {
return (
invoice.details.invoiceNumber ===
formValues.details.invoiceNumber
);
}
);
// If invoice already exists
if (existingInvoiceIndex !== -1) {
savedInvoices[existingInvoiceIndex] = formValues;
// Toast
modifiedInvoiceSuccess();
} else {
// Add the form values to the array
savedInvoices.push(formValues);
// Toast
saveInvoiceSuccess();
}
localStorage.setItem(
"savedInvoices",
JSON.stringify(savedInvoices)
);
setSavedInvoices(savedInvoices);
}
}
};
// TODO: Change function name. (deleteInvoiceData maybe?)
/**
* Delete an invoice from local storage based on the given index.
*
* @param {number} index - The index of the invoice to be deleted.
*/
const deleteInvoice = (index: number) => {
if (index >= 0 && index < savedInvoices.length) {
const updatedInvoices = [...savedInvoices];
updatedInvoices.splice(index, 1);
setSavedInvoices(updatedInvoices);
const updatedInvoicesJSON = JSON.stringify(updatedInvoices);
localStorage.setItem("savedInvoices", updatedInvoicesJSON);
}
};
/**
* Send the invoice PDF to the specified email address.
*
* @param {string} email - The email address to which the Invoice PDF will be sent.
* @returns {Promise} A promise that resolves once the email is successfully sent.
*/
const sendPdfToMail = (email: string) => {
const fd = new FormData();
fd.append("email", email);
fd.append("invoicePdf", invoicePdf, "invoice.pdf");
fd.append("invoiceNumber", getValues().details.invoiceNumber);
return fetch(SEND_PDF_API, {
method: "POST",
body: fd,
})
.then((res) => {
if (res.ok) {
// Successful toast msg
sendPdfSuccess();
} else {
// Error toast msg
sendPdfError({ email, sendPdfToMail });
}
})
.catch((error) => {
console.log(error);
// Error toast msg
sendPdfError({ email, sendPdfToMail });
});
};
/**
* Export an invoice in the specified format using the provided form values.
*
* This function initiates the export process with the chosen export format and the form data.
*
* @param {ExportTypes} exportAs - The format in which to export the invoice.
*/
const exportInvoiceAs = (exportAs: ExportTypes) => {
const formValues = getValues();
// Service to export invoice with given parameters
exportInvoice(exportAs, formValues);
};
/**
* Import an invoice from a JSON file.
*
* @param {File} file - The JSON file to import.
*/
const importInvoice = (file: File) => {
const reader = new FileReader();
reader.onload = (event) => {
try {
const importedData = JSON.parse(event.target?.result as string);
// Parse the dates
if (importedData.details) {
if (importedData.details.invoiceDate) {
importedData.details.invoiceDate = new Date(
importedData.details.invoiceDate
);
}
if (importedData.details.dueDate) {
importedData.details.dueDate = new Date(
importedData.details.dueDate
);
}
}
// Reset form with imported data
reset(importedData);
} catch (error) {
console.error("Error parsing JSON file:", error);
importInvoiceError();
}
};
reader.readAsText(file);
};
return (
{children}
);
};
```
## /contexts/Providers.tsx
```tsx path="/contexts/Providers.tsx"
"use client";
import React from "react";
// RHF
import { FormProvider, useForm } from "react-hook-form";
// Zod
import { zodResolver } from "@hookform/resolvers/zod";
// Schema
import { InvoiceSchema } from "@/lib/schemas";
// Context
import { ThemeProvider } from "@/contexts/ThemeProvider";
import { TranslationProvider } from "@/contexts/TranslationContext";
import { InvoiceContextProvider } from "@/contexts/InvoiceContext";
import { ChargesContextProvider } from "@/contexts/ChargesContext";
// Types
import { InvoiceType } from "@/types";
// Variables
import { FORM_DEFAULT_VALUES } from "@/lib/variables";
type ProvidersProps = {
children: React.ReactNode;
};
const Providers = ({ children }: ProvidersProps) => {
const form = useForm({
resolver: zodResolver(InvoiceSchema),
defaultValues: FORM_DEFAULT_VALUES,
});
return (
{children}
);
};
export default Providers;
```
The content has been capped at 50000 tokens, and files over NaN bytes have been omitted. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.