From 616d2212a382992b33e4b07e571336cd132faddf Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Sun, 6 Aug 2023 18:03:50 +0200 Subject: [PATCH] Add basic banner functionality --- app/hooks/useQuestionStateInUrl.ts | 12 +++---- app/root.css | 15 +++++++++ app/routes/questions/$question.tsx | 46 +++++++++++++++----------- app/server-utils/stampy.ts | 52 +++++++++++++++++++++++++++--- 4 files changed, 95 insertions(+), 30 deletions(-) diff --git a/app/hooks/useQuestionStateInUrl.ts b/app/hooks/useQuestionStateInUrl.ts index 55c25887..2bd7b5fc 100644 --- a/app/hooks/useQuestionStateInUrl.ts +++ b/app/hooks/useQuestionStateInUrl.ts @@ -1,14 +1,7 @@ import {useState, useRef, useEffect, useMemo, useCallback} from 'react' import type {MouseEvent} from 'react' import {useSearchParams, useTransition} from '@remix-run/react' -import { - Question, - QuestionState, - RelatedQuestions, - PageId, - QuestionStatus, - Glossary, -} from '~/server-utils/stampy' +import {Question, QuestionState, RelatedQuestions, PageId, Glossary} from '~/server-utils/stampy' import {fetchAllQuestionsOnSite} from '~/routes/questions/allQuestionsOnSite' import {fetchGlossary} from '~/routes/questions/glossary' import { @@ -32,6 +25,7 @@ function updateQuestionMap(question: Question, map: Map): Map< answerEditLink: null, relatedQuestions: [], tags: [], + banners: [], }) } return map @@ -94,6 +88,7 @@ export default function useQuestionStateInUrl(minLogo: boolean, initialQuestions relatedQuestions: [], questionState, tags: [], + banners: [], ...questionMap.get(pageid), })) }, [stateString, initialCollapsedState, questionMap]) @@ -255,6 +250,7 @@ export default function useQuestionStateInUrl(minLogo: boolean, initialQuestions answerEditLink: null, relatedQuestions: [], tags: [], + banners: [], } onLazyLoadQuestion(tmpQuestion) toggleQuestion(tmpQuestion, {moveToTop: true}) diff --git a/app/root.css b/app/root.css index f2cb0ebe..9496346c 100644 --- a/app/root.css +++ b/app/root.css @@ -411,6 +411,21 @@ tr:nth-child(even) { margin: 0; } +.answer .banner { + padding: 1px 10px 5px 10px; + margin: 10px; +} +.answer .banner h3 { + display: flex; + align-items: center; +} +.answer .banner h3 img { + width: 2em; +} +.answer .banner h3 .title { + padding-left: 10px; +} + .question-footer { display: grid; grid-template-columns: auto auto; diff --git a/app/routes/questions/$question.tsx b/app/routes/questions/$question.tsx index ece3b125..0edc0055 100644 --- a/app/routes/questions/$question.tsx +++ b/app/routes/questions/$question.tsx @@ -1,13 +1,8 @@ import type {LoaderArgs} from '@remix-run/cloudflare' -import { - GlossaryEntry, - loadQuestionDetail, - QuestionState, - QuestionStatus, -} from '~/server-utils/stampy' +import {GlossaryEntry, loadQuestionDetail, QuestionState} from '~/server-utils/stampy' import {useRef, useEffect, useState} from 'react' import AutoHeight from 'react-auto-height' -import type {Question, Glossary, PageId} from '~/server-utils/stampy' +import type {Question, Glossary, PageId, Banner as BannerType} from '~/server-utils/stampy' import type useQuestionStateInUrl from '~/hooks/useQuestionStateInUrl' import {Edit, Link as LinkIcon} from '~/components/icons-generated' import {Tags} from '~/routes/tags/$tag' @@ -34,6 +29,7 @@ export const loader = async ({request, params}: LoaderArgs) => { answerEditLink: null, relatedQuestions: [], tags: [], + banners: [], } return { error: error?.toString(), @@ -70,17 +66,7 @@ export function Question({ glossary: Glossary selectQuestion: (pageid: string, title: string) => void } & JSX.IntrinsicElements['div']) { - const { - pageid, - title: codaTitle, - status: codaStatus, - text, - answerEditLink, - questionState, - tags, - } = questionProps - const title = - codaStatus && codaStatus !== QuestionStatus.LIVE_ON_SITE ? `WIP - ${codaTitle}` : codaTitle + const {pageid, title, text, answerEditLink, questionState, tags, banners} = questionProps const isLoading = useRef(false) const isExpanded = questionState === QuestionState.OPEN @@ -133,6 +119,7 @@ export function Question({
+
{banners && banners.map(Banner)}
{isExpanded && ( <> @@ -166,6 +153,29 @@ export function Question({ ) } +const Banner = ({title, text, icon, backgroundColour, textColour}: BannerType) => { + return ( +
+

+ {icon?.name} + {title} +

+
+
+ ) +} + /* * Recursively go through the child nodes of the provided node, and replace all text nodes * with the result of calling `textProcessor(textNode)` diff --git a/app/server-utils/stampy.ts b/app/server-utils/stampy.ts index 688a5bd4..5fedad85 100644 --- a/app/server-utils/stampy.ts +++ b/app/server-utils/stampy.ts @@ -24,6 +24,13 @@ export enum QuestionStatus { LIVE_ON_SITE = 'Live on site', UNKNOWN = 'Unknown', } +export type Banner = { + title: string + text: string + icon: Record + textColour: string + backgroundColour: string +} export type GlossaryEntry = { term: string pageid: PageId @@ -49,6 +56,7 @@ export type Question = { relatedQuestions: RelatedQuestions questionState?: QuestionState tags: string[] + banners: Banner[] status?: QuestionStatus updatedAt?: string } @@ -93,6 +101,7 @@ type AnswersRow = CodaRowCommon & { 'Related Answers': '' | Entity[] 'Related IDs': '' | string[] Tags: '' | Entity[] + Banners: '' | Entity[] 'Rich Text': string } } @@ -112,7 +121,16 @@ type GlossaryRow = CodaRowCommon & { 'UI ID': string } } -type CodaRow = AnswersRow | TagsRow | GlossaryRow +type BannersRow = CodaRowCommon & { + values: { + Title: string + Icon: any[] + Text: string + 'Background colour': string + 'Text colour': string + } +} +type CodaRow = AnswersRow | TagsRow | GlossaryRow | BannersRow type CodaResponse = { items: CodaRow[] nextPageLink: string | null @@ -129,10 +147,12 @@ const INCOMING_QUESTIONS_TABLE = 'grid-S_6SYj6Tjm' // Incoming questions const TAGS_TABLE = 'grid-4uOTjz1Rkz' const WRITES_TABLE = 'table-eEhx2YPsBE' const GLOSSARY_TABLE = 'grid-_pSzs23jmw' +const BANNERS_TABLE = 'grid-3WgZ9_NkvO' const enc = encodeURIComponent const quote = (x: string) => encodeURIComponent(`"${x.replace(/"/g, '\\"')}"`) let allTags = {} as Record +let allBanners = {} as Record const sendToCoda = async ( url: string, @@ -238,14 +258,19 @@ const head = (item: string | string[]) => { } const extractText = (markdown: string) => head(markdown)?.replace(/^```|```$/g, '') const extractLink = (markdown: string) => markdown?.replace(/^.*\(|\)/g, '') +const extractJoined = (values: Entity[], mapper: Record) => + values + .map((e) => e.name) + .filter((name) => Object.prototype.hasOwnProperty.call(mapper, name)) + .map((name) => mapper[name]) + const convertToQuestion = ({name, values, updatedAt} = {} as AnswersRow): Question => ({ title: name, pageid: extractText(values['UI ID']), text: renderText(extractText(values['UI ID']), values['Rich Text']), answerEditLink: extractLink(values['Edit Answer']).replace(/\?.*$/, ''), - tags: ((values['Tags'] || []) as Entity[]) - .map((e) => e.name) - .filter((name) => Object.prototype.hasOwnProperty.call(allTags, name)), + tags: extractJoined(values['Tags'] || [], allTags).map((t) => t.name), + banners: extractJoined(values['Banners'] || [], allBanners), relatedQuestions: values['Related Answers'] && values['Related IDs'] ? values['Related Answers'].map(({name}, i) => ({ @@ -263,6 +288,10 @@ export const loadQuestionDetail = withCache('questionDetail', async (question: s const {data} = await loadTags('NEVER_RELOAD') allTags = Object.fromEntries(data.map((r) => [r.name, r])) as Record } + if (Object.keys(allBanners).length === 0) { + const {data} = await loadBanners('NEVER_RELOAD') + allBanners = data + } const rows = (await getCodaRows( QUESTION_DETAILS_TABLE, // ids are now alphanumerical, so not possible to detect id by regex match for \d @@ -299,6 +328,21 @@ export const loadGlossary = withCache('loadGlossary', async () => { ) }) +export const loadBanners = withCache('loadBanners', async (): Promise> => { + const rows = (await getCodaRows(BANNERS_TABLE)) as BannersRow[] + return Object.fromEntries( + rows + .map(({values}) => ({ + title: extractText(values.Title), + text: renderText('', values.Text) || '', + icon: values.Icon[0], + backgroundColour: extractText(values['Background colour']), + textColour: extractText(values['Text colour']), + })) + .map((item) => [item.title, item]) + ) +}) + export const loadOnSiteAnswers = withCache('onSiteAnswers', async () => { const rows = (await getCodaRows(ON_SITE_TABLE)) as AnswersRow[] return rows.map(convertToQuestion)