lint fix wip

This commit is contained in:
Nicolas Cantu 2026-01-09 09:22:30 +01:00
parent 81f6d303dd
commit 526fb5af6f
20 changed files with 260 additions and 127 deletions

View File

@ -308,7 +308,8 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?:
return return
} }
if (!userConfirm(t('presentation.delete.confirm'))) { const confirmed = await userConfirm(t('presentation.delete.confirm'))
if (!confirmed) {
return return
} }

View File

@ -179,7 +179,8 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps):
} }
async function handleRemoveApi(id: string): Promise<void> { async function handleRemoveApi(id: string): Promise<void> {
if (!userConfirm(t('settings.nip95.remove.confirm'))) { const confirmed = await userConfirm(t('settings.nip95.remove.confirm'))
if (!confirmed) {
return return
} }

View File

@ -210,7 +210,8 @@ export function RelayManager({ onConfigChange }: RelayManagerProps): React.React
} }
async function handleRemoveRelay(id: string): Promise<void> { async function handleRemoveRelay(id: string): Promise<void> {
if (!userConfirm(t('settings.relay.remove.confirm'))) { const confirmed = await userConfirm(t('settings.relay.remove.confirm'))
if (!confirmed) {
return return
} }

View File

@ -16,6 +16,12 @@ export function SponsoringForm({ author, onSuccess, onCancel }: SponsoringFormPr
const [text, setText] = useState('') const [text, setText] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [instructions, setInstructions] = useState<{
authorAddress: string
platformAddress: string
authorBtc: string
platformBtc: string
} | null>(null)
const handleSubmit = async (e: React.FormEvent): Promise<void> => { const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault() e.preventDefault()
@ -67,16 +73,15 @@ export function SponsoringForm({ author, onSuccess, onCancel }: SponsoringFormPr
totalAmount: result.split.totalSats, totalAmount: result.split.totalSats,
}) })
// Show instructions to user // Show instructions inline (no-alert)
alert(t('sponsoring.form.instructions', { setInstructions({
authorAddress: result.authorAddress, authorAddress: result.authorAddress,
platformAddress: result.platformAddress, platformAddress: result.platformAddress,
authorAmount: (result.split.authorSats / 100_000_000).toFixed(8), authorBtc: (result.split.authorSats / 100_000_000).toFixed(8),
platformAmount: (result.split.platformSats / 100_000_000).toFixed(8), platformBtc: (result.split.platformSats / 100_000_000).toFixed(8),
})) })
setText('') setText('')
onSuccess?.()
} catch (submitError) { } catch (submitError) {
setError(submitError instanceof Error ? submitError.message : t('sponsoring.form.error.paymentFailed')) setError(submitError instanceof Error ? submitError.message : t('sponsoring.form.error.paymentFailed'))
} finally { } finally {
@ -108,6 +113,44 @@ export function SponsoringForm({ author, onSuccess, onCancel }: SponsoringFormPr
) )
} }
if (instructions) {
return (
<div className="border border-neon-cyan/30 rounded-lg p-4 bg-cyber-dark space-y-4" role="dialog" aria-modal="true">
<h3 className="text-lg font-semibold text-neon-cyan">{t('sponsoring.form.title')}</h3>
<p className="text-sm text-cyber-accent/70">
{t('sponsoring.form.instructions', {
authorAddress: instructions.authorAddress,
platformAddress: instructions.platformAddress,
authorAmount: instructions.authorBtc,
platformAmount: instructions.platformBtc,
})}
</p>
<div className="flex gap-3">
<button
type="button"
onClick={() => {
setInstructions(null)
onSuccess?.()
}}
className="px-4 py-2 bg-neon-green/20 hover:bg-neon-green/30 text-neon-green rounded-lg font-medium transition-all border border-neon-green/50"
>
{t('common.close')}
</button>
<button
type="button"
onClick={() => {
setInstructions(null)
onCancel?.()
}}
className="px-4 py-2 bg-neon-cyan/10 hover:bg-neon-cyan/20 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/30"
>
{t('common.cancel')}
</button>
</div>
</div>
)
}
return ( return (
<form onSubmit={(e) => { <form onSubmit={(e) => {
void handleSubmit(e) void handleSubmit(e)

View File

@ -50,7 +50,7 @@ export function useDocs(docs: DocLink[]): {
}, [docs]) }, [docs])
useEffect(() => { useEffect(() => {
loadDoc('user-guide') void loadDoc('user-guide')
}, [loadDoc]) }, [loadDoc])
return { return {

View File

@ -31,12 +31,12 @@ export function useI18n(locale: Locale = 'fr'): {
if (frResponse.ok) { if (frResponse.ok) {
const frText = await frResponse.text() const frText = await frResponse.text()
await loadTranslations('fr', frText) loadTranslations('fr', frText)
} }
if (enResponse.ok) { if (enResponse.ok) {
const enText = await enResponse.text() const enText = await enResponse.text()
await loadTranslations('en', enText) loadTranslations('en', enText)
} }
setLocale(initialLocale) setLocale(initialLocale)

View File

@ -193,19 +193,23 @@ export class AlbyService {
// Generate a simple hash-like identifier // Generate a simple hash-like identifier
// In practice, payment verification should be done via zap receipts // In practice, payment verification should be done via zap receipts
if (typeof window !== 'undefined' && window.crypto) { if (typeof window !== 'undefined' && window.crypto) {
// Use a simple hash for identification const hash32 = hashStringToUint32(invoice)
let hash = 0 return hash32.toString(16).padStart(16, '0')
for (let i = 0; i < invoice.length; i++) {
const char = invoice.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // Convert to 32-bit integer
}
return Math.abs(hash).toString(16).padStart(16, '0')
} }
return Date.now().toString(16) return Date.now().toString(16)
} }
} }
function hashStringToUint32(value: string): number {
// Deterministic 32-bit hash without bitwise operators (rule: no-bitwise)
const mod = 2n ** 32n
let hash = 0n
for (let i = 0; i < value.length; i++) {
hash = (hash * 31n + BigInt(value.charCodeAt(i))) % mod
}
return Number(hash)
}
// Singleton instance // Singleton instance
let albyServiceInstance: AlbyService | null = null let albyServiceInstance: AlbyService | null = null

View File

@ -32,6 +32,7 @@ function requireCategory(category?: ArticleDraft['category']): asserts category
} }
async function ensurePresentation(authorPubkey: string): Promise<string> { async function ensurePresentation(authorPubkey: string): Promise<string> {
const { articlePublisher } = await import('./articlePublisher')
const presentation = await articlePublisher.getAuthorPresentation(authorPubkey) const presentation = await articlePublisher.getAuthorPresentation(authorPubkey)
if (!presentation) { if (!presentation) {
throw new Error('Vous devez créer un article de présentation avant de publier des articles.') throw new Error('Vous devez créer un article de présentation avant de publier des articles.')
@ -659,14 +660,14 @@ export async function deleteArticleEvent(articleId: string, authorPubkey: string
if (!privateKey) { if (!privateKey) {
throw new Error('Private key required for signing') throw new Error('Private key required for signing')
} }
const { writeOrchestrator } = await import('./writeOrchestrator') const { writeOrchestrator: writeOrchestratorInstance } = await import('./writeOrchestrator')
writeOrchestrator.setPrivateKey(privateKey) writeOrchestratorInstance.setPrivateKey(privateKey)
// Finalize event // Finalize event
const { finalizeEvent } = await import('nostr-tools') const { finalizeEvent: finalizeNostrEvent } = await import('nostr-tools')
const { hexToBytes } = await import('nostr-tools/utils') const { hexToBytes: hexToBytesUtil } = await import('nostr-tools/utils')
const secretKey = hexToBytes(privateKey) const secretKey = hexToBytesUtil(privateKey)
const event = finalizeEvent(deleteEventTemplate, secretKey) const event = finalizeNostrEvent(deleteEventTemplate, secretKey)
// Get active relays // Get active relays
const { relaySessionManager } = await import('./relaySessionManager') const { relaySessionManager } = await import('./relaySessionManager')
@ -675,7 +676,7 @@ export async function deleteArticleEvent(articleId: string, authorPubkey: string
const relays = activeRelays.length > 0 ? activeRelays : [await getPrimaryRelay()] const relays = activeRelays.length > 0 ? activeRelays : [await getPrimaryRelay()]
// Publish via writeOrchestrator (parallel network + local write) // Publish via writeOrchestrator (parallel network + local write)
const result = await writeOrchestrator.writeAndPublish( const result = await writeOrchestratorInstance.writeAndPublish(
{ {
objectType: 'publication', objectType: 'publication',
hash, hash,
@ -694,5 +695,5 @@ export async function deleteArticleEvent(articleId: string, authorPubkey: string
} }
// Re-export for convenience to avoid circular imports in hooks // Re-export for convenience to avoid circular imports in hooks
import { articlePublisher } from './articlePublisher' export { articlePublisher } from './articlePublisher'
export const getStoredContent = getStoredPrivateContent export const getStoredContent = getStoredPrivateContent

View File

@ -193,19 +193,9 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
} }
} else { } else {
// Generate hash from author data // Generate hash from author data
let mainnetAddress: string | undefined const mainnetAddress = profileData?.mainnetAddress ?? (typeof tags.mainnetAddress === 'string' ? tags.mainnetAddress : undefined)
if (profileData?.mainnetAddress) {
mainnetAddress = profileData.mainnetAddress
} else if (typeof tags.mainnetAddress === 'string') {
mainnetAddress = tags.mainnetAddress
}
let pictureUrl: string | undefined const pictureUrl = profileData?.pictureUrl ?? (typeof tags.pictureUrl === 'string' ? tags.pictureUrl : undefined)
if (profileData?.pictureUrl) {
pictureUrl = profileData.pictureUrl
} else if (typeof tags.pictureUrl === 'string') {
pictureUrl = tags.pictureUrl
}
hash = await generateAuthorHashId({ hash = await generateAuthorHashId({
pubkey: event.pubkey, pubkey: event.pubkey,

View File

@ -205,8 +205,7 @@ export async function encryptAndPublish(
if (publishResult && 'event' in publishResult && 'relayStatuses' in publishResult) { if (publishResult && 'event' in publishResult && 'relayStatuses' in publishResult) {
// New format with statuses // New format with statuses
event = publishResult.event ;({ event, relayStatuses } = publishResult)
relayStatuses = publishResult.relayStatuses
} else if (publishResult && 'id' in publishResult) { } else if (publishResult && 'id' in publishResult) {
// Old format (Event) // Old format (Event)
event = publishResult event = publishResult

View File

@ -32,6 +32,25 @@ export function useSyncProgress(options: UseSyncProgressOptions = {}): UseSyncPr
const onCompleteRef = useRef(onComplete) const onCompleteRef = useRef(onComplete)
const isMonitoringRef = useRef(false) const isMonitoringRef = useRef(false)
function stopMonitoring(): void {
if (!isMonitoringRef.current) {
return
}
isMonitoringRef.current = false
setIsSyncing(false)
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
}
// Update onComplete ref when it changes // Update onComplete ref when it changes
useEffect(() => { useEffect(() => {
onCompleteRef.current = onComplete onCompleteRef.current = onComplete
@ -72,25 +91,6 @@ export function useSyncProgress(options: UseSyncProgressOptions = {}): UseSyncPr
}, maxDuration) }, maxDuration)
} }
const stopMonitoring = (): void => {
if (!isMonitoringRef.current) {
return
}
isMonitoringRef.current = false
setIsSyncing(false)
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
}
// Cleanup on unmount // Cleanup on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {

View File

@ -83,13 +83,13 @@ function expandDictionary(): MnemonicIcon[] {
const DICTIONARY = expandDictionary() const DICTIONARY = expandDictionary()
function hashString(str: string): number { function hashString(str: string): number {
let hash = 0 // Deterministic hash without bitwise operators (rule: no-bitwise)
const mod = 2n ** 32n
let hash = 0n
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i) hash = (hash * 31n + BigInt(str.charCodeAt(i))) % mod
hash = ((hash << 5) - hash) + char
hash = hash & hash
} }
return Math.abs(hash) return Number(hash)
} }
export function generateMnemonicIcons(pubkey: string): string[] { export function generateMnemonicIcons(pubkey: string): string[] {

View File

@ -496,7 +496,7 @@ class NostrService {
* Searches all object types to find and update the event * Searches all object types to find and update the event
*/ */
private async updatePublishedStatus(eventId: string, published: false | string[]): Promise<void> { private async updatePublishedStatus(eventId: string, published: false | string[]): Promise<void> {
const { objectCache } = await import('./objectCache') const { objectCache: objectCacheModule } = await import('./objectCache')
const objectTypes: Array<import('./objectCache').ObjectType> = ['author', 'series', 'publication', 'review', 'purchase', 'sponsoring', 'review_tip', 'payment_note'] const objectTypes: Array<import('./objectCache').ObjectType> = ['author', 'series', 'publication', 'review', 'purchase', 'sponsoring', 'review_tip', 'payment_note']
// Load writeService once // Load writeService once
@ -505,7 +505,7 @@ class NostrService {
// First try to find in unpublished objects (faster) // First try to find in unpublished objects (faster)
for (const objectType of objectTypes) { for (const objectType of objectTypes) {
try { try {
const unpublished = await objectCache.getUnpublished(objectType) const unpublished = await objectCacheModule.getUnpublished(objectType)
const matching = unpublished.find((obj) => obj.event.id === eventId) const matching = unpublished.find((obj) => obj.event.id === eventId)
if (matching) { if (matching) {
await writeService.updatePublished(objectType, matching.id, published) await writeService.updatePublished(objectType, matching.id, published)
@ -521,7 +521,7 @@ class NostrService {
for (const objectType of objectTypes) { for (const objectType of objectTypes) {
try { try {
// Use getAll to search all objects // Use getAll to search all objects
const allObjects = await objectCache.getAll(objectType) const allObjects = await objectCacheModule.getAll(objectType)
const matching = allObjects.find((obj) => { const matching = allObjects.find((obj) => {
const cachedObj = obj as CachedObject const cachedObj = obj as CachedObject
return cachedObj.event?.id === eventId return cachedObj.event?.id === eventId

View File

@ -162,20 +162,13 @@ class NotificationDetector {
}) })
for (const obj of userObjects) { for (const obj of userObjects) {
if (!Array.isArray(obj.published) || obj.published.length === 0) { if (Array.isArray(obj.published) && obj.published.length > 0) {
continue
}
const eventId = obj.id.split(':')[1] ?? obj.id const eventId = obj.id.split(':')[1] ?? obj.id
const existing = await notificationService.getNotificationByEventId(eventId) const existing = await notificationService.getNotificationByEventId(eventId)
if (existing?.type === 'published') { const alreadyNotified = existing?.type === 'published'
continue const recentlyCreated = obj.createdAt * 1000 > oneHourAgo
}
if (obj.createdAt * 1000 <= oneHourAgo) {
continue
}
if (!alreadyNotified && recentlyCreated) {
const relays = obj.published const relays = obj.published
await notificationService.createNotification({ await notificationService.createNotification({
type: 'published', type: 'published',
@ -190,6 +183,8 @@ class NotificationDetector {
}, },
}) })
} }
}
}
} catch (error) { } catch (error) {
console.error(`[NotificationDetector] Error scanning published status for ${objectType}:`, error) console.error(`[NotificationDetector] Error scanning published status for ${objectType}:`, error)
} }

View File

@ -260,12 +260,12 @@ class ObjectCacheService {
cursor.continue() cursor.continue()
} else { } else {
// Sort by version descending and return the latest // Sort by version descending and return the latest
if (objects.length > 0) { if (objects.length === 0) {
resolve(null)
return
}
objects.sort((a, b) => b.version - a.version) objects.sort((a, b) => b.version - a.version)
resolve(objects[0]?.parsed ?? null) resolve(objects[0]?.parsed ?? null)
} else {
resolve(null)
}
} }
} }
@ -336,12 +336,12 @@ class ObjectCacheService {
cursor.continue() cursor.continue()
} else { } else {
// Sort by version descending and return the latest // Sort by version descending and return the latest
if (objects.length > 0) { if (objects.length === 0) {
resolve(null)
return
}
objects.sort((a, b) => b.version - a.version) objects.sort((a, b) => b.version - a.version)
resolve((objects[0]?.parsed ?? null) as AuthorPresentationArticle | null) resolve((objects[0]?.parsed ?? null) as AuthorPresentationArticle | null)
} else {
resolve(null)
}
} }
} }

View File

@ -129,24 +129,20 @@ export function verifyPaymentSplit(
platformAmount?: number platformAmount?: number
): boolean { ): boolean {
switch (type) { switch (type) {
case 'article': case 'article': {
const articleSplit = calculateArticleSplit(totalAmount) const articleSplit = calculateArticleSplit(totalAmount)
return ( return articleSplit.author === (authorAmount ?? 0) && articleSplit.platform === (platformAmount ?? 0)
articleSplit.author === (authorAmount ?? 0) && articleSplit.platform === (platformAmount ?? 0) }
)
case 'review': case 'review': {
const reviewSplit = calculateReviewSplit(totalAmount) const reviewSplit = calculateReviewSplit(totalAmount)
return ( return reviewSplit.reviewer === (authorAmount ?? 0) && reviewSplit.platform === (platformAmount ?? 0)
reviewSplit.reviewer === (authorAmount ?? 0) && reviewSplit.platform === (platformAmount ?? 0) }
)
case 'sponsoring': case 'sponsoring': {
const sponsoringSplit = calculateSponsoringSplit(totalAmount) const sponsoringSplit = calculateSponsoringSplit(totalAmount)
return ( return sponsoringSplit.authorSats === (authorAmount ?? 0) && sponsoringSplit.platformSats === (platformAmount ?? 0)
sponsoringSplit.authorSats === (authorAmount ?? 0) && }
sponsoringSplit.platformSats === (platformAmount ?? 0)
)
default: default:
return false return false

View File

@ -188,7 +188,7 @@ class PlatformSyncService {
sub.on('eose', (): void => { sub.on('eose', (): void => {
console.warn(`[PlatformSync] Relay ${relayUrl} sent EOSE signal`) console.warn(`[PlatformSync] Relay ${relayUrl} sent EOSE signal`)
finalize() void finalize()
resolve() resolve()
}) })
@ -424,7 +424,7 @@ class PlatformSyncService {
if (isReady) { if (isReady) {
await swClient.stopPlatformSync() await swClient.stopPlatformSync()
} }
} catch (error) { } catch {
// Ignore errors // Ignore errors
} }
} }

View File

@ -8,9 +8,111 @@
* that cannot be replicated with React modals without significant refactoring. * that cannot be replicated with React modals without significant refactoring.
* Used only for critical destructive actions (delete operations). * Used only for critical destructive actions (delete operations).
*/ */
export function userConfirm(message: string): boolean { export function userConfirm(message: string): Promise<boolean> {
// window.confirm is the native browser confirmation dialog return confirmOverlay(message)
// This is intentionally used here for critical confirmations }
// that must block the UI thread until user responds
return window.confirm(message) function confirmOverlay(message: string): Promise<boolean> {
const doc = globalThis.document
if (!doc) {
return Promise.resolve(false)
}
return new Promise((resolve) => {
const overlay = doc.createElement('div')
overlay.setAttribute('role', 'dialog')
overlay.setAttribute('aria-modal', 'true')
overlay.tabIndex = -1
overlay.style.position = 'fixed'
overlay.style.inset = '0'
overlay.style.background = 'rgba(0,0,0,0.6)'
overlay.style.display = 'flex'
overlay.style.alignItems = 'center'
overlay.style.justifyContent = 'center'
overlay.style.zIndex = '9999'
const panel = doc.createElement('div')
panel.style.background = '#fff'
panel.style.borderRadius = '12px'
panel.style.padding = '16px'
panel.style.maxWidth = '520px'
panel.style.width = 'calc(100% - 32px)'
panel.style.boxShadow = '0 10px 30px rgba(0,0,0,0.35)'
const text = doc.createElement('p')
text.textContent = message
text.style.margin = '0 0 16px 0'
text.style.color = '#111827'
const buttons = doc.createElement('div')
buttons.style.display = 'flex'
buttons.style.gap = '12px'
buttons.style.justifyContent = 'flex-end'
const cancel = doc.createElement('button')
cancel.type = 'button'
cancel.textContent = 'Cancel'
cancel.style.padding = '8px 12px'
cancel.style.borderRadius = '10px'
cancel.style.border = '1px solid #e5e7eb'
cancel.style.background = '#f3f4f6'
const confirm = doc.createElement('button')
confirm.type = 'button'
confirm.textContent = 'Confirm'
confirm.style.padding = '8px 12px'
confirm.style.borderRadius = '10px'
confirm.style.border = '1px solid #ef4444'
confirm.style.background = '#fee2e2'
confirm.style.color = '#991b1b'
buttons.append(cancel, confirm)
panel.append(text, buttons)
overlay.append(panel)
doc.body.append(overlay)
overlay.focus()
let resolved = false
const resolveOnce = (next: boolean): void => {
if (resolved) {
return
}
resolved = true
cleanup()
resolve(next)
}
function onCancel(): void {
resolveOnce(false)
}
function onConfirm(): void {
resolveOnce(true)
}
function onKeyDown(e: KeyboardEvent): void {
if (e.key === 'Escape') {
e.preventDefault()
resolveOnce(false)
return
}
if (e.key === 'Enter') {
e.preventDefault()
resolveOnce(true)
}
}
function cleanup(): void {
overlay.removeEventListener('keydown', onKeyDown)
cancel.removeEventListener('click', onCancel)
confirm.removeEventListener('click', onConfirm)
overlay.remove()
}
cancel.addEventListener('click', onCancel)
confirm.addEventListener('click', onConfirm)
overlay.addEventListener('keydown', onKeyDown)
})
} }

View File

@ -262,9 +262,9 @@ class WriteService {
resolve() resolve()
} else if (responseType === 'ERROR') { } else if (responseType === 'ERROR') {
const errorData = readWorkerErrorData(responseData) const errorData = readWorkerErrorData(responseData)
const {taskId} = errorData const { taskId } = errorData
const isUpdatePublished = const isUpdatePublished =
errorData.originalType === 'UPDATE_PUBLISHED' || (taskId !== undefined && taskId.startsWith('UPDATE_PUBLISHED')) errorData.originalType === 'UPDATE_PUBLISHED' || taskId?.startsWith('UPDATE_PUBLISHED') === true
if (!isUpdatePublished) { if (!isUpdatePublished) {
return return
} }
@ -320,9 +320,9 @@ class WriteService {
resolve() resolve()
} else if (responseType === 'ERROR') { } else if (responseType === 'ERROR') {
const errorData = readWorkerErrorData(responseData) const errorData = readWorkerErrorData(responseData)
const {taskId} = errorData const { taskId } = errorData
const isCreateNotification = const isCreateNotification =
errorData.originalType === 'CREATE_NOTIFICATION' || (taskId !== undefined && taskId.startsWith('CREATE_NOTIFICATION')) errorData.originalType === 'CREATE_NOTIFICATION' || taskId?.startsWith('CREATE_NOTIFICATION') === true
if (!isCreateNotification) { if (!isCreateNotification) {
return return
} }

View File

@ -361,7 +361,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Try to extract error message from HTML if possible // Try to extract error message from HTML if possible
const titleMatch = response.body.match(/<title[^>]*>([^<]+)<\/title>/i) const titleMatch = response.body.match(/<title[^>]*>([^<]+)<\/title>/i)
const h1Match = response.body.match(/<h1[^>]*>([^<]+)<\/h1>/i) const h1Match = response.body.match(/<h1[^>]*>([^<]+)<\/h1>/i)
const errorText = titleMatch?.[1] || h1Match?.[1] || 'HTML error page returned' const errorText = titleMatch?.[1] ?? h1Match?.[1] ?? 'HTML error page returned'
// Check if it's a 404 or other error page // Check if it's a 404 or other error page
const is404 = response.body.includes('404') || response.body.includes('Not Found') || titleMatch?.[1]?.includes('404') const is404 = response.body.includes('404') || response.body.includes('Not Found') || titleMatch?.[1]?.includes('404')