64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'motion/react';
|
|
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import AppLogo from './AppLogo';
|
|
import BottomNav from './BottomNav';
|
|
import { apiRequest } from '../lib/api';
|
|
import type { TestSummary } from '../lib/types';
|
|
|
|
export default function HomeFeed() {
|
|
const [tests, setTests] = useState<TestSummary[]>([]);
|
|
const [query, setQuery] = useState('');
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
apiRequest<TestSummary[]>('/api/v1/tests').then(setTests).catch((error: Error) => setError(error.message));
|
|
}, []);
|
|
|
|
const visible = useMemo(() => {
|
|
const keyword = query.trim().toLowerCase();
|
|
return tests.filter(test => `${test.title} ${test.subtitle}`.toLowerCase().includes(keyword));
|
|
}, [query, tests]);
|
|
|
|
return (
|
|
<main className="pageShell homePage">
|
|
<AppLogo />
|
|
<label className="searchBox">
|
|
<span aria-hidden>⌕</span>
|
|
<input value={query} onChange={event => setQuery(event.target.value)} placeholder="搜索测试" />
|
|
</label>
|
|
{error ? <ErrorState message={error} /> : null}
|
|
{!error && !tests.length ? <div className="emptyState">正在摆好擂台…</div> : null}
|
|
<section className="waterfall" aria-label="全部测试">
|
|
{visible.map((test, index) => (
|
|
<motion.article
|
|
key={test.slug}
|
|
className="testCard"
|
|
initial={{ opacity: 0, y: 18 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.045, duration: 0.32 }}
|
|
whileTap={{ scale: 0.975 }}
|
|
>
|
|
<Link href={`/tests/${test.slug}`}>
|
|
<Image src={`/assets/test-covers/${test.slug}.jpg`} width={900} height={1200} sizes="(max-width: 920px) 50vw, 450px" alt={`${test.title}封面`} />
|
|
<div className="cardCopy">
|
|
<h2>{test.title}</h2>
|
|
<p>{test.subtitle}</p>
|
|
<div className="cardMeta"><span>{test.candidate_count} 强</span><span>{test.run_count} 人完成</span></div>
|
|
</div>
|
|
</Link>
|
|
</motion.article>
|
|
))}
|
|
</section>
|
|
<BottomNav />
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function ErrorState({ message }: { message: string }) {
|
|
return <div className="emptyState"><strong>暂时没接上服务</strong><p>{message}</p></div>;
|
|
}
|