PROJECT OVERVIEW:
Develop Civic Pulse, a robust SaaS platform and mobile application designed to bridge the gap between local governance and citizens. The primary problem Civic Pulse solves is the 'Notification Problem' identified in communities like El Paso, where crucial decisions are made and implemented before the public is adequately informed or has a meaningful opportunity to participate. Civic Pulse aims to empower citizens by providing timely, relevant, and localized information about civic matters, enabling proactive engagement, fostering community organization, and strengthening local democracy. The core value proposition is to ensure that citizens know 'soon enough' about decisions that affect their lives and communities.
TECH STACK:
- Frontend: React (Next.js App Router)
- Styling: Tailwind CSS
- ORM: Drizzle ORM (with PostgreSQL as the database)
- UI Components: shadcn/ui
- Authentication: NextAuth.js (or Clerk for easier integration)
- State Management: React Context API / Zustand for global state
- Forms: React Hook Form
- Validation: Zod
- Data Fetching: Server Actions / React Query (for client-side caching if needed)
- Deployment: Vercel
- Database: PostgreSQL (managed via Supabase or Neon.tech for ease of use)
- Additional Packages: `date-fns` for date manipulation, `react-icons` for icons, `clsx` for conditional class names.
DATABASE SCHEMA (PostgreSQL with Drizzle ORM):
1. `users` table:
- `id` (uuid, primary key, default: uuid_generate_v4())
- `name` (text)
- `email` (text, unique)
- `emailVerified` (timestamp with time zone)
- `image` (text)
- `createdAt` (timestamp with time zone, default: now())
- `updatedAt` (timestamp with time zone)
2. `accounts` table (for NextAuth.js):
- `id` (uuid, primary key, default: uuid_generate_v4())
- `userId` (uuid, foreign key to `users.id`)
- `type` (text)
- `provider` (text)
- `providerAccountId` (text)
- `refresh_token` (text)
- `access_token` (text)
- `expires_at` (bigint)
- `token_type` (text)
- `scope` (text)
- `id_token` (text)
- `session_state` (text)
3. `sessions` table (for NextAuth.js):
- `id` (uuid, primary key, default: uuid_generate_v4())
- `userId` (uuid, foreign key to `users.id`)
- `expires` (timestamp with time zone)
- `sessionToken` (text, unique)
4. `organizations` table:
- `id` (uuid, primary key, default: uuid_generate_v4())
- `name` (text, not null)
- `description` (text)
- `website` (text)
- `logoUrl` (text)
- `createdAt` (timestamp with time zone, default: now())
5. `locations` table (Geographic Areas):
- `id` (uuid, primary key, default: uuid_generate_v4())
- `name` (text, not null) - e.g., "El Paso County", "Downtown District"
- `type` (text) - e.g., "County", "City", "Neighborhood"
- `geoJson` (jsonb) - Optional: For map-based filtering/display
- `createdAt` (timestamp with time zone, default: now())
6. `user_locations` table (User-defined notification areas):
- `userId` (uuid, foreign key to `users.id`)
- `locationId` (uuid, foreign key to `locations.id`)
- `notificationLevel` (text) - e.g., "High", "Medium", "Low"
- `primary` (boolean, default: false) - User's primary area
- `createdAt` (timestamp with time zone, default: now())
- Primary Key (`userId`, `locationId`)
7. `categories` table:
- `id` (uuid, primary key, default: uuid_generate_v4())
- `name` (text, not null, unique) - e.g., "Zoning", "Public Works", "Education", "Environment"
8. `announcements` table:
- `id` (uuid, primary key, default: uuid_generate_v4())
- `title` (text, not null)
- `content` (text, not null)
- `sourceUrl` (text) - Link to original source
- `sourceOrganizationId` (uuid, foreign key to `organizations.id`)
- `categoryId` (uuid, foreign key to `categories.id`)
- `locationId` (uuid, foreign key to `locations.id`)
- `meetingTimestamp` (timestamp with time zone) - If applicable
- `decisionTimestamp` (timestamp with time zone) - If applicable
- `createdAt` (timestamp with time zone, default: now())
- `publishedAt` (timestamp with time zone) - When it was made public on Civic Pulse
9. `user_subscriptions` table (User interest in categories):
- `userId` (uuid, foreign key to `users.id`)
- `categoryId` (uuid, foreign key to `categories.id`)
- `notificationPreference` (text) - e.g., "Email", "Push", "In-App"
- Primary Key (`userId`, `categoryId`)
10. `discussions` table:
- `id` (uuid, primary key, default: uuid_generate_v4())
- `announcementId` (uuid, foreign key to `announcements.id`) - Optional, can be general topic
- `title` (text, not null)
- `content` (text)
- `authorId` (uuid, foreign key to `users.id`)
- `locationId` (uuid, foreign key to `locations.id`) - For local discussions
- `createdAt` (timestamp with time zone, default: now())
11. `discussion_posts` table:
- `id` (uuid, primary key, default: uuid_generate_v4())
- `discussionId` (uuid, foreign key to `discussions.id`)
- `authorId` (uuid, foreign key to `users.id`)
- `content` (text, not null)
- `createdAt` (timestamp with time zone, default: now())
12. `events` table (For volunteer calls/opportunities):
- `id` (uuid, primary key, default: uuid_generate_v4())
- `title` (text, not null)
- `description` (text)
- `locationId` (uuid, foreign key to `locations.id`)
- `eventTimestamp` (timestamp with time zone)
- `organizerId` (uuid, foreign key to `organizations.id`)
- `createdAt` (timestamp with time zone, default: now())
CORE FEATURES & USER FLOW:
1. **User Authentication & Profile Management:**
* **Flow:** User lands on the homepage. Clicks "Sign Up" or "Log In".
* Sign Up: Uses email/password or OAuth (Google/GitHub). Upon successful sign up, user is redirected to onboarding.
* Log In: Enters credentials or uses OAuth. Redirected to dashboard or previously intended page.
* Onboarding: Prompt user to select their primary location (e.g., city, county) and key interest categories (e.g., zoning, environment). Guides them to set notification preferences.
* Profile Page: Accessible from navigation. Users can update their name, email, password, and manage their primary location and subscribed categories/locations.
* **Edge Cases:** Email already exists, invalid credentials, OAuth errors, session expiration.
2. **Location-Based Notification Settings:**
* **Flow:** Within onboarding or settings, user searches for a location (city, county, neighborhood). Selects it. Chooses notification level ('Critical', 'Important', 'All').
* The system stores the `userId` and `locationId` in `user_locations` table, potentially linking to `locations` data (could be pre-populated or dynamically fetched/added).
* **Edge Cases:** Location not found, multiple locations with similar names, user wants to define custom areas (future feature).
3. **Announcement Feed & Filtering:**
* **Flow:** Authenticated user lands on the dashboard. Sees a feed of announcements relevant to their selected `user_locations` and `user_subscriptions` (categories).
* Feed displays `announcements` sorted by `publishedAt` or `meetingTimestamp` (user configurable).
* Filtering options at the top: by location, category, date range, keywords.
* Each announcement item shows title, brief snippet, source org, category, and relevant dates. Clicking expands to show full content, source URL, and links to related discussions.
* **Edge Cases:** No announcements for the selected filters, handling different announcement types (meetings vs. decisions), empty state for new users.
4. **Category Subscription:**
* **Flow:** In settings or during onboarding, user browses available `categories`. Clicks to subscribe. System adds entry to `user_subscriptions` table.
* This dictates which categories appear in their filtered feed and powers push/email notifications.
* **Edge Cases:** User unsubscribes, category list is long (pagination/search needed).
5. **Basic Discussion Forum:**
* **Flow:** User navigates to the 'Discussions' section. Sees a list of discussion topics (potentially linked to specific announcements or locations).
* User can start a new discussion by providing a title and content.
* Clicking a discussion shows all posts. User can add a new post to the discussion.
* Posts display author, content, and timestamp.
* **Edge Cases:** Moderation needs (reporting posts), discussion formatting, handling threads within discussions.
6. **Event/Volunteer Board:**
* **Flow:** User navigates to 'Events'. Sees a list of upcoming volunteer opportunities or community events.
* Events are filterable by location and date.
* Each event shows details and organizer info. Possibly a button to RSVP or sign up (MVP might just display info).
* **Edge Cases:** Event is in the past, no events listed.
API & DATA FETCHING:
- **Next.js App Router Approach:** Utilize Server Components for fetching data directly from the database where possible, ensuring initial page loads are fast and SEO-friendly. Use Server Actions for mutations (creating/updating data) and form submissions.
- **API Routes (if needed):** Primarily for complex or real-time operations not directly suited for Server Actions, or for external integrations. Example: `POST /api/webhooks/scrape-announcements` (for a backend scraping job).
- **Data Fetching Logic:** Server Components will fetch data based on user session (userId), locationId, and subscribed categories. Example Server Component fetching announcements:
```jsx
// app/dashboard/page.tsx (Server Component)
import { db } from '@/lib/db';
import { announcements, userSubscriptions, categories, locations, userLocations } from '@/lib/schema';
import { eq, and, inArray, sql } from 'drizzle-orm';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import AnnouncementCard from './AnnouncementCard'; // Client Component
async function getAnnouncements(userId: string) {
const userLocs = await db.query.userLocations.findMany({
where: eq(userLocations.userId, userId),
with: { location: true },
});
const subscribedCats = await db.query.userSubscriptions.findMany({
where: eq(userSubscriptions.userId, userId),
});
const locationIds = userLocs.map(ul => ul.locationId);
const categoryIds = subscribedCats.map(us => us.categoryId);
// Fetch announcements matching user's locations OR subscribed categories
// Adjust logic based on desired priority/filtering
const announcementsData = await db.query.announcements.findMany({
where: (ann, { inArray, or, and, gt }) =>
or(
inArray(ann.locationId, locationIds),
inArray(ann.categoryId, categoryIds)
),
with: {
sourceOrganization: true,
category: true,
location: true,
},
orderBy: [sql`ann.publishedAt DESC`]
});
return announcementsData;
}
export default async function DashboardPage() {
const session = await getServerSession(authOptions);
const userId = session?.user?.id;
if (!userId) return <div>Please log in.</div>;
const announcements = await getAnnouncements(userId);
return (
<div className="p-4 space-y-4">
<h1 className="text-2xl font-bold">Your Civic Feed</h1>
{announcements.length > 0 ? (
announcements.map(ann => (
<AnnouncementCard key={ann.id} announcement={ann} />
))
) : (
<p>No announcements found for your preferences. Try broadening your search or updating your settings.</p>
)}
</div>
);
}
```
- **Mutations:** Server Actions will handle creating discussions, posts, updating profiles, etc. Example:
```jsx
// app/_actions/discussion.ts
'use server';
import { db } from '@/lib/db';
import { discussions, discussionPosts } from '@/lib/schema';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export async function createDiscussionPost(discussionId: string, content: string) {
const session = await getServerSession(authOptions);
const userId = session?.user?.id;
if (!userId) {
throw new Error('Authentication required');
}
await db.insert(discussionPosts).values({
discussionId,
content,
authorId: userId,
});
revalidatePath(`/discussions/${discussionId}`); // Clear cache for this path
}
```
COMPONENT BREAKDOWN (Next.js App Router):
- **Pages (App Router Routes):**
* `(app)/layout.tsx`: Root layout (includes providers, main nav, footer).
* `(app)/page.tsx`: Landing page (for unauthenticated users). Features value proposition, call to action.
* `(app)/dashboard/page.tsx`: Main dashboard (for authenticated users). Displays announcement feed.
* `(app)/dashboard/settings/page.tsx`: User profile, notification settings, location/category management.
* `(app)/announcements/[id]/page.tsx`: Detail page for a single announcement.
* `(app)/discussions/page.tsx`: List of all discussion topics.
* `(app)/discussions/[id]/page.tsx`: View a single discussion and its posts, form to add new posts.
* `(app)/events/page.tsx`: List of community events.
* `(app)/auth/signin/page.tsx`: Sign in page (if not using default NextAuth adapter).
* `(app)/layout.tsx`: Nested layout for authenticated routes (e.g., with sidebar).
- **UI Components (Client & Server):**
* `components/ui/` (shadcn/ui components): Button, Input, Card, Sheet, Dialog, DropdownMenu, Alert, etc.
* `components/layout/Navbar.tsx`: Main navigation bar (logo, links, auth status).
* `components/layout/Footer.tsx`: Footer with essential links.
* `components/dashboard/AnnouncementFeed.tsx`: (Server Component) Fetches and renders the list of announcements.
* `components/dashboard/AnnouncementCard.tsx`: (Client Component) Displays a single announcement summary. Handles interactions like 'view details'.
* `components/settings/NotificationSettings.tsx`: (Client Component) Form for managing location/category subscriptions and notification preferences.
* `components/settings/ProfileForm.tsx`: (Client Component) Form for updating user profile details.
* `components/discussions/DiscussionList.tsx`: (Server Component) Fetches and displays discussion topics.
* `components/discussions/DiscussionForm.tsx`: (Client Component) Form to create a new discussion.
* `components/discussions/PostList.tsx`: (Server Component) Fetches and displays posts for a specific discussion.
* `components/discussions/PostForm.tsx`: (Client Component) Form to add a new post to a discussion.
* `components/events/EventList.tsx`: (Server Component) Fetches and displays upcoming events.
* `components/common/LocationSearch.tsx`: (Client Component) Autocomplete/search for locations.
* `components/common/CategorySelector.tsx`: (Client Component) Allows selecting interest categories.
* `components/common/LoadingSpinner.tsx`: Global or component-specific loading indicator.
* `components/common/AuthButton.tsx`: Button handling login/logout.
- **State Management:** Use `useState` and `useReducer` for local component state. Use React Context API or Zustand for global state like user session, theme settings, potentially fetched data that needs to be accessed by multiple deeply nested client components.
UI/UX DESIGN & VISUAL IDENTITY:
- **Design Style:** Modern, Clean, Trustworthy, Accessible.
- **Color Palette:**
* Primary: A calming but professional blue (`#3b82f6` - Tailwind's `blue-500`)
* Secondary/Accent: A slightly muted teal or green (`#14b8a6` - Tailwind's `teal-400`)
* Background: Off-white (`#f8fafc` - Tailwind's `slate-50`)
* Dark Background (for dark mode): Very dark grey (`#1e293b` - Tailwind's `slate-800`)
* Text (Light): Dark grey (`#334155` - Tailwind's `slate-700`)
* Text (Dark): Light grey (`#f1f5f9` - Tailwind's `slate-200`)
* Alerts/Notifications: Muted orange/yellow (`#f59e0b` - Tailwind's `amber-500`)
- **Typography:** Sans-serif font family. Use Inter or Roboto for clean readability.
* Headings: `font-bold`, sizes like `text-3xl`, `text-2xl`.
* Body text: `font-normal`, `text-base` or `text-lg`.
- **Layout:**
* Use a responsive grid system (Tailwind's default).
* Max-width container for content (`max-w-6xl` or `max-w-7xl`).
* Clear visual hierarchy. Use whitespace effectively.
* Dashboard: Sidebar for navigation (if complex) or top navigation. Main content area.
* Forms: Clear labels, logical grouping, ample spacing.
- **Responsiveness:** Mobile-first approach. Ensure usability on small screens. Use Tailwind's responsive prefixes ( `sm:`, `md:`, `lg:`, `xl:`).
- **Visual Elements:** Subtle gradients on buttons or headers, clean icons, possibly a subtle background pattern on lighter themes. Data visualizations (if added later) should be clear and accessible.
ANIMATIONS:
- **Page Transitions:** Subtle fade-in/out or slide animations between pages using Next.js's built-in features or a library like `Framer Motion` (if needed, but keep it minimal for MVP).
- **Hover Effects:** Slight scaling or background color change on interactive elements (buttons, links, cards).
- **Loading States:** Use spinners (`components/common/LoadingSpinner.tsx`) or skeleton loaders for data fetching. Server components can show initial loading states with `Suspense` boundary.
- **Transitions:** Smooth transitions for UI element changes (e.g., expanding a card, dropdown menus opening) using Tailwind CSS transitions.
EDGE CASES & VALIDATION:
- **Authentication:** Handle unauthorized access gracefully (redirect to login, show messages). Implement JWT refresh logic if using NextAuth.js. Ensure session validity checks.
- **Empty States:** Design informative and helpful empty states for feeds, discussion lists, event boards when no data is available. Include calls to action (e.g., "Start a discussion", "Add your location").
- **Form Validation:** Use Zod with React Hook Form for robust client-side and server-side validation (e.g., email format, required fields, password strength).
- **Data Integrity:** Use database constraints (NOT NULL, UNIQUE) and Drizzle ORM's schema definitions. Implement transactional logic for critical operations.
- **Error Handling:** Implement try-catch blocks in Server Actions and API routes. Display user-friendly error messages. Log errors to a monitoring service (e.g., Sentry).
- **Rate Limiting:** Consider basic rate limiting on API endpoints to prevent abuse.
- **Permissions:** Ensure users can only modify their own data (profiles, settings, posts).
SAMPLE/MOCK DATA:
1. **Announcement 1 (Meeting):**
* `title`: "City Council Meeting: Proposed Zoning Change for District 4"
* `content`: "Discussion and vote on rezoning Parcel B-12 from residential to commercial..."
* `sourceOrganization.name`: "City of Metropolis"
* `category.name`: "Zoning"
* `location.name`: "Metropolis - District 4"
* `meetingTimestamp`: "2023-12-15T19:00:00Z"
* `sourceUrl`: "http://cityofmetropolis.gov/agendas/12345"
2. **Announcement 2 (Decision):**
* `title`: "New Park Development Approved for West Side"
* `content`: "The Parks Commission has approved the budget for the new Green Valley Park..."
* `sourceOrganization.name`: "Metropolis Parks Commission"
* `category.name`: "Public Works / Parks"
* `location.name`: "Metropolis - West Side"
* `decisionTimestamp`: "2023-11-20T10:30:00Z"
* `sourceUrl`: "http://parks.metropolis.gov/news/5678"
3. **Announcement 3 (Public Notice):**
* `title`: "Water Main Maintenance Scheduled - Oak Street Area"
* `content`: "Residents on Oak Street between 1st and 5th Ave may experience temporary water disruptions..."
* `sourceOrganization.name`: "Metropolis Water Department"
* `category.name`: "Utilities"
* `location.name`: "Metropolis - Oak Street Corridor"
* `publishedAt`: "2023-12-10T09:00:00Z"
* `sourceUrl`: null
4. **Discussion Topic 1:**
* `title`: "Concerns about the Oak Street Water Main Work"
* `content`: "Is anyone else worried about the duration? They said temporary but..."
* `author.name`: "Concerned Resident"
* `location.name`: "Metropolis - Oak Street Corridor"
5. **Event 1:**
* `title`: "Community Cleanup Day - Riverfront Park"
* `description`: "Join us to clean up litter along the river. Supplies provided."
* `organizer.name`: "Friends of Riverfront"
* `location.name`: "Metropolis - Riverfront Park"
* `eventTimestamp`: "2024-01-20T10:00:00Z"
6. **User 1:**
* `name`: "Alice Wonderland"
* `email`: "alice@example.com"
* `userLocations`: [{ `location.name`: "Metropolis - District 4", `notificationLevel`: "High" }]
* `userSubscriptions`: [{ `category.name`: "Zoning" }, { `category.name`: "Environment" }]
7. **User 2:**
* `name`: "Bob The Builder"
* `email`: "bob@example.com"
* `userLocations`: [{ `location.name`: "Metropolis - West Side", `notificationLevel`: "Medium" }]
* `userSubscriptions`: [{ `category.name`: "Public Works / Parks" }, { `category.name`: "Utilities" }]
TURKISH TRANSLATIONS (for UI elements, assuming components will dynamically use these or fetch from a locale file):
- **Navbar:**
* Logo: Civic Pulse
* Links: Dashboard (Gösterge Paneli), Discussions (Tartışmalar), Events (Etkinlikler), Settings (Ayarlar)
* Auth: Log In (Giriş Yap), Sign Up (Kayıt Ol), Log Out (Çıkış Yap)
- **Dashboard:**
* Title: Your Civic Feed (Sizin Yurttaşlık Akışınız)
* Empty State: "Gö preferênciasünüze uygun duyuru bulunamadı. Aramanızı genişletmeyi veya ayarlarınızı güncellemeyi deneyin."
- **Settings:**
* Title: Settings (Ayarlar)
* Section Titles: Profile (Profil), Notification Preferences (Bildirim Tercihleri), Manage Locations (Konumları Yönet), Manage Categories (Kategorileri Yönet)
* Labels: Full Name (Ad Soyad), Email (E-posta), Password (Şifre), Current Location (Güncel Konum), Subscribe to Categories (Kategorilere Abone Ol), Save Changes (Değişiklikleri Kaydet)
* Location Input Placeholder: "Şehir, İlçe veya Mahalle Adı Girin..."
* Category Options: İmar, Kamu Hizmetleri, Eğitim, Çevre, Altyapı, Sağlık, Güvenlik, Yerel Yönetim, Toplu Taşıma, Kültür/Sanat
- **Announcements:**
*