Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions components/LedgerItemsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,11 @@ export function LedgerItemsTable() {
</p>
<div className="flex items-center justify-center gap-4 mt-3">
<div className="flex items-center gap-1">
<kbd className="px-2 py-1 bg-muted/50 text-muted-foreground rounded text-[10px] font-medium border border-border/50">N</kbd>
<kbd className="px-2 py-1 bg-muted/50 text-muted-foreground rounded text-[10px] font-medium border border-border/50">⌘⇧N</kbd>
<span className="text-[11px] text-muted-foreground">New item</span>
</div>
<div className="flex items-center gap-1">
<kbd className="px-2 py-1 bg-muted/50 text-muted-foreground rounded text-[10px] font-medium border border-border/50">P</kbd>
<kbd className="px-2 py-1 bg-muted/50 text-muted-foreground rounded text-[10px] font-medium border border-border/50">⌘⇧P</kbd>
<span className="text-[11px] text-muted-foreground">Add person</span>
</div>
</div>
Expand Down
14 changes: 9 additions & 5 deletions components/MobileSpreadsheetView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,20 @@ export function MobileSpreadsheetView() {

// Load saved preference
useEffect(() => {
const savedView = localStorage.getItem("mobile-view-mode")
if (savedView === "grid" || savedView === "cards") {
setViewMode(savedView)
if (typeof window !== 'undefined') {
const savedView = localStorage.getItem("mobile-view-mode")
if (savedView === "grid" || savedView === "cards") {
setViewMode(savedView)
}
}
}, [])

// Save preference when changed
useEffect(() => {
localStorage.setItem("mobile-view-mode", viewMode)
analytics.trackFeatureUsed(`mobile_view_${viewMode}`)
if (typeof window !== 'undefined') {
localStorage.setItem("mobile-view-mode", viewMode)
analytics.trackFeatureUsed(`mobile_view_${viewMode}`)
}
}, [viewMode, analytics])

// Receipt scanner handler
Expand Down
12 changes: 12 additions & 0 deletions components/PersonChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,25 @@ export const PersonChip = memo(function PersonChip({
}
}

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleClick()
}
}

const baseClasses = "flex items-center gap-1.5 cursor-pointer transition-all border"
const selectedClasses = "bg-primary text-primary-foreground hover:bg-primary/90"
const unselectedClasses = "bg-muted hover:bg-muted/80 text-muted-foreground border-dashed"

return (
<div
onClick={handleClick}
onKeyDown={onToggle ? handleKeyDown : undefined}
tabIndex={onToggle ? 0 : undefined}
role={onToggle ? "button" : undefined}
aria-pressed={onToggle ? isSelected : undefined}
aria-label={onToggle ? `${isSelected ? 'Unselect' : 'Select'} ${person.name}` : undefined}
className={cn(
baseClasses,
sizeClasses[size],
Expand Down
113 changes: 67 additions & 46 deletions components/ProBillSplitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,18 @@ const GridCell = React.memo(({
onCellClick: (row: number, col: string) => void
editInputRef: React.RefObject<HTMLInputElement | null>
}) => {
// Use text type with numeric inputMode for number fields (removes spinner arrows)
const isNumericField = field === 'price' || field === 'qty'
const inputType = 'text'
const inputMode = isNumericField ? 'decimal' : undefined

if (isEditing) {
return (
<div className="absolute inset-0 z-30">
<input
ref={editInputRef}
type={type}
type={inputType}
inputMode={inputMode}
value={value}
onChange={e => onCellEdit(itemId, field, e.target.value)}
onClick={(e) => e.stopPropagation()}
Expand Down Expand Up @@ -417,7 +423,6 @@ function DesktopBillSplitter() {

// Check if this request is still current
if (loadBillRequestRef.current !== requestId) {
console.log('Bill load cancelled - newer request started')
return
}

Expand Down Expand Up @@ -565,9 +570,23 @@ function DesktopBillSplitter() {

// --- Global Keyboard Shortcuts ---
const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
// Check if we're in an input field
// Check if we're in an input field - comprehensive check
const target = e.target as HTMLElement
const isInInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.contentEditable === 'true'
const activeElement = document.activeElement as HTMLElement

// Check if user is currently typing in any input-like element
const isInInput =
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'SELECT' ||
target.contentEditable === 'true' ||
target.isContentEditable ||
activeElement?.tagName === 'INPUT' ||
activeElement?.tagName === 'TEXTAREA' ||
activeElement?.tagName === 'SELECT' ||
activeElement?.contentEditable === 'true' ||
activeElement?.isContentEditable ||
(target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"]') !== null)

// Escape key - close modals, menus, and exit edit mode
if (e.key === 'Escape') {
Expand Down Expand Up @@ -605,6 +624,7 @@ function DesktopBillSplitter() {
return
}

// Cmd+N: New bill
if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
e.preventDefault()
if (confirm('Start a new bill? Current bill will be lost if not shared.')) {
Expand All @@ -616,41 +636,38 @@ function DesktopBillSplitter() {
return
}

// Shortcuts that don't work in inputs
if (!isInInput) {
// N: Add new item
if (e.key === 'n' || e.key === 'N') {
e.preventDefault()
addItem()
analytics.trackFeatureUsed("keyboard_shortcut_add_item")
return
}
// Cmd+Shift+N: Add new item
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'N') {
e.preventDefault()
addItem()
analytics.trackFeatureUsed("keyboard_shortcut_add_item")
return
}

// P: Add person
if (e.key === 'p' || e.key === 'P') {
e.preventDefault()
addPerson()
analytics.trackFeatureUsed("keyboard_shortcut_add_person")
return
}
// Cmd+Shift+P: Add person
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'P') {
e.preventDefault()
addPerson()
analytics.trackFeatureUsed("keyboard_shortcut_add_person")
return
}

// C: Copy summary
if (e.key === 'c' || e.key === 'C') {
e.preventDefault()
copyBreakdown()
analytics.trackFeatureUsed("keyboard_shortcut_copy")
return
}
// Cmd+Shift+C: Copy summary
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'C') {
e.preventDefault()
copyBreakdown()
analytics.trackFeatureUsed("keyboard_shortcut_copy")
return
}

// S: Share (trigger click on share button)
if (e.key === 's' || e.key === 'S') {
e.preventDefault()
// Find and click the share button
const shareButton = document.querySelector('[data-share-trigger]') as HTMLButtonElement
if (shareButton) shareButton.click()
analytics.trackFeatureUsed("keyboard_shortcut_share")
return
}
// Cmd+S: Share
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key === 's') {
e.preventDefault()
// Find and click the share button
const shareButton = document.querySelector('[data-share-trigger]') as HTMLButtonElement
if (shareButton) shareButton.click()
analytics.trackFeatureUsed("keyboard_shortcut_share")
return
}

// Grid navigation - Excel-like behavior
Expand Down Expand Up @@ -928,7 +945,7 @@ function DesktopBillSplitter() {
{people.length === 0 ? (
<div className="flex items-center gap-2 text-xs text-slate-400 font-inter">
<Users size={14} className="text-slate-300" />
<span>Click <kbd className="px-1.5 py-0.5 bg-slate-200 rounded text-[10px] font-bold text-slate-600">+</kbd> above to add people or press <kbd className="px-1.5 py-0.5 bg-slate-200 rounded text-[10px] font-bold text-slate-600">P</kbd></span>
<span>Click <kbd className="px-1.5 py-0.5 bg-slate-200 rounded text-[10px] font-bold text-slate-600">+</kbd> above to add people or press <kbd className="px-1.5 py-0.5 bg-slate-200 rounded text-[10px] font-bold text-slate-600">⌘⇧P</kbd></span>
</div>
) : (
people.map(p => {
Expand Down Expand Up @@ -996,7 +1013,7 @@ function DesktopBillSplitter() {
</div>
<h3 className="text-lg font-bold text-slate-900 mb-2 font-inter">No items yet</h3>
<p className="text-sm text-slate-500 mb-6 max-w-sm font-inter">
Add your first item to start splitting the bill. Press <kbd className="px-2 py-1 bg-slate-100 rounded text-xs font-bold">N</kbd> or click the button below.
Add your first item to start splitting the bill. Press <kbd className="px-2 py-1 bg-slate-100 rounded text-xs font-bold">⌘⇧N</kbd> or click the button below.
</p>
<button
onClick={addItem}
Expand Down Expand Up @@ -1176,7 +1193,7 @@ function DesktopBillSplitter() {
<button
onClick={addItem}
className="w-full py-3 text-slate-400 text-xs font-bold uppercase tracking-wider hover:text-indigo-600 hover:bg-indigo-50/30 transition-all flex items-center justify-center gap-2 border-t border-slate-200 font-inter"
title="Add new item (Press N)"
title="Add new item (Cmd+Shift+N)"
>
<Plus size={14} /> Add Line Item
</button>
Expand Down Expand Up @@ -1241,7 +1258,8 @@ function DesktopBillSplitter() {
</div>
<div className="w-28 border-r border-slate-100/60 flex items-center justify-end px-2">
<input
type="number"
type="text"
inputMode="decimal"
value={state.currentBill.tax}
onChange={(e) => {
dispatch({ type: 'SET_TAX', payload: e.target.value })
Expand Down Expand Up @@ -1276,7 +1294,8 @@ function DesktopBillSplitter() {
</div>
<div className="w-28 border-r border-slate-100/60 flex items-center justify-end px-2">
<input
type="number"
type="text"
inputMode="decimal"
value={state.currentBill.tip}
onChange={(e) => {
dispatch({ type: 'SET_TIP', payload: e.target.value })
Expand Down Expand Up @@ -1311,7 +1330,8 @@ function DesktopBillSplitter() {
</div>
<div className="w-28 border-r border-slate-100/60 flex items-center justify-end px-2">
<input
type="number"
type="text"
inputMode="decimal"
value={state.currentBill.discount}
onChange={(e) => {
dispatch({ type: 'SET_DISCOUNT', payload: e.target.value })
Expand Down Expand Up @@ -1533,7 +1553,7 @@ function DesktopBillSplitter() {
<button
onClick={copyBreakdown}
className="flex items-center gap-2 text-xs font-bold text-indigo-600 hover:text-indigo-700 bg-indigo-50 px-3 py-1.5 rounded-md hover:bg-indigo-100 transition-colors font-inter"
title="Copy summary to clipboard (Press C)"
title="Copy summary to clipboard (Cmd+Shift+C)"
>
<ClipboardCopy size={14} /> Copy
</button>
Expand All @@ -1542,9 +1562,10 @@ function DesktopBillSplitter() {
{/* Right Section: Keyboard Shortcuts + Creator */}
<div className="flex items-center gap-3">
<div className="text-[10px] text-slate-400 font-inter flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 bg-slate-100 rounded text-slate-600 font-bold">N</kbd>
<kbd className="px-1.5 py-0.5 bg-slate-100 rounded text-slate-600 font-bold">P</kbd>
<kbd className="px-1.5 py-0.5 bg-slate-100 rounded text-slate-600 font-bold">C</kbd>
<kbd className="px-1.5 py-0.5 bg-slate-100 rounded text-slate-600 font-bold">⌘⇧N</kbd>
<kbd className="px-1.5 py-0.5 bg-slate-100 rounded text-slate-600 font-bold">⌘⇧P</kbd>
<kbd className="px-1.5 py-0.5 bg-slate-100 rounded text-slate-600 font-bold">⌘⇧C</kbd>
<kbd className="px-1.5 py-0.5 bg-slate-100 rounded text-slate-600 font-bold">⌘S</kbd>
<kbd className="px-1.5 py-0.5 bg-slate-100 rounded text-slate-600 font-bold">⌘Z</kbd>
</div>
<div className="h-3 w-px bg-slate-200"></div>
Expand Down
15 changes: 14 additions & 1 deletion lib/__tests__/env-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,29 @@ describe('env-validation', () => {
console.log = originalConsoleLog
console.error = originalConsoleError
console.warn = originalConsoleWarn

// Reset environment variables
delete process.env.REDIS_URL
delete process.env.NEXT_PUBLIC_POSTHOG_KEY
delete process.env.NEXT_PUBLIC_POSTHOG_HOST
delete process.env.GOOGLE_GENERATIVE_AI_API_KEY
delete process.env.GEMINI_API_KEY
delete process.env.GOOGLE_API_KEY
delete process.env.OPENAI_API_KEY
delete process.env.ANTHROPIC_API_KEY
delete process.env.OCR_PROVIDER
Object.defineProperty(process.env, 'NODE_ENV', { value: 'test', writable: true })
})

describe('validateEnvironment', () => {
it('passes validation with all required variables', () => {
// Clear any OCR API keys that might be set in CI environment
delete process.env.GOOGLE_GENERATIVE_AI_API_KEY
delete process.env.GEMINI_API_KEY
delete process.env.GOOGLE_API_KEY
delete process.env.OPENAI_API_KEY
delete process.env.ANTHROPIC_API_KEY

process.env.REDIS_URL = 'redis://localhost:6379'
process.env.NEXT_PUBLIC_POSTHOG_KEY = 'test-key'
process.env.NEXT_PUBLIC_POSTHOG_HOST = 'https://app.posthog.com'
Expand Down
Loading
Loading