Show invite link with copy functionality and QR-Code, add tests

This commit is contained in:
Maximilian Harz 2025-06-24 23:39:12 +02:00
parent 3ce71da9ff
commit 505ec10152
6 changed files with 178 additions and 10 deletions

20
lib/package-lock.json generated
View File

@ -37,6 +37,7 @@
"react-leaflet-cluster": "^2.1.0",
"react-markdown": "^9.0.1",
"react-photo-album": "^3.0.2",
"react-qr-code": "^2.0.16",
"react-router-dom": "^6.16.0",
"react-toastify": "^9.1.3",
"remark-breaks": "^4.0.0",
@ -11103,6 +11104,12 @@
"node": ">=6"
}
},
"node_modules/qr.js": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
"integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==",
"license": "MIT"
},
"node_modules/qs": {
"version": "6.13.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
@ -11319,6 +11326,19 @@
}
}
},
"node_modules/react-qr-code": {
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.16.tgz",
"integrity": "sha512-8f54aTOo7DxYr1LB47pMeclV5SL/zSbJxkXHIS2a+QnAIa4XDVIdmzYRC+CBCJeDLSCeFHn8gHtltwvwZGJD/w==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1",
"qr.js": "0.0.0"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",

View File

@ -125,6 +125,7 @@
"react-leaflet-cluster": "^2.1.0",
"react-markdown": "^9.0.1",
"react-photo-album": "^3.0.2",
"react-qr-code": "^2.0.16",
"react-router-dom": "^6.16.0",
"react-toastify": "^9.1.3",
"remark-breaks": "^4.0.0",

View File

@ -47,6 +47,7 @@ export default [
/node_modules\/tiptap-markdown/,
/node_modules\/markdown-it-task-lists/,
/node_modules\/classnames/,
/node_modules\/react-qr-code/,
],
requireReturnsDefault: 'auto',
}),

View File

@ -0,0 +1,75 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { InviteLinkView } from './InviteLinkView'
import type { Item } from '#types/Item'
const itemWithSecret: Item = {
secrets: [
{
secret: 'secret1',
},
],
id: '1',
name: 'Test Item',
}
const itemWithoutSecret: Item = {
secrets: [],
id: '2',
name: 'Test Item Without Secret',
}
const itemWithUndefinedSecrets: Item = {
id: '3',
name: 'Test Item With Undefined Secrets',
}
describe('<InviteLinkView />', () => {
let wrapper: ReturnType<typeof render>
const Wrapper = ({ item }: { item: Item }) => {
return render(<InviteLinkView item={item} />)
}
describe('when item does not have secrets', () => {
it('does not render anything', () => {
wrapper = Wrapper({ item: itemWithoutSecret })
expect(wrapper.container.firstChild).toBeNull()
})
})
describe('when item has secrets undefined', () => {
it('does not render anything', () => {
wrapper = Wrapper({ item: itemWithUndefinedSecrets })
expect(wrapper.container.firstChild).toBeNull()
})
})
describe('when item has secrets', () => {
beforeEach(() => {
wrapper = Wrapper({ item: itemWithSecret })
})
it('renders the secret', () => {
expect(wrapper.getByDisplayValue('secret1', { exact: false })).toBeInTheDocument()
})
it('matches the snapshot', () => {
expect(wrapper.container.firstChild).toMatchSnapshot()
})
it('copies the secret to clipboard when button is clicked', () => {
const copyButton = wrapper.getByRole('button')
expect(copyButton).toBeInTheDocument()
const clipboardSpy = vi.spyOn(navigator.clipboard, 'writeText')
fireEvent.click(copyButton)
// TODO Implement in a way that the URL stays consistent on CI
expect(clipboardSpy).toHaveBeenCalledWith('http://localhost:3000/invite/secret1')
})
})
})

View File

@ -1,28 +1,37 @@
import { TextView } from '#components/Map/Subcomponents/ItemPopupComponents'
import { ClipboardIcon } from '@heroicons/react/24/outline'
import QRCode from 'react-qr-code'
import { toast } from 'react-toastify'
import type { Item } from '#types/Item'
export const InviteLinkView = ({ item }: { item: Item }) => {
// TODO Only show if user has permission to view secrets.
// usePermission() seems to be useful.
// Only show if user has permission to view secrets.
if (!item.secrets || item.secrets.length === 0) return
if (!item.secrets || item.secrets.length === 0) {
console.log('No secrets found for item', item.id)
// Generate a new secret if none exists?
return
}
const link = `${window.location.origin}/invite/${item.secrets[0].secret}`
const copyToClipboard = () => {
void navigator.clipboard
.writeText(link)
.then(() => toast.success('Invite link copied to clipboard!'))
}
return (
<div className='tw:my-10 tw:mt-2 tw:px-6'>
<h2 className='tw:text-lg tw:font-semibold'>Invite</h2>
<div className='tw:mt-2 tw:text-sm'>
<div className='tw:mt-2 tw:text-sm tw:flex tw:gap-2 tw:mb-2'>
<input
type='text'
value={link}
readOnly
className='tw:w-full tw:mb-2 tw:p-2 tw:border tw:rounded'
className='tw:w-full tw:p-2 tw:border tw:rounded'
/>
<button onClick={copyToClipboard} className='btn btn-circle btn-primary'>
<ClipboardIcon className='w-6 h-6' />
</button>
</div>
<div className='tw:bg-white tw:p-2 tw:w-fit'>
<QRCode value={link} size={128} />
</div>
</div>
)

File diff suppressed because one or more lines are too long