a few changes to make A/B testing easier

This commit is contained in:
Danny Coates 2017-08-24 14:54:02 -07:00
parent b2f76d2df9
commit 53e822964e
No known key found for this signature in database
GPG key ID: 4C442633C62E00CB
94 changed files with 4566 additions and 3958 deletions

9
app/templates/blank.js Normal file
View file

@ -0,0 +1,9 @@
const html = require('choo/html');
module.exports = function(state) {
const div = html`<div id="page-one"></div>`;
if (state.layout) {
return state.layout(state, div);
}
return div;
};

View file

@ -0,0 +1,31 @@
const html = require('choo/html');
const progress = require('./progress');
const { fadeOut } = require('../utils');
module.exports = function(state, emit) {
const div = html`
<div id="download" class="fadeIn">
<div id="download-progress">
<div id="dl-title" class="title">${state.translate(
'downloadFinish'
)}</div>
<div class="description"></div>
${progress(1)}
<div class="upload">
<div class="progress-text"></div>
</div>
</div>
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
'sendYourFilesLink'
)}</a>
</div>
`;
async function sendNew(e) {
e.preventDefault();
await fadeOut('download');
emit('pushState', '/');
}
return div;
};

28
app/templates/download.js Normal file
View file

@ -0,0 +1,28 @@
const html = require('choo/html');
const progress = require('./progress');
const { bytes } = require('../utils');
module.exports = function(state) {
const transfer = state.transfer;
const div = html`
<div id="download-progress" class="fadeIn">
<div id="dl-title" class="title">${state.translate(
'downloadingPageProgress',
{
filename: state.fileInfo.name,
size: bytes(state.fileInfo.size)
}
)}</div>
<div class="description">${state.translate('downloadingPageMessage')}</div>
${progress(transfer.progressRatio)}
<div class="upload">
<div class="progress-text">${state.translate(
transfer.msg,
transfer.sizes
)}</div>
</div>
</div>
`;
return div;
};

12
app/templates/error.js Normal file
View file

@ -0,0 +1,12 @@
const html = require('choo/html');
const assets = require('../../common/assets');
module.exports = function(state) {
return html`
<div id="upload-error">
<div class="title">${state.translate('errorPageHeader')}</div>
<img id="upload-error-img" data-l10n-id="errorAltText" src="${assets.get(
'illustration_error.svg'
)}"/>
</div>`;
};

84
app/templates/file.js Normal file
View file

@ -0,0 +1,84 @@
const html = require('choo/html');
const assets = require('../../common/assets');
function timeLeft(milliseconds) {
const minutes = Math.floor(milliseconds / 1000 / 60);
const hours = Math.floor(minutes / 60);
const seconds = Math.floor(milliseconds / 1000 % 60);
if (hours >= 1) {
return `${hours}h ${minutes % 60}m`;
} else if (hours === 0) {
return `${minutes}m ${seconds}s`;
}
return null;
}
module.exports = function(file, state, emit) {
const ttl = file.expiresAt - Date.now();
const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
const row = html`
<tr id="${file.id}">
<td>${file.name}</td>
<td>
<img onclick=${copyClick} src="${assets.get(
'copy-16.svg'
)}" class="icon-copy" title="${state.translate('copyUrlHover')}">
<span class="text-copied" hidden="true">${state.translate(
'copiedUrl'
)}</span>
</td>
<td>${remaining}</td>
<td>
<img onclick=${showPopup} src="${assets.get(
'close-16.svg'
)}" class="icon-delete" title="${state.translate('deleteButtonHover')}">
<div class="popup">
<div class="popuptext" onblur=${cancel} tabindex="-1">
<div class="popup-message">${state.translate('deletePopupText')}</div>
<div class="popup-action">
<span class="popup-no" onclick=${cancel}>${state.translate(
'deletePopupCancel'
)}</span>
<span class="popup-yes" onclick=${deleteFile}>${state.translate(
'deletePopupYes'
)}</span>
</div>
</div>
</div>
</td>
</tr>
`;
function copyClick(e) {
emit('copy', { url: file.url, location: 'upload-list' });
const icon = e.target;
const text = e.target.nextSibling;
icon.hidden = true;
text.hidden = false;
setTimeout(() => {
icon.hidden = false;
text.hidden = true;
}, 500);
}
function showPopup() {
const tr = document.getElementById(file.id);
const popup = tr.querySelector('.popuptext');
popup.classList.add('show');
popup.focus();
}
function cancel(e) {
e.stopPropagation();
const tr = document.getElementById(file.id);
const popup = tr.querySelector('.popuptext');
popup.classList.remove('show');
}
function deleteFile() {
emit('delete', { file, location: 'upload-list' });
emit('render');
}
return row;
};

28
app/templates/fileList.js Normal file
View file

@ -0,0 +1,28 @@
const html = require('choo/html');
const file = require('./file');
module.exports = function(state, emit) {
let table = '';
if (state.storage.files.length) {
table = html`
<table id="uploaded-files">
<thead>
<tr>
<th id="uploaded-file">${state.translate('uploadedFile')}</th>
<th id="copy-file-list">${state.translate('copyFileList')}</th>
<th id="expiry-file-list">${state.translate('expiryFileList')}</th>
<th id="delete-file-list">${state.translate('deleteFileList')}</th>
</tr>
</thead>
<tbody>
${state.storage.files.map(f => file(f, state, emit))}
</tbody>
</table>
`;
}
return html`
<div id="file-list">
${table}
</div>
`;
};

38
app/templates/legal.js Normal file
View file

@ -0,0 +1,38 @@
const html = require('choo/html');
function replaceLinks(str, urls) {
let i = -1;
const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
i++;
return `<a href="${urls[i]}">${v}</a>`;
});
return [`<div class="description">${s}</div>`];
}
module.exports = function(state) {
const div = html`
<div id="page-one">
<div id="legal">
<div class="title">${state.translate('legalHeader')}</div>
${html(
replaceLinks(state.translate('legalNoticeTestPilot'), [
'https://testpilot.firefox.com/terms',
'https://testpilot.firefox.com/privacy',
'https://testpilot.firefox.com/experiments/send'
])
)}
${html(
replaceLinks(state.translate('legalNoticeMozilla'), [
'https://www.mozilla.org/privacy/websites/',
'https://www.mozilla.org/about/legal/terms/mozilla/'
])
)}
</div>
</div>
`;
if (state.layout) {
return state.layout(state, div);
}
return div;
};

27
app/templates/notFound.js Normal file
View file

@ -0,0 +1,27 @@
const html = require('choo/html');
const assets = require('../../common/assets');
module.exports = function(state) {
const div = html`
<div id="page-one">
<div id="download">
<div class="title">${state.translate('expiredPageHeader')}</div>
<div class="share-window">
<img src="${assets.get(
'illustration_expired.svg'
)}" id="expired-img" data-l10n-id="linkExpiredAlt"/>
</div>
<div class="expired-description">${state.translate(
'uploadPageExplainer'
)}</div>
<a class="send-new" href="/" data-state="notfound">${state.translate(
'sendYourFilesLink'
)}</a>
</div>
</div>`;
if (state.layout) {
return state.layout(state, div);
}
return div;
};

65
app/templates/preview.js Normal file
View file

@ -0,0 +1,65 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const notFound = require('./notFound');
const { bytes } = require('../utils');
function getFileFromDOM() {
const el = document.getElementById('dl-file');
if (!el) {
return null;
}
const data = el.dataset;
return {
name: data.name,
size: parseInt(data.size, 10),
ttl: parseInt(data.ttl, 10)
};
}
module.exports = function(state, emit) {
state.fileInfo = state.fileInfo || getFileFromDOM();
if (!state.fileInfo) {
return notFound(state, emit);
}
state.fileInfo.id = state.params.id;
state.fileInfo.key = state.params.key;
const fileInfo = state.fileInfo;
const size = bytes(fileInfo.size);
const div = html`
<div id="page-one">
<div id="download">
<div id="download-page-one">
<div class="title">
<span id="dl-file"
data-name="${fileInfo.name}"
data-size="${fileInfo.size}"
data-ttl="${fileInfo.ttl}">${state.translate('downloadFileName', {
filename: fileInfo.name
})}</span>
<span id="dl-filesize">${' ' +
state.translate('downloadFileSize', { size })}</span>
</div>
<div class="description">${state.translate('downloadMessage')}</div>
<img src="${assets.get(
'illustration_download.svg'
)}" id="download-img" alt="${state.translate('downloadAltText')}"/>
<div>
<button id="download-btn" class="btn" onclick=${download}>${state.translate(
'downloadButtonLabel'
)}</button>
</div>
</div>
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
</div>
</div>
`;
function download(event) {
event.preventDefault();
emit('download', fileInfo);
}
if (state.layout) {
return state.layout(state, div);
}
return div;
};

21
app/templates/progress.js Normal file
View file

@ -0,0 +1,21 @@
const html = require('choo/html');
const radius = 73;
const oRadius = radius + 10;
const oDiameter = oRadius * 2;
const circumference = 2 * Math.PI * radius;
module.exports = function(progressRatio) {
const dashOffset = (1 - progressRatio) * circumference;
const percent = Math.floor(progressRatio * 100);
const div = html`
<div class="progress-bar">
<svg id="progress" width="${oDiameter}" height="${oDiameter}" viewPort="0 0 ${oDiameter} ${oDiameter}" version="1.1">
<circle r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent"/>
<circle id="bar" r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent" transform="rotate(-90 ${oRadius} ${oRadius})" stroke-dasharray="${circumference}" stroke-dashoffset="${dashOffset}"/>
<text class="percentage" text-anchor="middle" x="50%" y="98"><tspan class="percent-number">${percent}</tspan><tspan class="percent-sign">%</tspan></text>
</svg>
</div>
`;
return div;
};

61
app/templates/share.js Normal file
View file

@ -0,0 +1,61 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const notFound = require('./notFound');
const { allowedCopy, delay, fadeOut } = require('../utils');
module.exports = function(state, emit) {
const file = state.storage.getFileById(state.params.id);
if (!file) {
return notFound(state, emit);
}
const div = html`
<div id="share-link" class="fadeIn">
<div class="title">${state.translate('uploadSuccessTimingHeader')}</div>
<div id="share-window">
<div id="copy-text">${state.translate('copyUrlFormLabelWithName', {
filename: file.name
})}</div>
<div id="copy">
<input id="link" type="url" value="${file.url}" readonly="true"/>
<button id="copy-btn" class="btn" onclick=${copyLink}>${state.translate(
'copyUrlFormButton'
)}</button>
</div>
<button id="delete-file" class="btn" onclick=${deleteFile}>${state.translate(
'deleteFileButton'
)}</button>
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
'sendAnotherFileLink'
)}</a>
</div>
</div>
`;
async function sendNew(e) {
e.preventDefault();
await fadeOut('share-link');
emit('pushState', '/');
}
async function copyLink() {
if (allowedCopy()) {
emit('copy', { url: file.url, location: 'success-screen' });
const copyBtn = document.getElementById('copy-btn');
copyBtn.disabled = true;
copyBtn.replaceChild(
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
copyBtn.firstChild
);
await delay(2000);
copyBtn.disabled = false;
copyBtn.textContent = state.translate('copyUrlFormButton');
}
}
async function deleteFile() {
emit('delete', { file, location: 'success-screen' });
await fadeOut('share-link');
emit('pushState', '/');
}
return div;
};

View file

@ -0,0 +1,50 @@
const html = require('choo/html');
const assets = require('../../common/assets');
module.exports = function(state) {
const msg =
state.params.reason === 'outdated'
? html`
<div id="unsupported-browser">
<div class="title">${state.translate('notSupportedHeader')}</div>
<div class="description">${state.translate(
'notSupportedOutdatedDetail'
)}</div>
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
<img src="${assets.get(
'firefox_logo-only.svg'
)}" class="firefox-logo" alt="Firefox"/>
<div class="unsupported-button-text">${state.translate(
'updateFirefox'
)}</div>
</a>
<div class="unsupported-description">${state.translate(
'uploadPageExplainer'
)}</div>
</div>`
: html`
<div id="unsupported-browser">
<div class="title">${state.translate('notSupportedHeader')}</div>
<div class="description">${state.translate('notSupportedDetail')}</div>
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">${state.translate(
'notSupportedLink'
)}</a></div>
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2">
<img src="${assets.get(
'firefox_logo-only.svg'
)}" class="firefox-logo" alt="Firefox"/>
<div class="unsupported-button-text">Firefox<br>
<span>${state.translate('downloadFirefoxButtonSub')}</span>
</div>
</a>
<div class="unsupported-description">${state.translate(
'uploadPageExplainer'
)}</div>
</div>`;
const div = html`<div id="page-one">${msg}</div>`;
if (state.layout) {
return state.layout(state, div);
}
return div;
};

38
app/templates/upload.js Normal file
View file

@ -0,0 +1,38 @@
const html = require('choo/html');
const progress = require('./progress');
const { bytes } = require('../utils');
module.exports = function(state, emit) {
const transfer = state.transfer;
const div = html`
<div id="upload-progress" class="fadeIn">
<div class="title" id="upload-filename">${state.translate(
'uploadingPageProgress',
{
filename: transfer.file.name,
size: bytes(transfer.file.size)
}
)}</div>
<div class="description"></div>
${progress(transfer.progressRatio)}
<div class="upload">
<div class="progress-text">${state.translate(
transfer.msg,
transfer.sizes
)}</div>
<button id="cancel-upload" onclick=${cancel}>${state.translate(
'uploadingPageCancel'
)}</button>
</div>
</div>
`;
function cancel() {
const btn = document.getElementById('cancel-upload');
btn.disabled = true;
btn.textContent = state.translate('uploadCancelNotification');
emit('cancel');
}
return div;
};

55
app/templates/welcome.js Normal file
View file

@ -0,0 +1,55 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const fileList = require('./fileList');
const { fadeOut } = require('../utils');
module.exports = function(state, emit) {
const div = html`
<div id="page-one" class="fadeIn">
<div class="title">${state.translate('uploadPageHeader')}</div>
<div class="description">
<div>${state.translate('uploadPageExplainer')}</div>
<a href="https://testpilot.firefox.com/experiments/send" class="link">${state.translate(
'uploadPageLearnMore'
)}</a>
</div>
<div class="upload-window" ondragover=${dragover} ondragleave=${dragleave}>
<div id="upload-img"><img src="${assets.get(
'upload.svg'
)}" title="${state.translate('uploadSvgAlt')}"/></div>
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
<span id="file-size-msg"><em>${state.translate(
'uploadPageSizeMessage'
)}</em></span>
<form method="post" action="upload" enctype="multipart/form-data">
<label for="file-upload" id="browse" class="btn">${state.translate(
'uploadPageBrowseButton1'
)}</label>
<input id="file-upload" type="file" name="fileUploaded" onchange=${upload} />
</form>
</div>
${fileList(state, emit)}
</div>
`;
function dragover(event) {
event.target.classList.add('ondrag');
}
function dragleave(event) {
event.target.classList.remove('ondrag');
}
async function upload(event) {
event.preventDefault();
const target = event.target;
const file = target.files[0];
await fadeOut('page-one');
emit('upload', { file, type: 'click' });
}
if (state.layout) {
return state.layout(state, div);
}
return div;
};