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
10 changes: 10 additions & 0 deletions app/b/[billId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Suspense } from "react"
import { ProBillSplitter } from "@/components/ProBillSplitter"

export default function SharedBillPage() {
return (
<Suspense>
<ProBillSplitter />
</Suspense>
)
}
45 changes: 42 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,48 @@
"use client"

import { Suspense } from "react"
import { redirect } from "next/navigation"
import { ProBillSplitter } from "@/components/ProBillSplitter"
import { buildSharedBillPath, getSharedBillIdFromSearch, stripSharedBillParams } from "@/lib/sharing"

interface HomePageProps {
searchParams?: Promise<Record<string, string | string[] | undefined>>
}

function getFirstParamValue(value: string | string[] | undefined): string | null {
if (Array.isArray(value)) {
return value[0] ?? null
}

return value ?? null
}

export default async function HomePage({ searchParams }: HomePageProps) {
const resolvedSearchParams = (await searchParams) ?? {}
const legacyBillId =
getFirstParamValue(resolvedSearchParams.bill) ||
getFirstParamValue(resolvedSearchParams.share)

if (legacyBillId) {
const rawSearch = new URLSearchParams()

for (const [key, value] of Object.entries(resolvedSearchParams)) {
if (Array.isArray(value)) {
value.forEach((entry) => {
if (entry !== undefined) {
rawSearch.append(key, entry)
}
})
continue
}

if (value !== undefined) {
rawSearch.set(key, value)
}
}

const nextSearch = stripSharedBillParams(`?${rawSearch.toString()}`)
redirect(`${buildSharedBillPath(legacyBillId)}${nextSearch}`)
}

export default function HomePage() {
return (
<Suspense>
<ProBillSplitter />
Expand Down
22 changes: 18 additions & 4 deletions components/BillLookup.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"use client"

import React, { useState } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { Label } from "@/components/ui/label"
import { Search, Loader2 } from "lucide-react"
import { extractBillIdFromInput, getBillFromCloud } from "@/lib/sharing"
import { buildAppUrl, buildSharedBillPath, extractBillIdFromInput, getBillFromCloud } from "@/lib/sharing"
import { useBill } from "@/contexts/BillContext"
import { useToast } from "@/hooks/use-toast"
import { migrateBillSchema } from "@/lib/validation"
Expand All @@ -23,6 +24,8 @@ export function BillLookup({ mode = "auto" }: BillLookupProps) {
const { toast } = useToast()
const analytics = useBillAnalytics()
const isMobile = useIsMobile()
const router = useRouter()
const searchParams = useSearchParams()
const [billId, setBillId] = useState("")
const [isLoading, setIsLoading] = useState(false)
const [isOpen, setIsOpen] = useState(false)
Expand Down Expand Up @@ -81,7 +84,18 @@ export function BillLookup({ mode = "auto" }: BillLookupProps) {
if (result.bill) {
const migratedBill = migrateBillSchema(result.bill)

dispatch({ type: "LOAD_BILL", payload: migratedBill })
dispatch({
type: "LOAD_BILL",
payload: {
bill: migratedBill,
source: "shared",
sharedOriginBillId: trimmedId,
},
})
const nextParams = new URLSearchParams(searchParams.toString())
nextParams.delete("bill")
nextParams.delete("share")
router.push(buildAppUrl(buildSharedBillPath(trimmedId), nextParams.toString()), { scroll: false })
setBillId("")
setIsOpen(false)

Expand Down Expand Up @@ -132,7 +146,7 @@ export function BillLookup({ mode = "auto" }: BillLookupProps) {
<SheetHeader className="p-6 pb-4">
<SheetTitle>Load Bill by ID</SheetTitle>
<SheetDescription>
Enter a bill ID to load a shared bill. You can find the bill ID in the share URL.
Enter a bill ID to load a shared bill. You can paste the full share link too.
</SheetDescription>
</SheetHeader>
<div className="px-6 pb-6 space-y-4">
Expand All @@ -158,7 +172,7 @@ export function BillLookup({ mode = "auto" }: BillLookupProps) {
<p className="text-xs text-destructive">{error}</p>
)}
<p className="text-xs text-muted-foreground">
Example: If the URL is <code className="px-1 bg-muted rounded text-[10px]">?bill=1763442653885-vlpkbu4</code>,
Example: If the URL is <code className="px-1 bg-muted rounded text-[10px]">/b/1763442653885-vlpkbu4</code>,
enter <code className="px-1 bg-muted rounded text-[10px]">1763442653885-vlpkbu4</code>
</p>
</div>
Expand Down
42 changes: 42 additions & 0 deletions components/BillSourceIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client"

import { Copy, Link2 } from "lucide-react"
import { useBill } from "@/contexts/BillContext"
import { cn } from "@/lib/utils"

const billSourceConfig = {
shared: {
icon: Link2,
label: "Viewing shared bill",
className: "text-sky-700 bg-sky-50",
},
shared_copy: {
icon: Copy,
label: "Editing local copy",
className: "text-amber-700 bg-amber-50",
},
} as const

export function BillSourceIndicator({ className }: { className?: string }) {
const { state } = useBill()

if (state.billSource === "draft") {
return null
}

const config = billSourceConfig[state.billSource]
const Icon = config.icon

return (
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium",
config.className,
className
)}
>
<Icon className="h-3 w-3" />
<span>{config.label}</span>
</span>
)
}
27 changes: 22 additions & 5 deletions components/ProBillSplitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ import { cn, formatCurrencyWithCents as formatCurrencySimple } from '@/lib/utils
import { generateSummaryText, copyToClipboard } from '@/lib/export'
import { useToast } from '@/hooks/use-toast'
import { ShareBill } from '@/components/ShareBill'
import { BillSourceIndicator } from '@/components/BillSourceIndicator'
import { SyncStatusIndicator } from '@/components/SyncStatusIndicator'
import { useBillAnalytics } from '@/hooks/use-analytics'
import { TIMING } from '@/lib/constants'
import { extractBillIdFromInput, getBillFromCloud } from '@/lib/sharing'
import { buildAppUrl, buildSharedBillPath, extractBillIdFromInput, getBillFromCloud, stripSharedBillLocation } from '@/lib/sharing'
import { migrateBillSchema } from '@/lib/validation'
import { useIsMobile } from '@/hooks/use-mobile'
import { MobileSpreadsheetView } from '@/components/MobileSpreadsheetView'
Expand Down Expand Up @@ -512,6 +513,8 @@ function DesktopBillSplitter() {
}, [dispatch, toast])

const confirmNewBill = useCallback(() => {
const nextUrl = stripSharedBillLocation(pathname, searchParams.toString())
router.replace(nextUrl, { scroll: false })
dispatch({ type: 'NEW_BILL' })
toast({ title: "New bill created", variant: "success" })
analytics.trackBillCreated()
Expand All @@ -520,7 +523,7 @@ function DesktopBillSplitter() {
)
newBillSourceRef.current = "button"
setIsNewBillDialogOpen(false)
}, [dispatch, toast, analytics])
}, [analytics, dispatch, pathname, router, searchParams, toast])

const openDeleteDialog = useCallback((item: Item) => {
setPendingDeleteItem(item)
Expand Down Expand Up @@ -672,7 +675,18 @@ function DesktopBillSplitter() {
}

const migratedBill = migrateBillSchema(result.bill)
dispatch({ type: 'LOAD_BILL', payload: migratedBill })
dispatch({
type: 'LOAD_BILL',
payload: {
bill: migratedBill,
source: 'shared',
sharedOriginBillId: trimmedId,
},
})
const nextParams = new URLSearchParams(searchParams.toString())
nextParams.delete('bill')
nextParams.delete('share')
router.push(buildAppUrl(buildSharedBillPath(trimmedId), nextParams.toString()), { scroll: false })
toast({
title: "Bill loaded!",
description: `Loaded "${migratedBill.title}"`,
Expand All @@ -686,7 +700,7 @@ function DesktopBillSplitter() {
setIsLoadingBill(false)
}
}
}, [billId, dispatch, toast, analytics])
}, [billId, dispatch, router, searchParams, toast, analytics])

// --- Copy Breakdown ---
const copyBreakdown = useCallback(async () => {
Expand Down Expand Up @@ -1085,7 +1099,10 @@ function DesktopBillSplitter() {
name="bill-title"
autoComplete="off"
/>
<SyncStatusIndicator inline />
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs">
<SyncStatusIndicator inline />
<BillSourceIndicator />
</div>
</div>
</div>

Expand Down
Loading
Loading