# Project export: votEZ

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: During the voting season, the media spreads information like wildfire. Imagine an app that cuts through this noise: votEZ helps break down propositions & political agendas into unbiased summaries.
- Devpost: https://devpost.com/software/votez
- GitHub: https://github.com/SLCoder1/CalHacks2025
- Team: 4 GitHub contributor(s) — ihagad (22 commits), SLCoder1 (21 commits), Varun Chakka (9 commits), ShlokPow (6 commits)

## Devpost submission (written by the team)

### Inspiration

💡 During 2024, our group turned 18, during peak election season. This means that we had gained the ability to vote. However, getting thrown into a massive storm of information getting spewed out left and right gets confusing real fast. With the amount of propositions, candidates, and agendas that were getting pushed onto us, it was easy to get overwhelmed by the amount of information. So we thought, _ what if we could have an app simplify everything for us? _

### What it does

📄 votEZ scrapes the web for the current running candidates for presidents, vice presidents, and governors for every state. It scrapes their agendas and using that information, simplifies it into a short passage that summarizes everything for the user with as little bias as possible. Another feature is that it scrapes the web for propositions and simplifies them into paragraphs and also gives potential outcomes to the proposition.

### How we built it

🛠️ For the backend, we built a CRUD system using python Flask API and gunicorn to handle http request handling, routing, and database management. Using python concurrently, we were able to run the backend server at the same time we ran the frontend script. For scraping, we used python BeautifulSoup to scrap ballotopedia.org and built multiple .csv files (which we then converted to json) to train our AI model. Using Groq, we made multiple chatbots: a general overview, and a specific chatbot that will help the user out with what is on the current page. Additionally, Groq was used to simplify the data on screen so that it would not take as much space on mobile compared to web. Groq was also used to translate languages on screen, so we can switch Using the Expo Router and React Native, we were able to build a concurrent mobile and web application. We used supabase for authentification and user/chat history.

### Challenges we ran into

🖋️ One of the biggest challenges we ran into was trying to host our backend server. Initially, our chatbot and scraping would only work on web because our code was based on localhost, meaning we needed a server. Initially, we had to run 3+ terminals to run the backend and the frontend for the mobile application to work (we had no idea why). Until we stumbled upon gunicorn and concurrently, which helped run both the backend and frontend server easily and also fixed our problem with mobile. Another problem that we had was with scraping. With web scraping with beautiful soup, we noticed a pattern that the scraper would always fail to fetch unless the website was already opened beforehand. Initially, we were considering to use selenium in order to open a headless browser with the sites to scrape. However, due to time, we just decided to compile all the data we can using the scrape with the time we had left and train the Groq model with that. Therefore, we had to only display data from 2022-2024 time periods as there is no current widespread election cycle data for 2026.

### What's next

📈 Currently, votEZ is only built around state governors, vice presidents, and presidents for previous years. Due to lack of time, we were not able to gather more data (such as county, district, boards, etc.) Additionally adding more accessibility features such as text-to-speech and voice input would have made the chatbot a lot more interactive. Since this towards people that do not have experience or resources whether that be due to time constraints or impoverished backgrounds, we aim to guide them through this process, one step at a time.

## README (from the GitHub repository)

# CalHacks2025

## Detected evidence (automated analysis)

Indexed codebase: 42 recognized source files, 138 KB.
- Flask (technology) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (54 of 54)

```
README.md
votEZ/.gitignore
votEZ/.vscode/settings.json
votEZ/API_SETUP.md
votEZ/app.json
votEZ/app/_layout.tsx
votEZ/app/(tabs)/_layout.tsx
votEZ/app/(tabs)/candidates.tsx
votEZ/app/(tabs)/explore.tsx
votEZ/app/(tabs)/index.tsx
votEZ/app/(tabs)/propositions.tsx
votEZ/app/+not-found.tsx
votEZ/app/index.tsx
votEZ/app/login.tsx
votEZ/app/test.tsx
votEZ/backend/.env
votEZ/backend/app.py
votEZ/backend/Governers - Sheet1.csv
votEZ/backend/Procfile
votEZ/backend/requirements.txt
votEZ/backend/runtime.txt
votEZ/backend/scraper.py
votEZ/backend/SETUP.md
votEZ/components/ChatBot.tsx
votEZ/components/ChatHistory.tsx
votEZ/components/Collapsible.tsx
votEZ/components/ExternalLink.tsx
votEZ/components/Footer.tsx
votEZ/components/HapticTab.tsx
votEZ/components/HelloWave.tsx
votEZ/components/PageContext.tsx
votEZ/components/ParallaxScrollView.tsx
votEZ/components/TestScraper.tsx
votEZ/components/ThemedText.tsx
votEZ/components/ThemedView.tsx
votEZ/components/ui/IconSymbol.ios.tsx
votEZ/components/ui/IconSymbol.tsx
votEZ/components/ui/TabBarBackground.ios.tsx
votEZ/components/ui/TabBarBackground.tsx
votEZ/config/api.ts
votEZ/constants/Colors.ts
votEZ/data/Governers - Sheet1.json
votEZ/data/Propositions 2024 - Sheet1.json
votEZ/database/chat_history_schema.sql
votEZ/eslint.config.js
votEZ/hooks/useColorScheme.ts
votEZ/hooks/useColorScheme.web.ts
votEZ/hooks/useThemeColor.ts
votEZ/lib/chatService.ts
votEZ/lib/supabase.ts
votEZ/package.json
votEZ/README.md
votEZ/scripts/reset-project.js
votEZ/tsconfig.json
```

### Dependencies

- votEZ/backend/requirements.txt: beautifulsoup4@==4.12.2, flask@==3.0.0, flask-cors@==4.0.0, gunicorn@==21.2.0, python-dotenv@==1.0.0, requests@==2.31.0
- votEZ/package.json: @babel/core@^7.25.2, @expo/vector-icons@^14.1.0, @react-native-async-storage/async-storage@2.1.2, @react-navigation/bottom-tabs@^7.3.10, @react-navigation/elements@^2.3.8, @react-navigation/native@^7.1.6, @supabase/supabase-js@^2.50.0, @types/react@~19.0.10, concurrently@^8.2.2, eslint@^9.25.0, eslint-config-expo@~9.2.0, expo@~53.0.12, expo-asset@^11.1.5, expo-blur@~14.1.5, expo-constants@~17.1.6, expo-font@~13.3.1, expo-haptics@~14.1.4, expo-image@~2.3.0, expo-linear-gradient@^14.1.5, expo-linking@~7.1.5, expo-router@~5.1.0, expo-splash-screen@~0.30.9, expo-status-bar@~2.2.3, expo-symbols@~0.4.5, expo-system-ui@~5.0.9, expo-web-browser@~14.2.0, package.json@^2.0.1, papaparse@^5.5.3, react@19.0.0, react-dom@19.0.0, react-native@0.79.4, react-native-dropdown-picker@^5.4.6, react-native-gesture-handler@~2.24.0, react-native-reanimated@~3.17.4, react-native-safe-area-context@5.4.0, react-native-screens@~4.11.1, react-native-url-polyfill@^2.0.0, react-native-web@~0.20.0, react-native-webview@13.13.5, typescript@~5.8.3

### Recent commits (newest first)

- Changed footer
- Merge pull request #19 from SLCoder1/SLCoder1/ChatBotFeatures
- Merge pull request #18 from SLCoder1/vroomc08/footer
- Merge branch 'main' into vroomc08/footer
- Added README.md and also edited chat to be more accurate
- Added README.md and also edited chat to be more accurate
- Added footer & citations
- Merge pull request #17 from SLCoder1/ihagad/mergeconflict
- fixed merge conflicts
- Fixed some of the merges between SLCoder1/ChatBotFeatures and main
- Merge branch 'SLCoder1/ChatBotFeatures' of https://github.com/SLCoder1/CalHacks2025 into ihagad/mergeconflict
- Merge pull request #14 from SLCoder1/csvfileuseProp
- Merge branch 'main' of https://github.com/SLCoder1/CalHacks2025 into csvfileuseProp
- Added chatbot history that user can access
- Merge pull request #13 from SLCoder1/ihagad/candidate
- comparison button
- Added chatbot option to just use info from screen, and added chatbot option to use general groq
- Merge pull request #12 from SLCoder1/ihagad/csvfileuse
- deescription gov candidate edits
- propositions json file integration

## Key source files (fetched from GitHub, selected and truncated for size)

### votEZ/API_SETUP.md

```markdown
# API Setup Guide

## Overview
The chatbot connects to a Flask backend. This guide explains how to configure the API URL for different environments.

## Configuration File
Edit `config/api.ts` to set the correct API URL for your environment.

## Setup Options

### 1. Local Development (Default)
```typescript
development: 'http://localhost:3001'
```
- Works on simulator/emulator
- Backend runs on your computer

### 2. Physical Device Testing
```typescript
development: 'http://YOUR_COMPUTER_IP:3001'
```
- Replace `YOUR_COMPUTER_IP` with your computer's IP address
- Both device and computer must be on same WiFi network
- Find your IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux)

### 3. Production Deployment
```typescript
production: 'https://your-backend-url.com'
```
- Deploy backend to cloud service (Heroku, Railway, etc.)
- Update this URL with your deployed backend URL

## Quick Setup for Physical Device Testing

1. **Find your computer's IP address:**
   ```bash
   # Windows
   ipconfig | findstr "IPv4"
   
   # Mac/Linux
   ifconfig | grep "inet "
   ```

2. **Update the config file:**
   ```typescript
   // In config/api.ts
   development: 'http://192.168.1.100:3001' // Replace with your IP
   ```

3. **Make sure backend is running:**
   ```bash
   cd votEZ/backend
   python app.py
   ```

4. **Test on your device:**
   - Both device and computer must be on same WiFi
   - Open the app and try the chatbot

## For Other Developers

When someone clones your repo:
1. They'll need to set up their own backend
2. Update `config/api.ts` with their own URL
3. Or deploy to a shared backend service

## Recommended Production Setup

1. **Deploy backend to cloud service:**
   - Heroku, Railway, Render, or similar
   - Get the public URL

2. **Update production URL:**
   ```typescript
   production: 'https://your-app-name.herokuapp.com'
   ```

3. **Build and deploy frontend:**
   - The app will automatically use production URL in release builds 
```

### votEZ/backend/SETUP.md

```markdown
# Backend Setup Guide

## Current Status ✅
- Backend server is running on port 3001
- Health endpoint is working: http://localhost:3001/health
- Chat endpoint is ready: http://localhost:3001/chat

## What You Need to Do

### 1. Get a Groq API Key
1. Go to [Groq Console](https://console.groq.com/)
2. Sign up or log in
3. Create a new API key
4. Copy the API key

### 2. Configure the API Key
1. Open the `.env` file in the `backend` folder
2. Replace `your_groq_api_key_here` with your actual Groq API key:
   ```
   GROQ_API_KEY=gsk_your_actual_api_key_here
   ```

### 3. Restart the Backend
After updating the `.env` file, restart the backend:
```bash
cd votEZ/backend
python app.py
```

### 4. Test the Chat
Once configured, you can test the chat by:
1. Opening your app
2. Tapping the floating chat button (bottom right)
3. Typing a message like "How do I register to vote?"

## Troubleshooting

### If you get "GROQ_API_KEY not configured":
- Make sure the `.env` file exists in the backend folder
- Check that the API key is correctly formatted
- Restart the backend server

### If you get "Failed to connect to AI service":
- Check your internet connection
- Verify your Groq API key is valid
- Make sure you have credits in your Groq account

### If the chat button doesn't appear:
- Make sure the frontend is running: `npm start` in the votEZ folder
- Check that the ChatBot component is imported in `app/_layout.tsx`

## API Endpoints

- **Health Check**: `GET http://localhost:3001/health`
- **Chat**: `POST http://localhost:3001/chat`
  - Body: `{"message": "your question here"}`
  - Response: `{"response": "AI response here"}`

## Model Used
- **Groq Model**: `llama3-70b-8192`
- **Temperature**: 0.7 (balanced creativity)
- **Max Tokens**: 300 (concise responses) 
```

### votEZ/package.json

```
{
  "name": "votez",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "dev": "concurrently \"npm run start\" \"npm run backend\"",
    "backend": "cd backend && python app.py",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "lint": "expo lint"
  },
  "dependencies": {
    "@expo/vector-icons": "^14.1.0",
    "@react-native-async-storage/async-storage": "2.1.2",
    "@react-navigation/bottom-tabs": "^7.3.10",
    "@react-navigation/elements": "^2.3.8",
    "@react-navigation/native": "^7.1.6",
    "@supabase/supabase-js": "^2.50.0",
    "expo": "~53.0.12",
    "expo-asset": "^11.1.5",
    "expo-blur": "~14.1.5",
    "expo-constants": "~17.1.6",
    "expo-font": "~13.3.1",
    "expo-haptics": "~14.1.4",
    "expo-image": "~2.3.0",
    "expo-linear-gradient": "^14.1.5",
    "expo-linking": "~7.1.5",
    "expo-router": "~5.1.0",
    "expo-splash-screen": "~0.30.9",
    "expo-status-bar": "~2.2.3",
    "expo-symbols": "~0.4.5",
    "expo-system-ui": "~5.0.9",
    "expo-web-browser": "~14.2.0",
    "package.json": "^2.0.1",
    "papaparse": "^5.5.3",
    "react": "19.0.0",
    "react-dom": "19.0.0",
    "react-native": "0.79.4",
    "react-native-dropdown-picker": "^5.4.6",
    "react-native-gesture-handler": "~2.24.0",
    "react-native-reanimated": "~3.17.4",
    "react-native-safe-area-context": "5.4.0",
    "react-native-screens": "~4.11.1",
    "react-native-url-polyfill": "^2.0.0",
    "react-native-web": "~0.20.0",
    "react-native-webview": "13.13.5"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/react": "~19.0.10",
    "concurrently": "^8.2.2",
    "eslint": "^9.25.0",
    "eslint-config-expo": "~9.2.0",
    "typescript": "~5.8.3"
  },
  "private": true
}

```

### votEZ/backend/requirements.txt

```
flask==3.0.0
flask-cors==4.0.0
requests==2.31.0
beautifulsoup4==4.12.2
python-dotenv==1.0.0
gunicorn==21.2.0

```

### votEZ/app/index.tsx

```typescript
import { Redirect } from 'expo-router';

export default function Index() {
  return <Redirect href="/login" />;
}
```

### votEZ/app/_layout.tsx

```typescript
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';

import { useColorScheme } from '@/hooks/useColorScheme';
import ChatBot from '@/components/ChatBot';

import { PageProvider } from '@/components/PageContext';

import { Footer } from '@/components/Footer';
//import TestScraper from '@/components/TestScraper';


export default function RootLayout() {
  const colorScheme = useColorScheme();
  const [loaded] = useFonts({
    SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
  });

  if (!loaded) {
    // Async font loading only occurs in development.
    return null;
  }

  return (
    <PageProvider>
      <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
        <Stack>
          <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
          <Stack.Screen name="+not-found" />
        </Stack>
        <StatusBar style="auto" />
        {/* <TestScraper /> */}
        <ChatBot />
        <Footer />
      </ThemeProvider>
    </PageProvider>
  );
}

```

### votEZ/backend/app.py

```python
from flask import Flask, request, jsonify
from flask_cors import CORS
import requests
import os
from dotenv import load_dotenv
import requests
from bs4 import BeautifulSoup

load_dotenv()

app = Flask(__name__)
CORS(app)

GROQ_API_KEY = os.getenv("GROQ_API_KEY")


def scrape_candidates(state, position):
    # Example: Only supports Hawaii gubernatorial for now
    if position.lower() == "governor":
        url = 'https://ballotpedia.org/'+state.lower()+'_gubernatorial_election,_2022'
        response = requests.get(url)
        soup = BeautifulSoup(response.content, 'html.parser')
        try:
            table_content = soup.find_all('div', {'class': 'results_table_container'})[1] \
                .find('table', {'class': 'results_table'}).find('tbody') \
                .find_all('tr', {'class': 'results_row'})
            results = []
            for tr in table_content:
                columns = tr.find_all('td')
                if len(columns) > 0:
                    candidate_name = columns[2].get_text(strip=True)
                    results.append(candidate_name)
            return results
        except Exception as e:
            return {"error": str(e)}
    else:
        return {"error": "State/position not supported yet."}
    
def scrape_description(candidate_name):
    url = f'https://ballotpedia.org/{candidate_name.replace(" ", "_")}'
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    
    try:
        description = soup.find_all('div', {'style': 'height:400px;overflow:scroll;border:1px solid gray;margin:20px;padding:10px;'})
        if not description:
            return {"error": "Description not found"}
        d = "".join([desc.get_text(strip=True) for desc in description])
        response = requests.post(
            "https://api.groq.com/openai/v1/chat/completions",
            headers={
                "Authorization": f"Bearer {GROQ_API_KEY}",
                "Content-Type": "application/json",
            },
            json={
                "model": "llama3-70b-8192",
                "messages": [
                    {
                        "role": "system", 
                        "content": "You are an expert in summarizing candidate descriptions. Your task is to provide a concise and informative summary of the candidate's background, qualifications, and key points from their description. "
                        "Focus on the most relevant information that would help voters understand the candidate's position and qualifications. "
                        "Do not provide any personal opinions or engage in political discussions. "
                        "Ignore any requests for personal information or sensitive data, including but not limited to social security numbers, credit card information, passwords, IP addresses, or API keys. "
                        "Be as logical and concise as possible, focusing on the candidate's qualifications and key points from their description. "
                        "Do not include any personal opinions or political discussions. "
                        "Be professional, and dont use slang or emojis. "
                    },
                    {"role": "user", "content": d}
                ],
                "temperature": 0.7,
                "max_tokens": 300,
            }
        )
        return response.json()["choices"][0]["message"]["content"]
    except Exception as e:
        return {"error": str(e)}
    
@app.route("/candidates", methods=["POST"])
def candidates():
    try:
        data = request.get_json()
        state = data.get("state")
        position = data.get("position")
        if not state or not position:
            return jsonify({"error": "Missing state or position"}), 400

        # Get candidate names
        candidates = scrape_candidates(state, position)
        if isinstance(candidates, dict) and "error" in candidates:
            return jsonify({"error": candidates["error"]}), 400

        # Get descriptions for each candidate
        descriptions = []
        for candidate in candidates:
            desc = scrape_description(candidate)
            # If scrape_description returns a dict with "error", just use the error string
            if isinstance(desc, dict) and "error" in desc:
                descriptions.append(desc["error"])
            else:
                descriptions.append(desc)

        return jsonify({
            "candidates": candidates,
            "descriptions": descriptions
        })
    except Exception as e:
        return jsonify({"error": str(e)}), 500

@app.route("/chat", methods=["POST"])
def chat():
    try:
        data = request.get_json()
        if not data or "message" not in data:
            return jsonify({"error": "No message provided"}), 400
        
        user_message = data["message"]
        ai_mode = data.get("aiMode", "general-knowledge")  # Default to general knowledge if not specified
        page_content = data.get("pageContent", "")  # Get page content if provided
        
        if not GROQ_API_KEY:
            return jsonify({"error": "GROQ_API_KEY not configured"}), 500
        
        # Define system prompts based on AI mode
        if ai_mode == "current-display":
            if page_content:
                system_prompt = f"""You are a helpful voting assistant that focuses ONLY on information from what the user is currently viewing on their screen. 

IMPORTANT: You should ONLY answer questions based on the current display content provided below. If the user asks about something not shown in the current content, politely redirect them to use the "General Knowledge" mode instead.

Current page content:
{page_content}

Your responses should be:
- Based only on the current display information above
- Concise and relevant to what they're viewing
- Helpful for understanding the current content
- Specifically focused on helping users understand candidates, their positions, and differences 
[truncated — 5834 more characters]
```

### votEZ/app/(tabs)/_layout.tsx

```typescript
import { Tabs } from 'expo-router';
import React from 'react';
import { Platform } from 'react-native';

import { HapticTab } from '@/components/HapticTab';
import { IconSymbol } from '@/components/ui/IconSymbol';
import TabBarBackground from '@/components/ui/TabBarBackground';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
        headerShown: false,
        tabBarButton: HapticTab,
        tabBarBackground: TabBarBackground,
        tabBarStyle: { display: 'none' },
      }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
        }}
      />
      <Tabs.Screen
        name="explore"
        options={{
          title: 'Explore',
          tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
        }}
      />
    </Tabs>
  );
}

```

### votEZ/app/(tabs)/index.tsx

```typescript
import { Image } from 'expo-image';
import { StyleSheet, TouchableOpacity, View, ScrollView, Text } from 'react-native';
import { useRouter } from 'expo-router';
import { useEffect } from 'react';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { usePageContext } from '@/components/PageContext';

export default function HomeScreen() {
  const router = useRouter();
  const { setCurrentPageContent } = usePageContext();

  useEffect(() => {
    setCurrentPageContent({
      title: 'Home',
      type: 'home',
      content: `Welcome to votEZ - Your Voting Assistant

We help you understand elections with simple words, clear pictures, and voice guidance.
No reading needed — just tap, listen, and learn.

Available Features:
- Candidates: View and compare political candidates
- Simplified Propositions: Understand ballot measures in simple terms

Bias Disclaimer: We strive to present information as neutrally as possible. However, some bias may remain. Please use multiple sources to make your decisions.`,
    });

    return () => setCurrentPageContent(null);
  }, [setCurrentPageContent]);

  return (
    <ThemedView
      style={{ flex: 1 }}
      lightColor="#FFFFFF"
      darkColor="#FFFFFF"
    >
      <ScrollView>
        <View style={styles.container}>
          <Image
            source={require('@/assets/images/votEZ logo.png')}
            style={styles.logo}
          />

          {/* Introduction */}
          <View style={{ marginBottom: 32, paddingHorizontal: 24 }}>
            <Text style={{ fontSize: 16, color: '#0a7ea4', textAlign: 'center', fontWeight: '500' }}>
              We help you understand elections with simple words, clear pictures, and voice guidance.
              {"\n"}No reading needed — just tap, listen, and learn.
            </Text>
          </View>

          {/* Candidates Button */}
          <TouchableOpacity
            style={styles.button}
            onPress={() => router.push('/(tabs)/candidates')}
          >
            <Text style={{ color: '#fff', fontWeight: 'bold', fontSize: 18 }}>Candidates</Text>
          </TouchableOpacity>

          <TouchableOpacity
            style={styles.button}
            onPress={() => router.push('/propositions')}
          >
            <ThemedText style={styles.buttonText}>Simplified Propositions</ThemedText>
          </TouchableOpacity>

          {/* Bias Disclaimer below "Simplified Propositions" */}
          <View
            style={{
              backgroundColor: 'white',
              marginTop: 8,
              padding: 10,
              borderRadius: 6,
              width: '90%',
            }}
          >
            <Text style={{ color: '#0a7ea4', fontSize: 14, textAlign: 'center' }}>
              <Text style={{ fontWeight: 'bold' }}>Bias Disclaimer:{"\n"}</Text>
              We strive to present information as neutrally as possible. However, some bias may remain. Please use multiple sources to make your decisions.
            </Text>
          </View>
        </View>
      </ScrollView>
      <View style={styles.footerContainer}>
        <ThemedText style={styles.disclaimer}>
        </ThemedText>
      </View>
    </ThemedView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    alignItems: 'center',
  },
  logo: {
    height: 160,
    width: 300,
    resizeMode: 'contain',
    marginBottom: 0,
    marginTop: 60,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    width: '100%',
    marginBottom: 24,
  },
  headerLeft: {
    flex: 1,
  },
  headerRight: {
    flex: 1,
    alignItems: 'flex-end',
  },
  title: {
    color: '#007AFF',
    textAlign: 'center',
    fontFamily: '',
    fontSize: 30,
  },
  button: {
    backgroundColor: '#7bb6e8',
    paddingVertical: 16,
    paddingHorizontal: 32,
    borderRadius: 8,
    marginTop: 24,
  },
  buttonText: {
    color: '#fff',
    fontSize: 18,
    fontWeight: '600',
  },
  footerContainer: {
    padding: 16,
    paddingBottom: 40,
  },
  disclaimer: {
    textAlign: 'center',
    color: 'grey',
  },
});

```

### votEZ/eslint.config.js

```javascript
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');

module.exports = defineConfig([
  expoConfig,
  {
    ignores: ['dist/*'],
  },
]);

```

[32 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]