Skip to content

Commit 46d4d46

Browse files
committed
feat: add invoice pages with dynamic [id] routing
- Add invoice index page and detail page with [id] routing - Add invoice page component and data module
1 parent 952578c commit 46d4d46

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

src/app/invoice/[id]/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { InvoicePage } from '@/components/invoice/invoice-page'
2+
3+
export default function InvoiceDetailPage() {
4+
return <InvoicePage />
5+
}

src/app/invoice/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { InvoicePage as InvoicePageContent } from '@/components/invoice/invoice-page'
2+
3+
export default function InvoicePage() {
4+
return <InvoicePageContent />
5+
}

src/components/invoice/data.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export const invoiceData = {
2+
identifier: 'CB-2024-117',
3+
createdAt: 'Nov 03, 2024',
4+
dueDate: 'Nov 03, 2024',
5+
client: {
6+
name: 'ACME Product Team',
7+
firm: 'ACME Labs LLC',
8+
address: '47 River Road\nAustin, TX 78701',
9+
},
10+
totalPaid: '1,500.00',
11+
totalUnpaid: '2,680.00',
12+
total: '4,180.00',
13+
lineItems: [
14+
{
15+
category: 'Application Engineering',
16+
description: 'Responsive front-end architecture, UI refinement, deployment prep',
17+
rate: '110.00',
18+
hours: '16',
19+
total: '1,760.00',
20+
},
21+
{
22+
category: 'API Integration',
23+
description: 'Payment, CRM, and workflow automation integration support',
24+
rate: '120.00',
25+
hours: '11',
26+
total: '1,320.00',
27+
},
28+
{
29+
category: 'Consulting / Planning',
30+
description: 'Architecture review, backlog planning, and implementation guidance',
31+
rate: '110.00',
32+
hours: '10',
33+
total: '1,100.00',
34+
},
35+
],
36+
}
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
'use client'
2+
3+
import Image from 'next/image'
4+
import Link from 'next/link'
5+
import { useState, useEffect } from 'react'
6+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
7+
import {
8+
faClock,
9+
faCreditCard,
10+
faDollarSign,
11+
faDownload,
12+
faFileInvoiceDollar,
13+
faHome,
14+
faPrint,
15+
faXmark,
16+
} from '@fortawesome/free-solid-svg-icons'
17+
import { faBitcoin, faPaypal } from '@fortawesome/free-brands-svg-icons'
18+
import { invoiceData } from './data'
19+
import { cn } from '@/lib/cn'
20+
import { Breadcrumbs, ActionButton, Separator } from '@/components/breadcrumbs'
21+
22+
function PaymentModal({ open, onClose }: { open: boolean; onClose: () => void }) {
23+
const [paymentType, setPaymentType] = useState<'full' | 'partial'>('full')
24+
const [paymentMethod, setPaymentMethod] = useState<'credit_card' | 'paypal' | 'bitcoin'>('credit_card')
25+
const [partialAmount, setPartialAmount] = useState('0.00')
26+
27+
useEffect(() => {
28+
if (!open) {
29+
setPaymentType('full')
30+
setPaymentMethod('credit_card')
31+
setPartialAmount('0.00')
32+
}
33+
}, [open])
34+
35+
if (!open) return null
36+
37+
const activeChooser =
38+
'border-[#09afdf] shadow-[inset_0_0_0_1px_#09afdf] bg-[linear-gradient(180deg,rgba(9,175,223,0.08)_0%,rgba(9,175,223,0.03)_100%)]'
39+
40+
return (
41+
<div className="fixed inset-0 z-[120] flex items-center justify-center p-4">
42+
<button
43+
type="button"
44+
className="absolute inset-0 bg-black/[0.54] backdrop-blur-[2px]"
45+
onClick={onClose}
46+
aria-label="Close payment modal"
47+
/>
48+
<div className="border border-[#ececec] rounded bg-white shadow-[0_8px_24px_rgba(0,0,0,0.04)] relative z-10 max-h-[90vh] w-full max-w-3xl overflow-auto">
49+
<div className="flex items-center justify-between border-b border-[#ececec] px-6 py-4">
50+
<h2 className="flex items-center gap-3 text-[24px] font-normal text-[#2f2f2f]">
51+
<FontAwesomeIcon icon={faDollarSign} />
52+
<span>Invoice Payment</span>
53+
</h2>
54+
<button type="button" onClick={onClose} className="text-[#888] hover:text-[#111]">
55+
<FontAwesomeIcon icon={faXmark} className="text-xl" />
56+
</button>
57+
</div>
58+
59+
<div className="space-y-8 px-6 py-6 text-[#555]">
60+
<table className="w-full text-left text-[14px]">
61+
<thead>
62+
<tr className="border-b border-[#ececec] text-[#666]">
63+
<th className="py-2">Hrs</th>
64+
<th className="py-2">Item</th>
65+
<th className="py-2 text-right">Subtotal</th>
66+
</tr>
67+
</thead>
68+
<tbody>
69+
{invoiceData.lineItems.map((item) => (
70+
<tr key={item.category} className="border-b border-[#f2f2f2]">
71+
<td className="py-3">{item.hours}</td>
72+
<td className="py-3">{item.category}</td>
73+
<td className="py-3 text-right">${item.total}</td>
74+
</tr>
75+
))}
76+
</tbody>
77+
</table>
78+
79+
<section>
80+
<label className="mb-3 block text-[14px] font-medium text-[#444]">Payment Type</label>
81+
<div className="grid gap-3 md:grid-cols-2">
82+
<button
83+
type="button"
84+
onClick={() => setPaymentType('full')}
85+
className={cn(
86+
'rounded-[3px] border p-4 text-left',
87+
paymentType === 'full' ? activeChooser : 'border-[#e4e4e4]'
88+
)}
89+
>
90+
<div className="text-[15px] font-medium text-[#2f2f2f]">Full Payment</div>
91+
<div className="mt-1 text-[26px] font-light text-[#555]">${invoiceData.total} USD</div>
92+
</button>
93+
<button
94+
type="button"
95+
onClick={() => setPaymentType('partial')}
96+
className={cn(
97+
'rounded-[3px] border p-4 text-left',
98+
paymentType === 'partial' ? activeChooser : 'border-[#e4e4e4]'
99+
)}
100+
>
101+
<div className="text-[15px] font-medium text-[#2f2f2f]">Partial Payment</div>
102+
<div className="mt-3 flex items-center gap-2 text-[18px]">
103+
<span>$</span>
104+
<input
105+
value={partialAmount}
106+
onChange={(event) => setPartialAmount(event.target.value)}
107+
className="w-32 rounded-[3px] border border-[#d5d5d5] px-3 py-2 outline-none"
108+
/>
109+
</div>
110+
</button>
111+
</div>
112+
</section>
113+
114+
<section>
115+
<label className="mb-3 block text-[14px] font-medium text-[#444]">Payment Method</label>
116+
<div className="grid gap-3 md:grid-cols-3">
117+
{[
118+
{ key: 'credit_card', label: 'Credit-Card', sublabel: 'Stripe.com', icon: faCreditCard, color: '#555' },
119+
{ key: 'paypal', label: 'PayPal', sublabel: 'PayPal.com', icon: faPaypal, color: '#179BD7' },
120+
{ key: 'bitcoin', label: 'Bitcoin', sublabel: 'Unique Address', icon: faBitcoin, color: '#F7931B' },
121+
].map((option) => (
122+
<button
123+
key={option.key}
124+
type="button"
125+
onClick={() => setPaymentMethod(option.key as typeof paymentMethod)}
126+
className={cn(
127+
'rounded-[3px] border p-4 text-left',
128+
paymentMethod === option.key ? activeChooser : 'border-[#e4e4e4]'
129+
)}
130+
>
131+
<div className="flex items-start gap-3">
132+
<FontAwesomeIcon icon={option.icon} className="mt-1 text-[30px]" style={{ color: option.color }} />
133+
<div>
134+
<div className="text-[15px] font-medium text-[#2f2f2f]">{option.label}</div>
135+
<div className="text-[13px] text-[#777]">{option.sublabel}</div>
136+
</div>
137+
</div>
138+
</button>
139+
))}
140+
</div>
141+
</section>
142+
143+
<section className="rounded-[3px] border border-[#ececec] bg-[#fafafa] p-5">
144+
{paymentMethod === 'credit_card' ? (
145+
<>
146+
<h3 className="text-[18px] font-normal text-[#666]">Credit Card</h3>
147+
<p className="mt-3 text-[13px] text-[#777]">Payment processed by Stripe.com.</p>
148+
<div className="mt-4 rounded-[3px] border border-[#dfdfdf] bg-white px-4 py-3 text-[14px] text-[#777]">
149+
Card element placeholder
150+
</div>
151+
</>
152+
) : null}
153+
154+
{paymentMethod === 'paypal' ? (
155+
<>
156+
<h3 className="text-[18px] font-normal text-[#666]">PayPal Checkout</h3>
157+
<p className="mt-3 text-[13px] text-[#777]">You will be redirected to PayPal.com for checkout.</p>
158+
</>
159+
) : null}
160+
161+
{paymentMethod === 'bitcoin' ? (
162+
<div className="grid gap-4 md:grid-cols-[140px_1fr] md:items-center">
163+
<div className="flex justify-center">
164+
<div className="flex h-[120px] w-[120px] items-center justify-center rounded-[3px] border border-[#d9d9d9] bg-white text-[12px] text-[#999]">
165+
QR Code
166+
</div>
167+
</div>
168+
<div>
169+
<h3 className="text-[18px] font-normal text-[#666]">Bitcoin Address</h3>
170+
<p className="mt-3 break-all rounded-[3px] border border-[#dfdfdf] bg-white px-4 py-3 text-[13px] text-[#777]">
171+
bc1qcodebuilderlegacyinvoicepaymentaddressmocked
172+
</p>
173+
</div>
174+
</div>
175+
) : null}
176+
177+
<div className="mt-6 flex items-center justify-between gap-4 text-[13px] text-[#777]">
178+
<div>Total:</div>
179+
<div className="text-[16px] font-semibold text-[#555]">
180+
${paymentType === 'full' ? invoiceData.total : partialAmount || '0.00'} USD
181+
</div>
182+
</div>
183+
</section>
184+
185+
<div className="flex items-center justify-end gap-3">
186+
<ActionButton onClick={onClose}>Close</ActionButton>
187+
<ActionButton variant="primary">Continue</ActionButton>
188+
</div>
189+
</div>
190+
</div>
191+
</div>
192+
)
193+
}
194+
195+
export function InvoicePage() {
196+
const [modalOpen, setModalOpen] = useState(false)
197+
198+
return (
199+
<div className="text-[#323232] bg-[#fff] pb-16">
200+
<Breadcrumbs
201+
items={[
202+
{ label: 'Home', href: '/', icon: <FontAwesomeIcon icon={faHome} className="text-[11px]" /> },
203+
{ label: `Invoice #${invoiceData.identifier}` },
204+
]}
205+
/>
206+
207+
<section className="mx-auto max-w-7xl px-6 pt-6 lg:px-8">
208+
<div className="border border-[#ececec] rounded bg-white shadow-[0_8px_24px_rgba(0,0,0,0.04)] p-6 lg:p-10">
209+
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
210+
<div>
211+
<h1 className="text-[34px] font-normal leading-[1.15] text-[#2a2a2a] flex items-center gap-3">
212+
<FontAwesomeIcon icon={faFileInvoiceDollar} />
213+
<span>Client Invoice</span>
214+
</h1>
215+
</div>
216+
<div className="flex flex-wrap gap-3 md:justify-end">
217+
<ActionButton>
218+
Download <FontAwesomeIcon icon={faDownload} />
219+
</ActionButton>
220+
<ActionButton>
221+
Print <FontAwesomeIcon icon={faPrint} />
222+
</ActionButton>
223+
</div>
224+
</div>
225+
226+
<Separator />
227+
228+
<div id="invoice-container" className="space-y-8">
229+
<div className="grid gap-8 md:grid-cols-[1fr_260px]">
230+
<div>
231+
<Image src="/images/mandala4_75.png" alt="CodeBuilder" width={75} height={75} />
232+
<address className="mt-4 not-italic text-[14px] leading-7 text-[#666]">
233+
<strong>CodeBuilder Inc.</strong>
234+
<br />
235+
1211 22nd Ave NE
236+
<br />
237+
Minneapolis, MN 55418
238+
<br />
239+
<abbr title="Phone">P:</abbr> (612) 567-2633
240+
<br />
241+
E-mail:{' '}
242+
<Link href="mailto:info@codebuilder.us" className="text-[#09afdf]">
243+
info@codebuilder.us
244+
</Link>
245+
</address>
246+
</div>
247+
<div className="text-left md:text-right">
248+
<p className="text-[14px] leading-7 text-[#666]">
249+
<strong>Invoice #{invoiceData.identifier}</strong>
250+
<br />
251+
Created At: {invoiceData.createdAt}
252+
<br />
253+
Due Date: {invoiceData.dueDate}
254+
</p>
255+
<h5 className="mt-4 text-[18px] font-normal text-[#444]">Bill To</h5>
256+
<p className="mt-2 whitespace-pre-line text-[14px] leading-7 text-[#666]">
257+
<span>{invoiceData.client.name}</span>
258+
<br />
259+
{invoiceData.client.firm}
260+
<br />
261+
{invoiceData.client.address}
262+
</p>
263+
</div>
264+
</div>
265+
266+
<div className="flex flex-col gap-2 text-[13px] text-[#666] md:flex-row md:items-center md:justify-between">
267+
<p>
268+
<strong>Comments:</strong> N/A.
269+
</p>
270+
<button type="button" className="inline-flex items-center gap-2 text-[#09afdf] hover:underline">
271+
<FontAwesomeIcon icon={faClock} />
272+
<span>View Timesheet</span>
273+
</button>
274+
</div>
275+
276+
<div className="overflow-x-auto">
277+
<table className="w-full min-w-[720px] border border-[#e5e5e5] text-left text-[14px]">
278+
<thead className="bg-[#fafafa] text-[#555]">
279+
<tr>
280+
<th className="border border-[#e5e5e5] px-4 py-3">Description</th>
281+
<th className="border border-[#e5e5e5] px-4 py-3">Price</th>
282+
<th className="border border-[#e5e5e5] px-4 py-3">Hours</th>
283+
<th className="border border-[#e5e5e5] px-4 py-3">Total</th>
284+
</tr>
285+
</thead>
286+
<tbody>
287+
{invoiceData.lineItems.map((item) => (
288+
<tr key={item.category}>
289+
<td className="border border-[#e5e5e5] px-4 py-4 text-[#666]">
290+
<div className="text-[#09afdf]">{item.category}</div>
291+
<small className="text-[12px] text-[#777]">{item.description}</small>
292+
</td>
293+
<td className="border border-[#e5e5e5] px-4 py-4">${item.rate}</td>
294+
<td className="border border-[#e5e5e5] px-4 py-4">{item.hours}</td>
295+
<td className="border border-[#e5e5e5] px-4 py-4">${item.total}</td>
296+
</tr>
297+
))}
298+
{[
299+
['Total Paid', `$${invoiceData.totalPaid}`],
300+
['Late Fees', '$0.00'],
301+
['Remaining Balance', `$${invoiceData.totalUnpaid}`],
302+
['Invoice Total', `$${invoiceData.total}`],
303+
].map(([label, value], index) => (
304+
<tr key={label}>
305+
<td colSpan={3} className="border border-[#e5e5e5] px-4 py-3 text-right font-medium text-[#555]">
306+
{label}
307+
</td>
308+
<td
309+
className={cn('border border-[#e5e5e5] px-4 py-3', index === 3 && 'font-semibold text-[#333]')}
310+
>
311+
{value}
312+
</td>
313+
</tr>
314+
))}
315+
</tbody>
316+
</table>
317+
</div>
318+
319+
<div className="flex flex-col gap-6 md:flex-row md:items-end md:justify-between">
320+
<p className="max-w-3xl text-[14px] leading-7 text-[#666]">
321+
If you have any questions concerning this invoice, please contact <strong>CodeBuilder, Inc.</strong>,
322+
tel: <strong>+1 (612) 567-2633</strong>, email: <strong>info@codebuilder.us</strong>
323+
</p>
324+
<div className="flex flex-col items-start gap-4 md:items-end">
325+
<Image
326+
src="/images/payment.png"
327+
alt="Payment methods"
328+
width={220}
329+
height={60}
330+
className="h-auto w-[220px]"
331+
/>
332+
<ActionButton onClick={() => setModalOpen(true)} variant="primary">
333+
Make A Payment <FontAwesomeIcon icon={faCreditCard} />
334+
</ActionButton>
335+
</div>
336+
</div>
337+
</div>
338+
</div>
339+
</section>
340+
341+
<PaymentModal open={modalOpen} onClose={() => setModalOpen(false)} />
342+
</div>
343+
)
344+
}

0 commit comments

Comments
 (0)