Skip to content

Instantly share code, notes, and snippets.

@Yukaii
Last active September 20, 2024 16:41
Show Gist options
  • Save Yukaii/e0c40a3de4e6bb3d08c3fb4598f420f9 to your computer and use it in GitHub Desktop.
Save Yukaii/e0c40a3de4e6bb3d08c3fb4598f420f9 to your computer and use it in GitHub Desktop.
Enhance Swap.work with a time tracking panel to record hours, manage entries, and export costs, using MUI styles and GraphQL API calls.

Swap.work enhanced panel

This userscript enhances the Swap.work platform by adding a time tracking panel that allows freelancers to easily record hours, manage time entries, and export costs directly to the project. Built using MUI styles to seamlessly integrate with the existing UI, this script leverages GraphQL API calls for efficient data handling. Time entries are saved within the project's comment field and can be exported as individual cost entries, making it easier to track and bill for time spent on tasks.

Demo

swap-costs-exporter.mp4

Installation

  1. Tampermonkey
  2. Visit https://gist.github.com/Yukaii/e0c40a3de4e6bb3d08c3fb4598f420f9/raw/swap-panel.user.js
// ==UserScript==
// @name Swap.work Time Tracking
// @namespace http://tampermonkey.net/
// @version 0.7
// @description Enhance Swap.work with a time tracking panel to record hours, manage entries, and export costs, using MUI styles and GraphQL API calls.
// @match https://swap.work/*
// @updateURL https://gist.github.com/Yukaii/e0c40a3de4e6bb3d08c3fb4598f420f9/raw/swap-panel.user.js
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict';
// Add necessary styles
GM_addStyle(`
#time-tracking-panel {
position: fixed;
right: -320px;
top: 45px;
width: 320px;
height: calc(100% - 45px);
background-color: #f5f5f5;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
transition: right 0.3s;
z-index: 1000;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
}
#time-tracking-panel.open {
right: 0;
}
#time-tracking-toggle {
position: fixed;
right: 0;
top: 45px;
width: 10px;
height: calc(100% - 45px);
background-color: rgba(0, 0, 0, 0.1);
z-index: 1001;
cursor: pointer;
transition: background-color 0.3s;
}
#time-tracking-toggle:hover {
background-color: rgba(0, 0, 0, 0.2);
}
.time-entry {
margin-bottom: 16px;
}
`);
// Helper function to get JWT token from localStorage
function getJWTToken() {
return localStorage.getItem('account');
}
// Helper function to make GraphQL requests
function graphqlRequest(query, variables = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.swap.work',
headers: {
'Content-Type': 'application/json',
'X-Authorization': getJWTToken()
},
data: JSON.stringify({ query, variables }),
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
resolve(JSON.parse(response.responseText));
} else {
reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
}
},
onerror: function(error) {
reject(error);
}
});
});
}
// Time tracking data structure
let timeTrackingData = GM_getValue('timeTrackingData', {});
let costPerHour = GM_getValue('costPerHour', 0);
// Create UI elements
const panel = document.createElement('div');
panel.id = 'time-tracking-panel';
panel.innerHTML = `
<h2 class="MuiTypography-root jss122 MuiTypography-h6">Time Tracking</h2>
<div class="MuiFormControl-root MuiTextField-root jss520 MuiFormControl-fullWidth" style="margin-bottom: 16px;">
<label class="MuiFormLabel-root MuiInputLabel-root jss525 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink jss526 MuiInputLabel-outlined" data-shrink="true">Cost per hour</label>
<div class="MuiInputBase-root MuiOutlinedInput-root jss521 MuiInputBase-fullWidth MuiInputBase-formControl">
<input id="cost-per-hour" type="number" class="MuiInputBase-input MuiOutlinedInput-input jss522" value="${costPerHour}">
<fieldset aria-hidden="true" class="jss190 MuiOutlinedInput-notchedOutline">
<legend class="jss192 jss193"><span>Cost per hour</span></legend>
</fieldset>
</div>
</div>
<div id="time-entries"></div>
<button id="add-time-entry" class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary" style="margin-right: 8px;">Add Time Entry</button>
<button id="export-to-costs" class="MuiButtonBase-root MuiButton-root MuiButton-contained">Export to Costs</button>
`;
const toggleButton = document.createElement('div');
toggleButton.id = 'time-tracking-toggle';
// Add elements to the page
document.body.appendChild(panel);
document.body.appendChild(toggleButton);
// Toggle panel visibility
toggleButton.addEventListener('click', () => {
panel.classList.toggle('open');
});
// Update cost per hour
document.getElementById('cost-per-hour').addEventListener('change', (e) => {
costPerHour = parseFloat(e.target.value);
GM_setValue('costPerHour', costPerHour);
});
// Add time entry
document.getElementById('add-time-entry').addEventListener('click', () => {
const projectId = getCurrentProjectId();
if (!projectId) {
alert('Please open a project first.');
return;
}
if (!timeTrackingData[projectId]) {
timeTrackingData[projectId] = [];
}
const newEntry = {
id: Date.now(),
date: new Date().toISOString().split('T')[0],
duration: 0,
description: ''
};
timeTrackingData[projectId].push(newEntry);
saveTimeTrackingData(projectId);
renderTimeEntries(projectId);
});
// Render time entries for the current project
function renderTimeEntries(projectId) {
const entriesContainer = document.getElementById('time-entries');
entriesContainer.innerHTML = '';
const entries = timeTrackingData[projectId] || [];
entries.forEach(entry => {
const entryElement = document.createElement('div');
entryElement.className = 'time-entry';
entryElement.innerHTML = `
<div class="MuiFormControl-root MuiTextField-root jss520 MuiFormControl-fullWidth" style="margin-bottom: 8px;">
<label class="MuiFormLabel-root MuiInputLabel-root jss525 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink jss526 MuiInputLabel-outlined" data-shrink="true">Date</label>
<div class="MuiInputBase-root MuiOutlinedInput-root jss521 MuiInputBase-fullWidth MuiInputBase-formControl">
<input type="date" class="MuiInputBase-input MuiOutlinedInput-input jss522" value="${entry.date}">
<fieldset aria-hidden="true" class="jss190 MuiOutlinedInput-notchedOutline">
<legend class="jss192 jss193"><span>Date</span></legend>
</fieldset>
</div>
</div>
<div class="MuiFormControl-root MuiTextField-root jss520 MuiFormControl-fullWidth" style="margin-bottom: 8px;">
<label class="MuiFormLabel-root MuiInputLabel-root jss525 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink jss526 MuiInputLabel-outlined" data-shrink="true">Duration (hours)</label>
<div class="MuiInputBase-root MuiOutlinedInput-root jss521 MuiInputBase-fullWidth MuiInputBase-formControl">
<input type="number" step="0.5" min="0" class="MuiInputBase-input MuiOutlinedInput-input jss522" value="${entry.duration}">
<fieldset aria-hidden="true" class="jss190 MuiOutlinedInput-notchedOutline">
<legend class="jss192 jss193"><span>Duration (hours)</span></legend>
</fieldset>
</div>
</div>
<div class="MuiFormControl-root MuiTextField-root jss520 MuiFormControl-fullWidth" style="margin-bottom: 8px;">
<label class="MuiFormLabel-root MuiInputLabel-root jss525 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink jss526 MuiInputLabel-outlined" data-shrink="true">Description</label>
<div class="MuiInputBase-root MuiOutlinedInput-root jss521 MuiInputBase-fullWidth MuiInputBase-formControl">
<input type="text" class="MuiInputBase-input MuiOutlinedInput-input jss522" value="${entry.description}">
<fieldset aria-hidden="true" class="jss190 MuiOutlinedInput-notchedOutline">
<legend class="jss192 jss193"><span>Description</span></legend>
</fieldset>
</div>
</div>
<button class="delete-entry MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textSecondary">Delete</button>
`;
entryElement.querySelector('input[type="date"]').addEventListener('change', (e) => {
entry.date = e.target.value;
saveTimeTrackingData(projectId);
});
entryElement.querySelector('input[type="number"]').addEventListener('change', (e) => {
entry.duration = parseFloat(e.target.value);
saveTimeTrackingData(projectId);
});
entryElement.querySelector('input[type="text"]').addEventListener('change', (e) => {
entry.description = e.target.value.replace(/\n/g, ' '); // Remove line breaks
saveTimeTrackingData(projectId);
});
entryElement.querySelector('.delete-entry').addEventListener('click', () => {
timeTrackingData[projectId] = entries.filter(e => e.id !== entry.id);
saveTimeTrackingData(projectId);
renderTimeEntries(projectId);
});
entriesContainer.appendChild(entryElement);
});
}
// Export time entries to costs
document.getElementById('export-to-costs').addEventListener('click', async () => {
const projectId = getCurrentProjectId();
if (!projectId) {
alert('Please open a project first.');
return;
}
const entries = timeTrackingData[projectId] || [];
try {
for (const entry of entries) {
const totalCost = entry.duration * costPerHour;
const content = `[TT] (${entry.duration}h) - ${entry.description}`;
const result = await graphqlRequest(`
mutation {
createProjectCostFromSOHO(inputData: {
project_id: "${projectId}"
content: "${content}"
value: ${totalCost}
}) {
message
errors
}
}
`);
if (result.data && result.data.createProjectCostFromSOHO.errors) {
throw new Error(result.data.createProjectCostFromSOHO.errors[0]);
}
}
alert('Time tracking data exported to costs successfully.');
} catch (error) {
console.error('Error exporting time tracking data:', error);
alert('Failed to export time tracking data. Please try again.');
}
});
// Helper function to get the current project ID from the URL
function getCurrentProjectId() {
const match = window.location.pathname.match(/\/member\/projects\/([^\/]+)/);
return match ? match[1] : null;
}
// Save time tracking data to project comment
async function saveTimeTrackingData(projectId) {
const data = JSON.stringify(timeTrackingData[projectId]);
try {
await graphqlRequest(`
mutation {
updateProjectFromSOHO(inputData: {
project_id: "${projectId}"
comment: "${data}"
}) {
status
message
errors
}
}
`);
GM_setValue('timeTrackingData', timeTrackingData);
} catch (error) {
console.error('Error saving time tracking data:', error);
}
}
// Load time tracking data from project comment
async function loadTimeTrackingData(projectId) {
try {
const result = await graphqlRequest(`
query {
getProjectsFromSOHO(filter: { id: "${projectId}" }) {
payload {
id
comment
}
}
}
`);
const project = result.data.getProjectsFromSOHO.payload[0];
if (project && project.comment) {
timeTrackingData[projectId] = JSON.parse(project.comment);
GM_setValue('timeTrackingData', timeTrackingData);
}
} catch (error) {
console.error('Error loading time tracking data:', error);
}
}
// Update time tracking panel when navigating to a project page
let currentProjectId = null;
async function checkForProjectChange() {
const projectId = getCurrentProjectId();
if (projectId && projectId !== currentProjectId) {
currentProjectId = projectId;
await loadTimeTrackingData(projectId);
renderTimeEntries(projectId);
}
}
// Check for project changes every second
setInterval(checkForProjectChange, 1000);
// Initial check
checkForProjectChange();
})();
@Yukaii
Copy link
Author

Yukaii commented Sep 20, 2024

Prompt for Claude 3.5 & gpt-4o

https://hackmd.io/@yukai/swap-panel-prompt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment