diff --git a/package-lock.json b/package-lock.json index dc3f7d1..962718f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,16 @@ "name": "eexiv-2", "version": "0.1.0", "dependencies": { + "minisearch": "^6.3.0", "next": "14.1.0", "react": "^18", "react-dom": "^18", + "react-icons": "^5.0.1", "react-toastify": "^10.0.4", "zustand": "^4.5.0" }, "devDependencies": { + "@redux-devtools/extension": "^3.3.0", "@types/file-saver": "^2.0.7", "@types/node": "^20", "@types/react": "^18", @@ -436,6 +439,19 @@ "node": ">=14" } }, + "node_modules/@redux-devtools/extension": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@redux-devtools/extension/-/extension-3.3.0.tgz", + "integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "immutable": "^4.3.4" + }, + "peerDependencies": { + "redux": "^3.1.0 || ^4.0.0 || ^5.0.0" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", @@ -2413,6 +2429,12 @@ "node": ">= 4" } }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3067,6 +3089,11 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minisearch": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz", + "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3725,6 +3752,14 @@ "react": "^18.2.0" } }, + "node_modules/react-icons": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", + "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -3764,6 +3799,13 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true, + "peer": true + }, "node_modules/reflect.getprototypeof": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz", diff --git a/package.json b/package.json index 9f3e518..77e4a3b 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,16 @@ "format": "prettier --write ." }, "dependencies": { + "minisearch": "^6.3.0", "next": "14.1.0", "react": "^18", "react-dom": "^18", + "react-icons": "^5.0.1", "react-toastify": "^10.0.4", "zustand": "^4.5.0" }, "devDependencies": { + "@redux-devtools/extension": "^3.3.0", "@types/file-saver": "^2.0.7", "@types/node": "^20", "@types/react": "^18", diff --git a/public/img/logos/fia.jpg b/public/img/logos/fia.jpg new file mode 100644 index 0000000..b0b843f Binary files /dev/null and b/public/img/logos/fia.jpg differ diff --git a/src/app/components/Badges.tsx b/src/app/components/Badges.tsx new file mode 100644 index 0000000..a347a87 --- /dev/null +++ b/src/app/components/Badges.tsx @@ -0,0 +1,65 @@ +import { DocumentStatus, DocumentType } from '@/app/db/data' + +export const Status = ({ + statusName, +}: Readonly<{ statusName: DocumentStatus }>) => { + let text = '' + let itemStyle: string = ''! + switch (statusName) { + case 'draft': + text = 'Draft' + itemStyle += 'badge-draft' + break + case 'published no review': + text = 'Published' + itemStyle += 'badge-published' + break + case 'reviewed': + text = 'Peer Reviewed' + itemStyle += 'badge-reviewed' + break + case 'under review': + text = 'Pending Review' + itemStyle = 'badge-under-review' + break + } + return {text} +} + +export const ItemBadge = ({ + itemName, +}: Readonly<{ itemName: DocumentType }>) => { + let text = '' + let itemStyle: string = ''! + switch (itemName) { + case 'report': + itemStyle = 'badge-report' + text = 'Report' + break + case 'presentation': + text = 'Presentation' + itemStyle = 'badge-presentation' + break + case 'white paper': + text = 'White Paper' + itemStyle = 'badge-white-paper' + break + case 'datasheet': + text = 'Datasheet' + itemStyle = 'badge-datasheet' + break + case 'dwm': + text = 'DWM' + itemStyle = 'badge-dwm' + break + case 'guide': + text = 'Guide' + itemStyle = 'badge-guide' + break + case 'other': + text = 'Other' + itemStyle = 'badge-other' + break + } + return {text} +} diff --git a/src/app/components/DataDisplay.tsx b/src/app/components/DataDisplay.tsx new file mode 100644 index 0000000..16a6868 --- /dev/null +++ b/src/app/components/DataDisplay.tsx @@ -0,0 +1,133 @@ +import { Fragment } from 'react' +import Link from 'next/link' +import { + topics as topicList, + authors as authorList, + reviewer, +} from '@/app/db/data' + +export const Code = ({ + code, + showTitle = true, +}: { + code: string[] | undefined + showTitle?: boolean +}) => { + if (code) { + return ( + <> + {showTitle ? Code: : null} + {code.map((c: string, i) => ( + + + {c} + + {i !== code.length - 1 ? ', ' : null} + + ))} + + ) + } +} + +export const References = ({ + references, + showTitle = true, +}: Readonly<{ references: string[] | undefined; showTitle?: boolean }>) => { + if (!references) return null + return ( + <> + {showTitle ? References: : null} + {references.map((r: string, i) => ( + + + {r} + + {i !== references.length - 1 ? ', ' : null} + + ))} + + ) +} + +export const Topics = ({ + topics, + showTitle = true, +}: Readonly<{ topics: string[]; showTitle?: boolean }>) => { + return ( + <> + {showTitle ? Topics: : null} + {topics.map((t: string, i) => ( + + + {topicList[t].name} + + {i !== topics.length - 1 ? ', ' : null} + + ))} + + ) +} + +export const Authors = ({ + authors, +}: Readonly<{ authors: string[]; noLink?: boolean }>) => { + return ( + <> + {authors.map((a: string, i) => ( + + + {authorList[a].name.first} {authorList[a].name.last} + + {i !== authors.length - 1 && authors.length > 2 ? ', ' : null} + {i === authors.length - 2 ? ' and ' : null} + + ))} + + ) +} + +export const Reviewers = ({ + reviewers, + showTitle = true, +}: Readonly<{ reviewers: reviewer[] | undefined; showTitle?: boolean }>) => { + if (!reviewers) return null + const ReviewerDisplay = ({ r }: Readonly<{ r: reviewer }>) => { + if (r.profile) { + return ( + <> + + {r.first} {r.last} + + + ) + } + if (r.url) { + return ( + <> + + {r.first} {r.last} + + + ) + } + return ( + + {r.first} {r.last} + + ) + } + + return ( + <> + {showTitle ? Reviewed by: : null} + {reviewers.map((r: reviewer, i) => ( + + + {i !== reviewers.length - 1 && reviewers.length > 2 ? ', ' : null} + {i === reviewers.length - 2 ? ' and ' : null} + + ))} + + ) +} diff --git a/src/app/components/MobileMenu.tsx b/src/app/components/MobileMenu.tsx new file mode 100644 index 0000000..443fa61 --- /dev/null +++ b/src/app/components/MobileMenu.tsx @@ -0,0 +1,61 @@ +'use client' +import { RxHamburgerMenu } from 'react-icons/rx' +import styles from './mobileMenu.module.css' +import { create } from 'zustand' +import SearchBar from '@/app/components/SearchBar' + +interface MobileMenuState { + isOpen: boolean + searchInput: string + setSearchInput: (newInput: string) => void + setIsOpen: (newState: boolean) => void +} + +const useMobileMenuState = create((set) => ({ + isOpen: false, + searchInput: '', + setSearchInput: (newInput: string) => set({ searchInput: newInput }), + setIsOpen: (newState: boolean) => set({ isOpen: newState }), +})) + +export default function MobileMenu() { + const isOpen = useMobileMenuState((state) => state.isOpen) + const setIsOpen = useMobileMenuState((state) => state.setIsOpen) + + const handleClick = () => { + if (isOpen) { + document.body.style.overflow = 'auto' + setIsOpen(false) + } else { + document.body.style.overflow = 'hidden' + setIsOpen(true) + } + } + + return ( +
+ +
+ + + +

+ We gratefully acknowledge support from our volunteer peer reviewers, + member institutions, and all{' '} + + open-source contributors + + . +

+
+
+ ) +} diff --git a/src/app/components/SearchBar.tsx b/src/app/components/SearchBar.tsx new file mode 100644 index 0000000..d1c4ccb --- /dev/null +++ b/src/app/components/SearchBar.tsx @@ -0,0 +1,55 @@ +'use client' +import { create } from 'zustand' +import { navigate } from '@/app/actions' + +interface SearchBarState { + searchInput: string + setSearchInput: (newInput: string) => void +} + +const useSearchBarStore = create((set) => ({ + searchInput: '', + setSearchInput: (newInput) => set({ searchInput: newInput }), +})) + +export default function SearchBar() { + const searchBarStore = useSearchBarStore((state) => state.searchInput) + const setSearchBarStore = useSearchBarStore((state) => state.setSearchInput) + + const handleClick = (event: React.MouseEvent) => { + event.preventDefault() + navigate(`/search?q=${searchBarStore.split(' ').join('+')}`) + } + + const handleInputChange = (e: React.ChangeEvent) => { + setSearchBarStore(e.target.value) + } + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + navigate(`/search?q=${searchBarStore.split(' ').join('+')}`) + } + } + + return ( +
+ + +
+ ) +} diff --git a/src/app/components/mobileMenu.module.css b/src/app/components/mobileMenu.module.css new file mode 100644 index 0000000..c0eaf24 --- /dev/null +++ b/src/app/components/mobileMenu.module.css @@ -0,0 +1,16 @@ +.menu { + @apply w-[100vw] h-full overflow-x-hidden left-[0] top-[235px] z-10 absolute bg-slate-200; + @apply transition-transform duration-300 ease-in-out; +} + +.search-bar { + @apply w-full flex justify-center z-10 px-4 py-2 bg-slate-300; +} + +.search-bar button { + @apply border-slate-600 text-slate-600 bg-slate-200; +} + +.menu-hidden { + @apply translate-y-[100vh] invisible; +} diff --git a/src/app/db/data.ts b/src/app/db/data.ts index 8f834a7..ab5b9bc 100644 --- a/src/app/db/data.ts +++ b/src/app/db/data.ts @@ -47,27 +47,26 @@ export type DocumentStatus = | 'under review' | 'reviewed' | 'published no review' -export interface DocumentDB { - [key: string]: { - manifest: { - title: string - authors: string[] - topics: string[] - dates: number[] - references?: string[] - code?: string[] - type: DocumentType - latest: number - keywords?: string[] - status: DocumentStatus - reviewers?: reviewer[] - } - abstract: string - file: FileType - citation?: string - } +export interface Document { + manifest: DocumentManifest + abstract: string + file: FileType + citation?: string } -export const documents: DocumentDB = { +export interface DocumentManifest { + title: string + authors: string[] + topics: string[] + dates: number[] + references?: string[] + code?: string[] + type: DocumentType + latest: number + keywords: string[] + status: DocumentStatus + reviewers?: reviewer[] +} +export const documents: { [key: string]: Document } = { 'day-5-principles': { manifest: { title: 'Day 5 Principles', @@ -486,6 +485,20 @@ export const authors: Authors = { nationality: ['chn', 'usa'], image: '/img/profiles/nluo.png', }, + jchan: { + name: { + first: 'Jason', + last: 'Chan', + }, + affiliation: [ + 'Chief Executive Officer@fia', + 'Intern@1280-eecs', + 'Student@srvhs', + ], + nationality: ['jpn', 'chn', 'usa'], + image: '/img/profiles/default.png', + website: 'https://futureinspireacademy.com', + }, } export interface Affiliations { @@ -636,6 +649,16 @@ Raid Zero's influence extends beyond the technical achievements in robotics comp [linebreak] Gonzaga University's School of Engineering and Applied Science stands out for its commitment to excellence, ethics, and the holistic development of its students. Graduates leave Gonzaga not only as skilled engineers but as compassionate leaders ready to make meaningful contributions to society.`, }, + fia: { + name: 'Future Inspire Academy', + short: 'FIA', + image: '/img/logos/fia.jpg', + description: ` + Future Inspire Academy (FIA) is a non-profit organization that strives to teach students how to apply their coding skills to game development in a fun and efficient way. Our mission is to create a platform to reward members who start their game development journey early. We give members all the resources to learn quickly and reward their efforts with points which can be used to upgrade their game jam prizes. Become a member today and reap all the benefits by joining our Discord Server! + [linebreak] + Our organization not only impacts our members from around the world but also our partners as we help promote their business and improve their products. Our new vision has been to help develop companies that would contribute to the future of game development and promote accessibility. Recently, we launched exclusive early access to Rosebud’s game maker platform for all of our members to try. In the future, we plan to host more exclusive events that revolve around our partners. +`, + }, } export interface Nationalities { @@ -686,4 +709,9 @@ export const nationalities: Nationalities = { demonym: 'Russian', flag: 'https://upload.wikimedia.org/wikipedia/en/thumb/f/f3/Flag_of_Russia.svg/2560px-Flag_of_Russia.svg.png', }, + jpn: { + name: 'Japan', + demonym: 'Japanese', + flag: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Flag_of_Japan.svg/1280px-Flag_of_Japan.svg.png', + }, } diff --git a/src/app/document/view/[slug]/page.tsx b/src/app/document/view/[slug]/page.tsx index f782d17..0c0f4c7 100644 --- a/src/app/document/view/[slug]/page.tsx +++ b/src/app/document/view/[slug]/page.tsx @@ -1,18 +1,19 @@ 'use client' import { Zilla_Slab } from 'next/font/google' -import { - DocumentType, - documents, - topics as topicList, - authors as authorList, - DocumentStatus, - reviewer, -} from '@/app/db/data' +import { DocumentType, documents, reviewer } from '@/app/db/data' import Link from 'next/link' import { notFound } from 'next/navigation' import { Fragment, useEffect } from 'react' import { epoch2datestring } from '@/app/utils/epoch2datestring' import { toast } from 'react-toastify' +import { ItemBadge, Status } from '@/app/components/Badges' +import { + Code, + References, + Topics, + Authors, + Reviewers, +} from '@/app/components/DataDisplay' const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] }) @@ -40,181 +41,12 @@ export default function Page({ useEffect(() => { if (status === 'reviewed' && !reviewers) { toast.warn( - `This document is marked reviewed, but the author + `This document is marked as peer reviewed, but the author forgot to add a list of reviewers.` ) } }, []) - const Topics = () => { - return ( - <> - Topics: - {topics.map((t: string, i) => ( - - - {topicList[t].name} - - {i !== topics.length - 1 ? ', ' : null} - - ))} - - ) - } - - const Code = () => { - if (code) { - return ( - <> - Code: - {code.map((c: string, i) => ( - - - {c} - - {i !== code.length - 1 ? ', ' : null} - - ))} - - ) - } - } - - const Authors = () => { - return ( - <> - {authors.map((a: string, i) => ( - - - {authorList[a].name.first} {authorList[a].name.last} - - {i !== authors.length - 1 && authors.length > 2 ? ', ' : null} - {i === authors.length - 2 ? ' and ' : null} - - ))} - - ) - } - - const References = () => { - if (!references) return null - return ( - <> - References: - {references.map((r: string, i) => ( - - - {r} - - {i !== references.length - 1 ? ', ' : null} - - ))} - - ) - } - - const Status = ({ statusName }: Readonly<{ statusName: DocumentStatus }>) => { - let text = '' - let itemStyle: string = ''! - switch (statusName) { - case 'draft': - text = 'Draft' - itemStyle += 'badge-draft' - break - case 'published no review': - text = 'Published' - itemStyle += 'badge-published' - break - case 'reviewed': - text = 'Peer Reviewed' - itemStyle += 'badge-reviewed' - break - case 'under review': - text = 'Pending Review' - itemStyle = 'badge-under-review' - break - } - return {text} - } - - const ItemBadge = ({ itemName }: Readonly<{ itemName: DocumentType }>) => { - let text = '' - let itemStyle: string = ''! - switch (itemName) { - case 'report': - itemStyle = 'badge-report' - text = 'Report' - break - case 'presentation': - text = 'Presentation' - itemStyle = 'badge-presentation' - break - case 'white paper': - text = 'White Paper' - itemStyle = 'badge-white-paper' - break - case 'datasheet': - text = 'Datasheet' - itemStyle = 'badge-datasheet' - break - case 'dwm': - text = 'DWM' - itemStyle = 'badge-dwm' - break - case 'guide': - text = 'Guide' - itemStyle = 'badge-guide' - break - case 'other': - text = 'Other' - itemStyle = 'badge-other' - break - } - return {text} - } - - const Reviewers = () => { - if (!reviewers) return null - const ReviewerDisplay = ({ r }: Readonly<{ r: reviewer }>) => { - if (r.profile) { - return ( - <> - - {r.first} {r.last} - - - ) - } - if (r.url) { - return ( - <> - - {r.first} {r.last} - - - ) - } - return ( - - {r.first} {r.last} - - ) - } - - return ( - <> - Reviewers: - {reviewers.map((r: reviewer, i) => ( - - - {i !== reviewers.length - 1 && reviewers.length > 2 ? ', ' : null} - {i === reviewers.length - 2 ? ' and ' : null} - - ))} - - ) - } - return (

- +

-

+

Latest revision published{' '} {epoch2datestring(dates[dates.length - 1])} @@ -246,16 +78,16 @@ export default function Page({ {abstract}

- +

- +

- +

- +

Cite as: diff --git a/src/app/globals.css b/src/app/globals.css index 358fc28..5b36aec 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -25,7 +25,7 @@ a:hover { } .badge-base { - @apply px-3 py-1.5 rounded inline-block w-fit mr-2 mt-4 text-slate-50 border-2; + @apply px-3 py-1.5 rounded inline-block w-fit mr-2 my-1 text-slate-50 border-2; } .badge-draft { diff --git a/src/app/help/accessibility/page.tsx b/src/app/help/accessibility/page.tsx index d7c1005..fd72b36 100644 --- a/src/app/help/accessibility/page.tsx +++ b/src/app/help/accessibility/page.tsx @@ -11,6 +11,18 @@ export default function Page() { pull request{' '} on GitHub.

+
+

+ It has also come to our attention that we may not be able to support + low-spec devices such as old phones, computers, or other devices with + little RAM. This is because we load the entire database of documents, + authors, topics, affiliations, and other data/metadata directly into + memory via JavaScript. As a result, the site may be slow or unusable on + low-spec devices and it can only get worse. If you would like to remedy + this issue, we again recommend you open a{' '} + pull request{' '} + and port our in memory database to an actual remote database. +

) } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6d02ea9..86e8a50 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,8 +3,9 @@ import { Inter, Zilla_Slab } from 'next/font/google' import './globals.css' import styles from './home.module.css' import Link from 'next/link' -import SearchBar from './searchBar/SearchBar' +import SearchBar from './components/SearchBar' import Container from './container/Container' +import MobileMenu from './components/MobileMenu' import { ToastContainer } from 'react-toastify' import 'react-toastify/dist/ReactToastify.css' @@ -63,6 +64,9 @@ export default function RootLayout({
+
+ +
{children} diff --git a/src/app/page.tsx b/src/app/page.tsx index 5d0b9eb..fcb9552 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -48,6 +48,9 @@ export default function Home() { case 'dwm': typeString = 'DWM' break + case 'guide': + typeString = 'guide' + break case 'other': typeString = 'document' break diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx new file mode 100644 index 0000000..59f6cd1 --- /dev/null +++ b/src/app/search/page.tsx @@ -0,0 +1,92 @@ +'use client' +import { useSearchParams } from 'next/navigation' +import { Zilla_Slab } from 'next/font/google' +import Link from 'next/link' +import { Topics, Authors } from '@/app/components/DataDisplay' +import { Status, ItemBadge } from '@/app/components/Badges' +import { epoch2datestring } from '../utils/epoch2datestring' +import searchDocs, { CustomSearchResult } from '@/app/utils/searchDocs' +import { useEffect } from 'react' +import { create } from 'zustand' +import { navigate } from '@/app/actions' + +const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500', '700'] }) + +interface SearchState { + results: CustomSearchResult[] + setResults: (newResults: CustomSearchResult[]) => void +} +const useSearchStore = create((set) => ({ + results: [], + setResults: (newResults: CustomSearchResult[]) => + set({ results: newResults }), +})) + +const SearchResult = ({ result }: { result: CustomSearchResult }) => { + const { manifest, abstract, id } = result + const { title, authors, topics, dates, status, type } = manifest + + const handleClick = (event: React.MouseEvent) => { + let target = event.target as HTMLElement + while (target != null) { + if (target.nodeName === 'A') { + return + } + target = target.parentNode as HTMLElement + } + navigate(`/document/view/${id}`) + } + + return ( +
+

{title}

+

+ +

+

+ Last updated on {epoch2datestring(dates[dates.length - 1])} +

+

+ +

+
+ +
+

Abstract

+

+ {abstract} +

+
+ ) +} + +export default function Page() { + const searchParams = useSearchParams() + const search = searchParams.get('q') as string + const searchStore = useSearchStore((state) => state.results) + const setSearchStore = useSearchStore((state) => state.setResults) + useEffect(() => { + setSearchStore(searchDocs(search)) + }, [search]) + + return ( +
+

+ Showing results for: + {`"`} + {search} + {`"`} +

+ {searchStore.length === 0 ? ( +

No results found.

+ ) : ( + searchStore.map((result) => ( + + )) + )} +
+ ) +} diff --git a/src/app/searchBar/SearchBar.tsx b/src/app/searchBar/SearchBar.tsx deleted file mode 100644 index 73a0572..0000000 --- a/src/app/searchBar/SearchBar.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client' -import { toast } from 'react-toastify' -export default function SearchBar() { - const handleClick = () => { - toast.warn('This feature is currently under active development!') - } - return ( -
- - -
- ) -} diff --git a/src/app/utils/navigate.ts b/src/app/utils/navigate.ts new file mode 100644 index 0000000..7045715 --- /dev/null +++ b/src/app/utils/navigate.ts @@ -0,0 +1,7 @@ +'use server' + +import { redirect } from 'next/navigation' + +export async function navigate(data: FormData) { + redirect(`/posts/${data.get('id')}`) +} diff --git a/src/app/utils/searchDocs.ts b/src/app/utils/searchDocs.ts new file mode 100644 index 0000000..bb5d571 --- /dev/null +++ b/src/app/utils/searchDocs.ts @@ -0,0 +1,41 @@ +import MiniSearch, { SearchResult } from 'minisearch' +import { documents, DocumentManifest } from '../db/data' + +const docs = Object.entries(documents).map(([key, value]) => ({ + id: key, + keywords: value.manifest.keywords.join(' '), + abstract: value.abstract, + topics: value.manifest.topics.join(' '), + authors: value.manifest.authors.join(' '), + title: value.manifest.title, + manifest: value.manifest, + type: value.manifest.type, +})) + +const miniSearch = new MiniSearch({ + fields: ['abstract', 'keywords', 'topics', 'authors', 'title', 'type'], + storeFields: ['key', 'abstract', 'manifest'], + searchOptions: { + boost: { + title: 2, + keywords: 2, + topics: 1, + authors: 2, + }, + fuzzy: 0.2, + prefix: true, + }, +}) +miniSearch.addAll(docs) + +export interface CustomSearchResult extends SearchResult { + manifest: DocumentManifest + abstract: string +} + +export default function searchDocs( + query: string, + limit = 10 +): CustomSearchResult[] { + return miniSearch.search(query).slice(0, limit) as CustomSearchResult[] +}