# Project export: Poligraph

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: CruzHacks 2026
- Tagline: The digital receipt for political promises. Verifying policy through public sentiment.
- Devpost: https://devpost.com/software/poligraph
- GitHub: https://github.com/BenjaminTelanoff/gov_props
- Demo: http://poli-graph.vercel.app/
- Video: https://www.youtube.com/embed/A53pi4GM2xs?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Sponsor - Mobbin] Best use of Mobbin)
- Team: 3 GitHub contributor(s) — aubreyshere (15 commits), Ben-ramen (5 commits), Varia Kniazhevich (5 commits)

## Devpost submission (written by the team)

### Inspiration

We were inspired to build this project to inform people about the work of various politicians, specifically focusing on their performance through both analytical data and public sentiment.

### What it does

The platform displays a politician's campaign propositions alongside detailed descriptions. It provides a verdict on how successfully those goals were completed and incorporates data on public sentiment toward each official.

### How we built it

We built the application using the Angular framework. Information was gathered through targeted research, AI summarization, web scraping, and the Reddit API.

### Challenges we ran into

We navigated several dependency version conflicts during development. We also faced a lack of structured information online and initially hit request limits with the Gemini API before transitioning to OpenRouter.

### Accomplishments we're proud of

Successfully getting the site fully functional and working exactly as intended!

### What's next

We plan to expand the platform to include more than the current 50 politicians as we move past our initial data restrictions.

## README (from the GitHub repository)

# GovProps

This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.13.

## Development server

To start a local development server, run:

```bash
ng serve
```

Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.

## Code scaffolding

Angular CLI includes powerful code scaffolding tools. To generate a new component, run:

```bash
ng generate component component-name
```

For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:

```bash
ng generate --help
```

## Building

To build the project run:

```bash
ng build
```

This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.

## Running unit tests

To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:

```bash
ng test
```

## Running end-to-end tests

For end-to-end (e2e) testing, run:

```bash
ng e2e
```

Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.

## Additional Resources

For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.


## Detected evidence (automated analysis)

Indexed codebase: 30 recognized source files, 82 KB.
- Angular (technology) — detected in the code
- Firebase (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (61 of 61)

```
.editorconfig
.firebaserc
.gitignore
.vscode/extensions.json
.vscode/launch.json
.vscode/tasks.json
angular.json
dataconnect/dataconnect.yaml
dataconnect/example/connector.yaml
dataconnect/example/mutations.gql
dataconnect/example/queries.gql
dataconnect/schema/schema.gql
dataconnect/seed_data.gql
eslint.config.js
firebase.json
firestore.indexes.json
firestore.rules
functions/.eslintrc.js
functions/.gitignore
functions/package.json
functions/src/index.ts
functions/tsconfig.dev.json
functions/tsconfig.json
package.json
README.md
requirements-scraper.txt
set-env.ts
src/app/app.config.ts
src/app/app.html
src/app/app.module.ts
src/app/app.routes.ts
src/app/app.scss
src/app/app.spec.ts
src/app/app.ts
src/app/data/consolidate_firestore.py
src/app/data/consolidate_politicians.py
src/app/data/data.json
src/app/data/data.json.backup
src/app/data/message.py
src/app/data/pol_desc_Scraper.py
src/app/data/proposition_sentiment_scraper.py
src/app/data/serviceAccountKey.json.example
src/app/data/to_firebase.py
src/app/main/homepage/homepage.html
src/app/main/homepage/homepage.scss
src/app/main/homepage/homepage.spec.ts
src/app/main/homepage/homepage.ts
src/app/main/profile-page/profile-page.html
src/app/main/profile-page/profile-page.scss
src/app/main/profile-page/profile-page.spec.ts
src/app/main/profile-page/profile-page.ts
src/d.env.ts
src/environments/environment.development.ts
src/environments/environment.ts
src/index.html
src/main.ts
src/styles.scss
storage.rules
tsconfig.app.json
tsconfig.json
tsconfig.spec.json
```

### Dependencies

- functions/package.json: @typescript-eslint/eslint-plugin@^5.12.0, @typescript-eslint/parser@^5.12.0, eslint@^8.9.0, eslint-config-google@^0.14.0, eslint-plugin-import@^2.25.4, firebase-admin@^13.6.0, firebase-functions@^7.0.0, firebase-functions-test@^3.4.1, typescript@^5.7.3
- package.json: @angular/animations@^20.3.16, @angular/build@^20.3.13, @angular/cdk@^20.2.14, @angular/cli@^20.3.13, @angular/common@^20.3.0, @angular/compiler@^20.3.0, @angular/compiler-cli@^20.3.0, @angular/core@^20.3.0, @angular/fire@^20.0.1, @angular/forms@^20.3.0, @angular/material@^20.2.14, @angular/platform-browser@^20.3.0, @angular/router@^20.3.0, @dataconnect/generated@file:src/dataconnect-generated, @ngrx/signals@^20.1.0, @ngx-env/builder@^20.1.1, @types/jasmine@~5.1.0, angular-eslint@21.0.1, eslint@^9.39.1, firebase@^11.10.0, jasmine-core@~5.9.0, karma@~6.4.0, karma-chrome-launcher@~3.2.0, karma-coverage@~2.2.0, karma-jasmine@~5.1.0, karma-jasmine-html-reporter@~2.1.0, rxjs@~7.8.0, tslib@^2.3.0, typescript@~5.9.2, typescript-eslint@8.46.4, zone.js@~0.15.0

### Recent commits (newest first)

- fixes: name updated
- feat: profile page background
- feat: profile page background
- fix: matching dependencies.
- feat: add website icon.
- fixes: appearance of profile and home pages
- feat: styling fixes
- feat: add similar politician to profile.
- feat: add satisfaction far and styling.
- style: add background to homepage and add color to parties.
- fix: styling.
- feat: add reddit scraper.
- Consolidating politicians so that different terms are combined
- Merge branch 'main' of github.com:BenjaminTelanoff/gov_props
- fix: create routing between homepage and profiles.
- Merge branch 'main' of https://github.com/BenjaminTelanoff/gov_props
- aaaaaaa
- build: update for env to be read.
- Merge branch 'main' of github.com:BenjaminTelanoff/gov_props
- feat: at search components to homepage.

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

### package.json

```
{
  "name": "gov-props",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test",
    "lint": "ng lint"
  },
  "prettier": {
    "printWidth": 100,
    "singleQuote": true,
    "overrides": [
      {
        "files": "*.html",
        "options": {
          "parser": "angular"
        }
      }
    ]
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^20.3.16",
    "@angular/cdk": "^20.2.14",
    "@angular/common": "^20.3.0",
    "@angular/compiler": "^20.3.0",
    "@angular/core": "^20.3.0",
    "@angular/fire": "^20.0.1",
    "@angular/forms": "^20.3.0",
    "@angular/material": "^20.2.14",
    "@angular/platform-browser": "^20.3.0",
    "@angular/router": "^20.3.0",
    "@dataconnect/generated": "file:src/dataconnect-generated",
    "@ngrx/signals": "^20.1.0",
    "firebase": "^11.10.0",
    "rxjs": "~7.8.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.15.0"
  },
  "devDependencies": {
    "@angular/build": "^20.3.13",
    "@angular/cli": "^20.3.13",
    "@angular/compiler-cli": "^20.3.0",
    "@ngx-env/builder": "^20.1.1",
    "@types/jasmine": "~5.1.0",
    "angular-eslint": "21.0.1",
    "eslint": "^9.39.1",
    "jasmine-core": "~5.9.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.2.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.1.0",
    "typescript": "~5.9.2",
    "typescript-eslint": "8.46.4"
  }
}

```

### functions/package.json

```
{
  "name": "functions",
  "scripts": {
    "lint": "eslint --ext .js,.ts .",
    "build": "tsc",
    "build:watch": "tsc --watch",
    "serve": "npm run build && firebase emulators:start --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "24"
  },
  "main": "lib/index.js",
  "dependencies": {
    "firebase-admin": "^13.6.0",
    "firebase-functions": "^7.0.0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.12.0",
    "@typescript-eslint/parser": "^5.12.0",
    "eslint": "^8.9.0",
    "eslint-config-google": "^0.14.0",
    "eslint-plugin-import": "^2.25.4",
    "firebase-functions-test": "^3.4.1",
    "typescript": "^5.7.3"
  },
  "private": true
}

```

### src/main.ts

```typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';

bootstrapApplication(App, appConfig)
  .catch((err) => console.error(err));

```

### src/app/app.ts

```typescript
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  templateUrl: './app.html',
  styleUrl: './app.scss'
})
export class App {
  protected readonly title = signal('gov-props');
}

```

### functions/src/index.ts

```typescript
/**
 * Import function triggers from their respective submodules:
 *
 * import {onCall} from "firebase-functions/v2/https";
 * import {onDocumentWritten} from "firebase-functions/v2/firestore";
 *
 * See a full list of supported triggers at https://firebase.google.com/docs/functions
 */

import {setGlobalOptions} from "firebase-functions";
import {onRequest} from "firebase-functions/https";
import * as logger from "firebase-functions/logger";

// Start writing functions
// https://firebase.google.com/docs/functions/typescript

// For cost control, you can set the maximum number of containers that can be
// running at the same time. This helps mitigate the impact of unexpected
// traffic spikes by instead downgrading performance. This limit is a
// per-function limit. You can override the limit for each function using the
// `maxInstances` option in the function's options, e.g.
// `onRequest({ maxInstances: 5 }, (req, res) => { ... })`.
// NOTE: setGlobalOptions does not apply to functions using the v1 API. V1
// functions should each use functions.runWith({ maxInstances: 10 }) instead.
// In the v1 API, each function can only serve one request per container, so
// this will be the maximum concurrent request count.
setGlobalOptions({ maxInstances: 10 });

// export const helloWorld = onRequest((request, response) => {
//   logger.info("Hello logs!", {structuredData: true});
//   response.send("Hello from Firebase!");
// });

```

### src/app/main/homepage/homepage.ts

```typescript
import { Component, OnInit, signal } from '@angular/core';
import { Router } from '@angular/router';
import {MatIconModule} from '@angular/material/icon';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Observable} from 'rxjs';
import {map, startWith} from 'rxjs/operators';
import {AsyncPipe} from '@angular/common';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';
import { Firestore, collection, collectionData } from '@angular/fire/firestore';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-homepage',
  imports: [
    FormsModule,
    MatFormFieldModule,
    MatInputModule,
    MatAutocompleteModule,
    ReactiveFormsModule,
    AsyncPipe,
    MatIconModule,
  ],
  templateUrl: './homepage.html',
  styleUrl: './homepage.scss',
})
export class Homepage implements OnInit {
  private topMatch: string | null = null;
  myControl = new FormControl('');
  filteredOptions!: Observable<string[]>;

  options = signal<string[]>([]);

  goToPage(inputValue: string | null): void {
    const destinationName = this.topMatch || inputValue;

    if (destinationName && destinationName.trim() !== '') {
      this.router.navigate(['/profile', destinationName]);
    }
  }

  constructor(
    private firestore: Firestore, 
    private router: Router,
  ) {
    const col = collection(this.firestore, 'Politicians');
    collectionData(col).pipe(
      takeUntilDestroyed(),
      map((items: any[]) => items.map(i => i.Name))
    ).subscribe((names: string[]) => {
      this.options.set(names);     
      this.myControl.setValue(this.myControl.value);
    });
  }

  private _filter(value: string): string[] {
    const filterValue = value.toLowerCase();

    return this.options().filter(option => 
      option && option.toLowerCase().includes(filterValue)
    );
  }

  ngOnInit() {
    this.filteredOptions = this.myControl.valueChanges.pipe(
      startWith(''),
      map(value => {
        const filtered = this._filter(value || '');
        // Store the first result as the "top match"
        this.topMatch = filtered.length > 0 ? filtered[0] : null;
        return filtered;
      }),
    );
  }
}

```

### src/app/main/profile-page/profile-page.ts

```typescript
import { Component, inject, OnInit, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { tap, shareReplay } from 'rxjs';
import { ActivatedRoute } from '@angular/router';
import { Firestore, collection, query, where, getDocs } from '@angular/fire/firestore';
import { CommonModule } from '@angular/common';
import { Observable, from, map, switchMap } from 'rxjs';
import { MatIconModule } from '@angular/material/icon';
import { Router } from '@angular/router';

@Component({
  selector: 'app-profile-page',
  standalone: true,
  imports: [
    CommonModule,
    MatIconModule,
  ],
  templateUrl: './profile-page.html',
  styleUrl: './profile-page.scss',
})
export class ProfilePage implements OnInit {
  private firestore: Firestore = inject(Firestore);
  private route: ActivatedRoute = inject(ActivatedRoute);

  constructor (private router: Router, private destroyRef: DestroyRef,) {}
  
  // Observable to hold the politician data from Firestore
  politician$: Observable<any> | undefined;

  allPoliticians$: Observable<any> | undefined;
  
  private validNames: string[] = []

  // Property to hold the proposition currently being viewed in the modal
  selectedProp: any = null;

  goBack(): void {
    this.router.navigate(['/']);
  }

  toPage(person: string): void {
    if (!this.validNames.includes(person)) {
      console.warn("Person not found in list");
      return;
    }
    this.router.navigate(['/profile', person]);
  }

  ngOnInit() {
    const col = collection(this.firestore, 'Politicians');

    // 1. Fetch list of names and sync with local array
    this.allPoliticians$ = from(getDocs(col)).pipe(
      map(snapshot => snapshot.docs.map(doc => doc.data()['Name'])),
      tap(names => this.validNames = names), 
      shareReplay(1),
      takeUntilDestroyed(this.destroyRef) // Ensures no memory leaks
    );

    // Initial trigger for the names list
    this.allPoliticians$.subscribe();

    // 2. Fetch specific politician data
    this.politician$ = this.route.params.pipe(
      switchMap(params => {
        const name = params['name'];
        const q = query(col, where('Name', '==', name));
        return from(getDocs(q)).pipe(
          map(snapshot => snapshot.empty ? null : snapshot.docs[0].data())
        );
      }),
      map(data => {
        if (!data) return null;
        const propsMap = data['Propositions'] || {};
        const propsList = Object.entries(propsMap)
          .sort(([a], [b]) => Number(a) - Number(b))
          .map(([_, value]) => value);
        
        return { ...data, propsList };
      })
    );
  }
  // Open modal and prevent background scrolling
  openModal(prop: any) {
    this.selectedProp = prop;
    document.body.style.overflow = 'hidden';
  }

  // Close modal and restore scrolling
  closeModal() {
    this.selectedProp = null;
    document.body.style.overflow = 'auto';
  }

  // Logic to determine status styling
  getStatusClass(status: string): string {
    const s = status?.toLowerCase() || '';
    if (s.includes('success')) {
      return 'status-success';
    }
    if (s.includes('compromised')) {
      return 'status-compromised';
    }
    if (s.includes('failed')) {
      return 'status-failed';
    }
    return 'status-default';
  }

  // Calculate sentiment color based on percentage (battery indicator style)
  getSentimentColor(percentage: number): string {
    // Red (0-33%) -> Yellow (33-66%) -> Green (66-100%)
    if (percentage <= 33) {
      // Red: #ef5350
      return '#ef5350';
    } else if (percentage <= 66) {
      // Yellow/Orange: #ffa726
      return '#ffa726';
    } else {
      // Green: #66bb6a
      return '#66bb6a';
    }
  }
}
```

### set-env.ts

```typescript
const fs = require('fs');
require('dotenv').config();

const targetPath = './src/environments/environment.ts';

const envConfigFile = `export const environment = {
  production: false,
  firebase: {
    apiKey: '${process.env.NG_APP_FIREBASE_DEV_API_KEY}',
    authDomain: '${process.env.NG_APP_FIREBASE_AUTH_DOMAIN}',
    projectId: '${process.env.NG_APP_FIREBASE_PROJECT_ID}',
    storageBucket: '${process.env.NG_APP_FIREBASE_STORAGE_BUCKET}',
    messagingSenderId: '${process.env.NG_APP_FIREBASE_MESSAGING_SENDER_ID}',
    appId: '${process.env.NG_APP_FIREBASE_APP_ID}',
    measurementId: '${process.env.NG_APP_FIREBASE_MEASUREMENT_ID}'
  }
};`;

fs.writeFileSync(targetPath, envConfigFile);
console.log('✅ Keys injected into environment.ts');
```

### eslint.config.js

```javascript
// @ts-check
const eslint = require("@eslint/js");
const { defineConfig } = require("eslint/config");
const tseslint = require("typescript-eslint");
const angular = require("angular-eslint");

module.exports = defineConfig([
  {
    files: ["**/*.ts"],
    extends: [
      eslint.configs.recommended,
      tseslint.configs.recommended,
      tseslint.configs.stylistic,
      angular.configs.tsRecommended,
    ],
    processor: angular.processInlineTemplates,
    rules: {
      "@angular-eslint/directive-selector": [
        "error",
        {
          type: "attribute",
          prefix: "app",
          style: "camelCase",
        },
      ],
      "@angular-eslint/component-selector": [
        "error",
        {
          type: "element",
          prefix: "app",
          style: "kebab-case",
        },
      ],
    },
  },
  {
    files: ["**/*.html"],
    extends: [
      angular.configs.templateRecommended,
      angular.configs.templateAccessibility,
    ],
    rules: {},
  }
]);

```

### dataconnect/dataconnect.yaml

```yaml
specVersion: "v1"
serviceId: "gov-props"
location: "us-east4"
schema:
  source: "./schema"
  datasource:
    postgresql:
      database: "fdcdb"
      cloudSql:
        instanceId: "gov-props-fdc"
        # schemaValidation: "STRICT"     # STRICT mode makes Postgres schema match Data Connect exactly.
        # schemaValidation: "COMPATIBLE" # COMPATIBLE mode makes Postgres schema compatible with Data Connect.
connectorDirs: ["./example"]

```

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