From 991ac031ece5dee6d1d6035be2a849c499e46b06 Mon Sep 17 00:00:00 2001 From: Nicholas Griffin Date: Sun, 25 Aug 2024 18:27:21 +0100 Subject: [PATCH] chore: minor changes and fixes --- .../homepage/search-result-item.tsx | 82 ++++++++++++++---- apps/web/app/components/providers/locale.tsx | 29 +++++++ apps/web/app/components/text/date.tsx | 21 +++++ apps/web/app/entry.client.tsx | 10 ++- apps/web/app/entry.server.tsx | 12 ++- apps/web/app/global.css | 23 +++++ apps/web/app/root.tsx | 2 + apps/web/app/routes/_index.tsx | 2 +- apps/web/package.json | 1 + .../public/assets/placeholders/150x150.png | Bin 0 -> 2300 bytes pnpm-lock.yaml | 9 ++ 11 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 apps/web/app/components/providers/locale.tsx create mode 100644 apps/web/app/components/text/date.tsx create mode 100644 apps/web/app/global.css create mode 100644 apps/web/public/assets/placeholders/150x150.png diff --git a/apps/web/app/components/homepage/search-result-item.tsx b/apps/web/app/components/homepage/search-result-item.tsx index 928e15c..e03d643 100644 --- a/apps/web/app/components/homepage/search-result-item.tsx +++ b/apps/web/app/components/homepage/search-result-item.tsx @@ -1,22 +1,20 @@ +import { Fragment } from 'react'; + import { Button } from '../ui/button'; import { Modal } from '../modal/base'; +import { RenderDate } from '../text/date'; -function formatDate(dateString: string): string { - const date = new Date(dateString); - const options: Intl.DateTimeFormatOptions = { - year: 'numeric', - month: 'long', - day: 'numeric', - }; - return date.toLocaleDateString(undefined, options); -} +import { Fragment } from 'react'; function stripHtmlTagsAndDecode(html: string): string { - // Strip HTML tags - const text = html.replace(/<\/?[^>]+(>|$)/g, ''); + // Replace paragraph tags with new lines + const textWithNewLines = html.replace(/<\/?p[^>]*>/g, '\n'); + + // Strip remaining HTML tags + const text = textWithNewLines.replace(/<\/?[^>]+(>|$)/g, ''); // Decode HTML entities - return text + const decodedText = text .replace(/&#(\d+);/g, (match, dec) => { return String.fromCharCode(dec); }) @@ -31,6 +29,49 @@ function stripHtmlTagsAndDecode(html: string): string { }; return entities[entity] || match; }); + + // Remove leading and trailing new lines and ensure no more than one consecutive new line + return decodedText.trim().replace(/\n+/g, '\n'); +} + +function renderTextWithNewLines(text: string, url: string): JSX.Element { + const urlRegex = /(https?:\/\/[^\s]+)/g; + const continueReadingRegex = /Continue reading\.\.\./g; + + return ( + <> + {text.split('\n').map((line, index) => ( + + {line + .split(urlRegex) + .map((part, i) => + urlRegex.test(part) ? ( + + {part} + + ) : ( + part + ) + ) + .map((part, i) => + continueReadingRegex.test(part as string) ? ( + + Continue reading... + + ) : ( + part + ) + )} +
+
+ ))} + + ); } export const SearchResultItem = ({ @@ -55,8 +96,8 @@ export const SearchResultItem = ({ )} Placeholder
@@ -64,7 +105,7 @@ export const SearchResultItem = ({ href={result.metadata.url} target="_blank" rel="noopener noreferrer" - className="text-lg font-bold" + className="text-lg font-bold title" > {result.metadata.title} @@ -76,18 +117,23 @@ export const SearchResultItem = ({ )} {result.metadata.published && ( - Published: {formatDate(result.metadata.published)} + Published:{' '} + )} {result.metadata.updated && ( - Updated: {formatDate(result.metadata.updated)} + Updated:{' '} + )}
{result.metadata.description && (

- {stripHtmlTagsAndDecode(result.metadata.description)} + {renderTextWithNewLines( + stripHtmlTagsAndDecode(result.metadata.description), + result.metadata.url + )}

)}
diff --git a/apps/web/app/components/providers/locale.tsx b/apps/web/app/components/providers/locale.tsx new file mode 100644 index 0000000..e2a8eb9 --- /dev/null +++ b/apps/web/app/components/providers/locale.tsx @@ -0,0 +1,29 @@ +import { createContext, ReactNode, useContext } from 'react'; + +type LocaleContext = { + locales: string[]; +}; + +type LocaleContextProviderProps = { + locales: string[]; + children: ReactNode; +}; + +const Context = createContext(null); + +export const LocaleContextProvider = ({ + locales, + children, +}: LocaleContextProviderProps) => { + const value = { locales }; + return {children}; +}; + +const throwIfNoProvider = () => { + throw new Error('Please wrap your application in a LocaleContextProvider.'); +}; + +export const useLocales = () => { + const { locales } = useContext(Context) ?? throwIfNoProvider(); + return locales; +}; diff --git a/apps/web/app/components/text/date.tsx b/apps/web/app/components/text/date.tsx new file mode 100644 index 0000000..f13f504 --- /dev/null +++ b/apps/web/app/components/text/date.tsx @@ -0,0 +1,21 @@ +import { useLocales } from '../providers/locale'; + +type IntlDateProps = { + date: string; + timeZone?: string; +}; + +export const RenderDate = ({ date, timeZone }: IntlDateProps) => { + const dateFromString = new Date(date); + + const locales = useLocales(); + const isoString = dateFromString.toISOString(); + const formattedDate = new Intl.DateTimeFormat(locales, { + year: 'numeric', + month: 'short', + day: 'numeric', + timeZone, + }).format(dateFromString); + + return ; +}; diff --git a/apps/web/app/entry.client.tsx b/apps/web/app/entry.client.tsx index 28fa925..49ca5b7 100644 --- a/apps/web/app/entry.client.tsx +++ b/apps/web/app/entry.client.tsx @@ -1,12 +1,18 @@ import { RemixBrowser } from '@remix-run/react'; -import { startTransition, StrictMode, useEffect } from 'react'; +import { startTransition, StrictMode } from 'react'; import { hydrateRoot } from 'react-dom/client'; +import { LocaleContextProvider } from './components/providers/locale'; + +const locales = window.navigator.languages; + startTransition(() => { hydrateRoot( document, - + + + ); }); diff --git a/apps/web/app/entry.server.tsx b/apps/web/app/entry.server.tsx index 547bab5..9c8968d 100644 --- a/apps/web/app/entry.server.tsx +++ b/apps/web/app/entry.server.tsx @@ -2,6 +2,9 @@ import type { EntryContext } from '@remix-run/cloudflare'; import { RemixServer } from '@remix-run/react'; import { isbot } from 'isbot'; import { renderToReadableStream } from 'react-dom/server'; +import { parseAcceptLanguage } from 'intl-parse-accept-language'; + +import { LocaleContextProvider } from './components/providers/locale'; export default async function handleRequest( request: Request, @@ -9,8 +12,15 @@ export default async function handleRequest( responseHeaders: Headers, remixContext: EntryContext ) { + const acceptLanguage = request.headers.get('accept-language'); + const locales = parseAcceptLanguage(acceptLanguage, { + validate: Intl.DateTimeFormat.supportedLocalesOf, + }); + const body = await renderToReadableStream( - , + + + , { signal: request.signal, onError(error: unknown) { diff --git a/apps/web/app/global.css b/apps/web/app/global.css new file mode 100644 index 0000000..54c1190 --- /dev/null +++ b/apps/web/app/global.css @@ -0,0 +1,23 @@ +a:not(.title) { + font-weight: 700; + -webkit-text-decoration: underline; + text-decoration: underline; + text-decoration-color: #207DE9; + text-decoration-thickness: 1px; + text-decoration-skip-ink: none; + text-underline-offset: 0.25em; +} + +a:not(.title):hover { + text-decoration-color: currentcolor; + text-decoration-thickness: 2px; + color: #207DE9; +} + +a.title:hover { + text-decoration-color: currentcolor; + text-decoration-thickness: 2px; + color: #207DE9; + -webkit-text-decoration: underline; + text-decoration: underline; +} \ No newline at end of file diff --git a/apps/web/app/root.tsx b/apps/web/app/root.tsx index ef450b2..c82634b 100644 --- a/apps/web/app/root.tsx +++ b/apps/web/app/root.tsx @@ -6,7 +6,9 @@ import { ScrollRestoration, useRouteError, } from '@remix-run/react'; + import './tailwind.css'; +import './global.css'; export function Layout({ children }: { children: React.ReactNode }) { return ( diff --git a/apps/web/app/routes/_index.tsx b/apps/web/app/routes/_index.tsx index e7a6738..e735f75 100644 --- a/apps/web/app/routes/_index.tsx +++ b/apps/web/app/routes/_index.tsx @@ -66,7 +66,7 @@ export default function Index() {
{isIntroVisible && } diff --git a/apps/web/package.json b/apps/web/package.json index b511914..dfb65ff 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,6 +22,7 @@ "@remix-run/react": "^2.11.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "intl-parse-accept-language": "^1.0.0", "isbot": "^5.1.17", "lucide-react": "^0.435.0", "react": "^18.3.1", diff --git a/apps/web/public/assets/placeholders/150x150.png b/apps/web/public/assets/placeholders/150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..523559737c5b94544ef45659d1eb2d062913da46 GIT binary patch literal 2300 zcmd5;eK^zW8z0Hb%=^M6l~~zt%}$3EhaKXWI5r{1oKaqj98I*Av`~__#E$tfD(ym) zdC5y_jkM%Ai>0N-Ft0yHCNJeB!+ziVd;b1i*YCRS=XtK{e(w8uxu4JddG1Vauj5)^ z6EFw_((-V3^#w-8_ol82^cbc1abVCOyAu;YAf3?fP3fUiRWt~s&h~J1_M;BYF{1+p zgP=F1up1t_k#}am+Re?&3Wa7cn>|%y^$z^-%|VE&`i)5%`G97GZc=kIZ~jXy*(^d{ zdUeIoa+lYM6IQxQFK1AQnJI$fl-bfd?;kLr1VOyb_h22&F6W`Eg_<#C~OC!?ZFQ zwZ|aSwZ660c9j?PB-4$h6fEAauAJ<+Z+y|trTi@uLaP#W^z>-yXR0WtzIYkW3(+Ym zDiYF89c5zCCS{k0(Re?^u7@>QeP>mCpn4G1eB8mYFWd+1Xr6dD)Dh%6V+mit!Um&~!h7I&NmprE4cY5Au}7EUx|v)PA@A)^#-zE>4+ z@*j;M-nE3jnZ!H7>BYr~<7=B+tyY8zUuRxOMyT<*Y;{mboe`7CJammMoo5ZiDub+x z-@Nx7U6q&FSCUW`Cb=%qYp2=^gwuU<6mjnxdRVOi69_7>s}FSX^RUBl43W{$(2z<( zso_QBOP4ZOES3>ZY?Rj+U2+Geu{Tc*kOElkE=--6`(ar&b%HU)S;m^@gFrc5;Y~rc zg>6rQYR6?V$UUcPcO9H>^7r=&h4B4I~nY@FbdIuKreS(bTy_#hk;Fx$K~?yKQZ40n{J` zX_e6lf*f0m4?=5V`0&usebLNmJ5v`%kiYmF?e@7 z`>NtgeE_55bPBD#{R8aV#4AMMuQqkF&$Rvh^|Uh@T3gkebxVoS;zI%ksYo!jkx%HH zT{bVqjeq{EUE!-?X=!Pk*Y%Xk-LViT9FWQnjVpsVq&T;`w00eT|NLJoN-MOevc`KO zipGz)+lq6qN`_*{++p_$UjRX54#`VR6w`bko+TOv zGl4bb(xqzy%aKGkq~R}!!k6;Zc|CCY=micSy5MwjaflLaOhKR1gZmpJd%eCmoh)5XZmZ_8a0j1UsFLf)#>JV_yUmJmy_*x7 zX(@h)oulysP4gF*Y*CVtdYk?SPZfP(wP`R9uhbZ$$^zI=7py#|6i29ZL0=8Cq`jc~ zb?5D#$)(oEv_{KG1p#@5;;2<(C271Zq|Kj6tExc&TqT%FGgCi{HaY+KE!CPnP#n`} zK!WZTq01V_544($H_Vg=^ssUOP?KJ z_Lv)HcQhiQ*`0--7>cY&;@Z-bCJKdO#J%d31nFY?{-V>?; z+;SFe*vr1ak;zCBr5&TORP78tM=2Izl;(aRQmzP58rojfc~Y91ZMTYF?)4zB3D!q ze{+*38g#;7M%UK|FYBApyKy_uFU6P6&Yo+M{QmFC%tdOB8*5DE=Y03a{)tkll+_>O zTmU;`;RnT5Sv>BKIe7x|+_2gYoMv`kI&V=VC>PDVI1I^gvq-exlf1dUtEAEy+F7h6 zIg19jO0@Wg0VXSxI+$wvAPV2kBvxiAiYD>0@XQCdSS$-taImxZA{3m?3HPmjKeWjL zKm|l&cKN$g2|tAWa%|!Kf5sAk)z1)VgVlorkNRWE1CirvYipgqEmsX}EG+1X#p1VU zt$=_4n{WVCD=RBepI|=Fcq;W436(+H&h1+d{*G{T)~)Aq<>P`;rk*AhqbC14V^Gp_ z(9i$kCsp}J^SyaLU=Gn?8v7iRj{#)+0De+u9n10o7^9iy*${Xm)L32hdV&+PJL2se z1vnu<)2B9xbpYqn=?3p(j!Zukv^iZ7CHAGZNawq{yEPDn;K8}05I*^@Oe$sz;j5HF vPD)x|_lJLzb|`Sp|4-lbKZ=A6m#v+CsqBO#xgiNCXFwiUFITq9>9qd>uii7! literal 0 HcmV?d00001 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f433341..1db33ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + intl-parse-accept-language: + specifier: ^1.0.0 + version: 1.0.0 isbot: specifier: ^5.1.17 version: 5.1.17 @@ -2467,6 +2470,10 @@ packages: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} + intl-parse-accept-language@1.0.0: + resolution: {integrity: sha512-YFMSV91JNBOSjw1cOfw2tup6hDP7mkz+2AUV7W1L1AM6ntgI75qC1ZeFpjPGMrWp+upmBRTX2fJWQ8c7jsUWpA==} + engines: {node: '>=14'} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -6969,6 +6976,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.0.6 + intl-parse-accept-language@1.0.0: {} + invariant@2.2.4: dependencies: loose-envify: 1.4.0