diff --git a/app/components/SearchInput/Input.tsx b/app/components/SearchInput/Input.tsx
index c313eeda..fd7a09e8 100644
--- a/app/components/SearchInput/Input.tsx
+++ b/app/components/SearchInput/Input.tsx
@@ -19,6 +19,7 @@ interface SearchInputProps {
}
export const SearchInput = ({onChange, expandable, placeholderText}: SearchInputProps) => {
const [search, setSearch] = useState('')
+
const handleSearch = (search: string) => {
setSearch(search)
if (onChange) {
diff --git a/app/components/search.tsx b/app/components/search.tsx
index c65daf31..1e403a6a 100644
--- a/app/components/search.tsx
+++ b/app/components/search.tsx
@@ -1,4 +1,5 @@
-import {useState} from 'react'
+import {useEffect, useState} from 'react'
+import {useSearchParams} from '@remix-run/react'
import debounce from 'lodash/debounce'
import {useSearch} from '~/hooks/useSearch'
import {SearchInput} from './SearchInput/Input'
@@ -12,10 +13,11 @@ type Props = {
}
export default function Search({limitFromUrl, className}: Props) {
+ const [searchParams] = useSearchParams()
const [showResults, setShowResults] = useState(false)
const [searchPhrase, setSearchPhrase] = useState('')
- const {search, isPendingSearch, results, clear} = useSearch(limitFromUrl)
+ const {search, isPendingSearch, results, clear, loadedQuestions} = useSearch(limitFromUrl)
const clickDetectorRef = useOutsideOnClick(() => setShowResults(false))
const searchFn = (rawValue: string) => {
@@ -31,6 +33,17 @@ export default function Search({limitFromUrl, className}: Props) {
}
}
+ useEffect(() => {
+ const query = searchParams.get('q')
+ if (loadedQuestions && query) {
+ searchFn(query)
+ setShowResults(true)
+ }
+ // Properly adding all dependancies would require transforming `searchFn` into a callback
+ // or something, which in turn would cause this `useEffect` to be called a lot more often.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [loadedQuestions, searchParams])
+
const handleChange = debounce(searchFn, 100)
return (
diff --git a/app/routes/editors.tsx b/app/routes/editors.tsx
index a227a699..b998b03d 100644
--- a/app/routes/editors.tsx
+++ b/app/routes/editors.tsx
@@ -4,32 +4,20 @@ import Page from '~/components/Page'
import Button from '~/components/Button'
import '~/components/Chatbot/widgit.css'
import {Question, QuestionStatus, loadAllQuestions} from '~/server-utils/stampy'
-import {downloadZip} from 'client-zip'
import {useLoaderData} from '@remix-run/react'
import {isAuthorized} from '~/routesMapper'
-
-const SINGLE_FILE_HTML = 'singleFileHtml'
-const SINGLE_FILE_MARKDOWN = 'singleFileMarkdown'
-const MULTI_FILE_HTML = 'multipleFilesMarkdown'
-const MULTI_FILE_MARKDOWN = 'multipleFileHtml'
-const SINGLE_FILE_JSON = 'singleFileJson'
-
-const downloadOptions = {
- [SINGLE_FILE_HTML]: 'Single HTML file',
- [MULTI_FILE_HTML]: 'Multiple HTML files',
- [SINGLE_FILE_MARKDOWN]: 'Single markdown file',
- [MULTI_FILE_MARKDOWN]: 'Multiple markdown files',
- [SINGLE_FILE_JSON]: 'As JSON',
-}
-
-const makeZipFile = async (questions: Question[], extention: string) =>
- downloadZip(
- questions.map((q) => ({
- name: `${q.title}.${extention}`,
- lastModified: q.updatedAt,
- input: (extention === 'md' ? q.markdown : q.text) || '',
- }))
- ).blob()
+import {
+ MULTI_FILE_HTML,
+ MULTI_FILE_MARKDOWN,
+ SINGLE_FILE_HTML,
+ SINGLE_FILE_JSON,
+ SINGLE_FILE_MARKDOWN,
+ downloadOptions,
+ htmlFile,
+ makeZipFile,
+ toHtmlChunk,
+ toMarkdownChunk,
+} from './questions.allQuestions'
const downloadBlob = async (filename: string, blob: Blob) => {
const link = document.createElement('a')
@@ -55,33 +43,16 @@ const makeFilename = (selectedOption: string) => {
}
}
-const toHtmlChunk = ({title, text}: Question) =>
- [
- '
',
- `
${title}
`,
- `
${text}
`,
- '
',
- ].join('\n')
-
-const toMarkdownChunk = ({title, markdown}: Question) => `# ${title}\n\n${markdown}`
-
-const htmlFile = (contents: string) => {
- const html = ['', '', ' ', contents, ' ', '/html>'].join(
- '\n'
- )
- return new Blob([html], {type: 'text/html'})
-}
-
const getData = async (questions: Question[], selectedOption: string) => {
switch (selectedOption) {
case SINGLE_FILE_HTML:
- return htmlFile(questions.map(toHtmlChunk).join('\n\n\n'))
+ return new Blob([htmlFile(questions.map(toHtmlChunk).join('\n\n\n'))], {type: 'text/html'})
case MULTI_FILE_HTML:
- return makeZipFile(questions, 'html')
+ return (await makeZipFile(questions, 'html')).blob()
case SINGLE_FILE_MARKDOWN:
return new Blob([questions.map(toMarkdownChunk).join('\n\n\n')], {type: 'text/markdown'})
case MULTI_FILE_MARKDOWN:
- return makeZipFile(questions, 'md')
+ return (await makeZipFile(questions, 'md')).blob()
case SINGLE_FILE_JSON:
return new Blob([JSON.stringify(questions, null, 2)], {type: 'application/json'})
}
@@ -89,9 +60,10 @@ const getData = async (questions: Question[], selectedOption: string) => {
type DownloadQuestionsProps = {
title: string
+ type: string
questions: Question[]
}
-const DownloadQuestions = ({title, questions}: DownloadQuestionsProps) => {
+const DownloadQuestions = ({title, type, questions}: DownloadQuestionsProps) => {
const [selectedOption, setSelectedOption] = useState(SINGLE_FILE_HTML)
const download = async () => {
@@ -109,11 +81,13 @@ const DownloadQuestions = ({title, questions}: DownloadQuestionsProps) => {
type="radio"
id={id}
checked={id === selectedOption}
- name="download-format"
+ name={`download-format-${type}`}
value={id}
onChange={(e) => setSelectedOption(e.target.id)}
/>
-
+
))}
@@ -146,13 +120,15 @@ export default function EditorHelpers() {
{(!questions || !questions.length) &&
Fetching questions...
}
-
+
q.status === QuestionStatus.LIVE_ON_SITE)}
/>
q.status !== QuestionStatus.LIVE_ON_SITE)}
/>
diff --git a/app/routes/questions.allQuestions.ts b/app/routes/questions.allQuestions.ts
index b3c0ae46..63e80abf 100644
--- a/app/routes/questions.allQuestions.ts
+++ b/app/routes/questions.allQuestions.ts
@@ -1,24 +1,106 @@
-import {LoaderFunctionArgs} from '@remix-run/cloudflare'
-import {reloadInBackgroundIfNeeded} from '~/server-utils/kv-cache'
-import {loadAllQuestions} from '~/server-utils/stampy'
+import {LoaderFunctionArgs, json} from '@remix-run/cloudflare'
+import {downloadZip} from 'client-zip'
+import '~/components/Chatbot/widgit.css'
+import {Question, QuestionStatus, loadAllQuestions} from '~/server-utils/stampy'
+import {isAuthorized} from '~/routesMapper'
+
+export const SINGLE_FILE_HTML = 'singleFileHtml'
+export const SINGLE_FILE_MARKDOWN = 'singleFileMarkdown'
+export const MULTI_FILE_HTML = 'multipleFilesMarkdown'
+export const MULTI_FILE_MARKDOWN = 'multipleFileHtml'
+export const SINGLE_FILE_JSON = 'singleFileJson'
+
+export const downloadOptions = {
+ [SINGLE_FILE_HTML]: 'Single HTML file',
+ [MULTI_FILE_HTML]: 'Multiple HTML files',
+ [SINGLE_FILE_MARKDOWN]: 'Single markdown file',
+ [MULTI_FILE_MARKDOWN]: 'Multiple markdown files',
+ [SINGLE_FILE_JSON]: 'As JSON',
+}
+
+export const makeZipFile = async (questions: Question[], extention: string) =>
+ downloadZip(
+ questions.map((q) => ({
+ name: `${q.title}.${extention}`,
+ lastModified: q.updatedAt,
+ input: (extention === 'md' ? q.markdown : q.text) || '',
+ }))
+ )
+
+export const toHtmlChunk = ({title, text}: Question) =>
+ [
+ ' ',
+ `
${title}
`,
+ `
${text}
`,
+ '
',
+ ].join('\n')
+
+export const toMarkdownChunk = ({title, markdown}: Question) => `# ${title}\n\n${markdown}`
+
+export const htmlFile = (contents: string) =>
+ ['', '', ' ', contents, ' ', '/html>'].join('\n')
+
+const getData = async (questions: Question[], selectedOption: string) => {
+ switch (selectedOption) {
+ case SINGLE_FILE_HTML:
+ return new Response(htmlFile(questions.map(toHtmlChunk).join('\n\n\n')), {
+ headers: {'Content-Type': 'text/html'},
+ })
+ case MULTI_FILE_HTML:
+ return makeZipFile(questions, 'html')
+ case SINGLE_FILE_MARKDOWN:
+ return new Response(questions.map(toMarkdownChunk).join('\n\n\n'), {
+ headers: {'Content-Type': 'text/markdown'},
+ })
+ case MULTI_FILE_MARKDOWN:
+ return makeZipFile(questions, 'md')
+ case SINGLE_FILE_JSON:
+ return json(questions)
+ default:
+ return json(
+ {
+ error:
+ 'Invalid dataType provided. Must be one of ' + Object.keys(downloadOptions).join(', '),
+ },
+ 400
+ )
+ }
+}
+
+export const headers = () => ({
+ 'WWW-Authenticate': 'Basic',
+})
+
+type QuestionsFilter = 'all' | 'live' | 'inProgress'
+const filteredQuestions = (questions: Question[], status: QuestionsFilter) => {
+ switch (status) {
+ case 'all':
+ return questions
+ case 'live':
+ return questions?.filter((q) => q.status === QuestionStatus.LIVE_ON_SITE)
+ case 'inProgress':
+ return questions?.filter((q) => q.status !== QuestionStatus.LIVE_ON_SITE)
+ default:
+ throw 'Invalid questions filter provided. Must be one of "all", "live" or "inProgress"'
+ }
+}
export const loader = async ({request}: LoaderFunctionArgs) => {
try {
- return await loadAllQuestions(request)
+ if (!isAuthorized(request)) {
+ return json({authorized: false, data: [] as Question[]}, {status: 401})
+ }
+
+ const url = new URL(request.url)
+ const params = new URLSearchParams(url.search)
+ const dataType = params.get('dataType') || SINGLE_FILE_JSON
+ const {data: allQuestions} = await loadAllQuestions(request)
+ return getData(
+ filteredQuestions(allQuestions, (params.get('questions') || 'all') as QuestionsFilter),
+ dataType
+ )
} catch (e) {
console.error(e)
- throw new Response('Could not fetch all articles', {status: 500})
+ throw new Response(`Could not fetch all articles: ${e}`, {status: 500})
}
}
-type Data = ReturnType
-
-export const fetchAllQuestions = async () => {
- const url = `/questions/allQuestions`
- const response = await fetch(url)
- const {data, timestamp}: Awaited = await response.json()
- const backgroundPromiseIfReloaded: Data | Promise = reloadInBackgroundIfNeeded(
- url,
- timestamp
- )
- return {data, backgroundPromiseIfReloaded}
-}
diff --git a/wrangler.toml.template b/wrangler.toml.template
index 2c0d98e5..932f49ce 100644
--- a/wrangler.toml.template
+++ b/wrangler.toml.template
@@ -25,4 +25,4 @@ MATOMO_DOMAIN = "{MATOMO_DOMAIN}"
DISCORD_LOGGING_CHANNEL_ID = "{DISCORD_LOGGING_CHANNEL_ID}"
DISCORD_LOGGING_TOKEN = "{DISCORD_LOGGING_TOKEN}"
EDITOR_USERNAME = "{EDITOR_USERNAME}"
-EDITOR_PASSWORD = "{EDITOR_PASSWORD}"
+EDITOR_PASSWORD = "{EDITOR_PASSWORD}"
\ No newline at end of file