6appName: 'RackTracker',
7version: '1.0.0',
8apiEndpoint: '/api'
9};
1018};
1920// API functions
21const api = {
22async getMatches() {
23try {
24const response = await fetch(`${initialData.apiEndpoint}/matches`);
25if (!response.ok) throw new Error('Failed to fetch matches');
26return await response.json();
33async getMatch(id) {
34try {
35const response = await fetch(`${initialData.apiEndpoint}/matches/${id}`);
36if (!response.ok) throw new Error('Failed to fetch match');
37return await response.json();
44async createMatch(matchData) {
45try {
46const response = await fetch(`${initialData.apiEndpoint}/matches`, {
47method: 'POST',
48headers: {
62async recordRack(matchId, winnerId, events = []) {
63try {
64const response = await fetch(`${initialData.apiEndpoint}/matches/${matchId}/rack`, {
65method: 'POST',
66headers: {
80async undoLastRack(matchId, rackId) {
81try {
82const response = await fetch(`${initialData.apiEndpoint}/matches/${matchId}/rack/${rackId}`, {
83method: 'DELETE',
84});
94async exportMatch(matchId, format = 'json') {
95try {
96const response = await fetch(`${initialData.apiEndpoint}/export/match/${matchId}?format=${format}`);
97if (!response.ok) throw new Error(`Failed to export match as ${format}`);
98
110async getMatchSummary(matchId) {
111try {
112const response = await fetch(`${initialData.apiEndpoint}/export/match/${matchId}/summary`);
113if (!response.ok) throw new Error('Failed to get match summary');
114return await response.text();
136
137// Load and display matches
138api.getMatches().then(matches => {
139state.matches = matches;
140
162matchCard.querySelector('.tournament-name').textContent = match.tournament_id ? 'Tournament' : 'Friendly Match';
163matchCard.querySelector('.game-type').textContent = match.game_type;
164matchCard.querySelector('.player-a').textContent = `Player A`; // Would need player names from API
165matchCard.querySelector('.player-b').textContent = `Player B`; // Would need player names from API
166matchCard.querySelector('.score').textContent = `${match.player_a_score}-${match.player_b_score}`;
167matchCard.querySelector('.race-info').textContent = `Race to ${match.race_to}`;
231}
232
233// Create match via API
234const match = await api.createMatch(matchData);
235
236if (match) {
247async renderMatchScoring(matchId) {
248// Fetch latest match data
249const match = await api.getMatch(matchId);
250if (!match) {
251alert('Failed to load match data');
268document.querySelector('.tournament-name').textContent = match.tournament_id ? 'Tournament' : 'Friendly Match';
269document.querySelector('.game-info').textContent = `${match.game_type} (${match.table_size})`;
270document.querySelector('.player-a-name').textContent = 'Player A'; // Would need player name from API
271document.querySelector('.player-b-name').textContent = 'Player B'; // Would need player name from API
272document.querySelector('.player-a-btn-name').textContent = 'Player A';
273document.querySelector('.player-b-btn-name').textContent = 'Player B';
314
315const lastRack = match.racks[match.racks.length - 1];
316const updatedMatch = await api.undoLastRack(match.id, lastRack.id);
317
318if (updatedMatch) {
330const confirm = window.confirm('End match before completion? This will mark the match as completed with the current score.');
331if (confirm) {
332// In a real implementation, we would call an API to end the match
333views.renderMatchComplete(match.id);
334}
341// Helper functions
342async function recordRackWin(winnerId) {
343// Convert pending events to API format
344const events = state.pendingEvents.map(eventId => {
345let eventType;
359});
360
361const updatedMatch = await api.recordRack(match.id, winnerId, events);
362
363if (updatedMatch) {
418async renderMatchComplete(matchId) {
419// Fetch latest match data
420const match = await api.getMatch(matchId);
421if (!match) {
422alert('Failed to load match data');
436document.querySelector('.tournament-name').textContent = match.tournament_id ? 'Tournament' : 'Friendly Match';
437document.querySelector('.game-info').textContent = `${match.game_type} (${match.table_size})`;
438document.querySelector('.player-a-name').textContent = 'Player A'; // Would need player name from API
439document.querySelector('.player-b-name').textContent = 'Player B'; // Would need player name from API
440document.querySelector('.score').textContent = `${match.player_a_score}-${match.player_b_score}`;
441
486// Export buttons
487document.getElementById('export-json').addEventListener('click', async () => {
488const data = await api.exportMatch(match.id, 'json');
489if (data) {
490downloadJSON(data, `match_${match.id}_export.json`);
495
496document.getElementById('export-csv').addEventListener('click', async () => {
497const data = await api.exportMatch(match.id, 'csv');
498if (data) {
499downloadText(data, `match_${match.id}_export.csv`);
504
505document.getElementById('export-summary').addEventListener('click', async () => {
506const data = await api.getMatchSummary(match.id);
507if (data) {
508showTextModal('Match Summary', data);
513
514document.getElementById('export-fargo').addEventListener('click', async () => {
515const data = await api.exportMatch(match.id, 'fargo');
516if (data) {
517downloadJSON(data, `match_${match.id}_fargo.json`);
170} else if (format === 'challonge') {
171// Generate Challonge compatible format
172// This is a simplified example - actual format would depend on Challonge's API
173const challongeFormat = {
174match: {
7## Key Principles & Design Philosophy
891. **Speed First**: Every interaction is optimized for rapid data entry during fast-paced tournaments.
102. **Focused Nuance**: Only capture objective, high-value data points that require minimal effort.
113. **Mobile Optimized**: Primary interface designed for smartphones and tablets used by TDs at the table.
87
88### Backend
89- **API Layer**: RESTful endpoints for data processing
90- **Authentication**: Simple user management for TDs
91- **Business Logic**: Match progression, statistics calculation
134### Potential Integrations
135- **FargoRate**: Export match results in compatible format
136- Investigation needed for direct API integration
137- Fallback to formatted export for manual entry
138
139- **Challonge**: Update bracket progress
140- Explore API options for automatic updates
141- Provide formatted data for manual entry
142186- React for the frontend interface
187- SQLite for data storage
188- RESTful API for data access
189- Service workers for offline capability
190
5## Structure
67- `index.ts` - Main entry point for the HTTP API
8- `cron.ts` - Cron job for periodically fetching and updating tweets
9- `database/` - Database schema and queries
10- `routes/` - API routes and static file serving
11- `services/` - External API integrations (Twitter API)
1213## API Endpoints
1415- `GET /api/tweets` - Get tweets with pagination
16- `GET /api/tweets/:id` - Get a single tweet by ID
17- `GET /api/tweets/:id/metrics` - Get metrics history for a tweet
18- `GET /api/top-tweets` - Get top tweets by engagement
19- `GET /api/fetch/:username` - Fetch tweets for a specific user
20- `GET /api/search?q=query` - Search tweets by keyword
2122## Environment Variables
2324- `TWITTER_API_KEY` - Twitter API key
25- `TWITTER_API_SECRET` - Twitter API secret
26- `TWITTER_BEARER_TOKEN` - Twitter bearer token
36}
3738// API Functions
39async function fetchTweets(page = 0, pageSize = 10) {
40showLoading();
41try {
42const response = await fetch(`/api/tweets?offset=${page * pageSize}&limit=${pageSize}`);
43if (!response.ok) throw new Error('Failed to fetch tweets');
44const data = await response.json();
55showLoading();
56try {
57const response = await fetch(`/api/top-tweets?limit=${limit}`);
58if (!response.ok) throw new Error('Failed to fetch top tweets');
59const data = await response.json();
70showLoading();
71try {
72const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
73if (!response.ok) throw new Error('Failed to search tweets');
74const data = await response.json();
85showLoading();
86try {
87const response = await fetch(`/api/fetch/${encodeURIComponent(username)}`);
88if (!response.ok) throw new Error('Failed to fetch user tweets');
89const data = await response.json();
100showLoading();
101try {
102const response = await fetch(`/api/tweets/${tweetId}/metrics`);
103if (!response.ok) throw new Error('Failed to fetch tweet metrics');
104const data = await response.json();
1import { Hono } from "https://esm.sh/hono@3.11.7";
2import { initDatabase } from "./database/migrations.ts";
3import apiRouter from "./routes/api.ts";
4import staticRouter from "./routes/static.ts";
52425// Mount routers
26app.route("/api", apiRouter);
27app.route("/", staticRouter);
28
9import { TwitterClient } from "../services/twitter.ts";
1011// Create API router
12const api = new Hono();
1314// Enable CORS
15api.use("/*", cors());
1617// Get tweets with pagination
18api.get("/tweets", async (c) => {
19const limit = parseInt(c.req.query("limit") || "50");
20const offset = parseInt(c.req.query("offset") || "0");
3031// Get a single tweet by ID
32api.get("/tweets/:id", async (c) => {
33const id = c.req.param("id");
34
4647// Get metrics history for a tweet
48api.get("/tweets/:id/metrics", async (c) => {
49const id = c.req.param("id");
50
5960// Get top tweets
61api.get("/top-tweets", async (c) => {
62const limit = parseInt(c.req.query("limit") || "10");
63
7273// Fetch tweets for a specific user
74api.get("/fetch/:username", async (c) => {
75const username = c.req.param("username");
76const count = parseInt(c.req.query("count") || "10");
8788// Search tweets
89api.get("/search", async (c) => {
90const query = c.req.query("q");
91if (!query) {
105});
106107export default api;
firsttwitter.ts7 matches
1import { Tweet, TwitterUser, upsertTweet, upsertUser, recordMetrics } from "../database/queries.ts";
23// Twitter API v2 endpoints
4const TWITTER_API_BASE = "https://api.twitter.com/2";
56/**
7* Twitter API client
8*/
9export class TwitterClient {
1920/**
21* Make an authenticated request to the Twitter API
22*/
23private async request(endpoint: string, params: Record<string, string> = {}): Promise<any> {
24const url = new URL(`${TWITTER_API_BASE}${endpoint}`);
25
26// Add query parameters
37if (!response.ok) {
38const errorText = await response.text();
39throw new Error(`Twitter API error (${response.status}): ${errorText}`);
40}
41193if (tweetIds.length === 0) return;
194
195// Twitter API allows up to 100 tweets per request
196const chunks = [];
197for (let i = 0; i < tweetIds.length; i += 100) {
1# Tweet Metrics Tracker
23This application scrapes tweets and other metrics from Twitter/X API, stores them in a SQLite database, and displays them in a web interface.
45## Features
67- Fetches tweets and metrics from Twitter/X API
8- Stores data in SQLite database
9- Periodically updates data using a cron job
19โ โ โโโ queries.ts # DB query functions
20โ โโโ services/
21โ โ โโโ twitter.ts # Twitter API integration
22โ โโโ routes/
23โ โ โโโ api.ts # API routes
24โ โ โโโ static.ts # Static file serving
25โ โโโ cron.ts # Cron job for data fetching
39401. Set up the following environment variables in Val Town:
41- `TWITTER_API_KEY` - Your Twitter/X API key
42- `TWITTER_API_SECRET` - Your Twitter/X API secret
43- `TWITTER_BEARER_TOKEN` - Your Twitter/X bearer token
4449- Access the web interface to view tweets and metrics
50- The data is automatically updated via a cron job
51- Use the API endpoints to access the data programmatically