feat(messages): add search and sort functionality to category messages
This commit is contained in:
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "مشتركين",
|
||||
"promptCount": "{count} أوامر",
|
||||
"subscriberCount": "{count} مشتركين",
|
||||
"confirmDelete": "هل أنت متأكد من حذف هذا التصنيف؟"
|
||||
"confirmDelete": "هل أنت متأكد من حذف هذا التصنيف؟",
|
||||
"searchPlaceholder": "البحث في الأوامر...",
|
||||
"sort": {
|
||||
"newest": "الأحدث",
|
||||
"oldest": "الأقدم",
|
||||
"most_upvoted": "الأكثر تصويتاً",
|
||||
"most_contributors": "الأكثر مساهمة"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "الوسوم",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "abunəçi",
|
||||
"promptCount": "{count} prompt",
|
||||
"subscriberCount": "{count} abunəçi",
|
||||
"confirmDelete": "Bu kateqoriyanı silmək istədiyinizə əminsiniz?"
|
||||
"confirmDelete": "Bu kateqoriyanı silmək istədiyinizə əminsiniz?",
|
||||
"searchPlaceholder": "Promptları axtar...",
|
||||
"sort": {
|
||||
"newest": "Ən yeni",
|
||||
"oldest": "Ən köhnə",
|
||||
"most_upvoted": "Ən çox səs alan",
|
||||
"most_contributors": "Ən çox töhfə verən"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Etiketlər",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "Abonnenten",
|
||||
"promptCount": "{count} Prompts",
|
||||
"subscriberCount": "{count} Abonnenten",
|
||||
"confirmDelete": "Sind Sie sicher, dass Sie diese Kategorie löschen möchten?"
|
||||
"confirmDelete": "Sind Sie sicher, dass Sie diese Kategorie löschen möchten?",
|
||||
"searchPlaceholder": "Prompts suchen...",
|
||||
"sort": {
|
||||
"newest": "Neueste",
|
||||
"oldest": "Älteste",
|
||||
"most_upvoted": "Meiste Stimmen",
|
||||
"most_contributors": "Meiste Mitwirkende"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "συνδρομητές",
|
||||
"promptCount": "{count} prompts",
|
||||
"subscriberCount": "{count} συνδρομητές",
|
||||
"confirmDelete": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την κατηγορία;"
|
||||
"confirmDelete": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την κατηγορία;",
|
||||
"searchPlaceholder": "Αναζήτηση prompts...",
|
||||
"sort": {
|
||||
"newest": "Νεότερα",
|
||||
"oldest": "Παλαιότερα",
|
||||
"most_upvoted": "Περισσότερες ψήφοι",
|
||||
"most_contributors": "Περισσότεροι συνεισφέροντες"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Ετικέτες",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "subscribers",
|
||||
"promptCount": "{count} prompts",
|
||||
"subscriberCount": "{count} subscribers",
|
||||
"confirmDelete": "Are you sure you want to delete this category?"
|
||||
"confirmDelete": "Are you sure you want to delete this category?",
|
||||
"searchPlaceholder": "Search prompts...",
|
||||
"sort": {
|
||||
"newest": "Newest",
|
||||
"oldest": "Oldest",
|
||||
"most_upvoted": "Most Upvoted",
|
||||
"most_contributors": "Most Contributors"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "suscriptores",
|
||||
"promptCount": "{count} prompts",
|
||||
"subscriberCount": "{count} suscriptores",
|
||||
"confirmDelete": "¿Estás seguro de que quieres eliminar esta categoría?"
|
||||
"confirmDelete": "¿Estás seguro de que quieres eliminar esta categoría?",
|
||||
"searchPlaceholder": "Buscar prompts...",
|
||||
"sort": {
|
||||
"newest": "Más recientes",
|
||||
"oldest": "Más antiguos",
|
||||
"most_upvoted": "Más votados",
|
||||
"most_contributors": "Más contribuidores"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Etiquetas",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "دنبالکننده",
|
||||
"promptCount": "{count} پرامپت",
|
||||
"subscriberCount": "{count} دنبالکننده",
|
||||
"confirmDelete": "آیا مطمئن هستید که میخواهید این دستهبندی را حذف کنید؟"
|
||||
"confirmDelete": "آیا مطمئن هستید که میخواهید این دستهبندی را حذف کنید؟",
|
||||
"searchPlaceholder": "جستجوی پرامپتها...",
|
||||
"sort": {
|
||||
"newest": "جدیدترین",
|
||||
"oldest": "قدیمیترین",
|
||||
"most_upvoted": "بیشترین رای",
|
||||
"most_contributors": "بیشترین مشارکتکننده"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "برچسبها",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "abonnés",
|
||||
"promptCount": "{count} prompts",
|
||||
"subscriberCount": "{count} abonnés",
|
||||
"confirmDelete": "Êtes-vous sûr de vouloir supprimer cette catégorie ?"
|
||||
"confirmDelete": "Êtes-vous sûr de vouloir supprimer cette catégorie ?",
|
||||
"searchPlaceholder": "Rechercher des prompts...",
|
||||
"sort": {
|
||||
"newest": "Plus récents",
|
||||
"oldest": "Plus anciens",
|
||||
"most_upvoted": "Plus votés",
|
||||
"most_contributors": "Plus de contributeurs"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "מנויים",
|
||||
"promptCount": "{count} פרומפטים",
|
||||
"subscriberCount": "{count} מנויים",
|
||||
"confirmDelete": "האם אתה בטוח שברצונך למחוק קטגוריה זו?"
|
||||
"confirmDelete": "האם אתה בטוח שברצונך למחוק קטגוריה זו?",
|
||||
"searchPlaceholder": "חיפוש פרומפטים...",
|
||||
"sort": {
|
||||
"newest": "החדשים ביותר",
|
||||
"oldest": "הישנים ביותר",
|
||||
"most_upvoted": "הכי מדורגים",
|
||||
"most_contributors": "הכי הרבה תורמים"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "תגיות",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "iscritti",
|
||||
"promptCount": "{count} prompt",
|
||||
"subscriberCount": "{count} iscritti",
|
||||
"confirmDelete": "Sei sicuro di voler eliminare questa categoria?"
|
||||
"confirmDelete": "Sei sicuro di voler eliminare questa categoria?",
|
||||
"searchPlaceholder": "Cerca prompt...",
|
||||
"sort": {
|
||||
"newest": "Più recenti",
|
||||
"oldest": "Più vecchi",
|
||||
"most_upvoted": "Più votati",
|
||||
"most_contributors": "Più contributori"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Tag",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "人の購読者",
|
||||
"promptCount": "{count}件のプロンプト",
|
||||
"subscriberCount": "{count}人の購読者",
|
||||
"confirmDelete": "このカテゴリーを削除してもよろしいですか?"
|
||||
"confirmDelete": "このカテゴリーを削除してもよろしいですか?",
|
||||
"searchPlaceholder": "プロンプトを検索...",
|
||||
"sort": {
|
||||
"newest": "新しい順",
|
||||
"oldest": "古い順",
|
||||
"most_upvoted": "投票数順",
|
||||
"most_contributors": "貢献者数順"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "タグ",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "구독자",
|
||||
"promptCount": "{count}개 프롬프트",
|
||||
"subscriberCount": "{count}명 구독자",
|
||||
"confirmDelete": "이 카테고리를 삭제하시겠습니까?"
|
||||
"confirmDelete": "이 카테고리를 삭제하시겠습니까?",
|
||||
"searchPlaceholder": "프롬프트 검색...",
|
||||
"sort": {
|
||||
"newest": "최신순",
|
||||
"oldest": "오래된순",
|
||||
"most_upvoted": "추천순",
|
||||
"most_contributors": "기여자순"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "태그",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "assinantes",
|
||||
"promptCount": "{count} prompts",
|
||||
"subscriberCount": "{count} assinantes",
|
||||
"confirmDelete": "Tem certeza que deseja excluir esta categoria?"
|
||||
"confirmDelete": "Tem certeza que deseja excluir esta categoria?",
|
||||
"searchPlaceholder": "Buscar prompts...",
|
||||
"sort": {
|
||||
"newest": "Mais recentes",
|
||||
"oldest": "Mais antigos",
|
||||
"most_upvoted": "Mais votados",
|
||||
"most_contributors": "Mais contribuidores"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "подписчиков",
|
||||
"promptCount": "{count} промптов",
|
||||
"subscriberCount": "{count} подписчиков",
|
||||
"confirmDelete": "Вы уверены, что хотите удалить эту категорию?"
|
||||
"confirmDelete": "Вы уверены, что хотите удалить эту категорию?",
|
||||
"searchPlaceholder": "Поиск промптов...",
|
||||
"sort": {
|
||||
"newest": "Новые",
|
||||
"oldest": "Старые",
|
||||
"most_upvoted": "По голосам",
|
||||
"most_contributors": "По участникам"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Теги",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "abone",
|
||||
"promptCount": "{count} prompt",
|
||||
"subscriberCount": "{count} abone",
|
||||
"confirmDelete": "Bu kategoriyi silmek istediğinizden emin misiniz?"
|
||||
"confirmDelete": "Bu kategoriyi silmek istediğinizden emin misiniz?",
|
||||
"searchPlaceholder": "Prompt ara...",
|
||||
"sort": {
|
||||
"newest": "En yeni",
|
||||
"oldest": "En eski",
|
||||
"most_upvoted": "En çok oylanan",
|
||||
"most_contributors": "En çok katkıda bulunan"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "Etiketler",
|
||||
|
||||
+8
-1
@@ -422,7 +422,14 @@
|
||||
"subscribers": "订阅者",
|
||||
"promptCount": "{count} 个提示词",
|
||||
"subscriberCount": "{count} 订阅者",
|
||||
"confirmDelete": "确定要删除此分类吗?"
|
||||
"confirmDelete": "确定要删除此分类吗?",
|
||||
"searchPlaceholder": "搜索提示词...",
|
||||
"sort": {
|
||||
"newest": "最新",
|
||||
"oldest": "最早",
|
||||
"most_upvoted": "最多投票",
|
||||
"most_contributors": "最多贡献者"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"title": "标签",
|
||||
|
||||
@@ -9,11 +9,12 @@ import config from "@/../prompts.config";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PromptList } from "@/components/prompts/prompt-list";
|
||||
import { SubscribeButton } from "@/components/categories/subscribe-button";
|
||||
import { CategoryFilters } from "@/components/categories/category-filters";
|
||||
import { McpServerPopup } from "@/components/mcp/mcp-server-popup";
|
||||
|
||||
interface CategoryPageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
searchParams: Promise<{ page?: string; sort?: string; q?: string }>;
|
||||
}
|
||||
|
||||
const PROMPTS_PER_PAGE = 30;
|
||||
@@ -37,8 +38,9 @@ export async function generateMetadata({ params }: CategoryPageProps): Promise<M
|
||||
|
||||
export default async function CategoryPage({ params, searchParams }: CategoryPageProps) {
|
||||
const { slug } = await params;
|
||||
const { page } = await searchParams;
|
||||
const { page, sort, q } = await searchParams;
|
||||
const currentPage = Math.max(1, parseInt(page || "1", 10) || 1);
|
||||
const sortOption = sort || "newest";
|
||||
const session = await auth();
|
||||
const t = await getTranslations();
|
||||
|
||||
@@ -67,26 +69,42 @@ export default async function CategoryPage({ params, searchParams }: CategoryPag
|
||||
})
|
||||
: null;
|
||||
|
||||
// Build where clause with optional search
|
||||
const whereClause = {
|
||||
categoryId: category.id,
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
...(q && {
|
||||
OR: [
|
||||
{ title: { contains: q, mode: "insensitive" as const } },
|
||||
{ content: { contains: q, mode: "insensitive" as const } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
// Build orderBy based on sort option
|
||||
const getOrderBy = () => {
|
||||
switch (sortOption) {
|
||||
case "oldest":
|
||||
return { createdAt: "asc" as const };
|
||||
case "most_upvoted":
|
||||
return { votes: { _count: "desc" as const } };
|
||||
case "most_contributors":
|
||||
return { contributors: { _count: "desc" as const } };
|
||||
default:
|
||||
return { createdAt: "desc" as const };
|
||||
}
|
||||
};
|
||||
|
||||
// Count total prompts for pagination
|
||||
const totalPrompts = await db.prompt.count({
|
||||
where: {
|
||||
categoryId: category.id,
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
const totalPrompts = await db.prompt.count({ where: whereClause });
|
||||
const totalPages = Math.ceil(totalPrompts / PROMPTS_PER_PAGE);
|
||||
|
||||
// Fetch prompts in this category
|
||||
const promptsRaw = await db.prompt.findMany({
|
||||
where: {
|
||||
categoryId: category.id,
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
where: whereClause,
|
||||
orderBy: getOrderBy(),
|
||||
skip: (currentPage - 1) * PROMPTS_PER_PAGE,
|
||||
take: PROMPTS_PER_PAGE,
|
||||
include: {
|
||||
@@ -158,6 +176,15 @@ export default async function CategoryPage({ params, searchParams }: CategoryPag
|
||||
<span>{t("categories.subscriberCount", { count: category._count.subscribers })}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<CategoryFilters categorySlug={slug} />
|
||||
{config.features.mcp !== false && <McpServerPopup initialCategories={[slug]} showOfficialBranding={!config.homepage?.useCloneBranding} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile filters */}
|
||||
<div className="flex md:hidden items-center gap-2 mt-4">
|
||||
<CategoryFilters categorySlug={slug} />
|
||||
{config.features.mcp !== false && <McpServerPopup initialCategories={[slug]} showOfficialBranding={!config.homepage?.useCloneBranding} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Search } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const SORT_OPTIONS = ["newest", "oldest", "most_upvoted", "most_contributors"] as const;
|
||||
type SortOption = (typeof SORT_OPTIONS)[number];
|
||||
|
||||
interface CategoryFiltersProps {
|
||||
categorySlug: string;
|
||||
}
|
||||
|
||||
export function CategoryFilters({ categorySlug }: CategoryFiltersProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const t = useTranslations("categories");
|
||||
|
||||
const currentSort = (searchParams?.get("sort") as SortOption) || "newest";
|
||||
const currentSearch = searchParams?.get("q") || "";
|
||||
|
||||
const updateParams = (key: string, value: string) => {
|
||||
const params = new URLSearchParams(searchParams?.toString() || "");
|
||||
if (value) {
|
||||
params.set(key, value);
|
||||
} else {
|
||||
params.delete(key);
|
||||
}
|
||||
// Reset to page 1 when filters change
|
||||
params.delete("page");
|
||||
router.push(`/categories/${categorySlug}?${params.toString()}`);
|
||||
};
|
||||
|
||||
const handleSearchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
updateParams("q", e.currentTarget.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.value !== currentSearch) {
|
||||
updateParams("q", e.currentTarget.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-1 md:flex-none">
|
||||
<div className="relative flex-1 md:flex-none">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder={t("searchPlaceholder")}
|
||||
defaultValue={currentSearch}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
onBlur={handleSearchBlur}
|
||||
className="pl-8 h-8 w-full md:w-[180px] text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Select value={currentSort} onValueChange={(value) => updateParams("sort", value)}>
|
||||
<SelectTrigger size="sm" className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{SORT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{t(`sort.${option}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user