Please connect to Internet to continue using hyperbooks. We are working on bringing offline support soon.
14 |12 | It seems like you are lost. Let's go back{' '} 13 | 14 | home 15 | 16 | . 17 |
18 |It’s free to get started. What are you waiting for?
15 |31 | Welcome back to your hyperbooks dashboard. 32 |
33 |35 | We use cookies and local storage for core functionality. Learn more. 36 |
37 |{count}
28 | {outstanding > 1 ?{children}
; 29 | } 30 | 31 | function A({ children, href, className, onClick }: Readonly<{ children: React.ReactNode; href: string; className?: string; onClick?: () => void }>) { 32 | return ( 33 |{count}
31 | {count > 1 ?19 | Your preferences will be saved locally. 20 |
21 |{template.title}
32 |33 | {template.description} 34 |
35 |You have reached the end of the list.
44 |Your have successfully subscribed to hyperbooks. Thank you for the support and enjoy the perks.
41 | > 42 | ) : ( 43 | <> 44 |Your subscription has been cancelled. You can re-subscribe anytime.
46 | > 47 | )} 48 |If you need any assistance contact us via,
49 |{row.index + 1}
, 18 | }, 19 | { 20 | accessorKey: 'description', 21 | header: 'Description', 22 | }, 23 | { 24 | accessorKey: 'category', 25 | header: 'Category', 26 | cell: ({ row }) => { 27 | const currentCategory = expenseCategories.find((cat) => cat.value === row.original.category); 28 | return{currentCategory?.label}
; 29 | }, 30 | }, 31 | { 32 | accessorKey: 'createdAt', 33 | header: 'Created At', 34 | cell: ({ row }) => { 35 | const date = row.getValue('createdAt') as Timestamp; 36 | const formattedDate = date.toDate(); 37 | const createdAt = new Date(formattedDate); 38 | return{createdAt.toLocaleString('en-US', { month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric' })}
; 39 | }, 40 | }, 41 | { 42 | accessorKey: 'amount', 43 | header: 'Amount', 44 | cell: ({ row }) => { 45 | const amount = row.getValue('amount') as number; 46 | returnLKR {amount.toFixed(2)}
; 47 | }, 48 | }, 49 | { 50 | id: 'actions', 51 | cell: ({ row }) => { 52 | return ( 53 |79 | {!hasMore && !loading && !billLoading && 'You have reached the end of the list.'} 80 |
81 |79 | {!hasMore && !loading && !invoiceLoading && 'You have reached the end of the list.'} 80 |
81 |{doc.description}
72 |{doc.formattedDate}
73 |Your recent bills will appear here.
82 |{doc.billedTo.name}
73 |{doc.formattedDate}
74 |Your recent invoices will appear here.
83 |
91 |
92 | 98 | {body} 99 |
100 | ); 101 | }); 102 | FormMessage.displayName = 'FormMessage'; 103 | 104 | export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField }; 105 | -------------------------------------------------------------------------------- /components/installer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import { Button } from './ui/button'; 5 | import { IconButton } from './ui/icon-button'; 6 | import { HardDriveDownloadIcon, Share2Icon } from 'lucide-react'; 7 | import { motion } from 'motion/react'; 8 | 9 | export function Installer() { 10 | const [deferredPrompt, setDeferredPrompt] = useState(null); 11 | const [showBanner, setShowBanner] = useState(false); 12 | const [isIOS, setIsIOS] = useState(false); 13 | const [isStandalone, setIsStandalone] = useState(false); 14 | 15 | useEffect(() => { 16 | // Check if already installed as PWA 17 | setIsStandalone(window.matchMedia('(display-mode: standalone)').matches); 18 | 19 | // Check if iOS 20 | // @ts-expect-error (window.MSStream is a property of the window object) 21 | const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 22 | setIsIOS(iOS); 23 | 24 | // Don't show if user opted out 25 | const neverShow = localStorage.getItem('hyperbooks-pwa-never-show'); 26 | if (neverShow === 'true' || isStandalone) return; 27 | 28 | // For non-iOS, listen for install prompt 29 | if (!iOS) { 30 | const handler = (e: any) => { 31 | e.preventDefault(); 32 | setDeferredPrompt(e); 33 | setShowBanner(true); 34 | }; 35 | 36 | window.addEventListener('beforeinstallprompt', handler); 37 | return () => window.removeEventListener('beforeinstallprompt', handler); 38 | } else { 39 | // For iOS, check if in Safari 40 | const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 41 | 42 | // Only show prompt in Safari and not in standalone mode 43 | // @ts-expect-error (navigator.standalone is a property of the navigator object) 44 | if (isSafari && !navigator.standalone) { 45 | // Delay showing the prompt to avoid immediate dismissal 46 | const timer = setTimeout(() => setShowBanner(true), 1000); 47 | return () => clearTimeout(timer); 48 | } 49 | } 50 | }, []); 51 | 52 | const handleInstallClick = async () => { 53 | if (isIOS) { 54 | // Can't programmatically trigger install on iOS, just show instructions 55 | alert('To install: tap the share icon in your browser and select "Add to Home Screen"'); 56 | } else if (deferredPrompt) { 57 | // @ts-expect-error (deferredPrompt is a property of the window object) 58 | deferredPrompt.prompt(); 59 | // @ts-expect-error (deferredPrompt is a property of the window object) 60 | const { outcome } = await deferredPrompt.userChoice; 61 | 62 | if (outcome === 'accepted') { 63 | setDeferredPrompt(null); 64 | setShowBanner(false); 65 | } 66 | } 67 | }; 68 | 69 | const handleDismiss = () => { 70 | setShowBanner(false); 71 | }; 72 | 73 | const handleDontAskAgain = () => { 74 | localStorage.setItem('hyperbooks-pwa-never-show', 'true'); 75 | setShowBanner(false); 76 | }; 77 | 78 | if (!showBanner) return null; 79 | 80 | return ( 81 |92 | {isIOS 93 | ? "Add hyperbooks to your home screen for quick access. Tap the share button and select 'Add to Home Screen'." 94 | : 'Install hyperbooks on your device for quick and easy access to your bookkeeping.'} 95 |
96 |{description}
29 |71 | Track finances effortlessly, access records anywhere, and keep data secure. Create invoices easily and gain AI-driven insights for smarter decisions. 72 |
73 |
96 |
97 |