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",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.20.2",
|
"@tanstack/react-query": "^5.20.2",
|
||||||
|
"fnv-plus": "^1.3.1",
|
||||||
"million": "^3.0.3",
|
"million": "^3.0.3",
|
||||||
"minisearch": "^6.3.0",
|
"minisearch": "^6.3.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.0.1",
|
||||||
|
@ -23,6 +25,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redux-devtools/extension": "^3.3.0",
|
"@redux-devtools/extension": "^3.3.0",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
|
"@types/fnv-plus": "^1.3.2",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
@ -1042,6 +1045,12 @@
|
||||||
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
|
@ -2654,6 +2663,11 @@
|
||||||
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
|
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||||
|
@ -4340,6 +4354,14 @@
|
||||||
"node": ">=6"
|
"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": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
|
|
@ -11,9 +11,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.20.2",
|
"@tanstack/react-query": "^5.20.2",
|
||||||
|
"fnv-plus": "^1.3.1",
|
||||||
"million": "^3.0.3",
|
"million": "^3.0.3",
|
||||||
"minisearch": "^6.3.0",
|
"minisearch": "^6.3.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.0.1",
|
||||||
|
@ -25,6 +27,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redux-devtools/extension": "^3.3.0",
|
"@redux-devtools/extension": "^3.3.0",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
|
"@types/fnv-plus": "^1.3.2",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
|
|
@ -14,6 +14,7 @@ import generateHash from '@/app/utils/hash'
|
||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
import { loadDocument } from '@/app/db/loaders'
|
import { loadDocument } from '@/app/db/loaders'
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import QRCode from 'qrcode.react'
|
||||||
|
|
||||||
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] })
|
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] })
|
||||||
|
|
||||||
|
@ -48,15 +49,20 @@ const DocumentViewer = ({ slug }: Readonly<{ slug: string }>) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-4xl lg:max-w-6xl mx-auto'>
|
<div className='max-w-4xl lg:max-w-6xl mx-auto'>
|
||||||
|
<div>
|
||||||
|
<div className='float-left mr-8 mb-4 mt-4'>
|
||||||
|
<QRCode value={citation ?? `eeXiv:${generateHash(slug)}`} />
|
||||||
|
</div>
|
||||||
<h1
|
<h1
|
||||||
className={`
|
className={`
|
||||||
text-slate-800 text-5xl mb-4
|
text-slate-800 text-5xl mb-4
|
||||||
${zillaSlab.className}
|
${zillaSlab.className}
|
||||||
text-wrap break-words hyphens-auto
|
text-wrap break-words hyphens-auto inline
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
</div>
|
||||||
<p className={`text-slate-800 mt-2`}>
|
<p className={`text-slate-800 mt-2`}>
|
||||||
<Authors authors={authors} />
|
<Authors authors={authors} />
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { loadAuthors } from '@/app/db/loaders'
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { epoch2date } from '@/app/utils/epoch2datestring'
|
import { epoch2date } from '@/app/utils/epoch2datestring'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
import generateHash from '@/app/utils/hash'
|
||||||
|
|
||||||
const VersionChooser = ({
|
const VersionChooser = ({
|
||||||
doc,
|
doc,
|
||||||
|
@ -26,7 +27,7 @@ const VersionChooser = ({
|
||||||
|
|
||||||
const fileEnding = file === 'other' ? '' : `.${file}`
|
const fileEnding = file === 'other' ? '' : `.${file}`
|
||||||
const [selectedRevision, setSelectedRevision] = useState<number>(latest) // Initialize the selected revision with the latest revision
|
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 handleClick = () => {
|
||||||
const bibtex = `@article{
|
const bibtex = `@article{
|
||||||
|
@ -45,7 +46,13 @@ const VersionChooser = ({
|
||||||
url={${window.location.href}}
|
url={${window.location.href}}
|
||||||
}`
|
}`
|
||||||
navigator.clipboard.writeText(bibtex)
|
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 (
|
return (
|
||||||
|
@ -75,6 +82,9 @@ const VersionChooser = ({
|
||||||
<button className='button-alternate' onClick={handleClick}>
|
<button className='button-alternate' onClick={handleClick}>
|
||||||
Export BibTeX
|
Export BibTeX
|
||||||
</button>
|
</button>
|
||||||
|
<button className='button-alternate' onClick={handleCopy}>
|
||||||
|
Copy citation
|
||||||
|
</button>
|
||||||
<select
|
<select
|
||||||
className='select-default'
|
className='select-default'
|
||||||
value={`v${selectedRevision}`}
|
value={`v${selectedRevision}`}
|
||||||
|
|
|
@ -7,6 +7,9 @@ import { epoch2datestring } from '../utils/epoch2datestring'
|
||||||
import searchDocs, { CustomSearchResult } from '@/app/utils/searchDocs'
|
import searchDocs, { CustomSearchResult } from '@/app/utils/searchDocs'
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import cardEffects from '@/app/styles/cardEffects.module.css'
|
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'] })
|
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500', '700'] })
|
||||||
|
|
||||||
|
@ -72,6 +75,30 @@ export default function Page() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const search = decodeURIComponent(searchParams.get('q') as string)
|
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({
|
const { data, error } = useSuspenseQuery({
|
||||||
queryKey: [search],
|
queryKey: [search],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
|
@ -93,7 +120,7 @@ export default function Page() {
|
||||||
{search}
|
{search}
|
||||||
{`"`}
|
{`"`}
|
||||||
</h1>
|
</h1>
|
||||||
{data.length === 0 ? (
|
{(data.length === 0 || invalid) ? (
|
||||||
<p className='text-lg px-2'>No results found.</p>
|
<p className='text-lg px-2'>No results found.</p>
|
||||||
) : (
|
) : (
|
||||||
data.map((result) => (
|
data.map((result) => (
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import crypto from 'crypto'
|
import { hash as fnv } from 'fnv-plus'
|
||||||
|
|
||||||
export default function hash(key: string) {
|
export default function hash(key: string) {
|
||||||
// git style hash
|
// fast non-cryptographic hash with error correction
|
||||||
return crypto.createHash('sha256').update(key).digest('hex').substring(0, 7)
|
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 { documents, authors, affiliations, topics } from '@/app/db/data'
|
||||||
import MiniSearch from 'minisearch'
|
import MiniSearch from 'minisearch'
|
||||||
import { CustomSearchResult } from './searchDocs'
|
import { CustomSearchResult } from './searchDocs'
|
||||||
import crypto from 'crypto'
|
|
||||||
import hash from './hash'
|
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 miniSearch = new MiniSearch({
|
||||||
const docs = Object.entries(documents).map(([key, value]) => ({
|
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,
|
id: key,
|
||||||
keywords: value.manifest.keywords.join(' '),
|
keywords: value.manifest.keywords.join(' '),
|
||||||
abstract: value.abstract,
|
abstract: value.abstract,
|
||||||
|
@ -36,37 +62,41 @@ const docs = Object.entries(documents).map(([key, value]) => ({
|
||||||
)
|
)
|
||||||
.join(' '),
|
.join(' '),
|
||||||
key: key,
|
key: key,
|
||||||
citation: value.citation ? value.citation : hash(key),
|
doi: value.doi ? value.doi : undefined
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
miniSearch.addAll(docs)
|
|
||||||
|
|
||||||
onmessage = (e: MessageEvent<string>) => {
|
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[])
|
postMessage(miniSearch.search(e.data) as CustomSearchResult[])
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue