Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/workflows/cf-pages-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Build and Deploy to CF Pages
name: Build and Deploy to CF Pages (Production)

on:
workflow_dispatch:
push:
branches: [main, master]
branches: [main]

jobs:
build:
Expand Down Expand Up @@ -40,11 +40,20 @@ jobs:
name: build-output
path: out/

- name: Deploy to Cloudflare Pages
- name: Deploy to Cloudflare Pages (Production)
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: transchinese-test
projectName: transchinese-org # 生产环境项目名
directory: out
gitHubToken: ${{ secrets.GITHUB_TOKEN }}

- name: Deploy to Cloudflare Pages (Test)
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: transchinese-test # 测试环境项目名
directory: out
Comment thread
weekendfish marked this conversation as resolved.
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
Empty file added ENDFILE
Empty file.
Empty file added EOF
Empty file.
141 changes: 31 additions & 110 deletions app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,130 +1,51 @@
'use client'
import React, { useState, useEffect, Suspense } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import React, { useState, useEffect } from 'react'
import { useSearchParams } from 'next/navigation'
import SearchForm from '@/components/search/SearchForm'
import SearchResults from '@/components/search/SearchResults'
import { SearchResult } from '@/components/search/SearchResult'
import { SearchParams as SearchParamsType } from '@/components/search/SearchResult'
import { useClientSearch } from '@/components/search/useClientSearch'

// Loading component
const SearchLoading = () => (
<div className="flex justify-center p-8">
<div className="animate-pulse">Loading...</div>
</div>
)

// Define proper types for search parameters
interface SearchParams {
query: string
domain?: string | null
tag?: string | null
year?: string | null
region?: string | null
}

// Main search component
function SearchContent() {
const router = useRouter()
export default function SearchPage() {
const searchParams = useSearchParams()
const [isSearching, setIsSearching] = useState(false)
const [error, setError] = useState<string | null>(null)
const [results, setResults] = useState<SearchResult[]>([])

// Check if any search parameters exist
const hasSearchParams = () => {
return Boolean(
searchParams.get('term') ||
searchParams.get('domain') ||
searchParams.get('tag') ||
searchParams.get('year') ||
searchParams.get('region')
)
}
const { search, isSearching, error, results } = useClientSearch()
const [hasInitialSearch, setHasInitialSearch] = useState(false)

// Perform initial search with params
const performInitialSearch = () => {
const query = searchParams.get('term')
const domain = searchParams.get('domain')
const tag = searchParams.get('tag')
const year = searchParams.get('year')
const region = searchParams.get('region')
useEffect(() => {
const query = searchParams.get('term') || ''
const domain = searchParams.get('domain') || undefined
const tag = searchParams.get('tag') || undefined
const year = searchParams.get('year') || undefined
const region = searchParams.get('region') || undefined

if (query || domain || tag || year || region) {
handleSearch({
query: query || '', // ensure query is never undefined
domain: domain || undefined,
tag: tag || undefined,
year: year || undefined,
region: region || undefined,
})
search({ query, domain, tag, year, region })
setHasInitialSearch(true)
}
}

// Update useEffect to use new functions
useEffect(() => {
if (hasSearchParams()) {
performInitialSearch()
}
}, [searchParams])

const handleSearch = async ({ query, domain, tag, year, region }: SearchParams) => {
setError(null)
setIsSearching(true)
setResults([])

try {
const params = new URLSearchParams({
term: query,
...(domain && { domain }),
...(tag && { tag }),
...(year && { year }),
...(region && { region }),
})
}, [searchParams, search])

router.push(`?${params.toString()}`, { scroll: false })

const response = await fetch(`/api/search?${params.toString()}`)
if (!response.ok) {
throw new Error('Search failed')
}
const data: SearchResult[] = await response.json()
setResults(data)
} catch (err) {
setError('Search failed. Please try again.')
} finally {
setIsSearching(false)
}
const handleSearch = (params: SearchParamsType) => {
search(params)
setHasInitialSearch(true)
}
Comment thread
weekendfish marked this conversation as resolved.
Outdated

return (
<div className="mx-auto max-w-7xl p-8">
<div className="mb-8 rounded-md bg-amber-50 p-4 text-center text-amber-800">
更完善的搜索功能请访问 <a href="https://tsindex.org">tsindex.org 多元性别搜索引擎</a>
</div>

<div className="flex flex-col items-center gap-6">
<h1 className="text-2xl font-bold">条目检索</h1>
<div className="mx-auto max-w-4xl px-4 py-8">
<h1 className="mb-8 text-center text-3xl font-bold text-gray-900 dark:text-gray-100">
搜索
</h1>

<div className="mb-8">
<SearchForm
onSearch={handleSearch}
isSearching={isSearching}
initialValues={{
query: searchParams.get('term') || '',
domain: searchParams.get('domain') || '',
tag: searchParams.get('tag') || '',
year: searchParams.get('year') || '',
region: searchParams.get('region') || '',
}}
initialValues={{ query: searchParams.get('term') || '' }}
Comment thread
weekendfish marked this conversation as resolved.
Outdated
/>
<SearchResults results={results} error={error} />
</div>
</div>
)
}

// Main exported component with Suspense boundary
export default function FileSearch() {
return (
<Suspense fallback={<SearchLoading />}>
<SearchContent />
</Suspense>
{(hasInitialSearch || results.length > 0) && (
<SearchResults results={results} error={error} />
)}
</div>
)
}
}
94 changes: 94 additions & 0 deletions components/search/useClientSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState, useCallback } from 'react'
import pako from 'pako'
import { SearchResult, SearchParams } from './SearchResult'

const REPO_INDEXES: Record<string, string> = {
"digital.transchinese.org": "/search-index/repo-digital-transchinese-org.json.gz",
"novel.transchinese.org": "/search-index/repo-novel-transchinese-org.json.gz",
"comic.transchinese.org": "/search-index/repo-comic-transchinese-org.json.gz",
"archive.cdtsf.com": "/search-index/repo-archive-cdtsf-com.json.gz",
"news.transchinese.org": "/search-index/repo-news-transchinese-org.json.gz",
Comment thread
weekendfish marked this conversation as resolved.
Outdated
}

const cache: Record<string, any> = {}
Comment thread
weekendfish marked this conversation as resolved.
Outdated

async function loadIndex(domain: string): Promise<any | null> {
if (cache[domain]) return cache[domain]
const file = REPO_INDEXES[domain]
if (!file) return null

try {
const res = await fetch(file)
const compressed = await res.arrayBuffer()
const decompressed = pako.inflate(new Uint8Array(compressed), { to: 'string' })
const data = JSON.parse(decompressed)
cache[domain] = data
return data
} catch (e) {
console.error('Failed to load', domain, e)
return null
Comment thread
weekendfish marked this conversation as resolved.
Outdated
}
}

export function useClientSearch() {
const [isSearching, setIsSearching] = useState(false)
const [error, setError] = useState<string | null>(null)
const [results, setResults] = useState<SearchResult[]>([])

const search = useCallback(async (params: SearchParams) => {
setIsSearching(true)
setError(null)
setResults([])

try {
const domains = params.domain ? params.domain.split(',') : Object.keys(REPO_INDEXES)
const lowerQuery = params.query.toLowerCase()
const found: SearchResult[] = []

for (const domain of domains) {
const index = await loadIndex(domain)
if (!index) continue

for (const [key, doc] of Object.entries(index)) {
const d = doc as any
const keyLower = key.toLowerCase()
const descLower = (d.description || '').toLowerCase()

if (lowerQuery && !keyLower.includes(lowerQuery) && !descLower.includes(lowerQuery)) {
Comment thread
weekendfish marked this conversation as resolved.
Outdated
continue
}

if (params.tag && d.tags && !d.tags.includes(params.tag)) continue
if (params.year && d.date && !d.date.includes(params.year)) continue
if (params.region && d.region !== params.region) continue

found.push({
url: 'https://' + domain + '/' + key.replace(/\.[^/.]+$/, ''),
description: d.description || '',
tags: d.tags || [],
type: d.type || '',
author: d.author || '',
date: d.date || '',
region: d.region || '',
format: d.format || '',
size: d.size || 0,
link: 'https://' + domain + '/' + key.replace(/\.[^/.]+$/, ''),
Comment thread
weekendfish marked this conversation as resolved.
Outdated
})

if (found.length >= 100) break
Comment thread
weekendfish marked this conversation as resolved.
Outdated
}

if (found.length >= 100) break
}
Comment thread
weekendfish marked this conversation as resolved.
Outdated

setResults(found)
} catch (e: any) {
setError(e.message)
} finally {
setIsSearching(false)
}
}, [])

return { search, isSearching, error, results }
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new client-side search functionality lacks test coverage. Given that this is a critical feature change (from server-side to client-side search), consider adding tests for:

  1. Index loading and caching
  2. Search filtering logic (query, tag, year, region)
  3. Error handling scenarios
  4. Result limit behavior

Copilot uses AI. Check for mistakes.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"next-contentlayer2": "0.5.3",
"next-themes": "^0.3.0",
"openai": "^4.72.0",
"pako": "^2.1.0",
"pliny": "^0.4.0",
"postcss": "^8.4.24",
"react": "rc",
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added public/search-index/repo-snovel-cdtsf-com.json.gz
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion scripts/rss.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path'
import { slug } from 'github-slugger'
import { escape } from 'pliny/utils/htmlEscaper.js'
import siteMetadata from '../data/siteMetadata.js'
import tagData from '../app/tag-data.json' assert { type: 'json' }
import tagData from '../app/tag-data.json' with { type: "json" }
Comment thread
weekendfish marked this conversation as resolved.
Outdated
import { allBlogs } from '../.contentlayer/generated/index.mjs'
import { sortPosts } from 'pliny/utils/contentlayer.js'

Expand Down
Binary file added search-index.json.gz
Binary file not shown.
6 changes: 6 additions & 0 deletions wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name = "transchinese-test"
compatibility_date = "2024-01-01"
account_id = "bc82e687de1803b47adc1dfe23aca0a4"
Comment thread
weekendfish marked this conversation as resolved.
Outdated

[site]
bucket = "./out"
6 changes: 6 additions & 0 deletions wrangler.toml.prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name = "transchinese-org"
compatibility_date = "2024-01-01"
account_id = "bc82e687de1803b47adc1dfe23aca0a4"

[site]
bucket = "./out"
Loading