mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(catalog): improve empty search state and footer layout
Show centered Nothing Found for catalog searches with no matches, mirror search results labels at the bottom footer, split nav from the style row, and drop the desktop catalog footer top margin.
This commit is contained in:
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "افتح الملف الأصلي",
|
||||
"media_failed_to_load_inline": "فشلت الصورة الخارجية من {{host}}. جرّب VPN.",
|
||||
"media_failed_to_load_inline_unknown": "فشلت الصورة الخارجية. جرّب VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "মূল ফাইল খুলুন",
|
||||
"media_failed_to_load_inline": "{{host}} থেকে বাইরের ছবি লোড হয়নি। VPN চেষ্টা করুন।",
|
||||
"media_failed_to_load_inline_unknown": "বাইরের ছবি লোড হয়নি। VPN চেষ্টা করুন।",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Otevřít původní soubor",
|
||||
"media_failed_to_load_inline": "Externí obrázek z {{host}} selhal. Zkuste VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Externí obrázek selhal. Zkuste VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Åbn originalfil",
|
||||
"media_failed_to_load_inline": "Eksternt billede fra {{host}} fejlede. Prøv VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Eksternt billede fejlede. Prøv VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Originaldatei öffnen",
|
||||
"media_failed_to_load_inline": "Externes Bild von {{host}} fehlgeschlagen. VPN versuchen.",
|
||||
"media_failed_to_load_inline_unknown": "Externes Bild fehlgeschlagen. VPN versuchen.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Άνοιγμα αρχικού αρχείου",
|
||||
"media_failed_to_load_inline": "Η εξωτερική εικόνα από {{host}} απέτυχε. Δοκιμάστε VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Η εξωτερική εικόνα απέτυχε. Δοκιμάστε VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"sort_by": "Sort by",
|
||||
"posts_last_synced_info": "Posts last synced {{time}}, this board might be offline and publishing might fail.",
|
||||
"no_threads": "No threads",
|
||||
"nothing_found": "Nothing Found",
|
||||
"hidden": "hidden",
|
||||
"save_options": "Save Options",
|
||||
"save_advanced_settings": "Save Advanced Settings",
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Abrir archivo original",
|
||||
"media_failed_to_load_inline": "Falló la imagen externa de {{host}}. Prueba una VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Falló la imagen externa. Prueba una VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "باز کردن فایل اصلی",
|
||||
"media_failed_to_load_inline": "تصویر خارجی از {{host}} بارگیری نشد. VPN را امتحان کنید.",
|
||||
"media_failed_to_load_inline_unknown": "تصویر خارجی بارگیری نشد. VPN را امتحان کنید.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Avaa alkuperäinen tiedosto",
|
||||
"media_failed_to_load_inline": "Ulkoinen kuva sivustolta {{host}} epäonnistui. Kokeile VPN:ää.",
|
||||
"media_failed_to_load_inline_unknown": "Ulkoinen kuva epäonnistui. Kokeile VPN:ää.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Buksan ang orihinal na file",
|
||||
"media_failed_to_load_inline": "Nabigo ang panlabas na larawan mula sa {{host}}. Subukan ang VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Nabigo ang panlabas na larawan. Subukan ang VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Ouvrir le fichier original",
|
||||
"media_failed_to_load_inline": "L'image externe de {{host}} a échoué. Essayez un VPN.",
|
||||
"media_failed_to_load_inline_unknown": "L'image externe a échoué. Essayez un VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "פתח את הקובץ המקורי",
|
||||
"media_failed_to_load_inline": "התמונה החיצונית מ-{{host}} נכשלה. נסה VPN.",
|
||||
"media_failed_to_load_inline_unknown": "התמונה החיצונית נכשלה. נסה VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "मूल फ़ाइल खोलें",
|
||||
"media_failed_to_load_inline": "{{host}} से बाहरी छवि लोड नहीं हुई। VPN आज़माएँ।",
|
||||
"media_failed_to_load_inline_unknown": "बाहरी छवि लोड नहीं हुई। VPN आज़माएँ।",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Eredeti fájl megnyitása",
|
||||
"media_failed_to_load_inline": "A külső kép innen nem töltődött be: {{host}}. Próbálj VPN-t.",
|
||||
"media_failed_to_load_inline_unknown": "A külső kép nem töltődött be. Próbálj VPN-t.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Buka file asli",
|
||||
"media_failed_to_load_inline": "Gambar eksternal dari {{host}} gagal. Coba VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Gambar eksternal gagal. Coba VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Apri file originale",
|
||||
"media_failed_to_load_inline": "Immagine esterna da {{host}} non caricata. Prova una VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Immagine esterna non caricata. Prova una VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "元のファイルを開く",
|
||||
"media_failed_to_load_inline": "{{host}} の外部画像を読み込めませんでした。VPN を試してください。",
|
||||
"media_failed_to_load_inline_unknown": "外部画像を読み込めませんでした。VPN を試してください。",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "원본 파일 열기",
|
||||
"media_failed_to_load_inline": "{{host}}의 외부 이미지를 불러오지 못했습니다. VPN을 시도하세요.",
|
||||
"media_failed_to_load_inline_unknown": "외부 이미지를 불러오지 못했습니다. VPN을 시도하세요.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "मूळ फाइल उघडा",
|
||||
"media_failed_to_load_inline": "{{host}} वरील बाह्य प्रतिमा लोड झाली नाही. VPN वापरून पाहा.",
|
||||
"media_failed_to_load_inline_unknown": "बाह्य प्रतिमा लोड झाली नाही. VPN वापरून पाहा.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Origineel bestand openen",
|
||||
"media_failed_to_load_inline": "Externe afbeelding van {{host}} mislukt. Probeer VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Externe afbeelding mislukt. Probeer VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Åpne originalfil",
|
||||
"media_failed_to_load_inline": "Eksternt bilde fra {{host}} kunne ikke lastes. Prøv VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Eksternt bilde kunne ikke lastes. Prøv VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Otwórz oryginalny plik",
|
||||
"media_failed_to_load_inline": "Obraz zewnętrzny z {{host}} nie załadował się. Spróbuj VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Obraz zewnętrzny nie załadował się. Spróbuj VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Abrir arquivo original",
|
||||
"media_failed_to_load_inline": "Falha na imagem externa de {{host}}. Tente uma VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Falha na imagem externa. Tente uma VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Deschide fișierul original",
|
||||
"media_failed_to_load_inline": "Imaginea externă de la {{host}} a eșuat. Încearcă VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Imaginea externă a eșuat. Încearcă VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Открыть исходный файл",
|
||||
"media_failed_to_load_inline": "Внешнее изображение с {{host}} не загрузилось. Попробуйте VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Внешнее изображение не загрузилось. Попробуйте VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Hap skedarin origjinal",
|
||||
"media_failed_to_load_inline": "Imazhi i jashtëm nga {{host}} dështoi. Provoni VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Imazhi i jashtëm dështoi. Provoni VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Öppna originalfil",
|
||||
"media_failed_to_load_inline": "Extern bild från {{host}} kunde inte läsas in. Prova VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Extern bild kunde inte läsas in. Prova VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "అసలు ఫైల్ను తెరవండి",
|
||||
"media_failed_to_load_inline": "{{host}} నుండి బాహ్య చిత్రం లోడ్ కాలేదు. VPN ప్రయత్నించండి.",
|
||||
"media_failed_to_load_inline_unknown": "బాహ్య చిత్రం లోడ్ కాలేదు. VPN ప్రయత్నించండి.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "เปิดไฟล์ต้นฉบับ",
|
||||
"media_failed_to_load_inline": "โหลดรูปภาพภายนอกจาก {{host}} ไม่สำเร็จ ลอง VPN",
|
||||
"media_failed_to_load_inline_unknown": "โหลดรูปภาพภายนอกไม่สำเร็จ ลอง VPN",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Orijinal dosyayı aç",
|
||||
"media_failed_to_load_inline": "{{host}} harici görseli yüklenemedi. VPN deneyin.",
|
||||
"media_failed_to_load_inline_unknown": "Harici görsel yüklenemedi. VPN deneyin.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Відкрити оригінальний файл",
|
||||
"media_failed_to_load_inline": "Зовнішнє зображення з {{host}} не завантажилося. Спробуйте VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Зовнішнє зображення не завантажилося. Спробуйте VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "اصل فائل کھولیں",
|
||||
"media_failed_to_load_inline": "{{host}} سے بیرونی تصویر لوڈ نہیں ہوئی۔ VPN آزمائیں۔",
|
||||
"media_failed_to_load_inline_unknown": "بیرونی تصویر لوڈ نہیں ہوئی۔ VPN آزمائیں۔",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "Mở tệp gốc",
|
||||
"media_failed_to_load_inline": "Không tải được ảnh bên ngoài từ {{host}}. Thử VPN.",
|
||||
"media_failed_to_load_inline_unknown": "Không tải được ảnh bên ngoài. Thử VPN.",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -388,5 +388,6 @@
|
||||
"media_failed_to_load_open_source": "打开原始文件",
|
||||
"media_failed_to_load_inline": "来自 {{host}} 的外部图片加载失败。试试 VPN。",
|
||||
"media_failed_to_load_inline_unknown": "外部图片加载失败。试试 VPN。",
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead."
|
||||
"expiring_media_link_alert": "Media from {{domain}} expires quickly. Use a stable media link instead.",
|
||||
"nothing_found": "Nothing Found"
|
||||
}
|
||||
|
||||
@@ -523,7 +523,6 @@ export const MobileBoardButtons = () => {
|
||||
const resolvedAddress = useResolvedCommunityAddress();
|
||||
const communityAddress = resolvedAddress || accountComment?.communityAddress;
|
||||
|
||||
const { filteredCount, searchText } = useCatalogFiltersStore();
|
||||
const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
|
||||
const isMultiboard = isInAllView || isInSubscriptionsView || isInModView;
|
||||
const showTimeFilter = isMultiboard;
|
||||
@@ -574,19 +573,7 @@ export const MobileBoardButtons = () => {
|
||||
isInModView={isInModView}
|
||||
isMobilePlacement={true}
|
||||
/>
|
||||
{searchText ? (
|
||||
<span className={styles.filteredThreadsCount}>
|
||||
{' '}
|
||||
- {t('search_results_for')}: <strong>{searchText}</strong>
|
||||
</span>
|
||||
) : (
|
||||
filteredCount > 0 && (
|
||||
<span className={styles.filteredThreadsCount}>
|
||||
{' '}
|
||||
- {t('filtered_threads')}: <strong>{filteredCount}</strong>
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
<CatalogSearchResultsLabel />
|
||||
{(isInAllView || showTimeFilter || isInCatalogView) && (
|
||||
<>
|
||||
<hr />
|
||||
@@ -683,6 +670,31 @@ export const PostPageStats = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const CatalogSearchResultsLabel = () => {
|
||||
const { t } = useTranslation();
|
||||
const { filteredCount, searchText } = useCatalogFiltersStore();
|
||||
|
||||
if (searchText) {
|
||||
return (
|
||||
<span className={styles.filteredThreadsCount}>
|
||||
{' '}
|
||||
— {t('search_results_for')}: <strong>{searchText}</strong>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredCount > 0) {
|
||||
return (
|
||||
<span className={styles.filteredThreadsCount}>
|
||||
{' '}
|
||||
— {t('filtered_threads')}: <strong>{filteredCount}</strong>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const DesktopBoardButtons = () => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
@@ -698,7 +710,6 @@ export const DesktopBoardButtons = () => {
|
||||
const isInModView = isModView(location.pathname);
|
||||
const isInModQueueView = isModQueueView(location.pathname);
|
||||
|
||||
const { filteredCount, searchText } = useCatalogFiltersStore();
|
||||
const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
|
||||
const isMultiboard = isInAllView || isInSubscriptionsView || isInModView;
|
||||
const showTimeFilter = isMultiboard;
|
||||
@@ -794,20 +805,7 @@ export const DesktopBoardButtons = () => {
|
||||
<ModQueueButton boardIdentifier={boardIdentifier} isMobile={false} />
|
||||
</>
|
||||
)}
|
||||
{isInCatalogView && searchText ? (
|
||||
<span className={styles.filteredThreadsCount}>
|
||||
{' '}
|
||||
- {t('search_results_for')}: <strong>{searchText}</strong>
|
||||
</span>
|
||||
) : (
|
||||
isInCatalogView &&
|
||||
filteredCount > 0 && (
|
||||
<span className={styles.filteredThreadsCount}>
|
||||
{' '}
|
||||
- {t('filtered_threads')}: <strong>{filteredCount}</strong>
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{isInCatalogView && <CatalogSearchResultsLabel />}
|
||||
<span className={styles.rightSideButtons}>
|
||||
{isInCatalogView && (
|
||||
<>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
CatalogFooterFirstRow,
|
||||
CatalogFooterStyleRow,
|
||||
PageFooterDesktop,
|
||||
PageFooterMobile,
|
||||
StyleOnlyFooterFirstRow,
|
||||
@@ -51,6 +52,7 @@ vi.mock('../../style-selector/style-selector', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../board-buttons/board-buttons', () => ({
|
||||
CatalogSearchResultsLabel: () => null,
|
||||
AutoButton: () => createElement('button', { type: 'button' }, 'auto-button'),
|
||||
CatalogButton: ({
|
||||
address,
|
||||
@@ -154,6 +156,7 @@ describe('footer', () => {
|
||||
isInAllView: true,
|
||||
communityAddress: 'music-posting.eth',
|
||||
}),
|
||||
createElement(CatalogFooterStyleRow),
|
||||
createElement(ThreadFooterStyleRow),
|
||||
createElement(PageFooterMobile, {
|
||||
children: createElement('div', { 'data-testid': 'mobile-child' }, 'mobile-child'),
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.footerCatalog {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.firstRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -76,7 +80,6 @@
|
||||
}
|
||||
|
||||
.styleLabel {
|
||||
font-size: 12px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,16 @@ import { useComment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import BoardsBar from '../boards-bar';
|
||||
import SiteLegalMeta from '../site-legal-meta';
|
||||
import StyleSelector from '../style-selector/style-selector';
|
||||
import { ReturnButton, CatalogButton, TopButton, UpdateButton, AutoButton, PostPageStats, RefreshButton } from '../board-buttons/board-buttons';
|
||||
import {
|
||||
CatalogSearchResultsLabel,
|
||||
ReturnButton,
|
||||
CatalogButton,
|
||||
TopButton,
|
||||
UpdateButton,
|
||||
AutoButton,
|
||||
PostPageStats,
|
||||
RefreshButton,
|
||||
} from '../board-buttons/board-buttons';
|
||||
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
|
||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||
import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store';
|
||||
@@ -22,15 +31,17 @@ import styles from './footer.module.css';
|
||||
|
||||
interface PageFooterDesktopProps {
|
||||
/** Mode-specific first row content (e.g. board pagination or thread controls) */
|
||||
firstRow: React.ReactNode;
|
||||
firstRow?: React.ReactNode;
|
||||
/** Optional row between first row and BoardsBar (e.g. style selector on thread page) */
|
||||
styleRow?: React.ReactNode;
|
||||
/** Catalog pages omit the default footer top margin */
|
||||
variant?: 'catalog';
|
||||
}
|
||||
|
||||
export const PageFooterDesktop = ({ firstRow, styleRow }: PageFooterDesktopProps) => (
|
||||
<footer className={styles.footer}>
|
||||
export const PageFooterDesktop = ({ firstRow, styleRow, variant }: PageFooterDesktopProps) => (
|
||||
<footer className={variant === 'catalog' ? `${styles.footer} ${styles.footerCatalog}` : styles.footer}>
|
||||
<hr />
|
||||
<div className={styles.firstRow}>{firstRow}</div>
|
||||
{firstRow != null ? <div className={styles.firstRow}>{firstRow}</div> : null}
|
||||
{styleRow != null ? <div className={styles.styleRow}>{styleRow}</div> : null}
|
||||
<div className={styles.boardsBarRow}>
|
||||
<BoardsBar />
|
||||
@@ -60,7 +71,7 @@ export const StyleOnlyFooterFirstRow = () => {
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* CatalogFooterFirstRow
|
||||
* Catalog footer first row: Return, Archive, Top, Refresh on left; Style selector on right.
|
||||
* Catalog footer nav row: Return, Catalog, Top, Refresh, plus search/filter label.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
interface CatalogFooterFirstRowProps {
|
||||
@@ -71,7 +82,6 @@ interface CatalogFooterFirstRowProps {
|
||||
}
|
||||
|
||||
export const CatalogFooterFirstRow = ({ communityAddress, isInAllView = false, isInSubscriptionsView = false, isInModView = false }: CatalogFooterFirstRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={styles.footerRow}>
|
||||
<div className={styles.footerLeft}>
|
||||
@@ -87,15 +97,27 @@ export const CatalogFooterFirstRow = ({ communityAddress, isInAllView = false, i
|
||||
<span>
|
||||
[<RefreshButton />]
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.footerRight}>
|
||||
<span className={styles.styleLabel}>{t('style')}:</span>
|
||||
<StyleSelector />
|
||||
<CatalogSearchResultsLabel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* CatalogFooterStyleRow
|
||||
* Catalog style selector on its own row (below nav, above BoardsBar).
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
export const CatalogFooterStyleRow = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span className={styles.styleRowContent}>
|
||||
<span className={styles.styleLabel}>{t('style')}:</span>
|
||||
<StyleSelector />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* ThreadFooterStyleRow
|
||||
* Style selector on its own row (thread page only, below first row, above BoardsBar).
|
||||
|
||||
@@ -3,6 +3,7 @@ export {
|
||||
PageFooterMobile,
|
||||
StyleOnlyFooterFirstRow,
|
||||
CatalogFooterFirstRow,
|
||||
CatalogFooterStyleRow,
|
||||
ThreadFooterFirstRow,
|
||||
ThreadFooterStyleRow,
|
||||
ThreadFooterMobile,
|
||||
|
||||
@@ -322,8 +322,15 @@ vi.mock('../../../components/catalog-row', () => ({
|
||||
|
||||
vi.mock('../../../components/footer', () => ({
|
||||
CatalogFooterFirstRow: ({ communityAddress }: { communityAddress?: string }) =>
|
||||
createElement('div', { 'data-testid': 'catalog-first-row' }, communityAddress || 'multi'),
|
||||
PageFooterDesktop: ({ firstRow }: { firstRow: React.ReactNode }) => createElement('div', { 'data-testid': 'catalog-footer-desktop' }, firstRow),
|
||||
createElement(
|
||||
'div',
|
||||
{ 'data-testid': 'catalog-first-row' },
|
||||
communityAddress || 'multi',
|
||||
testState.searchText ? ` - search_results_for: ${testState.searchText}` : null,
|
||||
),
|
||||
CatalogFooterStyleRow: () => createElement('div', { 'data-testid': 'catalog-style-row' }, 'style'),
|
||||
PageFooterDesktop: ({ firstRow, styleRow }: { firstRow?: React.ReactNode; styleRow?: React.ReactNode }) =>
|
||||
createElement('div', { 'data-testid': 'catalog-footer-desktop' }, firstRow, styleRow),
|
||||
PageFooterMobile: ({ children }: { children: React.ReactNode }) => createElement('div', { 'data-testid': 'catalog-footer-mobile' }, children),
|
||||
}));
|
||||
|
||||
@@ -902,6 +909,22 @@ describe('Catalog', () => {
|
||||
expect(container.querySelector('[data-testid="catalog-first-row"]')?.textContent).toBe('music-posting.eth');
|
||||
});
|
||||
|
||||
it('shows centered nothing found when catalog search has no matches', async () => {
|
||||
testState.feed = [{ cid: 'network-post', title: 'cats on stage', communityAddress: 'music-posting.eth' }];
|
||||
testState.searchText = 'asddasd';
|
||||
testState.hasMore = false;
|
||||
|
||||
await renderCatalog({ initialEntry: '/mu/catalog?q=asddasd', routePath: '/:boardIdentifier/catalog' });
|
||||
|
||||
expect(container.textContent).toContain('nothing_found');
|
||||
expect(container.textContent).not.toContain('no_threads');
|
||||
expect(container.querySelector('[role="status"]')?.className).toContain('searchNothingFound');
|
||||
expect(container.querySelector('[role="status"]')?.textContent).toBe('nothing_found');
|
||||
expect(container.querySelectorAll('[data-testid="catalog-row"]')).toHaveLength(0);
|
||||
expect(container.textContent).toContain('search_results_for');
|
||||
expect(container.textContent).toContain('asddasd');
|
||||
});
|
||||
|
||||
it('queries scoped recent account posts and still applies local search filtering before injecting them', async () => {
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||
testState.feed = [{ cid: 'network-post', title: 'cats on stage', communityAddress: 'music-posting.eth' }];
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
color: var(--post-mobile-abbr-text-color);
|
||||
}
|
||||
|
||||
.stateString {
|
||||
padding: 15px 0 10px 5px;
|
||||
color: var(--post-mobile-abbr-text-color);
|
||||
}
|
||||
|
||||
.searchNothingFound {
|
||||
color: red;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 1em 0;
|
||||
text-align: center;
|
||||
font-size: x-large;
|
||||
font-weight: bold;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.morePostsSuggestion {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import useSortingStore from '../../stores/use-sorting-store';
|
||||
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
||||
import { getCommunityAddress, isDirectoryBoard, normalizeMultiboardFeedPath } from '../../lib/utils/route-utils';
|
||||
import CatalogRow from '../../components/catalog-row';
|
||||
import { CatalogFooterFirstRow, PageFooterDesktop, PageFooterMobile } from '../../components/footer';
|
||||
import { CatalogFooterFirstRow, CatalogFooterStyleRow, PageFooterDesktop, PageFooterMobile } from '../../components/footer';
|
||||
import { ReturnButton, ArchiveButton, TopButton, RefreshButton } from '../../components/board-buttons/board-buttons';
|
||||
import mobileFooterStyles from '../../components/footer/footer.module.css';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
@@ -131,6 +131,20 @@ const CatalogFooter = ({
|
||||
|
||||
// Separate component for the loading state when there's no feed
|
||||
// This also calls useFeedStateString internally to isolate re-renders
|
||||
interface CatalogSearchNothingFoundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CatalogSearchNothingFound = ({ className }: CatalogSearchNothingFoundProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={className ?? styles.searchNothingFound} role='status'>
|
||||
{t('nothing_found')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface CatalogLoadingProps {
|
||||
communityAddresses: string[];
|
||||
hasMore: boolean;
|
||||
@@ -138,9 +152,10 @@ interface CatalogLoadingProps {
|
||||
state: string | undefined;
|
||||
subscriptionsLength: number;
|
||||
error: Error | undefined;
|
||||
hasActiveSearch: boolean;
|
||||
}
|
||||
|
||||
const CatalogLoading = ({ communityAddresses, hasMore, combinedFeedLength, state, subscriptionsLength, error }: CatalogLoadingProps) => {
|
||||
const CatalogLoading = ({ communityAddresses, hasMore, combinedFeedLength, state, subscriptionsLength, error, hasActiveSearch }: CatalogLoadingProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const rawFeedStateString = useFeedStateString(communityAddresses);
|
||||
@@ -153,7 +168,9 @@ const CatalogLoading = ({ communityAddresses, hasMore, combinedFeedLength, state
|
||||
) : subscriptionsLength === 0 ? (
|
||||
<span className='red'>{t('not_subscribed_to_any_board')}</span>
|
||||
) : !hasMore && combinedFeedLength === 0 ? (
|
||||
t('no_threads')
|
||||
hasActiveSearch ? null : (
|
||||
t('no_threads')
|
||||
)
|
||||
) : (
|
||||
hasMore && <LoadingEllipsis string={loadingStateString} />
|
||||
)}
|
||||
@@ -587,6 +604,11 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
const footerMoreThreadsSuggestion = showHiddenThreads ? null : moreThreadsSuggestion;
|
||||
const footerShowLoadingEllipsis = showHiddenThreads ? isLoadingHiddenCatalogThreads : effectiveInfiniteScroll;
|
||||
|
||||
const catalogFooterFirstRow = (
|
||||
<CatalogFooterFirstRow communityAddress={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
||||
);
|
||||
const catalogFooterStyleRow = <CatalogFooterStyleRow />;
|
||||
|
||||
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||
// Note: useFeedStateString is called inside CatalogFooter to isolate re-renders from backend state changes
|
||||
const footerComponents = useMemo(
|
||||
@@ -604,16 +626,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
onExpandTimeWindow={expandSuggestionTimeWindow}
|
||||
showLoadingEllipsis={footerShowLoadingEllipsis}
|
||||
/>
|
||||
<PageFooterDesktop
|
||||
firstRow={
|
||||
<CatalogFooterFirstRow
|
||||
communityAddress={communityAddress}
|
||||
isInAllView={isInAllView}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageFooterDesktop variant='catalog' firstRow={catalogFooterFirstRow} styleRow={catalogFooterStyleRow} />
|
||||
<PageFooterMobile>
|
||||
<div className={mobileFooterStyles.mobileFooterButtons}>
|
||||
<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
||||
@@ -633,6 +646,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
footerMoreThreadsSuggestion,
|
||||
moreThreadsSuggestionPathname,
|
||||
expandSuggestionTimeWindow,
|
||||
catalogFooterFirstRow,
|
||||
catalogFooterStyleRow,
|
||||
communityAddress,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
@@ -655,16 +670,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
onExpandTimeWindow={expandSuggestionTimeWindow}
|
||||
showLoadingEllipsis={footerShowLoadingEllipsis}
|
||||
/>
|
||||
<PageFooterDesktop
|
||||
firstRow={
|
||||
<CatalogFooterFirstRow
|
||||
communityAddress={communityAddress}
|
||||
isInAllView={isInAllView}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageFooterDesktop variant='catalog' firstRow={catalogFooterFirstRow} styleRow={catalogFooterStyleRow} />
|
||||
<PageFooterMobile>
|
||||
<div className={mobileFooterStyles.mobileFooterButtons}>
|
||||
<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
||||
@@ -683,6 +689,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
footerMoreThreadsSuggestion,
|
||||
moreThreadsSuggestionPathname,
|
||||
expandSuggestionTimeWindow,
|
||||
catalogFooterFirstRow,
|
||||
catalogFooterStyleRow,
|
||||
communityAddress,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
@@ -692,6 +700,9 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
],
|
||||
);
|
||||
const isFeedLoaded = feed.length > 0 || hiddenThreadsCount > 0 || state === 'failed';
|
||||
const hasActiveSearch = searchText.trim().length > 0;
|
||||
const showSearchNothingFound =
|
||||
hasActiveSearch && !footerHasMore && footerCombinedFeedLength === 0 && state !== 'failed' && !(isInSubscriptionsView && (subscriptions?.length || 0) === 0);
|
||||
|
||||
// Process the feed to move "top" posts to the top (applied after display sort)
|
||||
const processedFeed = useMemo(() => {
|
||||
@@ -896,6 +907,21 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : showSearchNothingFound ? (
|
||||
<>
|
||||
<CatalogSearchNothingFound />
|
||||
<hr />
|
||||
{catalogFooterFirstRow}
|
||||
<PageFooterDesktop variant='catalog' styleRow={catalogFooterStyleRow} />
|
||||
<PageFooterMobile>
|
||||
<div className={mobileFooterStyles.mobileFooterButtons}>
|
||||
<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
||||
<ArchiveButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
||||
<TopButton />
|
||||
<RefreshButton />
|
||||
</div>
|
||||
</PageFooterMobile>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.footer}>
|
||||
@@ -906,18 +932,10 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
state={state}
|
||||
subscriptionsLength={isInSubscriptionsView ? subscriptions?.length || 0 : 1}
|
||||
error={error}
|
||||
hasActiveSearch={hasActiveSearch}
|
||||
/>
|
||||
</div>
|
||||
<PageFooterDesktop
|
||||
firstRow={
|
||||
<CatalogFooterFirstRow
|
||||
communityAddress={communityAddress}
|
||||
isInAllView={isInAllView}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageFooterDesktop variant='catalog' firstRow={catalogFooterFirstRow} styleRow={catalogFooterStyleRow} />
|
||||
<PageFooterMobile>
|
||||
<div className={mobileFooterStyles.mobileFooterButtons}>
|
||||
<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
||||
|
||||
Reference in New Issue
Block a user