متن خبر

نحوه ساخت برنامه Invoice SaaS با Next.js، Resend، Clerk و Neon Postgres

نحوه ساخت برنامه Invoice SaaS با Next.js، Resend، Clerk و Neon Postgres

شناسهٔ خبر: 668953 -




در این آموزش یاد می گیرید که چگونه یک وب اپلیکیشن فاکتور بسازید که به کاربران امکان می دهد اطلاعات بانکی خود را اضافه کنند، فهرست ی از مشتریان را مدیریت کنند و فاکتورهایی را ایجاد و برای مشتریان ارسال کنند. همچنین می‌آموزید که چگونه اجزای React را به‌عنوان صورت‌حساب و قالب‌های ایمیل مستقیماً از برنامه به ایمیل مشتری چاپ و ارسال کنید.

این یک پروژه عالی خواهد بود که به شما کمک می‌کند یاد بگیرید چگونه برنامه‌های پشته کامل را کنار هم قرار دهید، و چگونه برنامه‌ای بسازید که در آن بک‌اند بتواند در زمان واقعی با فرانت‌اند ارتباط برقرار کند.

در حین ساخت برنامه، تجربه عملی کار با ابزارهای توسعه دهنده زیر را به دست خواهید آورد:

نئون : یک پایگاه داده Postgres که به ما امکان می دهد داده ها را به راحتی در برنامه ذخیره و بازیابی کنیم.

Clerk : یک سیستم احراز هویت کامل که تضمین می کند فقط کاربران تأیید شده می توانند اقدامات خاصی را در برنامه انجام دهند.

React-to-print : بسته ای که به ما امکان می دهد اجزای React را به صورت فایل PDF تبدیل و چاپ کنیم.

ارسال مجدد و واکنش ایمیل : برای ارسال فاکتورهای دیجیتالی با طراحی زیبا به طور مستقیم به ایمیل مشتریان.

این کد منبع است (به یاد داشته باشید که به آن ستاره بدهید ⭐).

فهرست مطالب

    چیست نئون؟

    ساخت برنامه فاکتور با Next.js

    نحوه احراز هویت کاربران با استفاده از Clerk

    چگونه نئون را به برنامه Next.js اضافه کنیم

    نحوه تنظیم درایور بدون سرور نئون با Drizzle ORM در Next.js

    ایجاد نقاط پایانی API برای برنامه

    نحوه چاپ و دانلود فاکتورها در Next.js

    نحوه ارسال فاکتورهای دیجیتال با ارسال مجدد و واکنش ایمیل

    مراحل بعدی

نئون چیست؟

Neon یک Postgres DB منبع باز، مقیاس پذیر و کارآمد است که محاسبات را از ذخیره سازی جدا می کند. این بدان معنی است که فرآیندهای محاسباتی پایگاه داده (پرس و جوها، تراکنش ها و غیره) توسط یک مجموعه از منابع (محاسبه) مدیریت می شوند، در حالی که خود داده ها در مجموعه جداگانه ای از منابع (ذخیره سازی) ذخیره می شوند.

این معماری امکان مقیاس پذیری و عملکرد بیشتر را فراهم می کند و نئون را به گزینه ای محکم برای برنامه های کاربردی وب مدرن تبدیل می کند.

نئون - پایگاه داده Postgres بدون سرور
نئون - پایگاه داده Postgres بدون سرور

ساخت برنامه فاکتور با Next.js

در این بخش، من شما را در ساخت صفحات مختلف برنامه صورتحساب با استفاده از Next.js راهنمایی می کنم. این برنامه به شش صفحه کلیدی تقسیم شده است که هر کدام هدف خاصی را دنبال می کنند:

صفحه اصلی : این صفحه فرود است. این یک نمای کلی از برنامه ارائه می دهد و کاربران را وارد برنامه می کند.

صفحه تنظیمات : در اینجا، کاربران می توانند اطلاعات بانکی خود را همانطور که در فاکتورها نمایش داده می شود، به روز کنند.

صفحه مشتریان : این صفحه به کاربران اجازه می دهد تا پایگاه مشتریان خود را مدیریت کنند و در صورت نیاز مشتریان را اضافه یا حذف کنند.

داشبورد : هسته برنامه که در آن کاربران می توانند فاکتورهای جدید ایجاد کنند. کاربران می توانند یک مشتری را انتخاب کنند، عنوان و شرح فاکتور را وارد کنند و فاکتور تولید کنند.

صفحه تاریخچه : این صفحه فاکتورهای اخیرا ایجاد شده را نمایش می دهد. این شامل پیوندهایی است که کاربران را قادر می‌سازد تا پیش‌نمایش هر فاکتور را مشاهده کنند و راهی سریع برای تحلیل تراکنش‌های گذشته ارائه دهند.

صفحه چاپ و ارسال فاکتور : این صفحه به کاربران امکان چاپ و ارسال فاکتورها را برای مشتریان می دهد.

قبل از ادامه، با اجرای قطعه کد زیر در ترمینال خود، یک پروژه TypeScript Next.js ایجاد کنید:

npx create-next-app invoice-app- with -neon

یک فایل 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&apos;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 را نصب کنید:

npm @clerk/nextjs را نصب کنید

یک فایل 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 .

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<Your_publishable_key>
CLERK_SECRET_KEY=<Your_secret_key>

Clerk راه های مختلفی برای خواندن داده های کاربر بر روی مشتری و سرور ارائه می دهد که برای شناسایی کاربران در برنامه ضروری است.

چگونه نئون را به برنامه Next.js اضافه کنیم

نئون از چندین چارچوب و کتابخانه پشتیبانی می کند و مستندات واضح و دقیقی را در مورد گفت ن نئون به آنها ارائه می دهد. درایور بدون سرور نئون به شما امکان می دهد در برنامه Next.js به Neon متصل شوید و با آن تعامل داشته باشید.

قبل از ادامه، اجازه دهید یک حساب نئون و پروژه ایجاد کنیم .

Neon-postgres-all-project-dashboard
Neon-postgres-all-project-dashboard

در داشبورد پروژه خود، یک رشته اتصال پایگاه داده را خواهید یافت. شما از این برای تعامل با پایگاه داده نئون خود استفاده خواهید کرد.

نئون-پروژه-داشبورد
نئون-پروژه-داشبورد

سپس بسته Neon Serverless را در پروژه Next.js نصب کنید:

npm @neondatabase /serverless را نصب کنید

رشته اتصال پایگاه داده خود را در فایل .env.local کپی کنید.

NEON_DATABASE_URL= "postgres://<user>:<password>@<endpoint_hostname>.neon.tech:<port>/<dbname>?sslmode=require"

یک پوشه 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 اجرا کنیم. این تابع نسخه پایگاه داده را برمی گرداند که سپس به کنسول وارد می شود.

{
نسخه: "PostgreSQL 16.3 در x86_64-pc-linux-gnu، کامپایل شده توسط gcc (Debian 10.2.1-6) 10.2.1 20210110، 64 بیتی"
}

تبریک – شما با موفقیت نئون را به برنامه Next.js خود اضافه کردید.

با این حال، تعامل با پایگاه داده نئون با نوشتن پرس و جوهای SQL به طور مستقیم می تواند نیاز به یادگیری اضافی داشته باشد یا پیچیدگی هایی را برای توسعه دهندگانی که با SQL آشنا نیستند، ایجاد کند. همچنین می تواند منجر به خطا یا مشکلات عملکرد هنگام انجام پرس و جوهای پیچیده شود.

به همین دلیل است که نئون از ORM های پایگاه داده مانند Drizzle ORM پشتیبانی می کند که یک رابط سطح بالاتر برای تعامل با پایگاه داده ارائه می دهد. Drizzle ORM شما را قادر می سازد تا توابع پرس و جو پیچیده بنویسید و با استفاده از TypeScript به راحتی با پایگاه داده تعامل داشته باشید.

نحوه تنظیم درایور بدون سرور نئون با Drizzle ORM در Next.js

Drizzle ORM به شما امکان می دهد داده ها را پرس و جو کنید و با استفاده از دستورات پرس و جوی TypeScript ساده، عملیات های مختلفی را روی پایگاه داده انجام دهید. این سبک وزن، نوع ساده و آسان برای استفاده است.

ابتدا باید Drizzle Kit و Drizzle ORM را نصب کنید.

Drizzle Kit به شما امکان می دهد طرحواره پایگاه داده و مهاجرت ها را مدیریت کنید.

npm من نم نم باران
کیت npm i -D

داخل پوشه db ، یک فایل actions.ts و schema.ts اضافه کنید:

سی دی دی بی
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 برای ایجاد مهاجرت پایگاه داده و ایجاد جداول باشد.

{
"اسکریپت ها" : {
"migrate" : "npx drizzle-kit generate -- dotenv_config_path='.env.local'" ,
"db-create" : "npx drizzle-kit push -- dotenv_config_path='.env.local'"
}
}

اکنون می توانید npm run db-create را اجرا کنید تا جداول پایگاه داده را به کنسول نئون فشار دهید.

نئون میز-داشبورد
نئون میز-داشبورد

ایجاد نقاط پایانی API برای برنامه

در قسمت قبل، توابع لازم برای تعامل با پایگاه داده را ایجاد کردید. در این بخش، نحوه ایجاد نقاط پایانی API برای هر عملیات پایگاه داده را یاد خواهید گرفت.

ابتدا یک پوشه api در پوشه برنامه Next.js ایجاد کنید. این شامل تمام مسیرهای API برای برنامه خواهد بود.

برنامه سی دی
mkdir api

یک پوشه bank-info حاوی route.ts در پوشه api اضافه کنید. این بدان معناست که مسیر API ( /api/bank-info ) به‌روزرسانی و واکشی اطلاعات بانکی کاربر را انجام می‌دهد.

cd api
mkdir bank-info && cd bank-info
route.ts را لمس کنید

قطعه کد زیر را در فایل /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 قابل دانلود تبدیل می کند.

ابتدا قطعه کد زیر را در ترمینال خود برای نصب بسته اجرا کنید:

npm install -save react-to-print

یک صفحه مشتری ایجاد کنید ( /invoice/[id].tsx ).

برای انجام این کار ، یک پوشه فاکتور حاوی یک زیر لایه [ID] را به فهرست برنامه بعدی اضافه کنید. در داخل پوشه [ID] ، یک پرونده page.tsx اضافه کنید. این صفحه تمام اطلاعات مربوط به فاکتور را نشان می دهد و به کاربران امکان می دهد تا فاکتورها را برای مشتریان چاپ ، بارگیری و ارسال کنند.

فاکتور-برنامه بارگیری-صفحه UI
فاکتور-برنامه بارگیری-صفحه UI

با کپی کردن قطعه کد زیر در پرونده 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 ، الگوهای ایمیل قابل استفاده مجدد و زیبا را ایجاد کنیم. هر دو بسته توسط شخص ایجاد می شوند و امکان ادغام صاف بین این دو سرویس را فراهم می کند.

هر دو بسته را با اجرای قطعه کد در زیر نصب کنید:

NPM نصب مجدد
NPM REACT -EMAIL -REACT -EMAIL/CONTEMENTS -E را نصب کنید

Recort Rect را با استفاده از اسکریپت زیر در پرونده Package.json خود پیکربندی کنید.

پرچم --dir دسترسی به ایمیل React را به الگوهای ایمیل واقع در پروژه می دهد. در این حالت ، الگوهای ایمیل در پوشه SRC/APP/ایمیل قرار دارند.

{
    "اسکریپت" : {
        "ایمیل" : "ایمیل dev -dir src/app/emails"
}
}

در مرحله بعد ، پوشه ایمیل حاوی الگوی ایمیل را برای ارسال به ایمیل مشتریان ایجاد کنید:

 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 برای ساخت برنامه های پیشرفته و مقیاس پذیر اطلاعات بیشتری کسب کنید ، می توانید منابع زیر را تحلیل کنید:

مستندات نئون

نئون عالی

پروژه های مثال نئون

نحوه ادغام نئون با Vercel

نحوه وارد کردن داده های خود از یک پایگاه داده Postgres به نئون

ممنون که خواندید

اگر این مقاله را مفید دیدید ، می توانید:

در خبرنامه من مشترک شوید.

مرا در توییتر دنبال کنید که در مورد سفر و نوشتن سفر ، پروژه های جانبی و یادگیری های فعلی پست می کنم.

برای آموزش های بیشتر مانند این در مورد ابزارهای توسعه دهنده ، وبلاگ من را جستجو کنید.

خبرکاو

ارسال نظر




تبليغات ايهنا تبليغات ايهنا

تمامی حقوق مادی و معنوی این سایت متعلق به خبرکاو است و استفاده از مطالب با ذکر منبع بلامانع است