Skip to content
Open
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
19 changes: 17 additions & 2 deletions messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,17 @@
"button": "Add by URL",
"title": "Add a group by URL",
"description": "If a group was shared with you, you can paste its URL here to add it to your list.",
"error": "Oops, we are not able to find the group from the URL you provided…"
"error": "Oops, we are not able to find the group from the URL you provided…",
"urlMode": "Enter URL",
"qrMode": "Scan QR",
"scanner": {
"startCamera": "Start Camera",
"stopScanning": "Stop Scanning",
"permissionDenied": "Camera permission denied. Please allow camera access to scan QR codes.",
"noCamera": "No camera found on this device.",
"cameraError": "Failed to start camera. Please try again.",
"permissionError": "Unable to access camera. Please check permissions."
}
},
"NotFound": {
"text": "This group does not exist.",
Expand Down Expand Up @@ -347,7 +357,12 @@
"title": "Share",
"description": "For other participants to see the group and add expenses, share its URL with them.",
"warning": "Warning!",
"warningHelp": "Every person with the group URL will be able to see and edit expenses. Share with caution!"
"warningHelp": "Every person with the group URL will be able to see and edit expenses. Share with caution!",
"qrCode": {
"title": "Share via QR Code",
"description": "Scan this QR code to open the group on any device.",
"download": "Download QR Code"
}
},
"SchemaErrors": {
"min1": "Enter at least one character.",
Expand Down
28 changes: 17 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"content-disposition": "^0.5.4",
"dayjs": "^1.11.10",
"embla-carousel-react": "^8.6.0",
"html5-qrcode": "^2.3.8",
"lucide-react": "^0.501.0",
"nanoid": "^5.0.4",
"negotiator": "^0.6.3",
Expand All @@ -57,6 +58,7 @@
"openai": "^4.25.0",
"pg": "^8.11.3",
"prisma": "^6.18.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-hook-form": "^7.68.0",
Expand Down
2 changes: 2 additions & 0 deletions src/app/groups/[groupId]/share-button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'
import { CopyButton } from '@/components/copy-button'
import { ShareQrCodeDialog } from '@/components/share-qr-code-dialog'
import { ShareUrlButton } from '@/components/share-url-button'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
Expand Down Expand Up @@ -39,6 +40,7 @@ export function ShareButton({ group }: Props) {
text={`Join my group ${group.name} on Spliit`}
url={url}
/>
<ShareQrCodeDialog url={url} groupName={group.name} />
</div>
)}
<p>
Expand Down
159 changes: 118 additions & 41 deletions src/app/groups/add-group-by-url-button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { saveRecentGroup } from '@/app/groups/recent-groups-helpers'
import { QrCodeScanner } from '@/components/qr-code-scanner'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Expand All @@ -8,7 +9,7 @@ import {
} from '@/components/ui/popover'
import { useMediaQuery } from '@/lib/hooks'
import { trpc } from '@/trpc/client'
import { Loader2, Plus } from 'lucide-react'
import { Loader2, Plus, QrCode, Link as LinkIcon } from 'lucide-react'
import { useTranslations } from 'next-intl'
import { useState } from 'react'

Expand All @@ -21,10 +22,52 @@ export function AddGroupByUrlButton({ reload }: Props) {
const isDesktop = useMediaQuery('(min-width: 640px)')
const [url, setUrl] = useState('')
const [error, setError] = useState(false)
const [scanError, setScanError] = useState('')
const [open, setOpen] = useState(false)
const [pending, setPending] = useState(false)
const [scanMode, setScanMode] = useState(false)
const utils = trpc.useUtils()

const processUrl = async (urlToProcess: string) => {
const [, groupId] =
urlToProcess.match(
new RegExp(`${window.location.origin}/groups/([^/]+)`),
) ??
urlToProcess.match(/\/groups\/([^/?]+)/) ?? // Also match relative URLs from QR
[]

if (!groupId) {
setError(true)
setScanError('')
setPending(false)
return
}

setPending(true)
try {
const { group } = await utils.groups.get.fetch({
groupId: groupId,
})
if (group) {
saveRecentGroup({ id: group.id, name: group.name })
reload()
setUrl('')
setOpen(false)
setScanMode(false)
setError(false)
setScanError('')
} else {
setError(true)
setScanError('')
}
} catch (err) {
setError(true)
setScanError('')
} finally {
setPending(false)
}
}

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
Expand All @@ -36,50 +79,84 @@ export function AddGroupByUrlButton({ reload }: Props) {
>
<h3 className="font-bold">{t('title')}</h3>
<p>{t('description')}</p>
<form
className="flex gap-2"
onSubmit={async (event) => {
event.preventDefault()
const [, groupId] =
url.match(
new RegExp(`${window.location.origin}/groups/([^/]+)`),
) ?? []
setPending(true)
const { group } = await utils.groups.get.fetch({
groupId: groupId,
})
if (group) {
saveRecentGroup({ id: group.id, name: group.name })
reload()
setUrl('')
setOpen(false)
} else {
setError(true)
setPending(false)
}
}}
>
<Input
type="url"
required
placeholder="https://spliit.app/..."
className="flex-1 text-base"
value={url}
disabled={pending}
onChange={(event) => {
setUrl(event.target.value)

{!isDesktop && (
<div className="flex gap-2 border-b pb-3 flex-wrap">
<Button
type="button"
variant={!scanMode ? 'default' : 'outline'}
size="sm"
className="flex-1 min-w-[120px]"
onClick={() => {
setScanMode(false)
setError(false)
setScanError('')
}}
>
<LinkIcon className="w-4 h-4 mr-2" />
{t('urlMode')}
</Button>
<Button
type="button"
variant={scanMode ? 'default' : 'outline'}
size="sm"
className="flex-1 min-w-[120px]"
onClick={() => {
setScanMode(true)
setError(false)
setScanError('')
}}
>
<QrCode className="w-4 h-4 mr-2" />
{t('qrMode')}
</Button>
</div>
)}

{!scanMode ? (
<form
className="flex gap-2 flex-wrap"
onSubmit={async (event) => {
event.preventDefault()
await processUrl(url)
}}
>
<Input
type="url"
required
placeholder="https://spliit.app/..."
className="flex-1 min-w-[200px] text-base"
value={url}
disabled={pending}
onChange={(event) => {
setUrl(event.target.value)
setError(false)
setScanError('')
}}
/>
<Button size="icon" type="submit" disabled={pending} className="flex-shrink-0">
{pending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Plus className="w-4 h-4" />
)}
</Button>
</form>
) : (
<QrCodeScanner
onScan={(scannedUrl) => {
processUrl(scannedUrl)
}}
onError={(errorMsg) => {
setScanError(errorMsg)
setError(false)
}}
onClose={() => setScanMode(false)}
/>
<Button size="icon" type="submit" disabled={pending}>
{pending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Plus className="w-4 h-4" />
)}
</Button>
</form>
)}

{error && <p className="text-destructive">{t('error')}</p>}
{scanError && <p className="text-destructive">{scanError}</p>}
</PopoverContent>
</Popover>
)
Expand Down
Loading