diff --git a/app/[filename]/ServerCategoryPage.tsx b/app/[filename]/ServerCategoryPage.tsx index a463ceff9..307ae5d88 100644 --- a/app/[filename]/ServerCategoryPage.tsx +++ b/app/[filename]/ServerCategoryPage.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { TinaMarkdown } from "tinacms/dist/rich-text"; import Breadcrumbs from "@/components/Breadcrumbs"; import { CategoryEdit } from "@/components/CategoryEdit"; -import RuleListWrapper from "@/components/rule-list/rule-list-wrapper"; +import RuleList from "@/components/rule-list/rule-list"; import MarkdownComponentMapping from "@/components/tina-markdown/markdown-component-mapping"; interface ServerCategoryPageProps { @@ -39,15 +39,7 @@ export default function ServerCategoryPage({ category, path, includeArchived, vi - +
diff --git a/app/latest-rules/client-page.tsx b/app/latest-rules/client-page.tsx index 6efb1b62f..57ecf279c 100644 --- a/app/latest-rules/client-page.tsx +++ b/app/latest-rules/client-page.tsx @@ -1,23 +1,35 @@ "use client"; +import { useMemo, useState } from "react"; import AboutSSWCard from "@/components/AboutSSWCard"; import Breadcrumbs from "@/components/Breadcrumbs"; import HelpCard from "@/components/HelpCard"; import HelpImproveCard from "@/components/HelpImproveCard"; import JoinConversationCard from "@/components/JoinConversationCard"; -import LatestRulesList from "@/components/LatestRulesList"; import RuleCount from "@/components/RuleCount"; +import RuleList from "@/components/rule-list/rule-list"; import SearchBar from "@/components/SearchBarWrapper"; import WhyRulesCard from "@/components/WhyRulesCard"; -import { LatestRule } from "@/models/LatestRule"; +import { RuleListFilter } from "@/types/ruleListFilter"; +import { RuleOrderBy } from "@/types/ruleOrderBy"; interface LatestRuleClientPageProps { ruleCount: number; - latestRulesByUpdated: LatestRule[]; - latestRulesByCreated: LatestRule[]; + latestRulesByUpdated: any[]; + latestRulesByCreated: any[]; } export default function LatestRuleClientPage({ ruleCount, latestRulesByUpdated, latestRulesByCreated }: LatestRuleClientPageProps) { + const [currentSort, setCurrentSort] = useState(RuleOrderBy.LastUpdated); + + const sortOptions = useMemo( + () => [ + { value: RuleOrderBy.LastUpdated, label: "Last Updated", rules: latestRulesByUpdated }, + { value: RuleOrderBy.Created, label: "Recently Created", rules: latestRulesByCreated }, + ], + [latestRulesByUpdated, latestRulesByCreated] + ); + return ( <> @@ -26,8 +38,19 @@ export default function LatestRuleClientPage({ ruleCount, latestRulesByUpdated,
- - +
+

Latest Rules

+ +
diff --git a/app/latest-rules/page.tsx b/app/latest-rules/page.tsx index a7733c064..246dd886b 100644 --- a/app/latest-rules/page.tsx +++ b/app/latest-rules/page.tsx @@ -8,27 +8,11 @@ export const revalidate = 300; const DEFAULT_SIZE = 50; const MAX_SIZE = 50; -type LatestRulePageProps = { - searchParams?: Promise<{ size?: string | string[] }>; -}; - -export default async function LatestRulePage({ searchParams }: LatestRulePageProps) { - const sp = (await searchParams) ?? {}; - const sizeRaw = sp.size; - const sizeStr = Array.isArray(sizeRaw) ? sizeRaw[0] : sizeRaw; - - let size = sizeStr ? parseInt(sizeStr, 10) : DEFAULT_SIZE; - - if (!Number.isFinite(size) || Number.isNaN(size) || size <= 0) { - size = DEFAULT_SIZE; - } else if (size > MAX_SIZE) { - size = MAX_SIZE; - } - +export default async function LatestRulePage() { const [ruleCount, latestRulesByUpdated, latestRulesByCreated] = await Promise.all([ fetchRuleCount(), - fetchLatestRules(size, "lastUpdated", true), - fetchLatestRules(size, "created", true), + fetchLatestRules(MAX_SIZE, "lastUpdated", true), + fetchLatestRules(MAX_SIZE, "created", true), ]); return ( diff --git a/app/search/client-page.tsx b/app/search/client-page.tsx index 9b6aec6c2..ca5412614 100644 --- a/app/search/client-page.tsx +++ b/app/search/client-page.tsx @@ -1,27 +1,26 @@ "use client"; -import { useSearchParams, useRouter } from "next/navigation"; -import { useState } from "react"; -import SearchBar from "@/components/SearchBarWrapper"; +import { useSearchParams } from "next/navigation"; +import { useMemo, useState } from "react"; import AboutSSWCard from "@/components/AboutSSWCard"; +import Breadcrumbs from "@/components/Breadcrumbs"; import HelpCard from "@/components/HelpCard"; import HelpImproveCard from "@/components/HelpImproveCard"; import JoinConversationCard from "@/components/JoinConversationCard"; +import LatestRulesCard from "@/components/LatestRulesCard"; import RuleCount from "@/components/RuleCount"; +import RuleList from "@/components/rule-list/rule-list"; +import SearchBar from "@/components/SearchBarWrapper"; import WhyRulesCard from "@/components/WhyRulesCard"; import { LatestRule } from "@/models/LatestRule"; -import LatestRulesCard from "@/components/LatestRulesCard"; -import Dropdown from "@/components/ui/dropdown"; -import { CgSortAz } from "react-icons/cg"; -import RuleCard from "@/components/RuleCard"; -import Spinner from '@/components/Spinner'; -import { useEffect } from 'react'; -import Breadcrumbs from "@/components/Breadcrumbs"; +import { RuleListFilter } from "@/types/ruleListFilter"; interface SearchResult { objectID: string; title: string; slug: string; + lastUpdated?: string; + lastUpdatedBy?: string; [key: string]: any; } @@ -32,28 +31,22 @@ interface RuleSearchClientPageProps { export default function RulesSearchClientPage({ ruleCount, latestRulesByUpdated }: RuleSearchClientPageProps) { const searchParams = useSearchParams(); - const router = useRouter(); const keyword = searchParams.get("keyword") || ""; const sortBy = searchParams.get("sortBy") || "lastUpdated"; const [searchResults, setSearchResults] = useState([]); - const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - setIsLoading(false); - }, [searchParams]); - - const handleSortChange = (newSort: string) => { - const params = new URLSearchParams(searchParams); - params.set("sortBy", newSort); - if (keyword) params.set("keyword", keyword); - setIsLoading(true); - router.push(`/search?${params.toString()}`); - }; - - const sortOptions = [ - { value: "lastUpdated", label: "Last Updated" }, - { value: "created", label: "Recently Created" } - ]; + // Transform search results to match RuleList expected format + const rulesForList = useMemo( + () => + searchResults.map((result) => ({ + guid: result.objectID, + uri: result.slug, + title: result.title, + lastUpdated: result.lastUpdated, + lastUpdatedBy: result.lastUpdatedBy, + })), + [searchResults] + ); return ( <> @@ -61,61 +54,27 @@ export default function RulesSearchClientPage({ ruleCount, latestRulesByUpdated
- +
- - {isLoading ? ( -
- -
- ) : keyword && searchResults.length === 0 ? ( -
-

- No results found for "{keyword}". Please try a different search term. -

-
- ) : ( - searchResults.length > 0 && ( -
-
-

- Search Results ({searchResults.length}) -

-
- - -
-
- {searchResults.map((result, index) => ( - - ))} + {searchResults.length > 0 && ( +
+

Search Results ({searchResults.length})

+
- ) - )} - + )}
-
- {ruleCount && } -
+
{ruleCount && }
diff --git a/components/LatestRulesCard.tsx b/components/LatestRulesCard.tsx index 47ac4c01b..a10242018 100644 --- a/components/LatestRulesCard.tsx +++ b/components/LatestRulesCard.tsx @@ -1,10 +1,10 @@ "use client"; +import Link from "next/link"; +import { RiTimeFill } from "react-icons/ri"; import { Card } from "@/components/ui/card"; -import { LatestRule } from "@/models/LatestRule"; import { timeAgo } from "@/lib/dateUtils"; -import { RiTimeFill } from "react-icons/ri"; -import Link from "next/link"; +import { LatestRule } from "@/models/LatestRule"; interface LatestRulesProps { rules: LatestRule[]; @@ -27,10 +27,7 @@ export default function LatestRulesCard({ rules }: LatestRulesProps) { ))} - + See more diff --git a/components/LatestRulesList.tsx b/components/LatestRulesList.tsx deleted file mode 100644 index 6c4b3a696..000000000 --- a/components/LatestRulesList.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { LatestRule } from "@/models/LatestRule"; -import { useState } from "react"; -import { CgSortAz } from "react-icons/cg"; -import Dropdown from "./ui/dropdown"; -import RuleCard from "./RuleCard"; - -interface LatestRulesListProps { - rulesByUpdated: LatestRule[]; - rulesByCreated: LatestRule[]; - title?: string; -} - -export default function LatestRulesList({ - rulesByUpdated, - rulesByCreated, - title, -}: LatestRulesListProps) { - const [currentSort, setCurrentSort] = useState<"lastUpdated" | "created">( - "lastUpdated" - ); - - const handleSortChange = (newSort: "lastUpdated" | "created") => { - setCurrentSort(newSort); - }; - - const currentRules = - currentSort === "lastUpdated" ? rulesByUpdated : rulesByCreated; - - const sortOptions = [ - { value: "lastUpdated", label: "Last Updated" }, - { value: "created", label: "Recently Created" } - ]; - - return ( -
-
- {title &&

{title}

} -
- - handleSortChange(value as "lastUpdated" | "created")} - /> -
-
- - {currentRules.map((rule, index) => ( - - ))} -
- ); -} diff --git a/components/LatestRulesTable.tsx b/components/LatestRulesTable.tsx deleted file mode 100644 index de87bb742..000000000 --- a/components/LatestRulesTable.tsx +++ /dev/null @@ -1,173 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { LatestRule } from "@/models/LatestRule"; -import { timeAgo } from "@/lib/dateUtils"; -import { useState, useEffect } from "react"; -import { CgSortAz } from "react-icons/cg"; -import { RiTimeFill } from "react-icons/ri"; -import { Card } from "./ui/card"; -import Dropdown from "./ui/dropdown"; - -interface LatestRulesTableProps { - rulesByUpdated: LatestRule[]; - rulesByCreated: LatestRule[]; - title?: string; -} - -export default function LatestRulesTable({ - rulesByUpdated, - rulesByCreated, - title, -}: LatestRulesTableProps) { - const [currentSort, setCurrentSort] = useState<"lastUpdated" | "created">( - "lastUpdated" - ); - - const handleSortChange = (newSort: "lastUpdated" | "created") => { - setCurrentSort(newSort); - }; - - const currentRules = - currentSort === "lastUpdated" ? rulesByUpdated : rulesByCreated; - - // Map of ruleUri -> github username - const [usernames, setUsernames] = useState>({}); - const [loadingMap, setLoadingMap] = useState>({}); - - // Prefetch usernames for visible rules - useEffect(() => { - let isMounted = true; - const urisToFetch = currentRules - .map((r) => r.uri) - .filter((u) => u && usernames[u] === undefined); - - if (urisToFetch.length === 0) return; - - (async () => { - for (const uri of urisToFetch) { - if (!uri) continue; - try { - if (!isMounted) return; - setLoadingMap((m) => ({ ...m, [uri]: true })); - const params = new URLSearchParams(); - params.set('ruleUri', uri); - const res = await fetch(`/api/github/rules/authors?${params.toString()}`); - if (!res.ok) throw new Error('Failed to fetch GitHub username'); - const { authors } = await res.json(); - // re-use existing helper in other file? simple heuristic: pick last modified - const lastModified = authors && authors.length ? authors[authors.length - 1] : null; - if (!isMounted) return; - setUsernames((m) => ({ ...m, [uri]: lastModified })); - } catch (e) { - // ignore prefetch errors - setUsernames((m) => ({ ...m, [uri]: null })); - } finally { - if (isMounted) setLoadingMap((m) => ({ ...m, [uri]: false })); - } - } - })(); - - return () => { - isMounted = false; - }; - }, [currentRules, usernames]); - - const openGithubProfile = async (uri: string) => { - if (!uri) return; - const username = usernames[uri]; - if (username) { - window.open(`https://github.com/${username}`, '_blank', 'noopener,noreferrer'); - return; - } - try { - setLoadingMap((m) => ({ ...m, [uri]: true })); - const params = new URLSearchParams(); - params.set('ruleUri', uri); - const res = await fetch(`/api/github/rules/authors?${params.toString()}`); - if (!res.ok) throw new Error('Failed to fetch GitHub username'); - const { authors } = await res.json(); - const lastModified = authors && authors.length ? authors[authors.length - 1] : null; - setUsernames((m) => ({ ...m, [uri]: lastModified })); - if (lastModified) window.open(`https://github.com/${lastModified}`, '_blank', 'noopener,noreferrer'); - } catch (e) { - console.error('Failed to fetch GitHub username for latest rules:', e); - } finally { - setLoadingMap((m) => ({ ...m, [uri]: false })); - } - }; - - const sortOptions = [ - { value: "lastUpdated", label: "Last Updated" }, - { value: "created", label: "Recently Created" }, - ]; - - return ( -
-
- {title &&

{title}

} -
- - - handleSortChange(value as "lastUpdated" | "created") - } - /> -
-
- - {currentRules.map((rule, index) => ( - -
- #{index + 1} -
- -

- {rule.title} -

- -

- {rule.lastUpdatedBy ? ( - { - // Prevent parent handlers from intercepting the click - e.stopPropagation(); - if (usernames[rule.uri]) { - // If we already have username, force-open the profile - e.preventDefault(); - window.open(`https://github.com/${usernames[rule.uri]}`, '_blank', 'noopener,noreferrer'); - return; - } - // Otherwise prevent default and fetch then open - e.preventDefault(); - if (!loadingMap[rule.uri]) openGithubProfile(rule.uri); - }} - className={`font-medium hover:text-ssw-red hover:underline ${loadingMap[rule.uri] ? 'opacity-50 cursor-not-allowed' : ''}`} - target={usernames[rule.uri] ? '_blank' : undefined} - rel={usernames[rule.uri] ? 'noopener noreferrer' : undefined} - title={usernames[rule.uri] ? `View ${usernames[rule.uri]}'s GitHub profile` : `View ${rule.lastUpdatedBy}'s rules`} - > - {loadingMap[rule.uri] ? 'Loading...' : (usernames[rule.uri] || rule.lastUpdatedBy)} - - ) : ( - Unknown - )} - {rule.lastUpdated ? ( -
- - {timeAgo(rule.lastUpdated)} -
- ) : ( - "" - )} -

-
-
-
- ))} -
- ); -} diff --git a/components/RuleActionButtons.tsx b/components/RuleActionButtons.tsx index 0c0725673..8d50c453e 100644 --- a/components/RuleActionButtons.tsx +++ b/components/RuleActionButtons.tsx @@ -23,7 +23,7 @@ export default function RuleActionButtons({ rule, showBookmark = true, showOpenI if (isAdminPage) return null; return ( -
+
{showBookmark && ( ...}> diff --git a/components/radio-button/radio-button.tsx b/components/radio-button/radio-button.tsx index 1841e13fa..da8e408e2 100644 --- a/components/radio-button/radio-button.tsx +++ b/components/radio-button/radio-button.tsx @@ -8,10 +8,10 @@ interface RadioButtonProps { selectedOption: string; handleOptionChange: (e: React.ChangeEvent) => void; labelText: string; - position?: "first" | "middle" | "last"; + className?: string; } -const RadioButton: React.FC = ({ id, value, selectedOption, handleOptionChange, labelText, position }) => { +const RadioButton: React.FC = ({ id, value, selectedOption, handleOptionChange, labelText, className = "" }) => { const isSelected = selectedOption === value; const handleButtonClick = () => { @@ -27,27 +27,11 @@ const RadioButton: React.FC = ({ id, value, selectedOption, ha handleOptionChange(syntheticEvent); }; - const getBorderClasses = () => { - if (!position) { - return "border rounded"; - } - - if (position === "first") { - return "border border-r-0 rounded-l-md rounded-r-none"; - } else if (position === "last") { - return "border border-l-0 rounded-r-md rounded-l-none"; - } else if (position === "middle") { - return "border rounded-none"; - } - - return "border rounded"; - }; - return (