Skip to content

Instantly share code, notes, and snippets.

@rushkeldon
Last active April 18, 2024 21:40
Show Gist options
  • Save rushkeldon/5df3ddfddaec0b3f21a659ca19d99b0d to your computer and use it in GitHub Desktop.
Save rushkeldon/5df3ddfddaec0b3f21a659ca19d99b0d to your computer and use it in GitHub Desktop.
Jira Addon : adds a standup button and gamifies Jira a bit with FX

HOW TO USE THIS

This is meant to work in Chrome.

  1. install this Chrome extension
  2. visit your instance of Jira
  3. click on the icon for the above extension
  4. copy the JS from this gist and paste into the JavaScript panel (you will likely have to edit the blockedFilters array)
  5. copy the CSS from this gist and paste into the CSS panel
  6. save
  7. enjoy!
a.btn-standup:hover {
color: black !important;
}
.popup-standup {
font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
position: fixed;
z-index: 5000;
padding: 0px 10px;
border: 1px solid black;
border-radius: 6px;
background: white;
right: 0px;
top: 41px;
min-width: 150px;
user-select: none;
cursor: default;
overflow: hidden;
transition: max-height 0.25s ease-in;
max-height: 25px;
}
.popup-standup.displayed {
max-height: 2000px;
}
.popup-standup.displayed .popup-header .btn-standup-close:after {
content: '▲';
}
.popup-standup .popup-header {
margin-left: -10px;
padding-left: 5px;
border-bottom: 1px solid black;
width: calc(100% + 20px);
height: 25px;
line-height: 25px;
font-size: 16px;
position: relative;
font-weight: bold;
}
.popup-standup .popup-header .btn-standup-close {
cursor: pointer;
position: absolute;
right: 0px;
padding: 0 10px;
text-align: center;
}
.popup-standup .popup-header .btn-standup-close:after {
content: '▼';
font-size: 16px;
}
.popup-standup .standup-list ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.popup-standup .standup-list ul li {
display: block;
list-style-type: none;
margin-left: 0;
padding-left: 5px;
padding: 3px;
font-size: 16px;
}
.popup-standup .standup-list ul li.done {
opacity: 0.5;
}
.popup-standup .standup-list ul li.done:before {
display: inline-block;
content: "✓";
width: 20px;
margin-left: -10px;
text-align: left;
padding-left: 4px;
}
canvas {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: 1000;
margin: auto;
transition: opacity 0.75s ease-out;
opacity: 1;
pointer-events: none;
}
canvas.outro {
opacity: 0;
}
// modify blockedFilters to include filters that aren't people
const blockedFilters = [
'Ralph',
'Blocked',
'blockedFilters',
'Unassigned',
'Bugs',
'Release Ticket',
'Only My Issues',
'Recently Updated',
'… Show more'
];
const standupCheckMark = '✓';
let standupPeople;
function getStandupList() {
addStandupBtn();
initStandupList();
}
function addStandupBtn() {
const auiNav = document.querySelector('.aui-nav');
const li = document.createElement('li');
li.innerHTML = `<a
class='aui-button aui-button-primary aui-style btn-standup'
resolved=''>Standup</a>`;
auiNav.appendChild(li);
const btn = document.querySelector('.btn-standup');
btn.addEventListener('click', createStandupPopup);
}
function removeStandupBtn() {
const btn = document.querySelector('.btn-standup');
btn.parentNode.removeChild(btn);
}
function initStandupList() {
const filtersContainer = document.querySelector('#js-work-quickfilters');
if (!filtersContainer) return window.setTimeout(initStandupList, 1000);
standupPeople = [...filtersContainer.querySelectorAll('a')].map(a => a.innerHTML);
standupPeople = standupPeople.filter(person => !blockedFilters.includes(person));
}
function createStandupPopup() {
removeStandupBtn();
const randomizedPeople = standupPeople.sort(() => Math.random() - 0.5);
const popup = document.createElement('div');
popup.className = 'popup-standup displayed';
popup.innerHTML = `
<div class='popup-header' onclick='toggleStandupPopup()'>
<span>standup</span>
<span class='btn-standup-close'></span>
</div>
<div class='standup-list'>
<ul>
${randomizedPeople.map(person => `<li onclick="this.classList.toggle('done')">${person}</li>`).join('')}
</ul>
</div>
`;
document.body.appendChild(popup);
}
function toggleStandupPopup() {
const popup = document.querySelector('.popup-standup');
if (popup) {
popup.classList.toggle('displayed');
}
}
// ======================================================================
const gamify = () => {
let tickets = document.querySelectorAll('.js-parent-drag');
if (!tickets.length) return;
tickets.forEach((ticket) => {
if (!isGamified(ticket)) {
ticket.removeEventListener('mousedown', ticketPressed, true);
ticket.addEventListener('mousedown', ticketPressed, true);
ticket.removeEventListener('mouseup', ticketReleased, true);
ticket.addEventListener('mouseup', ticketReleased, true);
setGamified(ticket);
}
});
};
function ticketPressed(e) {
e.target.setAttribute('data-x-coord', getX(e.target));
}
function ticketReleased(e) {
const startX = parseInt(e?.target?.getAttribute('data-x-coord'));
if (isNaN(startX)) return;
const endX = getX(e.target);
let mp3 = 'https://appcloud9.com/snd/';
switch (true) {
case Math.abs(startX - endX) <= 20 :
mp3 += 'neutral.mp3';
break;
case startX < endX :
mp3 += 'powerup.mp3';
createFireworks();
break;
case startX > endX :
mp3 += 'powerdown.mp3';
break;
default :
console.log('unhandled case encountered by ticketReleased.');
}
playSound(mp3);
unsetGamfied(e.target);
window.setTimeout(gamify, 250);
}
function setGamified(el) {
el.setAttribute('data-gamified', true);
}
function unsetGamfied(el) {
el.removeAttribute('data-gamified');
}
function isGamified(el) {
return el.hasAttribute('data-gamified');
}
function getX(el) {
return el.getBoundingClientRect().left;
}
function playSound(url) {
const audio = new Audio(url);
audio.play();
}
function createFireworks() {
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d'),
cw = window.innerWidth,
ch = window.innerHeight,
fireworks = [],
particles = [],
hue = 120,
limiterTotal = 20,
limiterTick = 0,
timerTotal = 0,
randomTime = 0,
timerTick = 0,
mousedown = false,
mx,
my;
canvas.width = cw;
canvas.height = ch;
var snd = new Audio('https://appcloud9.com/snd/firework.mp3');
function random(min, max) {
return min + Math.random() * (max - min);
}
function calculateDistance(p1x, p1y, p2x, p2y) {
return Math.sqrt((p1x - p2x) * (p1x - p2x) + (p1y - p2y) * (p1y - p2y));
}
function Firework(sx, sy, tx, ty) {
this.x = sx;
this.y = sy;
this.sx = sx;
this.sy = sy;
this.tx = tx;
this.ty = ty;
this.distanceToTarget = calculateDistance(sx, sy, tx, ty);
this.distanceTraveled = 0;
this.coordinates = [];
this.coordinateCount = 2;
while (this.coordinateCount--) {
this.coordinates.push([this.x, this.y]);
}
this.angle = Math.atan2(ty - sy, tx - sx);
this.speed = 1;
this.acceleration = 1.2;
this.brightness = random(50, 70);
this.tragetRadius = 1;
}
Firework.prototype.update = function(index) {
if (this.targetRadius < 8) {
this.targetRadius += 0.3;
} else {
this.targetRadius = 1;
}
this.speed *= this.acceleration;
var vx = Math.cos(this.angle) * this.speed,
vy = Math.sin(this.angle) * this.speed;
this.distanceTraveled = calculateDistance(this.sx, this.sy, this.x + vx, this.y + vy);
if (this.distanceTraveled >= this.distanceToTarget) {
this.coordinates.pop();
this.coordinates.unshift([this.tx, this.ty]);
createParticles(this.x, this.y);
snd.play();
this.draw();
fireworks.splice(index, 1);
} else {
this.x += vx;
this.y += vy;
}
this.coordinates.pop();
this.coordinates.unshift([this.x, this.y]);
};
Firework.prototype.draw = function() {
ctx.beginPath();
ctx.moveTo(this.coordinates[this.coordinates.length - 1][0], this.coordinates[this.coordinates.length - 1][1]);
ctx.lineTo(this.x, this.y);
ctx.strokeStyle = 'hsl(' + hue + ', 100%, ' + this.brightness + '%)';
ctx.stroke();
};
function Particle(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
this.coordinates = [];
this.coordinateCount = 6;
while (this.coordinateCount--) {
this.coordinates.push([this.x, this.y]);
}
var variation = random(1, 5);
this.friction = 0.95;
this.alpha = 1;
this.angle = random(0, Math.PI * 2);
switch (type) {
case 1:
switch (true) {
case variation < 2 :
this.speed = random(1, 15);
this.gravity = 4;
this.hue = random(hue - 50, hue + 50);
this.brightness = random(50, 80);
this.decay = random(0.01, 0.02);
break;
case variation < 3 :
this.speed = random(1, 5);
this.gravity = 3;
this.hue = random(hue - 50, hue);
this.brightness = random(50, 80);
this.decay = random(0.015, 0.03);
break;
case variation < 4 :
this.speed = random(1, 8);
this.gravity = 3;
this.hue = random(hue, hue + 50);
this.brightness = random(50, 80);
this.decay = random(0.015, 0.03);
break;
default :
this.speed = random(1, 15);
this.gravity = 3;
this.hue = random(hue - 50, hue + 50);
this.brightness = random(10, 20);
this.decay = random(0.015, 0.3);
}
break;
case 2:
this.hue = 100;
switch (true) {
case variation < 2 :
this.speed = random(1, 10);
this.gravity = 4;
this.brightness = random(50, 80);
this.decay = random(0.01, 0.02);
break;
case variation < 3 :
this.speed = random(1, 21);
this.gravity = 3;
this.brightness = random(50, 80);
this.decay = random(0.015, 0.03);
break;
case variation < 4 :
this.speed = random(1, 3);
this.gravity = 3;
this.brightness = random(50, 80);
this.decay = random(0.015, 0.03);
break;
default :
this.speed = random(1, 5);
this.gravity = 3;
this.brightness = random(10, 20);
this.decay = random(0.015, 0.3);
}
break;
case 3:
this.brightness = random(10, 20);
switch (true) {
case variation < 2 :
this.speed = random(10, 15);
this.gravity = 4;
this.hue = 60;
this.decay = random(0.01, 0.02);
break;
case variation < 3 :
this.speed = random(11, 15);
this.gravity = 3;
this.hue = 10;
this.decay = random(0.015, 0.03);
break;
case variation < 4 :
this.speed = random(11, 18);
this.gravity = 3;
this.hue = 90;
this.decay = random(0.015, 0.03);
break;
default :
this.speed = random(11, 15);
this.gravity = 3;
this.hue = 120;
this.decay = random(0.015, 0.3);
}
break;
case 4:
switch (true) {
case variation < 2:
this.speed = random(1, 10);
this.gravity = 4;
this.hue = 300;
this.brightness = random(50, 80);
this.decay = random(0.01, 0.02);
break;
case variation < 3:
this.speed = random(1, 21);
this.gravity = 3;
this.hue = 300;
this.brightness = random(50, 80);
this.decay = random(0.015, 0.03);
break;
case variation < 4:
this.speed = random(1, 3);
this.gravity = 3;
this.hue = 300;
this.brightness = random(50, 80);
this.decay = random(0.015, 0.03);
break;
default:
this.speed = random(1, 5);
this.gravity = 3;
this.hue = 100;
this.brightness = random(10, 20);
this.decay = random(0.015, 0.3);
}
break;
default:
}
}
Particle.prototype.update = function(index) {
this.speed *= this.friction;
this.x += Math.cos(this.angle) * this.speed;
this.y += Math.sin(this.angle) * this.speed + this.gravity;
this.alpha -= this.decay;
if (this.type == 4 && this.alpha <= 0.5) {
this.brightness += 50;
this.hue += 200;
if (this.brightness >= 200)
this.brightness = 0;
}
if (this.alpha <= this.decay) {
particles.splice(index, 1);
}
this.coordinates.pop();
this.coordinates.unshift([this.x, this.y]);
};
Particle.prototype.draw = function() {
ctx.beginPath();
ctx.moveTo(this.coordinates[this.coordinates.length - 1][0], this.coordinates[this.coordinates.length - 1][1]);
ctx.lineTo(this.x, this.y);
ctx.strokeStyle = 'hsla(' + this.hue + ', 100%, ' + this.brightness + '%, ' + this.alpha + ')';
ctx.stroke();
};
function createParticles(x, y) {
var particleCount = 300;
var type = Math.floor(random(1, 5));
while (particleCount--) {
particles.push(new Particle(x, y, type));
}
}
function loop() {
hue += 0.5;
ctx.globalCompositeOperation = 'destination-out';
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(0, 0, cw, ch);
ctx.globalCompositeOperation = 'lighter';
var i = fireworks.length;
while (i--) {
fireworks[i].draw();
fireworks[i].update(i);
}
var i = particles.length;
while (i--) {
particles[i].draw();
particles[i].update(i);
}
if (timerTick >= timerTotal + randomTime) {
if (!mousedown) {
var xPos = Math.pow(Math.floor((random(-Math.pow(cw / 2, 1 / 3), Math.pow(cw / 2, 1 / 3)))), 3);
xPos += cw / 2;
fireworks.push(new Firework(cw / 2, ch, xPos, random(0, ch / 2)));
timerTick = 0;
randomTime = Math.pow(random(2, 4), 2);
}
} else {
timerTick++;
}
if (limiterTick >= limiterTotal) {
if (mousedown) {
fireworks.push(new Firework(cw / 2, ch, mx, my));
limiterTick = 0;
} else {
limiterTick = limiterTotal;
}
} else {
limiterTick++;
}
}
canvas.addEventListener('mousemove', function(e) {
mx = e.pageX - canvas.offsetLeft;
my = e.pageY - canvas.offsetTop;
});
canvas.addEventListener('mousedown', function(e) {
e.preventDefault();
mousedown = true;
});
canvas.addEventListener('mouseup', function(e) {
e.preventDefault();
mousedown = false;
});
setInterval(loop, 25);
function outroed() {
canvas.removeEventListener( 'transitionend', outroed );
snd.pause();
snd.src = '';
const trash = document.createElement( 'div' );
trash.appendChild( canvas );
trash.innerHTML = '';
}
function removeFireworks(){
canvas.addEventListener( 'transitionend', outroed );
canvas.classList.add( 'outro' );
}
window.setTimeout( removeFireworks, 4000 );
}
function init() {
window.setTimeout(getStandupList, 1000);
window.setInterval(gamify, 500);
}
const whenDOMready = (func) => {
switch (document.readyState) {
case 'complete' :
case 'loaded' :
case 'interactive' :
func();
break;
default :
window.addEventListener('DOMContentLoaded', function(e) {
func();
});
}
};
whenDOMready(init);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment