Revamp documents
Include new citation hash function, search by citation hash, toasts, and QR codes
This commit is contained in:
parent
80fc777e1e
commit
ce107072a4
7 changed files with 147 additions and 47 deletions
22
package-lock.json
generated
22
package-lock.json
generated
|
@ -9,9 +9,11 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.20.2",
|
||||
"fnv-plus": "^1.3.1",
|
||||
"million": "^3.0.3",
|
||||
"minisearch": "^6.3.0",
|
||||
"next": "14.1.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^5.0.1",
|
||||
|
@ -23,6 +25,7 @@
|
|||
"devDependencies": {
|
||||
"@redux-devtools/extension": "^3.3.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/fnv-plus": "^1.3.2",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
|
@ -1042,6 +1045,12 @@
|
|||
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/fnv-plus": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/fnv-plus/-/fnv-plus-1.3.2.tgz",
|
||||
"integrity": "sha512-Bgr5yn2dph2q8HZKDS002Pob6vaRTRfhqN9E+TOhjKsJvnfZXULPR3ihH8dL5ZjgxbNhqhTn9hijpbAMPtKZzw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
|
@ -2654,6 +2663,11 @@
|
|||
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fnv-plus": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fnv-plus/-/fnv-plus-1.3.1.tgz",
|
||||
"integrity": "sha512-Gz1EvfOneuFfk4yG458dJ3TLJ7gV19q3OM/vVvvHf7eT02Hm1DleB4edsia6ahbKgAYxO9gvyQ1ioWZR+a00Yw=="
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
|
@ -4340,6 +4354,14 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode.react": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz",
|
||||
"integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
|
|
@ -11,9 +11,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.20.2",
|
||||
"fnv-plus": "^1.3.1",
|
||||
"million": "^3.0.3",
|
||||
"minisearch": "^6.3.0",
|
||||
"next": "14.1.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^5.0.1",
|
||||
|
@ -25,6 +27,7 @@
|
|||
"devDependencies": {
|
||||
"@redux-devtools/extension": "^3.3.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/fnv-plus": "^1.3.2",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
|
|
|
@ -14,6 +14,7 @@ import generateHash from '@/app/utils/hash'
|
|||
import { Suspense } from 'react'
|
||||
import { loadDocument } from '@/app/db/loaders'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import QRCode from 'qrcode.react'
|
||||
|
||||
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] })
|
||||
|
||||
|
@ -48,15 +49,20 @@ const DocumentViewer = ({ slug }: Readonly<{ slug: string }>) => {
|
|||
|
||||
return (
|
||||
<div className='max-w-4xl lg:max-w-6xl mx-auto'>
|
||||
<h1
|
||||
className={`
|
||||
text-slate-800 text-5xl mb-4
|
||||
${zillaSlab.className}
|
||||
text-wrap break-words hyphens-auto
|
||||
`}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<div>
|
||||
<div className='float-left mr-8 mb-4 mt-4'>
|
||||
<QRCode value={citation ?? `eeXiv:${generateHash(slug)}`} />
|
||||
</div>
|
||||
<h1
|
||||
className={`
|
||||
text-slate-800 text-5xl mb-4
|
||||
${zillaSlab.className}
|
||||
text-wrap break-words hyphens-auto inline
|
||||
`}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
<p className={`text-slate-800 mt-2`}>
|
||||
<Authors authors={authors} />
|
||||
</p>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { loadAuthors } from '@/app/db/loaders'
|
|||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { epoch2date } from '@/app/utils/epoch2datestring'
|
||||
import { toast } from 'react-toastify'
|
||||
import generateHash from '@/app/utils/hash'
|
||||
|
||||
const VersionChooser = ({
|
||||
doc,
|
||||
|
@ -26,7 +27,7 @@ const VersionChooser = ({
|
|||
|
||||
const fileEnding = file === 'other' ? '' : `.${file}`
|
||||
const [selectedRevision, setSelectedRevision] = useState<number>(latest) // Initialize the selected revision with the latest revision
|
||||
const notifyCopied = () => toast('BibTeX copied to clipboard!')
|
||||
const notifyCopied = (content: string) => toast.success(`${content} copied to clipboard!`)
|
||||
|
||||
const handleClick = () => {
|
||||
const bibtex = `@article{
|
||||
|
@ -45,7 +46,13 @@ const VersionChooser = ({
|
|||
url={${window.location.href}}
|
||||
}`
|
||||
navigator.clipboard.writeText(bibtex)
|
||||
notifyCopied()
|
||||
notifyCopied('BibTeX')
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
const id = doc.citation ? doc.citation : generateHash(slug)
|
||||
navigator.clipboard.writeText(`eeXiv:${id}`)
|
||||
notifyCopied('Citation')
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -75,6 +82,9 @@ const VersionChooser = ({
|
|||
<button className='button-alternate' onClick={handleClick}>
|
||||
Export BibTeX
|
||||
</button>
|
||||
<button className='button-alternate' onClick={handleCopy}>
|
||||
Copy citation
|
||||
</button>
|
||||
<select
|
||||
className='select-default'
|
||||
value={`v${selectedRevision}`}
|
||||
|
|
|
@ -7,6 +7,9 @@ import { epoch2datestring } from '../utils/epoch2datestring'
|
|||
import searchDocs, { CustomSearchResult } from '@/app/utils/searchDocs'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import cardEffects from '@/app/styles/cardEffects.module.css'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useEffect } from 'react'
|
||||
import { hash as fnv } from 'fnv-plus'
|
||||
|
||||
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500', '700'] })
|
||||
|
||||
|
@ -71,6 +74,30 @@ export default function Page() {
|
|||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const search = decodeURIComponent(searchParams.get('q') as string)
|
||||
|
||||
let invalid = false
|
||||
|
||||
if (search.toLowerCase().startsWith('eexiv')) {
|
||||
const id = search.slice(6)
|
||||
|
||||
if (id.length !== 12) {
|
||||
invalid = true
|
||||
} else {
|
||||
const base = id.slice(0, 10)
|
||||
const ending = id.slice(10, 12)
|
||||
const e_ending = fnv(base).str().substring(0, 2)
|
||||
|
||||
if (ending !== e_ending) {
|
||||
invalid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (invalid) {
|
||||
toast.error('Invalid eeXiv identifier')
|
||||
}
|
||||
}, [invalid])
|
||||
|
||||
const { data, error } = useSuspenseQuery({
|
||||
queryKey: [search],
|
||||
|
@ -93,7 +120,7 @@ export default function Page() {
|
|||
{search}
|
||||
{`"`}
|
||||
</h1>
|
||||
{data.length === 0 ? (
|
||||
{(data.length === 0 || invalid) ? (
|
||||
<p className='text-lg px-2'>No results found.</p>
|
||||
) : (
|
||||
data.map((result) => (
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import crypto from 'crypto'
|
||||
import { hash as fnv } from 'fnv-plus'
|
||||
|
||||
export default function hash(key: string) {
|
||||
// git style hash
|
||||
return crypto.createHash('sha256').update(key).digest('hex').substring(0, 7)
|
||||
// fast non-cryptographic hash with error correction
|
||||
const base = fnv(key).str().substring(0, 10)
|
||||
const ending = fnv(base).str().substring(0, 2)
|
||||
return `${base}${ending}`
|
||||
}
|
||||
|
|
|
@ -1,11 +1,37 @@
|
|||
import { documents, authors, affiliations, topics } from '@/app/db/data'
|
||||
import MiniSearch from 'minisearch'
|
||||
import { CustomSearchResult } from './searchDocs'
|
||||
import crypto from 'crypto'
|
||||
import hash from './hash'
|
||||
import { hash as fnv } from 'fnv-plus'
|
||||
import { Document } from '../db/data'
|
||||
|
||||
// converting the documents object into an array that can be index by minisearch
|
||||
const docs = Object.entries(documents).map(([key, value]) => ({
|
||||
const miniSearch = new MiniSearch({
|
||||
fields: [
|
||||
'abstract',
|
||||
'keywords',
|
||||
'topics',
|
||||
'authors',
|
||||
'title',
|
||||
'type',
|
||||
'affiliations',
|
||||
'citation',
|
||||
'doi',
|
||||
],
|
||||
storeFields: ['key', 'abstract', 'manifest'],
|
||||
searchOptions: {
|
||||
boost: {
|
||||
title: 3,
|
||||
keywords: 2,
|
||||
topics: 1,
|
||||
authors: 2,
|
||||
abstract: 0.3,
|
||||
},
|
||||
fuzzy: 0.2,
|
||||
prefix: true,
|
||||
},
|
||||
})
|
||||
|
||||
const serializeDoc = (key: string, value: Document) => ({
|
||||
id: key,
|
||||
keywords: value.manifest.keywords.join(' '),
|
||||
abstract: value.abstract,
|
||||
|
@ -36,37 +62,41 @@ const docs = Object.entries(documents).map(([key, value]) => ({
|
|||
)
|
||||
.join(' '),
|
||||
key: key,
|
||||
citation: value.citation ? value.citation : hash(key),
|
||||
doi: value.doi ? value.doi : undefined,
|
||||
}))
|
||||
|
||||
const miniSearch = new MiniSearch({
|
||||
fields: [
|
||||
'abstract',
|
||||
'keywords',
|
||||
'topics',
|
||||
'authors',
|
||||
'title',
|
||||
'type',
|
||||
'affiliations',
|
||||
'citation',
|
||||
'doi',
|
||||
],
|
||||
storeFields: ['key', 'abstract', 'manifest'],
|
||||
searchOptions: {
|
||||
boost: {
|
||||
title: 3,
|
||||
keywords: 2,
|
||||
topics: 1,
|
||||
authors: 2,
|
||||
abstract: 0.3,
|
||||
},
|
||||
fuzzy: 0.2,
|
||||
prefix: true,
|
||||
},
|
||||
doi: value.doi ? value.doi : undefined
|
||||
})
|
||||
miniSearch.addAll(docs)
|
||||
|
||||
onmessage = (e: MessageEvent<string>) => {
|
||||
// check if searching for eeXiv citation
|
||||
if (e.data.toLowerCase().startsWith('eexiv:')) {
|
||||
const id = e.data.slice(6)
|
||||
|
||||
if (id.length !== 12) {
|
||||
postMessage([])
|
||||
} else {
|
||||
const base = id.slice(0, 10)
|
||||
const ending = id.slice(10, 12)
|
||||
const e_ending = fnv(base).str().substring(0, 2)
|
||||
|
||||
if (ending !== e_ending) {
|
||||
postMessage([])
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(documents).forEach((key) => {
|
||||
if (documents[key].citation === id) {
|
||||
postMessage([serializeDoc(key, documents[key])])
|
||||
}
|
||||
|
||||
const hashed = hash(key)
|
||||
if (hashed === id) {
|
||||
postMessage([serializeDoc(key, documents[key])])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// converting the documents object into an array that can be index by minisearch
|
||||
const docs = Object.entries(documents).map(([key, value]) => (serializeDoc(key, value)))
|
||||
|
||||
miniSearch.addAll(docs)
|
||||
postMessage(miniSearch.search(e.data) as CustomSearchResult[])
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue