Skip to content

Commit

Permalink
Merge pull request #747 from StampyAI/sundry-fixes
Browse files Browse the repository at this point in the history
Sundry fixes
  • Loading branch information
mruwnik authored Jul 17, 2024
2 parents f3a5bff + dcb76f8 commit 387886b
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 68 deletions.
1 change: 1 addition & 0 deletions app/components/SearchInput/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
17 changes: 15 additions & 2 deletions app/components/search.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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) => {
Expand All @@ -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 (
Expand Down
72 changes: 24 additions & 48 deletions app/routes/editors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -55,43 +43,27 @@ const makeFilename = (selectedOption: string) => {
}
}

const toHtmlChunk = ({title, text}: Question) =>
[
' <div>',
` <h2>${title}</h2>`,
` <div>${text}</div>`,
' </div>',
].join('\n')

const toMarkdownChunk = ({title, markdown}: Question) => `# ${title}\n\n${markdown}`

const htmlFile = (contents: string) => {
const html = ['<!DOCTYPE html>', '<html>', ' <body>', contents, ' </body>', '/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'})
}
}

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 () => {
Expand All @@ -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)}
/>
<label htmlFor={id}>{label}</label>
<label htmlFor={id} onClick={() => setSelectedOption(id)}>
{label}
</label>
</div>
))}
<Button action={download}>Download</Button>
Expand Down Expand Up @@ -146,13 +120,15 @@ export default function EditorHelpers() {
<Page noFooter>
<div className="page-body full-height padding-top-32">
{(!questions || !questions.length) && <div>Fetching questions...</div>}
<DownloadQuestions title="Download all questions" questions={questions} />
<DownloadQuestions title="Download all questions" questions={questions} type="all" />
<DownloadQuestions
title="Download all published questions"
type="live"
questions={questions?.filter((q) => q.status === QuestionStatus.LIVE_ON_SITE)}
/>
<DownloadQuestions
title="Download all non published questions"
type="inProgress"
questions={questions?.filter((q) => q.status !== QuestionStatus.LIVE_ON_SITE)}
/>
</div>
Expand Down
116 changes: 99 additions & 17 deletions app/routes/questions.allQuestions.ts
Original file line number Diff line number Diff line change
@@ -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) =>
[
' <div>',
` <h2>${title}</h2>`,
` <div>${text}</div>`,
' </div>',
].join('\n')

export const toMarkdownChunk = ({title, markdown}: Question) => `# ${title}\n\n${markdown}`

export const htmlFile = (contents: string) =>
['<!DOCTYPE html>', '<html>', ' <body>', contents, ' </body>', '/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<typeof loader>

export const fetchAllQuestions = async () => {
const url = `/questions/allQuestions`
const response = await fetch(url)
const {data, timestamp}: Awaited<Data> = await response.json()
const backgroundPromiseIfReloaded: Data | Promise<void> = reloadInBackgroundIfNeeded(
url,
timestamp
)
return {data, backgroundPromiseIfReloaded}
}
2 changes: 1 addition & 1 deletion wrangler.toml.template
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

0 comments on commit 387886b

Please sign in to comment.