feat: extract shared search component (#669)

This commit is contained in:
shenlan 2025-11-11 14:02:23 +08:00 committed by GitHub
parent cf7c017a72
commit 43cdc009f8
3 changed files with 101 additions and 60 deletions

View File

@ -1,6 +1,7 @@
export const dynamic = 'force-dynamic'
import Link from 'next/link'
import SearchComponent from '@components/search'
import { getHomepagePosts } from '@cms/content'
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
@ -31,12 +32,13 @@ function formatDate(dateStr: string | undefined, language: 'zh' | 'en'): string
}
type PageProps = {
searchParams: { page?: string }
searchParams?: { page?: string } | Promise<{ page?: string }>
}
export default async function BlogPage({ searchParams }: PageProps) {
const posts = await getHomepagePosts()
const { page } = searchParams ?? {}
const resolvedSearchParams = await Promise.resolve(searchParams ?? {})
const { page } = resolvedSearchParams ?? {}
const postsPerPage = 10
const currentPage = parseInt(page || '1', 10)
const totalPages = Math.max(1, Math.ceil(posts.length / postsPerPage))
@ -57,6 +59,7 @@ export default async function BlogPage({ searchParams }: PageProps) {
<p className="text-lg text-slate-600">
Latest updates, releases, and insights from the Cloud-Neutral community.
</p>
<SearchComponent variant="hero" className="mt-6" />
</div>
{posts.length === 0 ? (

View File

@ -2,15 +2,15 @@
import Link from 'next/link'
import Image from 'next/image'
import { usePathname } from 'next/navigation'
import { FormEvent, useEffect, useMemo, useRef, useState } from 'react'
import { Mail, Search } from 'lucide-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Mail } from 'lucide-react'
import { useLanguage } from '../i18n/LanguageProvider'
import { translations } from '../i18n/translations'
import LanguageToggle from './LanguageToggle'
import ReleaseChannelSelector, { ReleaseChannel } from './ReleaseChannelSelector'
import { getFeatureToggleInfo } from '@lib/featureToggles'
import { useUser } from '@lib/userStore'
import { AskAIDialog } from './AskAIDialog'
import SearchComponent from './search'
const CHANNEL_ORDER: ReleaseChannel[] = ['stable', 'beta', 'develop']
const DEFAULT_CHANNELS: ReleaseChannel[] = ['stable']
@ -231,13 +231,8 @@ export default function Navbar() {
download: isChinese ? '下载' : 'Download',
openSource: isChinese ? '开源项目' : 'Open source',
moreServices: isChinese ? '更多服务' : 'More services',
searchPlaceholder: isChinese ? '请输入关键字搜索内容' : 'Ask anything about your docs',
}
const [searchValue, setSearchValue] = useState('')
const [askDialogOpen, setAskDialogOpen] = useState(false)
const [pendingQuestion, setPendingQuestion] = useState<{ key: number; text: string } | null>(null)
useEffect(() => {
if (typeof window === 'undefined') {
return
@ -281,15 +276,6 @@ export default function Navbar() {
{ key: 'xscopehub', label: 'XScopeHub', href: '/xscopehub' },
]
const handleSearchSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
const trimmed = searchValue.trim()
if (!trimmed) return
setPendingQuestion({ key: Date.now(), text: trimmed })
setAskDialogOpen(true)
setSearchValue('')
}
if (isHiddenRoute) {
return null
}
@ -397,22 +383,7 @@ export default function Navbar() {
</div>
<div className="hidden flex-1 items-center justify-end gap-4 lg:flex">
<form onSubmit={handleSearchSubmit} className="relative w-full max-w-xs">
<input
type="search"
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder={labels.searchPlaceholder}
className="w-full rounded-full border border-brand-border bg-brand-surface/60 py-2 pl-4 pr-10 text-sm text-brand-heading transition focus:border-brand focus:bg-white focus:outline-none focus:ring-2 focus:ring-brand/20"
/>
<button
type="submit"
className="absolute right-2 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full bg-brand text-white transition hover:bg-brand-light"
aria-label="Ask AI"
>
<Search className="h-4 w-4" />
</button>
</form>
<SearchComponent className="relative w-full max-w-xs" />
{user ? (
<div className="relative" ref={accountMenuRef}>
<button
@ -501,22 +472,11 @@ export default function Navbar() {
{menuOpen ? (
<div className="flex flex-col gap-4 border-t border-gray-200 py-4 lg:hidden">
<form onSubmit={handleSearchSubmit} className="relative">
<input
type="search"
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder={labels.searchPlaceholder}
className="w-full rounded-full border border-brand-border bg-brand-surface/60 py-2 pl-4 pr-10 text-sm text-brand-heading transition focus:border-brand focus:bg-white focus:outline-none focus:ring-2 focus:ring-brand/20"
/>
<button
type="submit"
className="absolute right-2 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-brand text-white transition hover:bg-brand-light"
aria-label="Ask AI"
>
<Search className="h-4 w-4" />
</button>
</form>
<SearchComponent
className="relative"
buttonClassName="h-8 w-8"
inputClassName="py-2 pr-12"
/>
<div className="flex flex-col gap-2 text-sm font-medium text-gray-700">
{mainLinks.map((link) => (
<Link key={link.key} href={link.href} className="py-2" onClick={() => setMenuOpen(false)}>
@ -632,15 +592,6 @@ export default function Navbar() {
</div>
</nav>
<AskAIDialog
open={askDialogOpen}
onMinimize={() => setAskDialogOpen(false)}
onEnd={() => {
setAskDialogOpen(false)
setPendingQuestion(null)
}}
initialQuestion={pendingQuestion ?? undefined}
/>
</>
)
}

View File

@ -0,0 +1,87 @@
'use client'
import { FormEvent, useState } from 'react'
import clsx from 'clsx'
import { Search } from 'lucide-react'
import { AskAIDialog } from '../AskAIDialog'
import { useLanguage } from '../i18n/LanguageProvider'
type SearchComponentProps = {
className?: string
inputClassName?: string
buttonClassName?: string
variant?: 'default' | 'hero'
}
type PendingQuestion = { key: number; text: string }
const baseWrapperClass = 'relative w-full max-w-xs'
const heroWrapperClass = 'relative w-full max-w-2xl'
const baseInputClass =
'w-full rounded-full border border-brand-border bg-brand-surface/60 py-2 pl-4 pr-10 text-sm text-brand-heading transition focus:border-brand focus:bg-white focus:outline-none focus:ring-2 focus:ring-brand/20'
const heroInputClass =
'w-full rounded-full border border-brand-border bg-white/80 py-3 pl-5 pr-12 text-base text-brand-heading shadow-sm transition focus:border-brand focus:bg-white focus:outline-none focus:ring-2 focus:ring-brand/20'
const baseButtonClass =
'absolute right-2 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full bg-brand text-white transition hover:bg-brand-light'
const heroButtonClass =
'absolute right-3 top-1/2 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-brand text-white shadow-sm transition hover:bg-brand-light'
export default function SearchComponent({
className,
inputClassName,
buttonClassName,
variant = 'default'
}: SearchComponentProps) {
const { language } = useLanguage()
const [searchValue, setSearchValue] = useState('')
const [askDialogOpen, setAskDialogOpen] = useState(false)
const [pendingQuestion, setPendingQuestion] = useState<PendingQuestion | null>(null)
const placeholder =
language === 'zh' ? '请输入关键字搜索内容' : 'Ask anything about your docs'
const handleSearchSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
const trimmed = searchValue.trim()
if (!trimmed) return
setPendingQuestion({ key: Date.now(), text: trimmed })
setAskDialogOpen(true)
setSearchValue('')
}
const wrapperClasses = clsx(variant === 'hero' ? heroWrapperClass : baseWrapperClass, className)
const inputClasses = clsx(variant === 'hero' ? heroInputClass : baseInputClass, inputClassName)
const submitButtonClasses = clsx(
variant === 'hero' ? heroButtonClass : baseButtonClass,
buttonClassName
)
return (
<>
<form onSubmit={handleSearchSubmit} className={wrapperClasses}>
<input
type="search"
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder={placeholder}
className={inputClasses}
/>
<button type="submit" className={submitButtonClasses} aria-label="Ask AI">
<Search className={variant === 'hero' ? 'h-5 w-5' : 'h-4 w-4'} />
</button>
</form>
<AskAIDialog
open={askDialogOpen}
onMinimize={() => setAskDialogOpen(false)}
onEnd={() => {
setAskDialogOpen(false)
setPendingQuestion(null)
}}
initialQuestion={pendingQuestion ?? undefined}
/>
</>
)
}