diff --git a/apps/web/app/search/page.test.tsx b/apps/web/app/search/page.test.tsx
new file mode 100644
index 0000000..3ee4e9e
--- /dev/null
+++ b/apps/web/app/search/page.test.tsx
@@ -0,0 +1,47 @@
+import { render, screen } from '@testing-library/react';
+import { SearchResults } from './page';
+
+const makeHit = (id: string, content: string) => ({
+ id,
+ content,
+ senderName: 'Alice',
+ sourceGroupName: 'UP Parivar Dallas',
+ tags: ['#important'],
+ approvedAt: 1748390400000,
+});
+
+describe('SearchResults', () => {
+ it('shows "no results" when hits array is empty', () => {
+ render();
+ expect(screen.getByText(/no results/i)).toBeInTheDocument();
+ });
+
+ it('renders each hit content', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText('Hello world')).toBeInTheDocument();
+ expect(screen.getByText('Event tonight')).toBeInTheDocument();
+ });
+
+ it('shows the total result count', () => {
+ render();
+ expect(screen.getByText(/42/)).toBeInTheDocument();
+ });
+
+ it('shows sender name and group name for each hit', () => {
+ render();
+ expect(screen.getByText(/alice/i)).toBeInTheDocument();
+ expect(screen.getByText(/UP Parivar Dallas/i)).toBeInTheDocument();
+ });
+
+ it('shows tags for each hit', () => {
+ render();
+ expect(screen.getByText('#important')).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/app/search/page.tsx b/apps/web/app/search/page.tsx
new file mode 100644
index 0000000..8a5adaf
--- /dev/null
+++ b/apps/web/app/search/page.tsx
@@ -0,0 +1,103 @@
+interface MeiliHit {
+ id: string;
+ content: string;
+ senderName: string;
+ sourceGroupName: string;
+ tags: string[];
+ approvedAt: number;
+}
+
+interface SearchResponse {
+ hits: MeiliHit[];
+ total: number;
+ page: number;
+ limit: number;
+ query: string;
+}
+
+export function SearchResults({
+ hits,
+ total,
+ q,
+ page,
+}: {
+ hits: MeiliHit[];
+ total: number;
+ q: string;
+ page: number;
+}) {
+ return (
+
+
+
+ {hits.length === 0 ? (
+
No results{q ? ` for "${q}"` : ''}.
+ ) : (
+ <>
+
+ {total} result{total !== 1 ? 's' : ''}
+
+
+ >
+ )}
+
+ );
+}
+
+export default async function SearchPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ q?: string; page?: string }>;
+}) {
+ const { q = '', page = '1' } = await searchParams;
+ const apiUrl = process.env.API_URL ?? 'http://localhost:3001';
+ const url = new URL(`${apiUrl}/search`);
+ url.searchParams.set('q', q);
+ url.searchParams.set('page', page);
+
+ let data: SearchResponse = { hits: [], total: 0, page: 1, limit: 20, query: q };
+ try {
+ const res = await fetch(url, { cache: 'no-store' });
+ if (res.ok) data = await res.json();
+ } catch {
+ // API unavailable — render empty results
+ }
+
+ return (
+
+
Search
+
+
+ );
+}