نحوه ساخت برنامه Invoice SaaS با Next.js، Resend، Clerk و Neon Postgres
در این آموزش یاد می گیرید که چگونه یک وب اپلیکیشن فاکتور بسازید که به کاربران امکان می دهد اطلاعات بانکی خود را اضافه کنند، فهرست ی از مشتریان را مدیریت کنند و فاکتورهایی را ایجاد و برای مشتریان ارسال کنند. همچنین میآموزید که چگونه اجزای React را بهعنوان صورتحساب و قالبهای ایمیل مستقیماً از برنامه به ایمیل مشتری چاپ و ارسال کنید.
این یک پروژه عالی خواهد بود که به شما کمک میکند یاد بگیرید چگونه برنامههای پشته کامل را کنار هم قرار دهید، و چگونه برنامهای بسازید که در آن بکاند بتواند در زمان واقعی با فرانتاند ارتباط برقرار کند.
در حین ساخت برنامه، تجربه عملی کار با ابزارهای توسعه دهنده زیر را به دست خواهید آورد:
نئون : یک پایگاه داده Postgres که به ما امکان می دهد داده ها را به راحتی در برنامه ذخیره و بازیابی کنیم.
Clerk : یک سیستم احراز هویت کامل که تضمین می کند فقط کاربران تأیید شده می توانند اقدامات خاصی را در برنامه انجام دهند.
React-to-print : بسته ای که به ما امکان می دهد اجزای React را به صورت فایل PDF تبدیل و چاپ کنیم.
ارسال مجدد و واکنش ایمیل : برای ارسال فاکتورهای دیجیتالی با طراحی زیبا به طور مستقیم به ایمیل مشتریان.
این کد منبع است (به یاد داشته باشید که به آن ستاره بدهید ⭐).
فهرست مطالب
نحوه احراز هویت کاربران با استفاده از Clerk
چگونه نئون را به برنامه Next.js اضافه کنیم
نحوه تنظیم درایور بدون سرور نئون با Drizzle ORM در Next.js
ایجاد نقاط پایانی API برای برنامه
نحوه چاپ و دانلود فاکتورها در Next.js
نئون چیست؟
Neon یک Postgres DB منبع باز، مقیاس پذیر و کارآمد است که محاسبات را از ذخیره سازی جدا می کند. این بدان معنی است که فرآیندهای محاسباتی پایگاه داده (پرس و جوها، تراکنش ها و غیره) توسط یک مجموعه از منابع (محاسبه) مدیریت می شوند، در حالی که خود داده ها در مجموعه جداگانه ای از منابع (ذخیره سازی) ذخیره می شوند.
این معماری امکان مقیاس پذیری و عملکرد بیشتر را فراهم می کند و نئون را به گزینه ای محکم برای برنامه های کاربردی وب مدرن تبدیل می کند.
ساخت برنامه فاکتور با Next.js
در این بخش، من شما را در ساخت صفحات مختلف برنامه صورتحساب با استفاده از Next.js راهنمایی می کنم. این برنامه به شش صفحه کلیدی تقسیم شده است که هر کدام هدف خاصی را دنبال می کنند:
صفحه اصلی : این صفحه فرود است. این یک نمای کلی از برنامه ارائه می دهد و کاربران را وارد برنامه می کند.
صفحه تنظیمات : در اینجا، کاربران می توانند اطلاعات بانکی خود را همانطور که در فاکتورها نمایش داده می شود، به روز کنند.
صفحه مشتریان : این صفحه به کاربران اجازه می دهد تا پایگاه مشتریان خود را مدیریت کنند و در صورت نیاز مشتریان را اضافه یا حذف کنند.
داشبورد : هسته برنامه که در آن کاربران می توانند فاکتورهای جدید ایجاد کنند. کاربران می توانند یک مشتری را انتخاب کنند، عنوان و شرح فاکتور را وارد کنند و فاکتور تولید کنند.
صفحه تاریخچه : این صفحه فاکتورهای اخیرا ایجاد شده را نمایش می دهد. این شامل پیوندهایی است که کاربران را قادر میسازد تا پیشنمایش هر فاکتور را مشاهده کنند و راهی سریع برای تحلیل تراکنشهای گذشته ارائه دهند.
صفحه چاپ و ارسال فاکتور : این صفحه به کاربران امکان چاپ و ارسال فاکتورها را برای مشتریان می دهد.
قبل از ادامه، با اجرای قطعه کد زیر در ترمینال خود، یک پروژه TypeScript Next.js ایجاد کنید:
یک فایل type.d.ts در پوشه پروژه اضافه کنید. این شامل اعلان های نوع برای متغیرهای داخل برنامه خواهد بود.
interface Item { id: string; name: string; cost: number; quantity: number; price: number; } interface Invoice { id?: string, created_at?: string, user_id: string, customer_id: number, title: string, items: string, total_amount: number, } interface Customer { user_id: string, name: string, email: string, address: string } interface BankInfo { user_id: string, account_name: string, account_number: number, bank_name: string, currency: string }
صفحه نخست
قطعه کد زیر را در فایل app/page.tsx کپی کنید. اطلاعات مختصری در مورد برنامه و دکمه ای که کاربران را بسته به وضعیت احراز هویت آنها به داشبورد یا صفحه ورود هدایت می کند، نمایش می دهد.
import Link from "next/link"; export default function Home() { return ( <main className='w-full'> <section className='p-8 h-[90vh] md:w-2/3 mx-auto text-center w-full flex flex-col items-center justify-center'> <h2 className='text-3xl font-bold mb-4 md:text-4xl'> Create invoices for your customers </h2> <p className='opacity-70 mb-4 text-sm md:text-base leading-loose'> Invoicer is an online invoicing software that helps you craft and print professional invoices for your customers for free! Keep your business and clients with one invoicing software. </p> <Link href='/dashboard' className='rounded w-[200px] px-2 py-3 bg-blue-500 text-gray-50' > LOG IN </Link> </section> </main> ); }
صفحه تنظیمات
یک پوشه تنظیمات حاوی یک فایل page.tsx را در فهرست برنامه Next.js اضافه کنید و قطعه کد زیر را در فایل کپی کنید:
"use client"; import { ChangeEvent, useEffect, useState, useCallback } from "react"; import SideNav from "@/app/components/SideNav"; export default function Settings() { //👇🏻 default bank info const [bankInfo, setBankInfo] = useState({ account_name: "", account_number: 1234567890, bank_name: "", currency: "", }); //👇🏻 bank info from the form entries const [inputBankInfo, setInputBankInfo] = useState({ accountName: "", accountNumber: 1234567890, bankName: "", currency: "", }); //👇🏻 updates the form entries state const handleUpdateBankInfo = ( e: ChangeEvent<HTMLInputElement | HTMLSelectElement> ) => { const { name, value } = e.target; setInputBankInfo((prevState) => ({ ...prevState, [name]: value, })); }; //👇🏻 updates the bank info const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); console.log("Tries to update bank info..."); }; return () }
قطعه کد بالا نشان می دهد که صفحه اطلاعات بانکی کاربر را نمایش می دهد و همچنین به کاربر اجازه می دهد در صورت لزوم آن را به روز کند.
عناصر UI زیر را از مؤلفه برگردانید:
export default function Settings() { //…React states and functions return ( <div className='w-full'> <main className='min-h-[90vh] flex items-start'> <SideNav /> <div className='md:w-5/6 w-full h-full p-6'> <h2 className='text-2xl font-bold'>Bank Information</h2> <p className='opacity-70 mb-4'> Update your bank account information </p> <div className='flex md:flex-row flex-col items-start justify-between w-full md:space-x-4'> <section className='md:w-1/3 w-full bg-blue-50 h-full p-3 rounded-md space-y-3'> <p className='text-sm opacity-75'> Account Name: {bankInfo.account_name} </p> <p className='text-sm opacity-75'> Account Number: {bankInfo.account_number} </p> <p className='text-sm opacity-75'> Bank Name: {bankInfo.bank_name} </p> <p className='text-sm opacity-75'> Currency: {bankInfo.currency} </p> </section> <form className='md:w-2/3 w-full p-3 flex flex-col' method='POST' onSubmit={handleSubmit} > <label htmlFor='accountName' className='text-sm'> Account Name </label> <input type='text' name='accountName' id='accountName' className='border-[1px] p-2 rounded mb-3' required value={inputBankInfo.accountName} onChange={handleUpdateBankInfo} /> <label htmlFor='accountNumber' className='text-sm'> Account Number </label> <input type='number' name='accountNumber' id='accountNumber' className='border-[1px] p-2 rounded mb-3' required value={inputBankInfo.accountNumber} onChange={handleUpdateBankInfo} /> <label htmlFor='bankName' className='text-sm'> Bank Name </label> <input type='text' name='bankName' id='bankName' className='border-[1px] p-2 rounded mb-3' required value={inputBankInfo.bankName} onChange={handleUpdateBankInfo} /> <label htmlFor='currency' className='text-sm'> Currency </label> <select name='currency' id='currency' className='border-[1px] p-2 rounded mb-3' required value={inputBankInfo.currency} onChange={handleUpdateBankInfo} > <option value=''>Select</option> <option value='$'>USD</option> <option value='€'>EUR</option> <option value='£'>GBP</option> </select> <div className='flex items-center justify-end'> <button type='submit' className='bg-blue-500 text-white p-2 w-[200px] rounded' > Update Bank Info </button> </div> </form> </div> </div> </main> </div> ); }
صفحه مشتریان
پوشه مشتریان حاوی فایل page.tsx را در پوشه Next.js اضافه کنید و قطعه کد زیر را در فایل کپی کنید:
import CustomersTable from "../components/CustomersTable"; import { useCallback, useEffect, useState } from "react"; import SideNav from "@/app/components/SideNav"; export default function Customers() { const [customerName, setCustomerName] = useState<string>(""); const [customerEmail, setCustomerEmail] = useState<string>(""); const [customerAddress, setCustomerAddress] = useState<string>(""); const [loading, setLoading] = useState<boolean>(false); const [customers, setCustomers] = useState([]); const handleAddCustomer = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); // 👉🏻 createCustomer(); }; return ( <div className='w-full'> <main className='min-h-[90vh] flex items-start'> <SideNav /> <div className='md:w-5/6 w-full h-full p-6'> <h2 className='text-2xl font-bold'>Customers</h2> <p className='opacity-70 mb-4'>Create and view all your customers</p> <form className='w-full' onSubmit={handleAddCustomer} method='POST'> <div className='w-full flex items-center space-x-4 mb-3'> <section className='w-1/2'> <label>Customer's Name</label> <input type='text' className='w-full p-2 border border-gray-200 rounded-sm' value={customerName} required onChange={(e) => setCustomerName(e.target.value)} /> </section> <section className='w-1/2'> <label>Email Address</label> <input type='email' className='w-full p-2 border border-gray-200 rounded-sm' value={customerEmail} onChange={(e) => setCustomerEmail(e.target.value)} required /> </section> </div> <label htmlFor='address'>Billing Address</label> <textarea name='address' id='address' rows={3} className='w-full p-2 border border-gray-200 rounded-sm' value={customerAddress} onChange={(e) => setCustomerAddress(e.target.value)} required /> <button className='bg-blue-500 text-white p-2 rounded-md mb-6' disabled={loading} > {loading ? "Adding..." : "Add Customer"} </button> </form> <CustomersTable customers={customers} /> </div> </main> </div> ); }
قطعه کد بالا به کاربران امکان مشاهده، ایجاد و حذف مشتریان از برنامه را می دهد.
صفحه داشبورد
یک پوشه داشبورد حاوی page.tsx در فهرست برنامه Next.js ایجاد کنید و قطعه کد زیر را در فایل کپی کنید:
"use client"; import InvoiceTable from "@/app/components/InvoiceTable"; import React, { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import SideNav from "@/app/components/SideNav"; export default function Dashboard() { const { isLoaded, isSignedIn, user } = useUser(); const [itemList, setItemList] = useState<Item[]>([]); const [customer, setCustomer] = useState<string>(""); const [invoiceTitle, setInvoiceTitle] = useState<string>(""); const [itemCost, setItemCost] = useState<number>(1); const [itemQuantity, setItemQuantity] = useState<number>(1); const [itemName, setItemName] = useState<string>(""); const [customers, setCustomers] = useState([]); const router = useRouter(); const handleAddItem = (e: React.FormEvent) => { e.preventDefault(); if (itemName.trim() && itemCost > 0 && itemQuantity >= 1) { setItemList([ ...itemList, { id: Math.random().toString(36).substring(2, 9), name: itemName, cost: itemCost, quantity: itemQuantity, price: itemCost * itemQuantity, }, ]); } setItemName(""); setItemCost(0); setItemQuantity(0); }; const getTotalAmount = () => { let total = 0; itemList.forEach((item) => { total += item.price; }); return total; }; const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); //👉🏻 createInvoice(); }; return ( <div className='w-full'> <main className='min-h-[90vh] flex items-start'> <SideNav /> <div className='md:w-5/6 w-full h-full p-6'> <h2 className='font-bold text-2xl mb-3'>Add new invoice</h2> <form className='w-full flex flex-col' onSubmit={handleFormSubmit}> <label htmlFor='customer'>Customer</label> <select className='border-[1px] p-2 rounded-sm mb-3' required value={customer} onChange={(e) => setCustomer(e.target.value)} > {customers.map((customer: any) => ( <option key={customer.id} value={customer.name}> {customer.name} </option> ))} </select> <label htmlFor='title'>Title</label> <input className='border-[1px] rounded-sm mb-3 py-2 px-3' required value={invoiceTitle} onChange={(e) => setInvoiceTitle(e.target.value)} /> <div className='w-full flex justify-between flex-col'> <h3 className='my-4 font-bold'>Items List</h3> <div className='flex space-x-3'> <div className='flex flex-col w-1/4'> <label htmlFor='itemName' className='text-sm'> Name </label> <input type='text' name='itemName' placeholder='Name' className='py-2 px-4 mb-6 bg-gray-100' value={itemName} onChange={(e) => setItemName(e.target.value)} /> </div> <div className='flex flex-col w-1/4'> <label htmlFor='itemCost' className='text-sm'> Cost </label> <input type='number' name='itemCost' placeholder='Cost' className='py-2 px-4 mb-6 bg-gray-100' value={itemCost} onChange={(e) => setItemCost(Number(e.target.value))} /> </div> <div className='flex flex-col justify-center w-1/4'> <label htmlFor='itemQuantity' className='text-sm'> Quantity </label> <input type='number' name='itemQuantity' placeholder='Quantity' className='py-2 px-4 mb-6 bg-gray-100' value={itemQuantity} onChange={(e) => setItemQuantity(Number(e.target.value))} /> </div> <div className='flex flex-col justify-center w-1/4'> <p className='text-sm'>Price</p> <p className='py-2 px-4 mb-6 bg-gray-100'> {Number(itemCost * itemQuantity).toLocaleString("en-US")} </p> </div> </div> <button className='bg-blue-500 text-gray-100 w-[100px] p-2 rounded' onClick={handleAddItem} > Add Item </button> </div> <InvoiceTable itemList={itemList} /> <button className='bg-blue-800 text-gray-100 w-full p-4 rounded my-6' type='submit' > SAVE & PREVIEW INVOICE </button> </form> </div> </main> </div> ); }
قطعه کد بالا فرمی را نمایش می دهد که جزئیات فاکتور مانند نام مشتری، عنوان فاکتور و فهرست اقلام مورد نیاز برای ایجاد یک فاکتور را می پذیرد.
صفحه تاریخچه
یک پوشه تاریخچه حاوی فایل page.tsx در فهرست برنامه Next.js ایجاد کنید و کد زیر را در فایل کپی کنید:
"use client"; import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import SideNav from "@/app/components/SideNav"; export default function History() { const { isLoaded, isSignedIn, user } = useUser(); const [invoices, setInvoices] = useState<Invoice[]>([]); return ( <div className='w-full'> <main className='min-h-[90vh] flex items-start'> <SideNav /> <div className='md:w-5/6 w-full h-full p-6'> <h2 className='text-2xl font-bold'>History</h2> <p className='opacity-70 mb-4'>View all your invoices and their status</p> {invoices.map((invoice) => ( <div className='bg-blue-50 w-full mb-3 rounded-md p-3 flex items-center justify-between' key={invoice.id} > <div> <p className='text-sm text-gray-500 mb-2'> Invoice - #0{invoice.id} issued to{" "} <span className='font-bold'>{invoice.customer_id}</span> </p> <h3 className='text-lg font-bold mb-[1px]'> {Number(invoice.total_amount).toLocaleString()} </h3> </div> <Link href={{ pathname: `/invoices/${invoice.id}`, query: { customer: invoice.customer_id }, }} className='bg-blue-500 text-blue-50 rounded p-3' > Preview </Link> </div> ))} </div> </main> </div> ); }
قطعه کد بالا فاکتورهای اخیرا ایجاد شده را نمایش می دهد و به کاربران امکان می دهد در صورت نیاز پیش نمایش آنها را مشاهده کنند.
نحوه احراز هویت کاربران با استفاده از Clerk
Clerk یک پلت فرم مدیریت کاربر کامل است که شما را قادر می سازد تا اشکال مختلف احراز هویت را به برنامه های نرم افزاری خود اضافه کنید. این مؤلفههای رابط کاربری آسان و انعطافپذیر و APIهایی را ارائه میکند که میتوانند بهطور یکپارچه در برنامه شما ادغام شوند.
با اجرای قطعه کد زیر در ترمینال خود، Clerk Next.js SDK را نصب کنید:
یک فایل middleware.ts
در پوشه Next.js src ایجاد کنید و قطعه کد زیر را در فایل کپی کنید:
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; // the createRouteMatcher function accepts an array of routes to be protected const protectedRoutes = createRouteMatcher([ "/customers", "/settings", "/dashboard", "/history", "/invoices(.*)", ]); // protects the route export default clerkMiddleware((auth, req) => { if (protectedRoutes(req)) { auth().protect(); } }); export const config = { matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], };
تابع createRouteMatcher()
آرایهای حاوی مسیرهایی را میپذیرد که از کاربران احراز هویت نشده محافظت شوند و تابع clerkMiddleware()
از محافظت از مسیرها اطمینان میدهد.
سپس اجزای Clerk زیر را در فایل app/layout.tsx وارد کنید و تابع RootLayout
مطابق شکل زیر به روز کنید:
import { ClerkProvider, SignInButton, SignedIn, SignedOut, UserButton, } from "@clerk/nextjs"; import Link from "next/link"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <ClerkProvider> <html lang='en'> <body className={inter.className}> <nav className='flex justify-between items-center h-[10vh] px-8 border-b-[1px]'> <Link href='/' className='text-xl font-extrabold text-blue-700'> Invoicer </Link> <div className='flex items-center gap-5'> {/*-- if user is signed out --*/} <SignedOut> <SignInButton mode='modal' /> </SignedOut> {/*-- if user is signed in --*/} <SignedIn> <Link href='/dashboard' className=''> Dashboard </Link> <UserButton showName /> </SignedIn> </div> </nav> {children} </body> </html> </ClerkProvider> ); }
هنگامی که کاربر وارد نشده است، جزء دکمه ورود به سیستم ارائه می شود.
سپس، پس از ورود به برنامه، کامپوننت Clerk's User Button و پیوندی به داشبورد نمایش داده می شود.
بعد، یک حساب کارمند ایجاد کنید و یک پروژه برنامه جدید اضافه کنید.
ایمیل را به عنوان روش احراز هویت انتخاب کنید و پروژه Clerk را ایجاد کنید.
در نهایت، کلیدهای قابل انتشار و مخفی Clerk خود را به . فایل env.local .
Clerk راه های مختلفی برای خواندن داده های کاربر بر روی مشتری و سرور ارائه می دهد که برای شناسایی کاربران در برنامه ضروری است.
چگونه نئون را به برنامه Next.js اضافه کنیم
نئون از چندین چارچوب و کتابخانه پشتیبانی می کند و مستندات واضح و دقیقی را در مورد گفت ن نئون به آنها ارائه می دهد. درایور بدون سرور نئون به شما امکان می دهد در برنامه Next.js به Neon متصل شوید و با آن تعامل داشته باشید.
قبل از ادامه، اجازه دهید یک حساب نئون و پروژه ایجاد کنیم .
در داشبورد پروژه خود، یک رشته اتصال پایگاه داده را خواهید یافت. شما از این برای تعامل با پایگاه داده نئون خود استفاده خواهید کرد.
سپس بسته Neon Serverless را در پروژه Next.js نصب کنید:
رشته اتصال پایگاه داده خود را در فایل .env.local کپی کنید.
یک پوشه db حاوی یک فایل index.ts در فهرست برنامه Next.js ایجاد کنید و قطعه کد زیر را در فایل کپی کنید:
import { neon } from '@neondatabase/serverless'; if (!process.env.NEON_DATABASE_URL) { throw new Error('NEON_DATABASE_URL must be a Neon postgres connection string') } export const getDBVersion = async() => { const sql = neon(process.env.NEON_DATABASE_URL!); const response = await sql`SELECT version()`; return { version: response[0].version } }
فایل app/page.tsx را به یک جزء سرور تبدیل کنید و تابع getDBVersion()
را اجرا کنید:
import { getDBVersion } from "./db"; export default async function Home() { const { version } = await getDBVersion(); console.log({version}) return (<div>{/** -- UI elements -- */}</div>) }
تابع getDBVersion()
با پایگاه داده نئون ارتباط برقرار می کند و به ما امکان می دهد پرس و جوهای SQL را با استفاده از سرویس گیرنده Postgres اجرا کنیم. این تابع نسخه پایگاه داده را برمی گرداند که سپس به کنسول وارد می شود.
تبریک – شما با موفقیت نئون را به برنامه Next.js خود اضافه کردید.
با این حال، تعامل با پایگاه داده نئون با نوشتن پرس و جوهای SQL به طور مستقیم می تواند نیاز به یادگیری اضافی داشته باشد یا پیچیدگی هایی را برای توسعه دهندگانی که با SQL آشنا نیستند، ایجاد کند. همچنین می تواند منجر به خطا یا مشکلات عملکرد هنگام انجام پرس و جوهای پیچیده شود.
به همین دلیل است که نئون از ORM های پایگاه داده مانند Drizzle ORM پشتیبانی می کند که یک رابط سطح بالاتر برای تعامل با پایگاه داده ارائه می دهد. Drizzle ORM شما را قادر می سازد تا توابع پرس و جو پیچیده بنویسید و با استفاده از TypeScript به راحتی با پایگاه داده تعامل داشته باشید.
نحوه تنظیم درایور بدون سرور نئون با Drizzle ORM در Next.js
Drizzle ORM به شما امکان می دهد داده ها را پرس و جو کنید و با استفاده از دستورات پرس و جوی TypeScript ساده، عملیات های مختلفی را روی پایگاه داده انجام دهید. این سبک وزن، نوع ساده و آسان برای استفاده است.
ابتدا باید Drizzle Kit و Drizzle ORM را نصب کنید.
Drizzle Kit به شما امکان می دهد طرحواره پایگاه داده و مهاجرت ها را مدیریت کنید.
داخل پوشه db ، یک فایل actions.ts و schema.ts اضافه کنید:
فایل actions.ts شامل پرس و جوها و عملیات پایگاه داده مورد نیاز است، در حالی که فایل schema.ts طرح پایگاه داده را برای برنامه صورتحساب تعریف می کند.
طراحی پایگاه داده برای برنامه فاکتور
به یاد داشته باشید که کاربران می توانند مشتریان خود را اضافه کنند، اطلاعات بانکی خود را به روز کنند و در برنامه فاکتور ایجاد کنند. پس باید جداول پایگاه داده برای داده ها در نئون ایجاد کنید.
شناسه کاربر به عنوان یک کلید خارجی برای شناسایی هر ردیف از دادهها که متعلق به یک کاربر خاص است استفاده میشود.
قطعه کد زیر را در فایل db/schema.ts کپی کنید:
import { text, serial, pgTable, timestamp, numeric } from "drizzle-orm/pg-core"; //👇🏻 invoice table with its column types export const invoicesTable = pgTable("invoices", { id: serial("id").primaryKey().notNull(), owner_id: text("owner_id").notNull(), customer_id: text("customer_id").notNull(), title: text("title").notNull(), items: text("items").notNull(), created_at: timestamp("created_at").defaultNow(), total_amount: numeric("total_amount").notNull(), }); //👇🏻 customers table with its column types export const customersTable = pgTable("customers", { id: serial("id").primaryKey().notNull(), created_at: timestamp("created_at").defaultNow(), owner_id: text("owner_id").notNull(), name: text("name").notNull(), email: text("email").notNull(), address: text("address").notNull(), }) //👇🏻 bank_info table with its column types export const bankInfoTable = pgTable("bank_info", { id: serial("id").primaryKey().notNull(), owner_id: text("owner_id").notNull().unique(), bank_name: text("bank_name").notNull(), account_number: numeric("account_number").notNull(), account_name: text("account_name").notNull(), created_at: timestamp("created_at").defaultNow(), currency: text("currency").notNull(), })
فایل actions.ts شامل عملیات های مختلف پایگاه داده مورد نیاز در برنامه خواهد بود. ابتدا قطعه کد زیر را به فایل اضافه کنید:
import { invoicesDB, customersDB, bankInfoDB } from "."; import { invoicesTable, customersTable, bankInfoTable } from './schema'; import { desc, eq } from "drizzle-orm"; //👇🏻 add a new row to the invoices table export const createInvoice = async (invoice: any) => { await invoicesDB.insert(invoicesTable).values({ owner_id: invoice.user_id, customer_id: invoice.customer_id, title: invoice.title, items: invoice.items, total_amount: invoice.total_amount, }); }; //👇🏻 get all user's invoices export const getUserInvoices = async (user_id: string) => { return await invoicesDB.select().from(invoicesTable).where(eq(invoicesTable.owner_id, user_id)).orderBy(desc(invoicesTable.created_at)); }; //👇🏻 get single invoice export const getSingleInvoice = async (id: number) => { return await invoicesDB.select().from(invoicesTable).where(eq(invoicesTable.id, id)); };
تابع createInvoice
جزئیات فاکتور را به عنوان پارامتر می پذیرد و یک ردیف جدید از داده ها را به جدول فاکتور خود اضافه می کند. تابع getUserInvoices
جدول را فیلتر می کند و آرایه ای از فاکتورهای ایجاد شده توسط کاربر را برمی گرداند. تابع getSingleInvoice
شناسه فاکتور را میپذیرد، جدول را فیلتر میکند و فاکتور را با شناسه منطبق برمیگرداند.
توابع زیر را به فایل db/actions اضافه کنید:
//👇🏻 get customers list export const getCustomers = async (user_id: string) => { return await customersDB.select().from(customersTable).where(eq(customersTable.owner_id, user_id)).orderBy(desc(customersTable.created_at)); }; //👇🏻 get single customer export const getSingleCustomer = async (name: string) => { return await customersDB.select().from(customersTable).where(eq(customersTable.name, name)); }; //👇🏻 add a new row to the customers table export const addCustomer = async (customer: Customer) => { await customersDB.insert(customersTable).values({ owner_id: customer.user_id, name: customer.name, email: customer.email, address: customer.address, }); }; //👇🏻 delete a customer export const deleteCustomer = async (id: number) => { await customersDB.delete(customersTable).where(eq(customersTable.id, id)); };
این قطعه کد به کاربران امکان می دهد تا همه مشتریان خود را از پایگاه داده بازیابی کنند، یک مشتری را از طریق شناسه آن دریافت کنند، مشتریان جدید اضافه کنند و مشتریان را از جدول مشتریان حذف کنند.
در نهایت، این را نیز به فایل db/actions.ts اضافه کنید:
//👇🏻 get user's bank info export const getUserBankInfo = async (user_id: string) => { return await bankInfoDB.select().from(bankInfoTable).where(eq(bankInfoTable.owner_id, user_id)); }; //👇🏻 update bank info table export const updateBankInfo = async (info: any) => { await bankInfoDB.insert(bankInfoTable) .values({ owner_id: info.user_id, bank_name: info.bank_name, account_number: info.account_number, account_name: info.account_name, currency: info.currency, }) .onConflictDoUpdate({ target: bankInfoTable.owner_id, set: { bank_name: info.bank_name, account_number: info.account_number, account_name: info.account_name, currency: info.currency, }, }); };
تابع getUserBankInfo
اطلاعات بانکی کاربر را از پایگاه داده واکشی می کند، در حالی که تابع updateBankInfo
آن را به روز می کند. اگر کاربر قبلاً یکی داشته باشد، تابع آن را با جزئیات جدید به روز می کند - در غیر این صورت، یک ورودی جدید ایجاد می کند.
در مرحله بعد، فایل db/index.ts را برای اتصال به پایگاه داده Neon به روز کنید و نمونه Drizzle را برای هر جدول صادر کنید. این برای اجرای پرسوجوهای Typeafe SQL در برابر پایگاه داده Postgres شما که در Neon میزبانی شده است استفاده میشود.
import { neon } from '@neondatabase/serverless'; import { drizzle } from 'drizzle-orm/neon-http'; import { invoicesTable, customersTable, bankInfoTable } from './schema'; if (!process.env.NEON_DATABASE_URL) { throw new Error('DATABASE_URL must be a Neon postgres connection string') } const sql = neon(process.env.NEON_DATABASE_URL!); export const invoicesDB = drizzle(sql, { schema: { invoicesTable } }); export const customersDB = drizzle(sql, { schema: { customersTable } }); export const bankInfoDB = drizzle(sql, { schema: { bankInfoTable } });
یک فایل drizzle.config.ts در ریشه پوشه Next.js ایجاد کنید و پیکربندی زیر را اضافه کنید. مطمئن شوید که بسته Dotenv را نصب کرده اید.
import type { Config } from "drizzle-kit"; import * as dotenv from "dotenv"; dotenv.config(); if (!process.env.NEON_DATABASE_URL) throw new Error("NEON DATABASE_URL not found in environment"); export default { schema: "./src/app/db/schema.ts", out: "./src/app/db/migrations", dialect: "postgresql", dbCredentials: { url: process.env.NEON_DATABASE_URL, }, strict: true, } satisfies Config;
فایل drizzle.config.ts حاوی تمام اطلاعات مربوط به اتصال پایگاه داده، پوشه مهاجرت و فایل های طرحواره شما است.
در نهایت، فایل package.json را بهروزرسانی کنید تا شامل دستورات Drizzle Kit برای ایجاد مهاجرت پایگاه داده و ایجاد جداول باشد.
اکنون می توانید npm run db-create
را اجرا کنید تا جداول پایگاه داده را به کنسول نئون فشار دهید.
ایجاد نقاط پایانی API برای برنامه
در قسمت قبل، توابع لازم برای تعامل با پایگاه داده را ایجاد کردید. در این بخش، نحوه ایجاد نقاط پایانی API برای هر عملیات پایگاه داده را یاد خواهید گرفت.
ابتدا یک پوشه api
در پوشه برنامه Next.js ایجاد کنید. این شامل تمام مسیرهای API برای برنامه خواهد بود.
یک پوشه bank-info
حاوی route.ts در پوشه api
اضافه کنید. این بدان معناست که مسیر API ( /api/bank-info ) بهروزرسانی و واکشی اطلاعات بانکی کاربر را انجام میدهد.
قطعه کد زیر را در فایل /bank-info/route.ts کپی کنید. روش درخواست POST اطلاعات بانکی کاربر را به روز می کند و پاسخی را برمی گرداند و روش درخواست GET اطلاعات بانک را با استفاده از شناسه کاربر از پایگاه داده بازیابی می کند.
import { updateBankInfo, getUserBankInfo } from "@/app/db/actions"; import { NextRequest, NextResponse } from "next/server"; export async function POST(req: NextRequest) { const { accountName, userID, accountNumber, bankName, currency } = await req.json(); try { await updateBankInfo({ user_id: userID, bank_name: bankName, account_number: Number(accountNumber), account_name: accountName, currency: currency, }); return NextResponse.json({ message: "Bank Details Updated!" }, { status: 201 }); } catch (err) { return NextResponse.json( { message: "An error occurred", err }, { status: 400 } ); } } export async function GET(req: NextRequest) { const userID = req.nextUrl.searchParams.get("userID"); try { const bankInfo = await getUserBankInfo(userID!); return NextResponse.json({ message: "Fetched bank details", bankInfo }, { status: 200 }); } catch (err) { return NextResponse.json( { message: "An error occurred", err }, { status: 400 } ); } }
سپس، یک پوشه فاکتور حاوی یک فایل route.ts را به دایرکتوری api
اضافه کنید. قطعه کد زیر را در فایل /api/invoice/route.ts کپی کنید:
import { createInvoice, getUserInvoices } from "@/app/db/actions"; import { NextRequest, NextResponse } from "next/server"; export async function POST(req: NextRequest) { const { customer, title, items, total, ownerID } = await req.json(); try { await createInvoice({ user_id: ownerID, customer_id: customer, title, total_amount: total, items: JSON.stringify(items), }) return NextResponse.json( { message: "New Invoice Created!" }, { status: 201 } ); } catch (err) { return NextResponse.json( { message: "An error occurred", err }, { status: 400 } ); } } export async function GET(req: NextRequest) { const userID = req.nextUrl.searchParams.get("userID"); try { const invoices = await getUserInvoices(userID!); return NextResponse.json({message: "Invoices retrieved successfully!", invoices}, { status: 200 }); } catch (err) { return NextResponse.json( { message: "An error occurred", err }, { status: 400 } ); } }
روش درخواست POST یک فاکتور جدید ایجاد می کند و روش درخواست GET تمام فاکتورهای کاربر را از پایگاه داده برمی گرداند.
همچنین میتوانید یک پوشه فرعی به نام single
در پوشه /api/invoices ایجاد کنید و یک فایل route.ts در آن اضافه کنید.
import { NextRequest, NextResponse } from "next/server"; import { getSingleInvoice } from "@/app/db/actions"; export async function GET(req: NextRequest) { const invoiceID = req.nextUrl.searchParams.get("id"); try { const invoice = await getSingleInvoice(invoiceID); return NextResponse.json({ message: "Inovice retrieved successfully!", invoice }, { status: 200 }); } catch (err) { return NextResponse.json( { message: "An error occurred", err }, { status: 400 } ); } }
قطعه کد بالا یک شناسه فاکتور را می پذیرد و تمام داده های موجود در جدول پایگاه داده را بازیابی می کند. شما می توانید همین کار را با جدول مشتریان نیز انجام دهید.
تبریک می گویم! شما یاد گرفته اید که چگونه داده ها را از پایگاه داده Neon Postgres ایجاد ، ذخیره و بازیابی کنید . در بخشهای آینده، نحوه چاپ و ارسال فاکتورها برای مشتریان را خواهید دید.
نحوه چاپ و دانلود فاکتورها در Next.js
بسته React-to-print یک کتابخانه ساده جاوا اسکریپت است که به شما امکان می دهد محتویات یک جزء React را به راحتی و بدون دستکاری در سبک های CSS کامپوننت چاپ کنید. کامپوننت های React را دقیقاً همانطور که هستند به فایل های PDF قابل دانلود تبدیل می کند.
ابتدا قطعه کد زیر را در ترمینال خود برای نصب بسته اجرا کنید:
یک صفحه مشتری ایجاد کنید ( /invoice/[id].tsx ).
برای انجام این کار ، یک پوشه فاکتور حاوی یک زیر لایه [ID] را به فهرست برنامه بعدی اضافه کنید. در داخل پوشه [ID] ، یک پرونده page.tsx اضافه کنید. این صفحه تمام اطلاعات مربوط به فاکتور را نشان می دهد و به کاربران امکان می دهد تا فاکتورها را برای مشتریان چاپ ، بارگیری و ارسال کنند.
با کپی کردن قطعه کد زیر در پرونده Page.tsx ، یک طرح فاکتور شبیه به تصویر بالا ایجاد کنید:
const ComponentToPrint = forwardRef<HTMLDivElement, Props>((props, ref) => { const { id, customer, invoice, bankInfo } = props as Props; return ( <div className='w-full px-2 py-8' ref={ref}> <div className='lg:w-2/3 w-full mx-auto shadow-md border-[1px] rounded min-h-[75vh] p-5'> <header className='w-full flex items-center space-x-4 justify-between'> <div className='w-4/5'> <h2 className='text-lg font-semibold mb-3'>INVOICE #0{id}</h2> <section className='mb-6'> <p className='opacity-60'>Issuer Name: {bankInfo?.account_name}</p> <p className='opacity-60'>Date: {formatDateString(invoice?.created_at!)}</p> </section> <h2 className='text-lg font-semibold mb-2'>TO:</h2> <section className='mb-6'> <p className='opacity-60'>Name: {invoice?.customer_id}</p> <p className='opacity-60'>Address: {customer?.address}</p> <p className='opacity-60'>Email: {customer?.email}</p> </section> </div> <div className='w-1/5 flex flex-col'> <p className='font-extrabold text-2xl'> {`${bankInfo?.currency}${Number(invoice?.total_amount).toLocaleString()}`} </p> <p className='text-sm opacity-60'>Total Amount</p> </div> </header> <div> <p className='opacity-60'>Subject:</p> <h2 className='text-lg font-semibold'>{invoice?.title}</h2> </div> <InvoiceTable itemList={invoice?.items ? JSON.parse(invoice.items) : []} /> </div> </div> ); }); ComponentToPrint.displayName = "ComponentToPrint";
قطعه کد جزئیات فاکتور ، از جمله اطلاعات مشتری مشتری و کاربر را می پذیرد و آنها را در مؤلفه ارائه می دهد.
بالاخره ، شما باید این مؤلفه را با والدین دیگری ببندید و به چاپ به چاپ برای چاپ مؤلفه فرعی دستور دهید. قطعه کد زیر را در زیر مؤلفه ComponentToPrint
اضافه کنید.
import { useReactToPrint } from "react-to-print"; export default function Invoices() { const { id } = useParams<{ id: string }>(); // Reference to the component to be printed const componentRef = useRef<any>(); // States for the data const [customer, setCustomer] = useState<Customer>(); const [bankInfo, setBankInfo] = useState<BankInfo>(); const [invoice, setInvoice] = useState<Invoice>(); // Function that sends invoice via email const handleSendInvoice = async () => {}; // Function that prints the invoice const handlePrint = useReactToPrint({ documentTitle: "Invoice", content: () => componentRef.current, }); return ( <main className='w-full min-h-screen'> <section className='w-full flex p-4 items-center justify-center space-x-5 mb-3'> <button className='p-3 text-blue-50 bg-blue-500 rounded-md' onClick={handlePrint} > Download </button> <button className='p-3 text-blue-50 bg-green-500 rounded-md' onClick={() => { handleSendInvoice(); }} > Send Invoice </button> </section> <ComponentToPrint ref={componentRef} id={id} customer={customer} bankInfo={bankInfo} invoice={invoice} /> </main> ); }
این مؤلفه مؤلفه ComponentToPrint
را ارائه می دهد ، مرجع آن را ایجاد می کند و آن را با استفاده از قلاب usereacttoprint چاپ می کند.
نحوه ارسال فاکتورهای دیجیتال با ایمیل RESEND و React
RENEND یک سرویس API است که ما را قادر می سازد تا ایمیل ها را به صورت برنامه ای ارسال و مدیریت کنیم ، و این باعث می شود که عملکرد ایمیل در برنامه های نرم افزاری ادغام شود.
React Email یک کتابخانه است که به ما امکان می دهد تا با استفاده از اجزای React ، الگوهای ایمیل قابل استفاده مجدد و زیبا را ایجاد کنیم. هر دو بسته توسط شخص ایجاد می شوند و امکان ادغام صاف بین این دو سرویس را فراهم می کند.
هر دو بسته را با اجرای قطعه کد در زیر نصب کنید:
Recort Rect را با استفاده از اسکریپت زیر در پرونده Package.json خود پیکربندی کنید.
پرچم --dir
دسترسی به ایمیل React را به الگوهای ایمیل واقع در پروژه می دهد. در این حالت ، الگوهای ایمیل در پوشه SRC/APP/ایمیل قرار دارند.
در مرحله بعد ، پوشه ایمیل حاوی الگوی ایمیل را برای ارسال به ایمیل مشتریان ایجاد کنید:
import { Heading, Hr, Text } from "@react-email/components"; export default function EmailTemplate({ invoiceID, items, amount, issuerName, accountNumber, currency, }: Props) { return ( <div> <Heading as='h2' style={{ color: "#0ea5e9" }}> Purhcase Invoice from {issuerName} </Heading> <Text style={{ marginBottom: 5 }}>Invoice No: INV0{invoiceID}</Text> <Heading as='h3'> Payment Details:</Heading> <Text>Account Details: {issuerName}</Text> <Text>Account Number: {accountNumber}</Text> <Text>Total Amount: {`${currency}${amount}`}</Text> <Hr /> <Heading as='h3'> Items: </Heading> {items && items.map((item, index) => ( <div key={index}> <Text> {item.cost} x {item.quantity} = {item.price} </Text> </div> ))} </div> ); }
الگوی ایمیل تمام جزئیات فاکتور را به عنوان غرفه می پذیرد و یک الگوی ایمیل پویا را برای کاربر ارسال می کند. همچنین می توانید با اجرای npm run email
در ترمینال خود ، طرح فاکتور را پیش نمایش کنید.
در مرحله بعد ، یک حساب RESEND ایجاد کنید و API Keys را از منوی نوار کناری در داشبورد خود انتخاب کنید تا یکی از آنها ایجاد شود.
کلید API را در پرونده .env.local کپی کنید.
بالاخره ، یک نقطه پایانی API ایجاد کنید که جزئیات فاکتور را از جلوی آن بپذیرد و فاکتور حاوی داده ها را به مشتری ارسال می کند.
import { NextRequest, NextResponse } from "next/server"; import EmailTemplate from "@/app/emails/email"; import { Resend } from "resend"; const resend = new Resend(process.env.RESEND_API_KEY!); export async function POST(req: NextRequest) { const { invoiceID, items, title, amount, customerEmail, issuerName, accountNumber, currency, } = await req.json(); try { const { data, error } = await resend.emails.send({ from: "Acme <onboarding@resend.dev>", to: [customerEmail], subject: title, react: EmailTemplate({ invoiceID, items: JSON.parse(items), amount: Number(amount), issuerName, accountNumber, currency, }) as React.ReactElement, }); if (error) { return Response.json( { message: "Email not sent!", error }, { status: 500 } ); } return NextResponse.json({ message: "Email delivered!" }, { status: 200 }); } catch (error) { return NextResponse.json( { message: "Email not sent!", error }, { status: 500 } ); } }
قطعه کد بالا جزئیات فاکتور را از جلوی آن می پذیرد ، داده های مورد نیاز را به الگوی ایمیل منتقل می کند و یک ایمیل را به کاربر ارسال می کند.
مراحل بعدی
تبریک می گویم. در حال حاضر ، شما باید درک خوبی از نحوه ساخت برنامه های تمام پشته با منشی ، ارسال مجدد ، Neon Postgres و Next.js. داشته باشید.
اگر می خواهید در مورد چگونگی استفاده از Neon Postgres برای ساخت برنامه های پیشرفته و مقیاس پذیر اطلاعات بیشتری کسب کنید ، می توانید منابع زیر را تحلیل کنید:
نحوه وارد کردن داده های خود از یک پایگاه داده Postgres به نئون
ممنون که خواندید
اگر این مقاله را مفید دیدید ، می توانید:
مرا در توییتر دنبال کنید که در مورد سفر و نوشتن سفر ، پروژه های جانبی و یادگیری های فعلی پست می کنم.
برای آموزش های بیشتر مانند این در مورد ابزارهای توسعه دهنده ، وبلاگ من را جستجو کنید.
ارسال نظر