madjin / bounty-board

dework bounty visualizer
0 stars 0 forks source link

Bounty-Board 0.01a

image

Project Details

Instructions

How to get CSV files from DeWork


Scripts

update-tasks.yml

https://github.com/gm3/bounty-board/blob/main/.github/workflows/update_tasks.yml

``` name: Update Tasks Text Files on: push: paths: - '**.csv' jobs: update_files: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v3 # Use the latest version if available - name: Set up Python uses: actions/setup-python@v3 # Use the latest version if available with: python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip pip install pandas - name: Run script to update text files run: python scripts/update_tasks.py - name: Check for file changes id: git-check run: echo ::set-output name=status::$(git status --porcelain) - name: Configure Git run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - name: List files in scripts directory run: ls -l ./scripts/ - name: Commit and push changes run: | git add ./scripts/*.txt index.html tasks.json git commit -m "Update tasks text files" || echo "No changes to commit" git pull --rebase origin main git push origin main if: steps.git-check.outputs.status != '' ```

update_tasks.py

``` import os import pandas as pd from datetime import datetime import re from html import escape import json # <- Import json # Starting with the directory where the CSVs are stored directory = './bounties/' # Directory to save the text files output_directory = '.' # Checking and printing the number of CSV files detected csv_files = [f for f in os.listdir(directory) if f.endswith('.csv')] print(f"Number of CSV files detected: {len(csv_files)}") # List to store tasks details tasks = [] # Loop through each CSV for filename in csv_files: try: # Read the CSV df = pd.read_csv(os.path.join(directory, filename)) # Extract the required details for index, row in df.iterrows(): task_name = row.get('Name', 'N/A') amount = row.get('Reward', '$TBD') if pd.notna(row.get('Reward')) else '$TBD' task_link = row.get('Link', '#') # Extract the link or use a placeholder if not present activity = row.get('Activities', None) if activity and "created on" in activity: date_posted = ' '.join(activity.split("created on")[1].split()[0:4]) else: date_posted = 'N/A' tasks.append({ 'name': task_name, 'amount': amount, 'date_posted': date_posted, 'link': task_link # Add the link to the task details }) print(f"Extracted {df.shape[0]} tasks from {filename}.") except Exception as e: print(f"Error processing file {filename}: {e}") # Save tasks to a JSON file after extracting tasks from all CSVs json_file_path = os.path.join(output_directory, "tasks.json") with open(json_file_path, "w") as f: json.dump(tasks, f) print(f"JSON file generated: {json_file_path}") print(f"Total tasks extracted: {len(tasks)}") # Filter out tasks with valid date_posted filtered_tasks = [task for task in tasks if task['date_posted'] != 'N/A'] month_abbr_to_num = { "Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4, "May": 5, "Jun": 6, "Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12 } # Convert date_posted to a datetime object for all tasks for task in filtered_tasks: cleaned_date = task['date_posted'].strip() try: match = re.match(r"(\w{3}) (\d{1,2}), (\d{4}) (\d{1,2}):(\d{2})", cleaned_date) if match: month_str, day_str, year_str, hour_str, minute_str = match.groups() task['date_posted_dt'] = datetime(int(year_str), month_abbr_to_num[month_str], int(day_str), int(hour_str), int(minute_str)) else: raise ValueError("Invalid date format") except ValueError: print(f"Error parsing date: {cleaned_date} for task: {task['name']}") task['date_posted_dt'] = datetime.min # Sort tasks by date_posted to get the newest tasks sorted_tasks = sorted(filtered_tasks, key=lambda x: x['date_posted_dt'], reverse=True) top_5_tasks = sorted_tasks[:5] # Format the tasks to display just the amount and the name formatted_tasks = [f"{task['amount']} | {task['name']} | " for task in top_5_tasks] # ... [Rest of the script] ... # Create a simple HTML page with the list of bounties html_output = """ MetaBounty Hunter

All Bounties

    """ # Iterate through all tasks and generate list items for "All Bounties" for task in tasks: print(f"Processing task: {task}") # Debug: print the task being processed # Only escape if it's a string amount = escape(task.get('amount', '$TBD')) if isinstance(task.get('amount', '$TBD'), str) else task.get('amount', '$TBD') date_posted_dt = task.get('date_posted_dt', 'No Date') date_posted_dt = escape(str(date_posted_dt)) if isinstance(date_posted_dt, datetime) else 'No Date' link = escape(task.get('link', '#')) if isinstance(task.get('link', '#'), str) else task.get('link', '#') name = escape(task.get('name', 'Unnamed Task')) if isinstance(task.get('name', 'Unnamed Task'), str) else task.get('name', 'Unnamed Task') html_output += f'
  • {name} | {amount} | {date_posted_dt}
  • \n' html_output += """

New Bounties

    """ print(f"top_5_tasks contains: {top_5_tasks}") # Debug: check if top_5_tasks is populated if top_5_tasks: for task in top_5_tasks: print(f"Processing task: {task}") # Debug: print the task being processed # Only escape if it's a string amount = escape(task.get('amount', '$TBD')) if isinstance(task.get('amount', '$TBD'), str) else task.get('amount', '$TBD') date_posted_dt = task.get('date_posted_dt', 'No Date') date_posted_dt = escape(str(date_posted_dt)) if isinstance(date_posted_dt, datetime) else 'No Date' link = escape(task.get('link', '#')) if isinstance(task.get('link', '#'), str) else task.get('link', '#') name = escape(task.get('name', 'Unnamed Task')) if isinstance(task.get('name', 'Unnamed Task'), str) else task.get('name', 'Unnamed Task') html_output += f'
  • {name} | {amount} | {date_posted_dt}
  • \n' else: print("top_5_tasks is empty.") # Debug: if top_5_tasks is empty, this line will print html_output += """
Badge 1 Badge 2 Badge 3 Badge 4 Badge 5 Badge 6 Badge 7 Badge 8 Badge 9 Badge 10
""" # Save the generated HTML to a file html_file_path = os.path.join(output_directory, "index.html") with open(html_file_path, 'w', encoding="utf-8") as html_file: html_file.write(html_output) print(f"HTML page generated: {html_file_path}") # ... [Rest of the script] ... absolute_directory = './scripts/' # Check and create target directory if not os.path.exists(absolute_directory): os.makedirs(absolute_directory) # Generate the text files for index, task in enumerate(formatted_tasks, 1): file_path = os.path.join(absolute_directory, f"task{index}.txt") with open(file_path, "w") as file: file.write(task) print(f"Saved: {file_path}") print("Text files updated successfully!") ```

script.js

https://github.com/gm3/bounty-board/blob/main/scripts/script.js

``` document.addEventListener('mousemove', function(ev) { const tooltip = document.getElementById('tooltip'); tooltip.style.left = (ev.clientX + 10) + 'px'; tooltip.style.top = (ev.clientY - 10) + 'px'; }, false); const defaultRadius = 10; const fixedRadius = 20; // Extract tasks from the DOM const taskListItems = Array.from(document.querySelectorAll("#task-list li")); const tasks = taskListItems.map((li, index) => { const anchor = li.querySelector('a'); const link = anchor ? anchor.getAttribute('href') : null; const name = anchor ? anchor.textContent : null; const remainingText = li.textContent.replace(anchor ? anchor.textContent : '', '').trim(); // Extracting taskId from the link const taskIdMatch = link ? link.match(/taskId=([a-z0-9-]+)/i) : null; const taskId = taskIdMatch ? taskIdMatch[1] : null; let amount; let currency; if (remainingText.includes("$TBD")) { amount = "$TBD"; currency = null; } else { const amountMatch = remainingText.match(/\|\s*(\d+(\.\d{1,2})?)\s*(USDC|USDT)?\s*\|/); amount = amountMatch ? parseFloat(amountMatch[1]) : NaN; currency = amountMatch ? amountMatch[3] : null; } const dateMatch = remainingText.match(/\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/); const date = dateMatch ? new Date(dateMatch[0]) : null; return { index, link, name, taskId, amount, currency, date, }; }); // Populate the task list with D3 // Populate the task list with D3 const taskList = d3.select("#task-list"); tasks.forEach(task => { const listItem = taskList.append("li"); listItem.text(`${task.description}: ${isNaN(task.amount) ? '$TBD' : task.amount}$`); listItem.on("click", function() { window.open(task.link, '_blank'); }); }); console.log(tasks); // Min max of amounts const maxAmount = d3.max(tasks, d => d.amount); const minAmount = d3.min(tasks, d => d.amount); // Color scale const colorScale = d3.scaleLinear() .domain([minAmount, (maxAmount - minAmount) / 2, maxAmount]) .range(["#006400", "#32CD32", "#7FFF00"]) // Dark green to light green .interpolate(d3.interpolateRgb); // Initialize dimensions let svgWidth = document.getElementById('svg-section').offsetWidth; let svgHeight = window.innerHeight; // Initialize viewBox dimensions let viewBoxWidth = svgWidth; let viewBoxHeight = svgHeight; const svg = d3.select("#svg-section").append("svg") .attr("width", svgWidth) .attr("height", svgHeight) .attr("viewBox", `${-viewBoxWidth / 2} ${-viewBoxHeight / 2} ${viewBoxWidth} ${viewBoxHeight}`) .style("background-color", "black") .call(d3.zoom().scaleExtent([0.5, 5]) .on("zoom", function() { let event = d3.event; // Get the event in D3 v5 g.attr("transform", event.transform); })) .on("contextmenu", function() { let event = d3.event; // Get the event in D3 v5 // Prevent the default right-click menu from showing event.preventDefault(); }) .on("mousedown", function() { let event = d3.event; // Get the event in D3 v5 // Check for right click (or middle mouse click for panning) if (event.button === 2 || event.button === 1) { let startX = event.clientX; let startY = event.clientY; const initialTranslate = d3.zoomTransform(svg.node()); svg.on("mousemove.pan", function() { let event = d3.event; // Get the event in D3 v5 let diffX = event.clientX - startX; let diffY = event.clientY - startY; let newTransform = d3.zoomIdentity .translate(initialTranslate.x + diffX, initialTranslate.y + diffY) .scale(initialTranslate.k); svg.call(d3.zoom().transform, newTransform); }); svg.on("mouseup.pan", function() { svg.on("mousemove.pan", null); svg.on("mouseup.pan", null); }); } }); const g = svg.append("g"); // New simulation setup const simulation = d3.forceSimulation(tasks) .force('center', d3.forceCenter(0, 0)) .force('collision', d3.forceCollide().radius(fixedRadius)) .on('tick', ticked); function ticked() { const circles = g.selectAll("circle") .data(tasks, d => d.id); const newCircles = circles.enter() .append('circle') .attr('fill', d => { if (isNaN(d.amount) || d.amount === undefined) { return 'gray'; // default color if data is invalid } return colorScale(d.amount); }) .attr('r', fixedRadius) .on("mouseover", function(d) { const tooltip = document.getElementById('tooltip'); tooltip.style.display = "inline"; tooltip.innerText = `${d.name}` + '|' + `${d.amount}` + '|' + `${d.date}` + '|'; }) .on("mouseout", function() { const tooltip = document.getElementById('tooltip'); tooltip.style.display = "none"; }) .on("click", function(d) { window.open(d.link, '_blank'); }); newCircles.merge(circles) .attr('cx', d => d.x) .attr('cy', d => d.y); circles.exit().remove(); } // Re-adjust on window resize window.addEventListener("resize", function() { svgWidth = document.getElementById('svg-section').offsetWidth; svgHeight = window.innerHeight; viewBoxWidth = svgWidth; viewBoxHeight = svgHeight; svg.attr("width", svgWidth) .attr("height", svgHeight) .attr("viewBox", `${-viewBoxWidth / 2} ${-viewBoxHeight / 2} ${viewBoxWidth} ${viewBoxHeight}`); simulation.force('center', d3.forceCenter(0, 0)) .restart(); }); ```