Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
71 changes: 70 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Technology stack:
**Layout** (`app/layout.tsx`):
- Theme provider (dark mode via next-themes)
- Site header with navigation (site-header.tsx)
- Footer, toast notifications, Vercel Analytics
- Footer, toast notifications, Vercel Analytics, Google Analytics

**Navigation** (`config/site.ts`): Home, Chat, About, Proceedings, Acts, Constitution

Expand Down Expand Up @@ -173,6 +173,8 @@ Technology stack:
- `pehchan-button.tsx` - OAuth login button
- `site-header.tsx` - Navigation header
- `message-threads-sidebar.tsx` - Chat history sidebar
- `google-analytics.tsx` - GA4 script loader
- `*-view-tracker.tsx` - Analytics tracking wrappers for server components
- `footer.tsx`, `theme-provider.tsx`

## Development Guidelines
Expand Down Expand Up @@ -285,13 +287,80 @@ CREATE INDEX ON embeddings USING hnsw (embedding vector_cosine_ops);
### Auth Token Expiry
Pehchan tokens expire. If users report auth issues, they need to re-login. Implement token refresh if needed.

## Analytics & Monitoring

### Google Analytics (GA4)

**Tracking ID**: `G-QMPHXVV7TX`

**Implementation** (`components/google-analytics.tsx`, `lib/analytics.ts`):
- Next.js Script component with `afterInteractive` strategy for optimized loading
- Comprehensive event tracking across all user interactions
- Client-side wrappers for server components to track page views

**Key Metrics Tracked**:

1. **User Engagement**
- Page views (automatic)
- Session duration
- Active users

2. **Chat Interactions**
- `trackChatMessage(threadId?)` - Message sending
- `trackNewChatThread()` - New conversation creation

3. **Authentication**
- `trackPehchanLogin(success: boolean)` - Login attempts

4. **Document Views**
- `trackBillView(billId)` - Bill detail views
- `trackProceedingView(proceedingId)` - Proceeding views
- `trackConstitutionView()` - Constitution page views
- `trackDocumentView(type, id)` - Generic document tracking

5. **Admin Actions**
- `trackDocumentUpload(documentType)` - Document uploads

6. **Search & RAG**
- `trackSearch(query, resultCount)` - Search queries
- `trackRAGQuery(relevantChunks)` - RAG retrieval performance

**Tracking Components**:
- `components/bill-view-tracker.tsx` - Bill view tracking wrapper
- `components/proceeding-view-tracker.tsx` - Proceeding view wrapper
- `components/constitution-view-tracker.tsx` - Constitution view wrapper

These client components wrap server components to trigger GA events on mount.

**Adding New Tracking Events**:

1. Add function to `lib/analytics.ts`:
```typescript
export const trackCustomEvent = (label?: string) => {
trackEvent("event_name", "Category", label)
}
```

2. Import and use in components:
```typescript
import { trackCustomEvent } from "@/lib/analytics"

// In your component
trackCustomEvent("context")
```

**View Analytics**: https://analytics.google.com (Property: G-QMPHXVV7TX)

**Documentation**: See `docs/google-analytics-integration.md` for comprehensive setup details, metric recommendations, and troubleshooting.

## External Services

- **Pehchan**: Pakistan's national digital identity (OAuth provider)
- **OpenAI**: Embeddings and chat completions
- **AWS S3**: Document storage
- **Upstash QStash**: Background job queue
- **Vercel**: Hosting and analytics
- **Google Analytics**: User behavior and engagement tracking (GA4)

## Related Services

Expand Down
47 changes: 42 additions & 5 deletions app/api/bills/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,53 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { bills } from '@/lib/db/schema/bills';
import { desc } from 'drizzle-orm';
import { desc, or, ilike, count } from 'drizzle-orm';

export const dynamic = 'force-dynamic'; // Disable caching for this route

export async function GET() {
export async function GET(request: Request) {
try {
const allBills = await db.query.bills.findMany({
orderBy: [desc(bills.createdAt)],
const { searchParams } = new URL(request.url);
const search = searchParams.get('search') || '';
const page = parseInt(searchParams.get('page') || '1', 10);
const limit = parseInt(searchParams.get('limit') || '10', 10);
const offset = (page - 1) * limit;

// Build search condition
const searchCondition = search
? or(
ilike(bills.title, `%${search}%`),
ilike(bills.billNumber, `%${search}%`),
ilike(bills.status, `%${search}%`)
)
: undefined;

// Fetch paginated bills
const allBills = await db
.select()
.from(bills)
.where(searchCondition)
.orderBy(desc(bills.createdAt))
.limit(limit)
.offset(offset);

// Get total count for pagination
const [{ value: totalCount }] = await db
.select({ value: count() })
.from(bills)
.where(searchCondition);

const totalPages = Math.ceil(totalCount / limit);

return NextResponse.json({
data: allBills,
pagination: {
page,
limit,
totalCount,
totalPages,
},
});
return NextResponse.json(allBills);
} catch (error) {
console.error('Failed to fetch bills:', error);
return NextResponse.json({ error: 'Failed to fetch bills' }, { status: 500 });
Expand Down
56 changes: 56 additions & 0 deletions app/api/proceedings/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { parliamentaryProceedings } from '@/lib/db/schema/parliamentary-proceedings';
import { desc, ilike, count } from 'drizzle-orm';

export const dynamic = 'force-dynamic'; // Disable caching for this route

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const search = searchParams.get('search') || '';
const page = parseInt(searchParams.get('page') || '1', 10);
const limit = parseInt(searchParams.get('limit') || '10', 10);
const offset = (page - 1) * limit;

// Build search condition
const searchCondition = search
? ilike(parliamentaryProceedings.title, `%${search}%`)
: undefined;

// Fetch paginated proceedings
const proceedings = await db
.select({
id: parliamentaryProceedings.id,
title: parliamentaryProceedings.title,
date: parliamentaryProceedings.date,
createdAt: parliamentaryProceedings.createdAt,
})
.from(parliamentaryProceedings)
.where(searchCondition)
.orderBy(desc(parliamentaryProceedings.date))
.limit(limit)
.offset(offset);

// Get total count for pagination
const [{ value: totalCount }] = await db
.select({ value: count() })
.from(parliamentaryProceedings)
.where(searchCondition);

const totalPages = Math.ceil(totalCount / limit);

return NextResponse.json({
data: proceedings,
pagination: {
page,
limit,
totalCount,
totalPages,
},
});
} catch (error) {
console.error('Failed to fetch proceedings:', error);
return NextResponse.json({ error: 'Failed to fetch proceedings' }, { status: 500 });
}
}
120 changes: 96 additions & 24 deletions app/bills/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,60 @@

import { useState, useEffect } from 'react';
import Link from 'next/link';
import { Search } from 'lucide-react';
import { Skeleton } from "@/components/ui/skeleton";
import { Input } from "@/components/ui/input";
import { Pagination } from "@/components/ui/pagination";

// Define the type for a bill
interface Bill {
id: string;
title: string;
status: string;
createdAt: string; // Assuming createdAt is a string, adjust if it's a Date object
createdAt: string;
billNumber?: string;
}

interface BillsResponse {
data: Bill[];
pagination: {
page: number;
limit: number;
totalCount: number;
totalPages: number;
};
}

export default function BillsPage() {
const [allBills, setAllBills] = useState<Bill[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pagination, setPagination] = useState({
page: 1,
limit: 10,
totalCount: 0,
totalPages: 0,
});

useEffect(() => {
const fetchBills = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/bills');
const params = new URLSearchParams({
page: currentPage.toString(),
limit: '10',
...(searchQuery && { search: searchQuery }),
});
const response = await fetch(`/api/bills?${params}`);
if (!response.ok) {
throw new Error('Failed to fetch bills');
}
const data = await response.json();
setAllBills(data);
const data: BillsResponse = await response.json();
setAllBills(data.data);
setPagination(data.pagination);
} catch (err) {
setError((err as Error).message);
} finally {
Expand All @@ -36,7 +64,7 @@ export default function BillsPage() {
};

fetchBills();
}, []);
}, [currentPage, searchQuery]);

const getStatusClassName = (status: string) => {
switch (status) {
Expand All @@ -46,10 +74,32 @@ export default function BillsPage() {
}
};

const handleSearch = (value: string) => {
setSearchQuery(value);
setCurrentPage(1); // Reset to first page on new search
};

const handlePageChange = (page: number) => {
setCurrentPage(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
};

return (
<div className="container py-8">
<h1 className="mb-8 text-3xl font-bold">Acts of Parliament</h1>


{/* Search Bar */}
<div className="relative mb-6">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="text"
placeholder="Search bills by title, number, or status..."
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
className="pl-10"
/>
</div>

{loading && (
<div className="grid gap-4">
{[...Array(3)].map((_, index) => (
Expand All @@ -63,25 +113,47 @@ export default function BillsPage() {
)}

{!loading && !error && (
<div className="grid gap-4">
{allBills.map((bill) => (
<Link
key={bill.id}
href={`/bills/${bill.id}`}
className="block p-6 rounded-lg border bg-card text-card-foreground shadow-sm transition-shadow hover:shadow-md"
>
<h2 className="text-xl font-semibold mb-2 tracking-tight">{bill.title}</h2>
<div className="flex gap-4 text-sm text-muted-foreground">
<span className={`
px-2 py-0.5 rounded-full text-xs font-medium
${getStatusClassName(bill.status)}
`}>
{bill.status.charAt(0).toUpperCase() + bill.status.slice(1)}
</span>
<>
{allBills.length === 0 ? (
<div className="text-center py-12">
<p className="text-muted-foreground">No bills found matching your search.</p>
</div>
) : (
<>
<div className="grid gap-4 mb-8">
{allBills.map((bill) => (
<Link
key={bill.id}
href={`/bills/${bill.id}`}
className="block p-6 rounded-lg border bg-card text-card-foreground shadow-sm transition-shadow hover:shadow-md"
>
<h2 className="text-xl font-semibold mb-2 tracking-tight">{bill.title}</h2>
<div className="flex gap-4 text-sm text-muted-foreground">
<span className={`
px-2 py-0.5 rounded-full text-xs font-medium
${getStatusClassName(bill.status)}
`}>
{bill.status.charAt(0).toUpperCase() + bill.status.slice(1)}
</span>
</div>
</Link>
))}
</div>
</Link>
))}
</div>

{/* Pagination */}
<div className="flex flex-col items-center gap-4">
<Pagination
currentPage={pagination.page}
totalPages={pagination.totalPages}
onPageChange={handlePageChange}
/>
<p className="text-sm text-muted-foreground">
Showing {allBills.length} of {pagination.totalCount} bills
</p>
</div>
</>
)}
</>
)}
</div>
);
Expand Down
Loading
Loading