Merge Featured and All sections on discovery pages

Campaigns, Groups, and Pledges each previously stacked a Featured
shelf above an All-X section. Collapse them into a single section
titled simply 'Campaigns' / 'Groups' / 'Pledges' that:

- Idle (no query, no sort, no country) shows the moderator-featured
  grid. If nothing is featured yet, falls back to the chronological
  all-X grid so the page is never blank.
- Active (the user typed, picked Top/New, or chose a country) shows
  the full result set, ranked or chronological per the toolbar.

The shared toolbar drops the 'default' sort option from its dropdown
(now only Top and New). Clicking an already-active sort returns the
page to the curated idle view, giving users a clear exit affordance
now that 'default' is no longer an explicit menu choice.

Personal shelves (My pledges / My groups / Your campaigns) stay
above the unified section as separate, user-scoped lists.
This commit is contained in:
lemon
2026-05-28 13:28:35 -07:00
parent 51d3acd076
commit eebb6bf424
20 changed files with 326 additions and 320 deletions
@@ -133,7 +133,14 @@ export function DiscoverySearchToolbar({
key={value}
checked={sort === value}
onCheckedChange={(checked) => {
// `checked === false` means the user clicked the
// currently-active item — return to the curated
// `default` view (featured-first) rather than leaving
// them stuck on Top/New with no exit affordance now
// that `default` is no longer an exposed option in the
// dropdown.
if (checked) onSortChange(value);
else onSortChange('default');
}}
// The checkbox slot on the left is hidden in favour of an
// explicit `Check` on the right (matches the
+6 -6
View File
@@ -216,8 +216,8 @@
"myPledgesTagline": "التعهدات التي أنشأتها.",
"featuredPledges": "تعهدات مميزة",
"featuredPledgesTagline": "تعهدات يسلّط فريق {{appName}} الضوء عليها.",
"allPledges": "كل التعهدات",
"allPledgesTagline": "تصفّح كل تعهد على الشبكة.",
"allPledges": "التعهدات",
"allPledgesTagline": "مختارة من قِبل المشرفين. ابحث أو رتّب لتصفّح كل تعهد.",
"sectionActive": "التعهدات النشطة",
"sectionUpcoming": "التعهدات القادمة",
"sectionPast": "التعهدات السابقة",
@@ -347,8 +347,8 @@
"myGroupsTagline": "المجموعات التي أسستها أو تشرف عليها أو تتابعها.",
"featuredGroups": "المجموعات المميزة",
"featuredGroupsTagline": "مجموعات بارزة تستحق اهتمامك.",
"allGroups": "كل المجموعات",
"allGroupsTagline": "تصفّح المجموعات أو ابحث.",
"allGroups": "المجموعات",
"allGroupsTagline": "مختارة من قِبل المشرفين. ابحث أو رتّب لتصفّح كل مجموعة.",
"loginToSeeTitle": "سجّل الدخول لرؤية مجموعاتك",
"loginToSeeBody": "ستظهر هنا المجموعات التي أسستها أو التي تشرف عليها.",
"noGroupsTitle": "لا توجد مجموعات بعد",
@@ -704,10 +704,10 @@
"emptyHint": "كن أول من يبدأ حملة تمويل على {{appName}}. اروِ قصتك، اختر المستفيدين، وشارك الرابط."
},
"all": {
"title": "كل الحملات",
"title": "الحملات",
"seoTitle": "كل الحملات",
"description": "تصفّح كل الحملات المنشورة على Agora.",
"sectionTagline": "تصفّح كل قضيّة على الشبكة.",
"sectionTagline": "مختارة من قِبل المشرفين. ابحث أو رتّب لتصفّح الشبكة بأكملها.",
"heroKicker": "الحملات",
"heroHeading": "كل قضيّة،",
"heroHeadingLine2": "في مكان واحد.",
+6 -6
View File
@@ -644,8 +644,8 @@
"myPledgesTagline": "Pledges you've created.",
"featuredPledges": "Featured pledges",
"featuredPledgesTagline": "Pledges highlighted by the {{appName}} team.",
"allPledges": "All pledges",
"allPledgesTagline": "Browse every pledge on the network.",
"allPledges": "Pledges",
"allPledgesTagline": "Highlighted by moderators. Search or sort to browse every pledge.",
"sectionActive": "Active pledges",
"sectionUpcoming": "Upcoming pledges",
"sectionPast": "Past pledges",
@@ -779,8 +779,8 @@
"myGroupsTagline": "Groups you've founded, moderate, or follow.",
"featuredGroups": "Featured groups",
"featuredGroupsTagline": "Standout groups worth your attention.",
"allGroups": "All groups",
"allGroupsTagline": "Browse groups or search.",
"allGroups": "Groups",
"allGroupsTagline": "Highlighted by moderators. Search or sort to browse every group.",
"searchPlaceholder": "Search groups\u2026",
"searchAriaLabel": "Search groups",
"noMatch": "No groups match \u201c{{query}}\u201d",
@@ -1144,10 +1144,10 @@
"emptyHint": "Be the first to start a fundraiser on {{appName}}. Tell your story, choose your beneficiaries, and share the link."
},
"all": {
"title": "All Campaigns",
"title": "Campaigns",
"seoTitle": "All campaigns",
"description": "Browse every campaign published on Agora.",
"sectionTagline": "Browse every cause on the network.",
"sectionTagline": "Highlighted by moderators. Search or sort to browse the full network.",
"heroKicker": "Campaigns",
"heroHeading": "Every cause,",
"heroHeadingLine2": "in one place.",
+6 -6
View File
@@ -220,8 +220,8 @@
"myPledgesTagline": "Promesas que has creado.",
"featuredPledges": "Promesas destacadas",
"featuredPledgesTagline": "Promesas destacadas por el equipo de {{appName}}.",
"allPledges": "Todas las promesas",
"allPledgesTagline": "Explora todas las promesas de la red.",
"allPledges": "Promesas",
"allPledgesTagline": "Destacadas por los moderadores. Busca u ordena para explorar todas las promesas.",
"sectionActive": "Promesas activas",
"sectionUpcoming": "Promesas próximas",
"sectionPast": "Promesas pasadas",
@@ -355,8 +355,8 @@
"myGroupsTagline": "Grupos que fundaste, moderas o sigues.",
"featuredGroups": "Grupos destacados",
"featuredGroupsTagline": "Grupos destacados que merecen tu atención.",
"allGroups": "Todos los grupos",
"allGroupsTagline": "Explora grupos o busca.",
"allGroups": "Grupos",
"allGroupsTagline": "Destacados por los moderadores. Busca u ordena para explorar todos los grupos.",
"loginToSeeTitle": "Inicia sesión para ver tus grupos",
"loginToSeeBody": "Los grupos que fundaste o moderas aparecerán aquí.",
"noGroupsTitle": "Aún no hay grupos",
@@ -720,10 +720,10 @@
"noMatchHint": "Prueba con otro término de búsqueda o bórrala."
},
"all": {
"title": "Todas las campañas",
"title": "Campañas",
"seoTitle": "Todas las campañas",
"description": "Explora todas las campañas publicadas en Agora.",
"sectionTagline": "Explora cada causa en la red.",
"sectionTagline": "Destacadas por los moderadores. Busca u ordena para explorar toda la red.",
"heroKicker": "Campañas",
"heroHeading": "Cada causa,",
"heroHeadingLine2": "en un solo lugar.",
+6 -6
View File
@@ -220,8 +220,8 @@
"myPledgesTagline": "تعهدهایی که ایجاد کرده‌ای.",
"featuredPledges": "تعهدهای ویژه",
"featuredPledgesTagline": "تعهدهایی که تیم {{appName}} برجسته کرده است.",
"allPledges": "همهٔ تعهدها",
"allPledgesTagline": "همهٔ تعهدهای موجود در شبکه را مرور کن.",
"allPledges": "تعهدها",
"allPledgesTagline": "برگزیده‌ٔ ناظران. برای مرور همهٔ تعهدها جستجو یا مرتب‌سازی کن.",
"sectionActive": "تعهدهای فعال",
"sectionUpcoming": "تعهدهای پیش‌رو",
"sectionPast": "تعهدهای گذشته",
@@ -355,8 +355,8 @@
"myGroupsTagline": "گروه‌هایی که ساخته‌ای، مدیریت می‌کنی یا دنبال می‌کنی.",
"featuredGroups": "گروه‌های ویژه",
"featuredGroupsTagline": "گروه‌های برجسته‌ای که ارزش توجه تو را دارند.",
"allGroups": "همهٔ گروه‌ها",
"allGroupsTagline": "گروه‌ها را مرور کن یا جستجو کن.",
"allGroups": "گروه‌ها",
"allGroupsTagline": "برگزیده‌ٔ ناظران. برای مرور همهٔ گروه‌ها جستجو یا مرتب‌سازی کن.",
"loginToSeeTitle": "برای دیدن گروه‌هایت وارد شو",
"loginToSeeBody": "گروه‌هایی که ساخته‌ای یا مدیریت می‌کنی اینجا ظاهر می‌شوند.",
"noGroupsTitle": "هنوز گروهی نیست",
@@ -720,10 +720,10 @@
"noMatchHint": "عبارت جستجوی دیگری را امتحان کنید، یا جستجو را پاک کنید."
},
"all": {
"title": "همه کمپین‌ها",
"title": "کمپین‌ها",
"seoTitle": "همه کمپین‌ها",
"description": "همه کمپین‌های منتشرشده در Agora را مرور کنید.",
"sectionTagline": "هر هدفی را در شبکه مرور کنید.",
"sectionTagline": "برگزیده‌ٔ ناظران. برای مرور کل شبکه جستجو یا مرتب‌سازی کن.",
"heroKicker": "کمپین‌ها",
"heroHeading": "هر هدف،",
"heroHeadingLine2": "در یک جا.",
+6 -6
View File
@@ -642,8 +642,8 @@
"myPledgesTagline": "Les promesses que vous avez créées.",
"featuredPledges": "Promesses en vedette",
"featuredPledgesTagline": "Promesses mises en avant par l'équipe {{appName}}.",
"allPledges": "Toutes les promesses",
"allPledgesTagline": "Parcourez toutes les promesses du réseau.",
"allPledges": "Promesses",
"allPledgesTagline": "Sélectionnées par les modérateurs. Recherchez ou triez pour parcourir toutes les promesses.",
"sectionActive": "Promesses actives",
"sectionUpcoming": "Promesses à venir",
"sectionPast": "Promesses passées",
@@ -777,8 +777,8 @@
"myGroupsTagline": "Les groupes que vous avez fondés, modérez ou suivez.",
"featuredGroups": "Groupes mis en avant",
"featuredGroupsTagline": "Des groupes remarquables qui méritent votre attention.",
"allGroups": "Tous les groupes",
"allGroupsTagline": "Parcourez les groupes ou recherchez.",
"allGroups": "Groupes",
"allGroupsTagline": "Sélectionnés par les modérateurs. Recherchez ou triez pour parcourir tous les groupes.",
"loginToSeeTitle": "Connectez-vous pour voir vos groupes",
"loginToSeeBody": "Les groupes que vous avez fondés ou modérés apparaîtront ici.",
"noGroupsTitle": "Aucun groupe pour l'instant",
@@ -1142,10 +1142,10 @@
"noMatchHint": "Essayez un autre terme de recherche, ou effacez la recherche."
},
"all": {
"title": "Toutes les campagnes",
"title": "Campagnes",
"seoTitle": "Toutes les campagnes",
"description": "Parcourez toutes les campagnes publiées sur Agora.",
"sectionTagline": "Parcourez toutes les causes du réseau.",
"sectionTagline": "Sélectionnées par les modérateurs. Recherchez ou triez pour parcourir tout le réseau.",
"heroKicker": "Campagnes",
"heroHeading": "Chaque cause,",
"heroHeadingLine2": "au même endroit.",
+6 -6
View File
@@ -652,8 +652,8 @@
"myPledgesTagline": "आपके बनाए हुए प्लेज।",
"featuredPledges": "विशेष प्लेज",
"featuredPledgesTagline": "{{appName}} टीम द्वारा चुने गए प्लेज।",
"allPledges": "सभी प्लेज",
"allPledgesTagline": "नेटवर्क पर मौजूद हर प्लेज देखें।",
"allPledges": "प्लेज",
"allPledgesTagline": "मॉडरेटर द्वारा चयनित। हर प्लेज देखने के लिए खोजें या क्रमबद्ध करें।",
"sectionActive": "सक्रिय प्लेज",
"sectionUpcoming": "आने वाले प्लेज",
"sectionPast": "पिछले प्लेज",
@@ -787,8 +787,8 @@
"myGroupsTagline": "जिन ग्रुप को आपने बनाया, मॉडरेट किया, या फ़ॉलो किया है।",
"featuredGroups": "फ़ीचर्ड ग्रुप",
"featuredGroupsTagline": "आपके ध्यान के लायक ख़ास ग्रुप।",
"allGroups": "सभी ग्रुप",
"allGroupsTagline": "ग्रुप ब्राउज़ करें या खोजें।",
"allGroups": "ग्रुप",
"allGroupsTagline": "मॉडरेटर द्वारा चयनित। हर ग्रुप देखने के लिए खोजें या क्रमबद्ध करें।",
"loginToSeeTitle": "अपने ग्रुप देखने के लिए लॉग इन करें",
"loginToSeeBody": "आपने जो ग्रुप बनाए या मॉडरेट किए हैं वे यहाँ दिखेंगे।",
"noGroupsTitle": "अभी कोई ग्रुप नहीं",
@@ -1152,10 +1152,10 @@
"noMatchHint": "अलग खोज शब्द आज़माएँ, या खोज साफ़ करें।"
},
"all": {
"title": "सभी कैंपेन",
"title": "कैंपेन",
"seoTitle": "सभी कैंपेन",
"description": "Agora पर पब्लिश हुए हर कैंपेन को देखें।",
"sectionTagline": "नेटवर्क पर हर मक़सद को देखें।",
"sectionTagline": "मॉडरेटर द्वारा चयनित। पूरे नेटवर्क को देखने के लिए खोजें या क्रमबद्ध करें।",
"heroKicker": "कैंपेन",
"heroHeading": "हर मक़सद,",
"heroHeadingLine2": "एक ही जगह।",
+6 -6
View File
@@ -652,8 +652,8 @@
"myPledgesTagline": "Ikrar yang Anda buat.",
"featuredPledges": "Ikrar pilihan",
"featuredPledgesTagline": "Ikrar yang disorot oleh tim {{appName}}.",
"allPledges": "Semua ikrar",
"allPledgesTagline": "Jelajahi setiap ikrar di jaringan.",
"allPledges": "Ikrar",
"allPledgesTagline": "Disorot oleh moderator. Cari atau urutkan untuk menjelajahi setiap ikrar.",
"sectionActive": "Ikrar aktif",
"sectionUpcoming": "Ikrar mendatang",
"sectionPast": "Ikrar lampau",
@@ -787,8 +787,8 @@
"myGroupsTagline": "Grup yang Anda dirikan, moderasi, atau ikuti.",
"featuredGroups": "Grup unggulan",
"featuredGroupsTagline": "Grup menonjol yang layak Anda perhatikan.",
"allGroups": "Semua grup",
"allGroupsTagline": "Jelajahi grup atau cari.",
"allGroups": "Grup",
"allGroupsTagline": "Disorot oleh moderator. Cari atau urutkan untuk menjelajahi setiap grup.",
"loginToSeeTitle": "Masuk untuk melihat grup Anda",
"loginToSeeBody": "Grup yang Anda dirikan atau moderasi akan muncul di sini.",
"noGroupsTitle": "Belum ada grup",
@@ -1152,10 +1152,10 @@
"noMatchHint": "Coba kata pencarian lain, atau bersihkan pencarian."
},
"all": {
"title": "Semua Kampanye",
"title": "Kampanye",
"seoTitle": "Semua kampanye",
"description": "Telusuri setiap kampanye yang dipublikasikan di Agora.",
"sectionTagline": "Jelajahi setiap aksi di jaringan.",
"sectionTagline": "Disorot oleh moderator. Cari atau urutkan untuk menjelajahi seluruh jaringan.",
"heroKicker": "Kampanye",
"heroHeading": "Setiap aksi,",
"heroHeadingLine2": "dalam satu tempat.",
+6 -6
View File
@@ -220,8 +220,8 @@
"myPledgesTagline": "ការសន្យាដែលអ្នកបានបង្កើត។",
"featuredPledges": "ការសន្យាលេចធ្លោ",
"featuredPledgesTagline": "ការសន្យាដែលត្រូវបានរំលេចដោយក្រុម {{appName}}។",
"allPledges": "ការសន្យាទាំងអស់",
"allPledgesTagline": "រកមើលការសន្យាគ្រប់ៗមួយនៅលើបណ្ដាញ។",
"allPledges": "ការសន្យា",
"allPledgesTagline": "រំលេចដោយអ្នកសម្របសម្រួល។ ស្វែងរក ឬតម្រៀបដើម្បីរកមើលការសន្យាគ្រប់ៗមួយ។",
"sectionActive": "ការសន្យាសកម្ម",
"sectionUpcoming": "ការសន្យាខាងមុខ",
"sectionPast": "ការសន្យាកន្លងមក",
@@ -355,8 +355,8 @@
"myGroupsTagline": "ក្រុមដែលអ្នកបានបង្កើត គ្រប់គ្រង ឬដាក់តាមដាន។",
"featuredGroups": "ក្រុមលេចធ្លោ",
"featuredGroupsTagline": "ក្រុមលេចធ្លោដែលសក្តិសមនឹងការយកចិត្តទុកដាក់របស់អ្នក។",
"allGroups": "ក្រុមទាំងអស់",
"allGroupsTagline": "រកមើលក្រុម ឬស្វែងរក។",
"allGroups": "ក្រុម",
"allGroupsTagline": "រំលេចដោយអ្នកសម្របសម្រួល។ ស្វែងរក ឬតម្រៀបដើម្បីរកមើលក្រុមគ្រប់ៗមួយ។",
"loginToSeeTitle": "ចូលដើម្បីមើលក្រុមរបស់អ្នក",
"loginToSeeBody": "ក្រុមដែលអ្នកបានបង្កើត ឬគ្រប់គ្រងនឹងបង្ហាញនៅទីនេះ។",
"noGroupsTitle": "មិនទាន់មានក្រុមទេ",
@@ -720,10 +720,10 @@
"noMatchHint": "សាកល្បងពាក្យស្វែងរកផ្សេង ឬសម្អាតការស្វែងរក។"
},
"all": {
"title": "យុទ្ធនាការទាំងអស់",
"title": "យុទ្ធនាការ",
"seoTitle": "យុទ្ធនាការទាំងអស់",
"description": "រកមើលរាល់យុទ្ធនាការដែលផ្សព្វផ្សាយលើ Agora។",
"sectionTagline": "រកមើលរាល់បុព្វហេតុនៅលើបណ្ដាញ។",
"sectionTagline": "រំលេចដោយអ្នកសម្របសម្រួល។ ស្វែងរក ឬតម្រៀបដើម្បីរកមើលបណ្ដាញទាំងមូល។",
"heroKicker": "យុទ្ធនាការ",
"heroHeading": "រាល់បុព្វហេតុ",
"heroHeadingLine2": "នៅកន្លែងតែមួយ។",
+6 -6
View File
@@ -220,8 +220,8 @@
"myPledgesTagline": "هغه ژمنې چې تاسو جوړې کړې دي.",
"featuredPledges": "ځانګړې ژمنې",
"featuredPledgesTagline": "هغه ژمنې چې د {{appName}} ټیم په ګوته کړې دي.",
"allPledges": "ټولې ژمنې",
"allPledgesTagline": "په شبکه کې هره ژمنه وګورئ.",
"allPledges": "ژمنې",
"allPledgesTagline": "د څارونکو لخوا ځانګړې شوې. د ټولو ژمنو لیدلو لپاره لټون یا ترتیب وکړئ.",
"sectionActive": "فعالې ژمنې",
"sectionUpcoming": "راتلونکې ژمنې",
"sectionPast": "تېرې ژمنې",
@@ -355,8 +355,8 @@
"myGroupsTagline": "هغه ډلې چې تاسو یې جوړې کړي، اداره کوئ، یا یې تعقیبوئ.",
"featuredGroups": "ځانګړې ډلې",
"featuredGroupsTagline": "ځانګړې ډلې چې ستاسو د پاملرنې وړ دي.",
"allGroups": "ټولې ډلې",
"allGroupsTagline": "ډلې وڅیړئ یا لټون وکړئ.",
"allGroups": "ډلې",
"allGroupsTagline": "د څارونکو لخوا ځانګړې شوې. د ټولو ډلو لیدلو لپاره لټون یا ترتیب وکړئ.",
"loginToSeeTitle": "د خپلو ډلو لیدلو لپاره ننوځئ",
"loginToSeeBody": "هغه ډلې چې تاسو یې جوړې کړي یا یې اداره کوئ به دلته راڅرګندې شي.",
"noGroupsTitle": "تر اوسه ډلې نشته",
@@ -726,10 +726,10 @@
"heroBody": "په Nostr کې خپور شوی هر د مرستو راټولولو کمپاین په یوه ځای کې راټول شوی. د بشپړې شبکې لټون وکړئ، هغه هدف ومومئ چې درته اهمیت لري، او په مستقیم ډول یې د بټکوین له لارې ملاتړ وکړئ.",
"campaignsCount_one": "په شبکه کې کمپاین",
"campaignsCount_other": "په شبکه کې کمپاینونه",
"title": "ټول کمپاینونه",
"title": "کمپاینونه",
"seoTitle": "ټول کمپاینونه",
"description": "په Agora کې خپور شوي ټول کمپاینونه وګورئ.",
"sectionTagline": "په شبکه کې هر هدف وپلټئ.",
"sectionTagline": "د څارونکو لخوا ځانګړي شوي. د ټولې شبکې لیدلو لپاره لټون یا ترتیب وکړئ.",
"searchAriaLabel": "د کمپاینونو لټون",
"searchPlaceholder": "د کمپاینونو لټون…",
"clearSearch": "د لټون پاکول",
+6 -6
View File
@@ -652,8 +652,8 @@
"myPledgesTagline": "Promessas que você criou.",
"featuredPledges": "Promessas em destaque",
"featuredPledgesTagline": "Promessas destacadas pela equipe do {{appName}}.",
"allPledges": "Todas as promessas",
"allPledgesTagline": "Explore todas as promessas da rede.",
"allPledges": "Promessas",
"allPledgesTagline": "Destacadas pelos moderadores. Pesquise ou ordene para explorar todas as promessas.",
"sectionActive": "Promessas ativas",
"sectionUpcoming": "Promessas futuras",
"sectionPast": "Promessas passadas",
@@ -787,8 +787,8 @@
"myGroupsTagline": "Grupos que você fundou, modera ou segue.",
"featuredGroups": "Grupos em destaque",
"featuredGroupsTagline": "Grupos que se destacam e merecem sua atenção.",
"allGroups": "Todos os grupos",
"allGroupsTagline": "Explore os grupos ou pesquise.",
"allGroups": "Grupos",
"allGroupsTagline": "Destacados pelos moderadores. Pesquise ou ordene para explorar todos os grupos.",
"loginToSeeTitle": "Entre para ver seus grupos",
"loginToSeeBody": "Grupos que você fundou ou modera aparecerão aqui.",
"noGroupsTitle": "Nenhum grupo ainda",
@@ -1152,10 +1152,10 @@
"noMatchHint": "Tente um termo de pesquisa diferente, ou limpe a pesquisa."
},
"all": {
"title": "Todas as campanhas",
"title": "Campanhas",
"seoTitle": "Todas as campanhas",
"description": "Navegue por todas as campanhas publicadas no Agora.",
"sectionTagline": "Conheça todas as causas da rede.",
"sectionTagline": "Destacadas pelos moderadores. Pesquise ou ordene para explorar toda a rede.",
"heroKicker": "Campanhas",
"heroHeading": "Cada causa,",
"heroHeadingLine2": "em um só lugar.",
+6 -6
View File
@@ -652,8 +652,8 @@
"myPledgesTagline": "Обещания, которые вы создали.",
"featuredPledges": "Избранные обещания",
"featuredPledgesTagline": "Обещания, отмеченные командой {{appName}}.",
"allPledges": "Все обещания",
"allPledgesTagline": "Просматривайте все обещания в сети.",
"allPledges": "Обещания",
"allPledgesTagline": "Отмечено модераторами. Используйте поиск или сортировку, чтобы просмотреть все обещания.",
"sectionActive": "Активные обещания",
"sectionUpcoming": "Предстоящие обещания",
"sectionPast": "Прошлые обещания",
@@ -787,8 +787,8 @@
"myGroupsTagline": "Группы, которые вы основали, модерируете или на которые подписаны.",
"featuredGroups": "Избранные группы",
"featuredGroupsTagline": "Заметные группы, достойные вашего внимания.",
"allGroups": "Все группы",
"allGroupsTagline": "Просматривайте группы или ищите.",
"allGroups": "Группы",
"allGroupsTagline": "Отмечено модераторами. Используйте поиск или сортировку, чтобы просмотреть все группы.",
"loginToSeeTitle": "Войдите, чтобы увидеть свои группы",
"loginToSeeBody": "Группы, которые вы основали или модерируете, появятся здесь.",
"noGroupsTitle": "Пока нет групп",
@@ -1152,10 +1152,10 @@
"noMatchHint": "Попробуйте другой поисковый запрос или очистите поиск."
},
"all": {
"title": "Все кампании",
"title": "Кампании",
"seoTitle": "Все кампании",
"description": "Просмотрите все кампании, опубликованные на Agora.",
"sectionTagline": "Просмотрите каждое дело в сети.",
"sectionTagline": "Отмечено модераторами. Используйте поиск или сортировку, чтобы просмотреть всю сеть.",
"heroKicker": "Кампании",
"heroHeading": "Каждое дело —",
"heroHeadingLine2": "в одном месте.",
+6 -6
View File
@@ -220,8 +220,8 @@
"myPledgesTagline": "Zvitsidziro zvawakagadzira.",
"featuredPledges": "Zvitsidziro zvakasarudzwa",
"featuredPledgesTagline": "Zvitsidziro zvakasimudzirwa nechikwata che{{appName}}.",
"allPledges": "Zvitsidziro zvose",
"allPledgesTagline": "Tarisa chitsidziro chega chega chiri pamutambo wenetiweki.",
"allPledges": "Zvitsidziro",
"allPledgesTagline": "Zvakatarisirwa nevatariri. Tsvaga kana kuronga kuti utarise chitsidziro chega chega.",
"sectionActive": "Zvitsidziro zviri kushanda",
"sectionUpcoming": "Zvitsidziro zviri kuuya",
"sectionPast": "Zvitsidziro zvapfuura",
@@ -355,8 +355,8 @@
"myGroupsTagline": "Mapoka awakatanga, aunotungamira, kana aunotevera.",
"featuredGroups": "Mapoka anokurumbira",
"featuredGroupsTagline": "Mapoka anobudirira anokodzera kutariswa nemi.",
"allGroups": "Mapoka ose",
"allGroupsTagline": "Tarisa mapoka kana kutsvaga.",
"allGroups": "Mapoka",
"allGroupsTagline": "Zvakatarisirwa nevatariri. Tsvaga kana kuronga kuti utarise boka rega rega.",
"loginToSeeTitle": "Pinda kuti uone mapoka ako",
"loginToSeeBody": "Mapoka awakatanga kana aunotungamira achaonekwa pano.",
"noGroupsTitle": "Hapana mapoka parizvino",
@@ -720,10 +720,10 @@
"noMatchHint": "Edza chimwe chinotsvagwa, kana ubvise kutsvaga."
},
"all": {
"title": "Mishandirapamwe Yose",
"title": "Mishandirapamwe",
"seoTitle": "Mishandirapamwe yose",
"description": "Tarisa mishandirapamwe yose yakaiswa paAgora.",
"sectionTagline": "Tarisa chinangwa chimwe nechimwe panetwork.",
"sectionTagline": "Zvakatarisirwa nevatariri. Tsvaga kana kuronga kuti utarise netiweki yose.",
"heroKicker": "Mishandirapamwe",
"heroHeading": "Chinangwa chimwe nechimwe,",
"heroHeadingLine2": "panzvimbo imwe chete.",
+6 -6
View File
@@ -651,8 +651,8 @@
"myPledgesTagline": "Ahadi ulizounda.",
"featuredPledges": "Ahadi maalum",
"featuredPledgesTagline": "Ahadi zilizoangaziwa na timu ya {{appName}}.",
"allPledges": "Ahadi zote",
"allPledgesTagline": "Vinjari kila ahadi kwenye mtandao.",
"allPledges": "Ahadi",
"allPledgesTagline": "Zimeangaziwa na wasimamizi. Tafuta au panga ili kuvinjari kila ahadi.",
"sectionActive": "Ahadi zinazoendelea",
"sectionUpcoming": "Ahadi zijazo",
"sectionPast": "Ahadi zilizopita",
@@ -786,8 +786,8 @@
"myGroupsTagline": "Vikundi ulivyounda, unavyosimamia, au unavyofuata.",
"featuredGroups": "Vikundi maarufu",
"featuredGroupsTagline": "Vikundi vinavyojitokeza vinavyostahili usikivu wako.",
"allGroups": "Vikundi vyote",
"allGroupsTagline": "Vinjari vikundi au tafuta.",
"allGroups": "Vikundi",
"allGroupsTagline": "Vimeangaziwa na wasimamizi. Tafuta au panga ili kuvinjari kila kikundi.",
"loginToSeeTitle": "Ingia ili kuona vikundi vyako",
"loginToSeeBody": "Vikundi ulivyounda au unavyosimamia vitaonekana hapa.",
"noGroupsTitle": "Hakuna vikundi bado",
@@ -1151,10 +1151,10 @@
"noMatchHint": "Jaribu neno tofauti la utafutaji, au futa utafutaji."
},
"all": {
"title": "Kampeni Zote",
"title": "Kampeni",
"seoTitle": "Kampeni zote",
"description": "Vinjari kila kampeni iliyochapishwa kwenye Agora.",
"sectionTagline": "Vinjari kila lengo kwenye mtandao.",
"sectionTagline": "Zimeangaziwa na wasimamizi. Tafuta au panga ili kuvinjari mtandao mzima.",
"searchAriaLabel": "Tafuta kampeni",
"searchPlaceholder": "Tafuta kampeni…",
"clearSearch": "Futa utafutaji",
+6 -6
View File
@@ -651,8 +651,8 @@
"myPledgesTagline": "Oluşturduğunuz taahhütler.",
"featuredPledges": "Öne çıkan taahhütler",
"featuredPledgesTagline": "{{appName}} ekibinin öne çıkardığı taahhütler.",
"allPledges": "Tüm taahhütler",
"allPledgesTagline": "Ağdaki her taahhüde göz atın.",
"allPledges": "Taahhütler",
"allPledgesTagline": "Moderatörler tarafından öne çıkarıldı. Her taahhüde göz atmak için arama yapın veya sıralayın.",
"sectionActive": "Aktif taahhütler",
"sectionUpcoming": "Yaklaşan taahhütler",
"sectionPast": "Geçmiş taahhütler",
@@ -786,8 +786,8 @@
"myGroupsTagline": "Kurduğunuz, yönettiğiniz veya takip ettiğiniz gruplar.",
"featuredGroups": "Öne çıkan gruplar",
"featuredGroupsTagline": "Dikkatinize değer öne çıkan gruplar.",
"allGroups": "Tüm gruplar",
"allGroupsTagline": "Gruplara göz atın veya arama yapın.",
"allGroups": "Gruplar",
"allGroupsTagline": "Moderatörler tarafından öne çıkarıldı. Her gruba göz atmak için arama yapın veya sıralayın.",
"loginToSeeTitle": "Gruplarınızı görmek için giriş yapın",
"loginToSeeBody": "Kurduğunuz veya yönettiğiniz gruplar burada görünecek.",
"noGroupsTitle": "Henüz grup yok",
@@ -1151,10 +1151,10 @@
"noMatchHint": "Farklı bir arama terimi deneyin ya da aramayı temizleyin."
},
"all": {
"title": "Tüm Kampanyalar",
"title": "Kampanyalar",
"seoTitle": "Tüm kampanyalar",
"description": "Agora'da yayımlanmış her kampanyaya göz atın.",
"sectionTagline": "Ağdaki her davaya göz atın.",
"sectionTagline": "Moderatörler tarafından öne çıkarıldı. Ağın tamamına göz atmak için arama yapın veya sıralayın.",
"searchAriaLabel": "Kampanyaları ara",
"searchPlaceholder": "Kampanya ara…",
"clearSearch": "Aramayı temizle",
+6 -6
View File
@@ -220,8 +220,8 @@
"myPledgesTagline": "你建立的懸賞。",
"featuredPledges": "精選懸賞",
"featuredPledgesTagline": "{{appName}} 團隊重點推薦的懸賞。",
"allPledges": "全部懸賞",
"allPledgesTagline": "瀏覽網路上的每一個懸賞。",
"allPledges": "懸賞",
"allPledgesTagline": "由版主重點推薦。搜尋或排序以瀏覽所有懸賞。",
"sectionActive": "進行中的懸賞",
"sectionUpcoming": "即將開始的懸賞",
"sectionPast": "已結束的懸賞",
@@ -355,8 +355,8 @@
"myGroupsTagline": "你建立、管理或追蹤的群組。",
"featuredGroups": "精選群組",
"featuredGroupsTagline": "值得你關注的出色群組。",
"allGroups": "全部群組",
"allGroupsTagline": "瀏覽群組或搜尋。",
"allGroups": "群組",
"allGroupsTagline": "由版主重點推薦。搜尋或排序以瀏覽所有群組。",
"loginToSeeTitle": "登入以檢視你的群組",
"loginToSeeBody": "你建立或管理的群組會顯示在這裡。",
"noGroupsTitle": "還沒有群組",
@@ -720,10 +720,10 @@
"noMatchHint": "請嘗試不同的搜尋字詞,或清除搜尋。"
},
"all": {
"title": "所有活動",
"title": "活動",
"seoTitle": "所有活動",
"description": "瀏覽 Agora 上釋出的所有活動。",
"sectionTagline": "瀏覽網路上的每一項事業。",
"sectionTagline": "由版主重點推薦。搜尋或排序以瀏覽整個網路。",
"heroKicker": "活動",
"heroHeading": "每一份心意,",
"heroHeadingLine2": "匯聚於此。",
+6 -6
View File
@@ -220,8 +220,8 @@
"myPledgesTagline": "你创建的悬赏。",
"featuredPledges": "精选悬赏",
"featuredPledgesTagline": "{{appName}} 团队重点推荐的悬赏。",
"allPledges": "全部悬赏",
"allPledgesTagline": "浏览网络上的每一个悬赏。",
"allPledges": "悬赏",
"allPledgesTagline": "由版主精选推荐。可搜索或排序以浏览全部悬赏。",
"sectionActive": "进行中的悬赏",
"sectionUpcoming": "即将开始的悬赏",
"sectionPast": "已结束的悬赏",
@@ -355,8 +355,8 @@
"myGroupsTagline": "你创建、管理或关注的群组。",
"featuredGroups": "精选群组",
"featuredGroupsTagline": "值得你关注的出色群组。",
"allGroups": "全部群组",
"allGroupsTagline": "浏览群组或搜索。",
"allGroups": "群组",
"allGroupsTagline": "由版主精选推荐。可搜索或排序以浏览全部群组。",
"loginToSeeTitle": "登录以查看你的群组",
"loginToSeeBody": "你创建或管理的群组会显示在这里。",
"noGroupsTitle": "还没有群组",
@@ -720,10 +720,10 @@
"noMatchHint": "尝试其他搜索词,或清除搜索。"
},
"all": {
"title": "所有活动",
"title": "活动",
"seoTitle": "所有活动",
"description": "浏览 Agora 上发布的所有活动。",
"sectionTagline": "浏览网络上的每一项事业。",
"sectionTagline": "由版主精选推荐。可搜索或排序以浏览整个网络。",
"heroKicker": "活动",
"heroHeading": "每一个理念,",
"heroHeadingLine2": "汇聚于此。",
+47 -43
View File
@@ -36,7 +36,7 @@ import {
import {
HandHeart, PlusCircle, ChevronDown, ChevronUp, Loader2,
Link as LinkIcon, Check, MoreHorizontal, Trash2,
Megaphone, Sparkles, EyeOff,
Megaphone, EyeOff,
} from 'lucide-react';
// ─────────────────────────────────────────────────────────────────────────────
@@ -323,6 +323,10 @@ export default function ActionsPage() {
return { searchHits: visible, searchHiddenCount: hidden };
}, [searchHitsRaw, pledgeModeration, canShowHidden]);
// `actions` is the chronological pledge list filtered by country, with
// moderator-hidden items dropped (unless `showHidden` is on). Featured
// pledges are NOT excluded here — the idle render path pulls them
// separately, and the active render path shows the full list.
const { actions, listHiddenCount } = useMemo(() => {
if (!rawActions) return { actions: undefined, listHiddenCount: 0 };
const hiddenCoords = pledgeModeration?.hiddenCoords ?? new Set<string>();
@@ -365,16 +369,18 @@ export default function ActionsPage() {
const DEFAULT_VISIBLE = 4;
const [showAllMine, setShowAllMine] = useState(false);
const [showAllFeatured, setShowAllFeatured] = useState(false);
const [showAllPledges, setShowAllPledges] = useState(false);
const allPledges = useMemo(
() => (actions ?? []).filter((action) => !featuredPledgeCoordSet.has(getPledgeCoord(action))),
[actions, featuredPledgeCoordSet],
);
const visibleMine = showAllMine ? (myPledges ?? []) : (myPledges ?? []).slice(0, DEFAULT_VISIBLE);
const visibleFeatured = showAllFeatured ? orderedFeaturedPledges : orderedFeaturedPledges.slice(0, DEFAULT_VISIBLE);
const visibleAllPledges = showAllPledges ? allPledges : allPledges.slice(0, DEFAULT_VISIBLE);
// Idle list: featured first; if none are featured, fall back to the
// chronological all-pledges grid so the page is never blank.
const idlePledges = useMemo<Action[]>(() => {
if (orderedFeaturedPledges.length > 0) return orderedFeaturedPledges;
return (actions ?? []).filter(
(action) => !featuredPledgeCoordSet.has(getPledgeCoord(action)),
);
}, [orderedFeaturedPledges, actions, featuredPledgeCoordSet]);
const hiddenPledges = useMemo<Action[]>(() => {
if (!isMod || !pledgeModerationReady) return [];
return (allPledgesForMods ?? []).filter((pledge) => pledgeModeration.hiddenCoords.has(getPledgeCoord(pledge)));
@@ -386,6 +392,7 @@ export default function ActionsPage() {
onQueryChange={setSearchInput}
sort={sortMode}
onSortChange={setSortMode}
sortOptions={['top', 'new']}
searchPlaceholderKey="pledges.list.searchPlaceholder"
searchAriaLabelKey="pledges.list.searchAriaLabel"
showHidden={isMod ? {
@@ -424,37 +431,18 @@ export default function ActionsPage() {
</section>
)}
{orderedFeaturedPledges.length > 0 && (
<section className="space-y-5">
<div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight inline-flex items-center gap-2">
<Sparkles className="size-6 text-primary" />
{t('pledges.list.featuredPledges')}
</h2>
<p className="text-sm text-muted-foreground mt-1">{t('pledges.list.featuredPledgesTagline')}</p>
</div>
<ActionSection
items={visibleFeatured}
total={orderedFeaturedPledges.length}
visible={DEFAULT_VISIBLE}
showAll={showAllFeatured}
onToggle={() => setShowAllFeatured(!showAllFeatured)}
btcPrice={btcPrice}
/>
</section>
)}
{/* Unified Pledges section.
- Idle (no search / no sort / no country): renders the
moderator-featured grid, or a chronological fallback when
no pledges are featured yet.
- Active: renders the full search/sort/country result set. */}
<section className="space-y-5">
<div className="flex flex-col items-stretch gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
{trimmedSearch
? t('common.search')
: isSearching && sortMode === 'top'
? t('common.sortTop')
: isSearching && sortMode === 'new'
? t('common.sortNew')
: t('pledges.list.allPledges')}
: t('pledges.list.allPledges')}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{isSearching && searchHits
@@ -521,15 +509,31 @@ export default function ActionsPage() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
{Array.from({ length: 8 }).map((_, i) => <ActionSkeleton key={i} />)}
</div>
) : allPledges.length > 0 ? (
<ActionSection
items={visibleAllPledges}
total={allPledges.length}
visible={DEFAULT_VISIBLE}
showAll={showAllPledges}
onToggle={() => setShowAllPledges(!showAllPledges)}
btcPrice={btcPrice}
/>
) : idlePledges.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
{idlePledges.map((action) => (
<PledgeCard
key={`${action.pubkey}:${action.id}`}
action={action}
btcPrice={btcPrice}
showAuthor
showTranslate
topRight={
<>
<ModerationOverlay
coord={getPledgeCoord(action)}
entityTitle={action.title}
surface="pledge"
axes={['hide', 'featured']}
showMenu={false}
className="flex items-center"
/>
<ActionShareMenu action={action} displayTitle={action.title} />
</>
}
/>
))}
</div>
) : (
<Card className="border-dashed">
<div className="py-12 px-8 text-center space-y-4">
+142 -136
View File
@@ -23,38 +23,47 @@ import { cn } from '@/lib/utils';
import type { Nip50Sort } from '@/hooks/useNip50Search';
import type { ParsedCampaign } from '@/lib/campaign';
/** Type-guard for the `?sort=` URL param. Default is `top` (most-zapped). */
function parseSort(value: string | null): CampaignSort {
return value === 'none' ? 'none' : 'top';
/**
* Type-guard for the `?sort=` URL param.
*
* - `top` and `new` map to the toolbar's active sort modes.
* - Anything else (missing, empty, legacy values) collapses to
* `'default'`, the curated featured-first idle state.
*/
function parseSort(value: string | null): Nip50Sort {
if (value === 'top') return 'top';
if (value === 'new') return 'new';
return 'default';
}
/**
* Map between the shared toolbar's sort vocabulary (`default` / `top` /
* `new`) and the `useAllCampaigns` hook's vocabulary (`top` / `none`).
*
* AllCampaignsPage doesn't have a curated/default layout — it's the
* "show me everything" page — so the toolbar's 'default' option falls
* through to 'top' here, the page's canonical ranked view. The legacy
* `none` value is preserved on the URL so existing share links keep
* working.
* Map the toolbar's sort vocabulary (`default` / `top` / `new`) to the
* `useAllCampaigns` hook's vocabulary (`top` / `none`). `'new'` and
* `'default'` both map to `'none'` (chronological) — the page handles
* the "show featured only" framing on top of that.
*/
const toToolbarSort = (s: CampaignSort): Nip50Sort => (s === 'none' ? 'new' : 'top');
const toQuerySort = (s: Nip50Sort): CampaignSort => (s === 'new' ? 'none' : 'top');
const toQuerySort = (s: Nip50Sort): CampaignSort => (s === 'top' ? 'top' : 'none');
/**
* Lists every campaign found on relays. Two sort modes:
* Lists every campaign found on relays. The page has two display modes:
*
* - **Top** (default): ranked by total sats raised (kind 8333 donation receipts).
* - **New**: chronological by `created_at`.
* 1. **Idle** (no search, no sort, no country picked) — shows
* moderator-featured campaigns only. If there are no featured
* campaigns, falls back to the latest chronological grid so the page
* is never blank.
* 2. **Active** (user typed a query, picked Top/New, or chose a
* country) — shows the full set, ranked by sats raised (Top) or
* chronological (New / search default).
*
* Both modes share a free-text search bar that filters across title,
* summary, story, location, and category tags client-side.
* Search filters across title, summary, story, location, and category
* tags client-side.
*
* Hidden campaigns are excluded by default — flip the "Show hidden"
* toggle (inside the toolbar's filter popover) to include them.
*
* URL state: `?sort=none&q=<search>`. Default values are stripped so the
* canonical URL stays clean. Useful for sharing search results.
* URL state: `?sort=top|new&q=<search>&country=<iso>`. Default values
* are stripped so the canonical URL stays clean. Useful for sharing
* search results.
*/
export function AllCampaignsPage() {
const { t } = useTranslation();
@@ -79,7 +88,7 @@ export function AllCampaignsPage() {
// Sync the debounced search → URL. Empty / default values are stripped
// so the canonical URL is `/campaigns/all` (not
// `/campaigns/all?sort=none&q=`).
// `/campaigns/all?sort=&q=`).
useEffect(() => {
const next = new URLSearchParams(searchParams);
const trimmed = debouncedSearch.trim();
@@ -102,8 +111,8 @@ export function AllCampaignsPage() {
const setSortFromToolbar = (value: Nip50Sort) => {
const next = new URLSearchParams(searchParams);
const queryValue = toQuerySort(value);
if (queryValue === 'none') next.set('sort', 'none');
if (value === 'top') next.set('sort', 'top');
else if (value === 'new') next.set('sort', 'new');
else next.delete('sort');
setSearchParams(next, { replace: true });
};
@@ -118,7 +127,7 @@ export function AllCampaignsPage() {
};
const { data: campaigns, isLoading } = useAllCampaigns({
sort,
sort: toQuerySort(sort),
search: debouncedSearch.trim(),
countryCode: urlCountry,
limit: 200,
@@ -148,10 +157,21 @@ export function AllCampaignsPage() {
description: t('campaigns.all.description'),
});
const activeQuery = debouncedSearch.trim();
// The unified section is in "active" mode when the user has expressed
// intent to browse the full set: typed a query, picked Top/New, or
// chosen a country. Otherwise it's the curated featured-first view.
const isActive = activeQuery !== '' || sort !== 'default' || !!urlCountry;
// Visible campaigns in the **active** branch: every campaign matching
// the search/sort/country, minus hidden (unless the moderator opted
// in to seeing hidden). Featured items are intentionally NOT pulled
// out of this list — when the user is actively browsing, they want a
// ranked or chronological grid, not the curated shelf.
const { visible, hiddenCount, hiddenCampaigns } = useMemo(() => {
const all = campaigns ?? [];
const hiddenCoords = moderation?.hiddenCoords ?? new Set<string>();
const featuredCoordSet = new Set(featuredCoords);
let hiddenCount = 0;
const visible: ParsedCampaign[] = [];
const hiddenCampaigns: ParsedCampaign[] = [];
@@ -161,13 +181,13 @@ export function AllCampaignsPage() {
hiddenCount += 1;
hiddenCampaigns.push(c);
if (isMod && showHidden) visible.push(c);
} else if (!featuredCoordSet.has(c.aTag)) {
} else {
visible.push(c);
}
}
return { visible, hiddenCount, hiddenCampaigns };
}, [campaigns, featuredCoords, isMod, moderation, showHidden]);
}, [campaigns, isMod, moderation, showHidden]);
const orderedFeaturedCampaigns = useMemo(() => {
if (!featuredCampaigns) return [] as ParsedCampaign[];
@@ -176,19 +196,23 @@ export function AllCampaignsPage() {
);
}, [featuredCampaigns, moderation]);
// Idle-mode list: featured first; if none are featured, fall back to
// the latest chronological grid so the page never lands on an empty
// state when there's content to show.
const idleCampaigns = useMemo<ParsedCampaign[]>(() => {
if (orderedFeaturedCampaigns.length > 0) return orderedFeaturedCampaigns;
return visible;
}, [orderedFeaturedCampaigns, visible]);
const DEFAULT_VISIBLE = 4;
const [showAllMine, setShowAllMine] = useState(false);
const [showAllFeatured, setShowAllFeatured] = useState(false);
const visibleMine = showAllMine ? (myCampaigns ?? []) : (myCampaigns ?? []).slice(0, DEFAULT_VISIBLE);
const visibleFeatured = showAllFeatured ? orderedFeaturedCampaigns : orderedFeaturedCampaigns.slice(0, DEFAULT_VISIBLE);
const showSkeleton = isLoading || !moderationReady;
const activeQuery = debouncedSearch.trim();
const totalCampaigns = campaigns?.length ?? 0;
return (
<main className="min-h-screen pb-16">
<AllCampaignsHero campaignCount={totalCampaigns} />
<AllCampaignsHero campaignCount={visible.length} />
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-10 lg:py-14 space-y-8">
{user && myCampaigns && myCampaigns.length > 0 && (
@@ -211,118 +235,100 @@ export function AllCampaignsPage() {
</section>
)}
{orderedFeaturedCampaigns.length > 0 && (
<section className="space-y-5">
{/* Unified Campaigns section.
- Idle (no search / no sort / no country): renders the
moderator-curated featured grid, or a chronological
fallback if nothing is featured yet.
- Active: renders the full search/sort/country result set. */}
<section className="space-y-5">
<div className="flex flex-col items-stretch gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
{t('campaigns.home.featured')}
{activeQuery
? t('common.search')
: t('campaigns.all.title')}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{t('campaigns.home.featuredDesc', { appName: config.appName })}
{activeQuery
? t('common.searchResultsCount', { count: visible.length })
: t('campaigns.all.sectionTagline')}
</p>
</div>
<CampaignSection
campaigns={visibleFeatured}
total={orderedFeaturedCampaigns.length}
visible={DEFAULT_VISIBLE}
showAll={showAllFeatured}
onToggle={() => setShowAllFeatured(!showAllFeatured)}
<DiscoverySearchToolbar
query={searchInput}
onQueryChange={setSearchInput}
sort={sort}
onSortChange={setSortFromToolbar}
sortOptions={['top', 'new']}
searchPlaceholderKey="campaigns.all.searchPlaceholder"
searchAriaLabelKey="campaigns.all.searchAriaLabel"
showHidden={isMod ? {
value: showHidden,
onChange: setShowHidden,
count: hiddenCount,
} : undefined}
country={urlCountry}
onCountryChange={setCountry}
/>
</section>
)}
</div>
{/* Section heading — matches the `/pledges` and `/groups` pages
so the discovery surfaces all share the same large-bold
section header pattern. Title switches between Search / Top /
New based on toolbar state; tagline stays constant.
Search input + filter button cluster on the right, paired
with the heading on the left in a single row. */}
<div className="flex flex-col items-stretch gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
{activeQuery
? t('common.search')
: t('campaigns.all.title')}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{activeQuery
? t('common.searchResultsCount', { count: visible.length })
: t('campaigns.all.sectionTagline')}
</p>
</div>
<DiscoverySearchToolbar
query={searchInput}
onQueryChange={setSearchInput}
sort={toToolbarSort(sort)}
onSortChange={setSortFromToolbar}
sortOptions={['top', 'new']}
searchPlaceholderKey="campaigns.all.searchPlaceholder"
searchAriaLabelKey="campaigns.all.searchAriaLabel"
showHidden={isMod ? {
value: showHidden,
onChange: setShowHidden,
count: hiddenCount,
} : undefined}
country={urlCountry}
onCountryChange={setCountry}
/>
</div>
{/* Grid — widens to 3 columns at lg and 4 at xl so desktop users
can scan more campaigns at once, matching the Pledge index's
card density. Mobile and small tablets stay single / double
column so the cards keep their tappable size. */}
{showSkeleton ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
{Array.from({ length: 8 }).map((_, i) => (
<CampaignCardSkeleton key={i} />
))}
</div>
) : visible.length === 0 ? (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center space-y-4">
<HandHeart className="size-10 text-muted-foreground mx-auto" />
<div className="space-y-1.5">
{activeQuery ? (
<>
<h2 className="text-lg font-semibold">
{t('campaigns.all.noMatch', { query: activeQuery })}
</h2>
<p className="text-muted-foreground max-w-sm mx-auto">
{t('campaigns.all.noMatchHint')}
</p>
</>
) : hiddenCount > 0 && !showHidden ? (
<>
<h2 className="text-lg font-semibold">{t('campaigns.all.allHidden')}</h2>
<p className="text-muted-foreground max-w-sm mx-auto">
{t('campaigns.all.allHiddenHint')}
</p>
</>
) : (
<>
<h2 className="text-lg font-semibold">{t('campaigns.all.empty')}</h2>
<p className="text-muted-foreground max-w-sm mx-auto">
{t('campaigns.all.emptyHint')}
</p>
</>
)}
</div>
<Button asChild>
<Link to="/campaigns/new">
<PlusCircle className="size-4 mr-2" />
{t('campaigns.all.startCampaign')}
</Link>
</Button>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
{visible.map((campaign) => (
<CampaignCard key={campaign.aTag} campaign={campaign} />
))}
</div>
)}
{/* Grid — widens to 3 columns at lg and 4 at xl so desktop
users can scan more campaigns at once, matching the Pledge
index's card density. Mobile and small tablets stay
single / double column so the cards keep their tappable
size. */}
{showSkeleton ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
{Array.from({ length: 8 }).map((_, i) => (
<CampaignCardSkeleton key={i} />
))}
</div>
) : (isActive ? visible : idleCampaigns).length === 0 ? (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center space-y-4">
<HandHeart className="size-10 text-muted-foreground mx-auto" />
<div className="space-y-1.5">
{activeQuery ? (
<>
<h2 className="text-lg font-semibold">
{t('campaigns.all.noMatch', { query: activeQuery })}
</h2>
<p className="text-muted-foreground max-w-sm mx-auto">
{t('campaigns.all.noMatchHint')}
</p>
</>
) : hiddenCount > 0 && !showHidden ? (
<>
<h2 className="text-lg font-semibold">{t('campaigns.all.allHidden')}</h2>
<p className="text-muted-foreground max-w-sm mx-auto">
{t('campaigns.all.allHiddenHint')}
</p>
</>
) : (
<>
<h2 className="text-lg font-semibold">{t('campaigns.all.empty')}</h2>
<p className="text-muted-foreground max-w-sm mx-auto">
{t('campaigns.all.emptyHint')}
</p>
</>
)}
</div>
<Button asChild>
<Link to="/campaigns/new">
<PlusCircle className="size-4 mr-2" />
{t('campaigns.all.startCampaign')}
</Link>
</Button>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
{(isActive ? visible : idleCampaigns).map((campaign) => (
<CampaignCard key={campaign.aTag} campaign={campaign} />
))}
</div>
)}
</section>
{/* Moderator-only: every hidden campaign on the network. Mirrors
the section on `/campaigns` so moderators see the same
+34 -45
View File
@@ -65,8 +65,9 @@ export function CommunitiesPage() {
// On-page NIP-50 search + sort + show-hidden toolbar state.
//
// Default sort, empty query → curated "My groups" / "Featured" /
// moderator shelves below.
// Default sort, empty query → curated featured-first idle view.
// If no groups are featured yet, falls back to the chronological
// "all groups" grid so the page is never blank.
// Default sort, with query → relay search for kind 34550, results
// post-filtered against name/description/content client-side.
// Top / New → always active. Top sends `sort:top`;
@@ -122,9 +123,14 @@ export function CommunitiesPage() {
}, [searchHitsRaw, orgModeration, showHidden]);
const { data: allOrgs, isLoading: allOrgsLoading } = useDiscoverCommunities({ limit: 200 });
// `allGroups` is the chronological "every Agora-tagged group" list,
// used both as the fallback for the idle view (when nothing is
// featured yet) and as the result list when the user switches to
// Top/New sort with no typed query. Featured groups are NOT removed
// here — when the user is actively browsing they want the full set;
// the idle-mode rendering path handles the featured-first framing.
const { allGroups, allHiddenCount, hiddenGroups } = useMemo(() => {
const hiddenCoords = orgModeration?.hiddenCoords ?? new Set<string>();
const featuredCoords = orgModeration?.featuredCoords ?? new Set<string>();
let hidden = 0;
const visible: ParsedCommunity[] = [];
const hiddenList: ParsedCommunity[] = [];
@@ -133,20 +139,34 @@ export function CommunitiesPage() {
hidden += 1;
hiddenList.push(org);
if (isMod && showHidden) visible.push(org);
} else if (hasAgoraTag(org.tags) && !featuredCoords.has(org.aTag)) {
} else if (hasAgoraTag(org.tags)) {
visible.push(org);
}
}
return { allGroups: visible, allHiddenCount: hidden, hiddenGroups: hiddenList };
}, [allOrgs, isMod, orgModeration, showHidden]);
// Search + sort + show-hidden cluster for the All section.
// Featured groups for the idle view. When there are none, the idle
// view falls back to `allGroups` so users always see something.
const { data: featuredOrgs } = useFeaturedOrganizations();
const featuredGroups = useMemo<ParsedCommunity[]>(() => {
if (!featuredOrgs) return [];
const hiddenCoords = orgModeration?.hiddenCoords ?? new Set<string>();
return featuredOrgs
.map((entry) => entry.community)
.filter((c) => !hiddenCoords.has(c.aTag));
}, [featuredOrgs, orgModeration]);
const idleGroups = featuredGroups.length > 0 ? featuredGroups : allGroups;
// Search + sort + show-hidden cluster for the unified section.
const searchToolbar = (
<DiscoverySearchToolbar
query={searchInput}
onQueryChange={setSearchInput}
sort={sortMode}
onSortChange={setSortMode}
sortOptions={['top', 'new']}
searchPlaceholderKey="groups.list.searchPlaceholder"
searchAriaLabelKey="groups.list.searchAriaLabel"
showHidden={isMod ? {
@@ -166,19 +186,18 @@ export function CommunitiesPage() {
userOrganizations={userOrganizations}
/>
<FeaturedOrganizationsShelf />
{/* Unified Groups section.
- Idle (no search / no sort): renders moderator-featured
groups, or the chronological "all groups" fallback when
nothing is featured yet.
- Active: renders the full search/sort result set. */}
<section className="space-y-5">
<div className="flex flex-col items-stretch gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
{trimmedSearch
? t('common.search')
: isSearching && sortMode === 'top'
? t('common.sortTop')
: isSearching && sortMode === 'new'
? t('common.sortNew')
: t('groups.list.allGroups')}
: t('groups.list.allGroups')}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{isSearching && searchHits
@@ -234,9 +253,9 @@ export function CommunitiesPage() {
<CommunityMiniCardSkeleton key={i} className="w-full" />
))}
</CommunityGrid>
) : allGroups.length > 0 ? (
) : idleGroups.length > 0 ? (
<CommunityGrid>
{allGroups.map((community) => (
{idleGroups.map((community) => (
<CommunityMiniCard
key={community.aTag}
community={community}
@@ -536,36 +555,6 @@ function MyCommunitiesShelf({
);
}
function FeaturedOrganizationsShelf() {
const { data: featured } = useFeaturedOrganizations();
if (!featured || featured.length === 0) return null;
return (
<section className="space-y-5">
<FeaturedOrganizationsHeading />
<CommunityGrid>
{featured.map((entry) => (
<CommunityMiniCard
key={entry.community.aTag}
community={entry.community}
className="w-full"
/>
))}
</CommunityGrid>
</section>
);
}
function FeaturedOrganizationsHeading() {
const { t } = useTranslation();
return (
<div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
{t('groups.list.featuredGroups')}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{t('groups.list.featuredGroupsTagline')}
</p>
</div>
);
}