a new approach for the ui
This commit is contained in:
parent
cc85486414
commit
f0cfc19f8c
34 changed files with 2246 additions and 146 deletions
63
app/ui/account.js
Normal file
63
app/ui/account.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
const html = require('choo/html');
|
||||
const itemClass =
|
||||
'block p-2 text-grey-darkest hover:bg-blue hover:text-white cursor-pointer';
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
if (!state.capabilities.account) {
|
||||
return null;
|
||||
}
|
||||
const user = state.user;
|
||||
const menuItems = [];
|
||||
if (user.loggedIn) {
|
||||
menuItems.push(html`<li class="p-2 text-grey-dark">${user.email}</li>`);
|
||||
menuItems.push(
|
||||
html`<li><a class="${itemClass}" onclick=${logout}>${state.translate(
|
||||
'logOut'
|
||||
)}</a></li>`
|
||||
);
|
||||
} else {
|
||||
menuItems.push(
|
||||
html`<li class=""><a class="${itemClass}" onclick=${login}>${state.translate(
|
||||
'signInMenuOption'
|
||||
)}</a></li>`
|
||||
);
|
||||
}
|
||||
return html`<div class="relative">
|
||||
<input
|
||||
type="image"
|
||||
alt="${user.email}"
|
||||
class="w-8 h-8 rounded-full text-white"
|
||||
src="${user.avatar}"
|
||||
onclick=${avatarClick}/>
|
||||
<ul
|
||||
id="accountMenu"
|
||||
class="invisible list-reset absolute pin-t pin-r mt-10 pt-2 pb-2 bg-white shadow-md whitespace-no-wrap outline-none z-50"
|
||||
onblur="${hideMenu}"
|
||||
tabindex="-1">
|
||||
${menuItems}
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
function avatarClick(event) {
|
||||
event.preventDefault();
|
||||
const menu = document.getElementById('accountMenu');
|
||||
menu.classList.toggle('invisible');
|
||||
menu.focus();
|
||||
}
|
||||
|
||||
function hideMenu(event) {
|
||||
event.stopPropagation();
|
||||
const menu = document.getElementById('accountMenu');
|
||||
menu.classList.add('invisible');
|
||||
}
|
||||
|
||||
function login(event) {
|
||||
event.preventDefault();
|
||||
emit('login');
|
||||
}
|
||||
|
||||
function logout(event) {
|
||||
event.preventDefault();
|
||||
emit('logout');
|
||||
}
|
||||
};
|
41
app/ui/archiveList.js
Normal file
41
app/ui/archiveList.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const { list } = require('../utils');
|
||||
const archiveTile = require('./archiveTile');
|
||||
|
||||
function intro(state) {
|
||||
return html`
|
||||
<article class="flex flex-col items-center justify-center bg-white border border-grey-light p-2">
|
||||
<p class="text-center">
|
||||
<div class="font-semibold">${state.translate('uploadPageHeader')}</div>
|
||||
<div class="italic">${state.translate('pageHeaderCredits')}</div>
|
||||
</p>
|
||||
<img src="${assets.get('illustration_download.svg')}"/>
|
||||
<p class="m-4 max-w-sm text-sm font-light">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</p>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const archives = state.storage.files.map(archive =>
|
||||
archiveTile(state, emit, archive)
|
||||
);
|
||||
let wip = '';
|
||||
if (state.uploading) {
|
||||
wip = archiveTile.uploading(state, emit);
|
||||
} else if (state.archive) {
|
||||
wip = archiveTile.wip(state, emit);
|
||||
} else {
|
||||
wip = archiveTile.empty(state, emit);
|
||||
}
|
||||
archives.reverse();
|
||||
if (archives.length < 1) {
|
||||
archives.push(intro(state));
|
||||
}
|
||||
return html`
|
||||
<section class="relative h-full w-full px-6">
|
||||
<div class="pt-4 pb-2">${wip}</div>
|
||||
${list(archives, 'list-reset h-full overflow-y-scroll foo', 'py-2')}
|
||||
</section>`;
|
||||
};
|
260
app/ui/archiveTile.js
Normal file
260
app/ui/archiveTile.js
Normal file
|
@ -0,0 +1,260 @@
|
|||
const html = require('choo/html');
|
||||
const raw = require('choo/html/raw');
|
||||
const assets = require('../../common/assets');
|
||||
const { bytes, copyToClipboard, list, percent, timeLeft } = require('../utils');
|
||||
const expiryOptions = require('./expiryOptions');
|
||||
|
||||
function expiryInfo(translate, archive) {
|
||||
const l10n = timeLeft(archive.expiresAt - Date.now());
|
||||
return raw(
|
||||
translate('frontPageExpireInfo', {
|
||||
downloadCount: translate('downloadCount', {
|
||||
num: archive.dlimit - archive.dtotal
|
||||
}),
|
||||
timespan: translate(l10n.id, l10n)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function fileInfo(file, action) {
|
||||
return html`
|
||||
<article class="flex flex-row items-start p-3">
|
||||
<img class="" src="${assets.get('blue_file.svg')}"/>
|
||||
<p class="ml-3 w-full">
|
||||
<h1 class="text-base font-semibold">${file.name}</h1>
|
||||
<div class="text-sm font-light">${bytes(file.size)}</div>
|
||||
<div class="hidden">${file.type}</div>
|
||||
</p>
|
||||
${action}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function archiveDetails(translate, archive) {
|
||||
if (archive.manifest.files.length > 1) {
|
||||
return html`
|
||||
<details class="w-full">
|
||||
<summary>${translate('fileCount', {
|
||||
num: archive.manifest.files.length
|
||||
})}</summary>
|
||||
${list(archive.manifest.files.map(f => fileInfo(f)), 'list-reset')}
|
||||
</details>`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function(state, emit, archive) {
|
||||
return html`
|
||||
<article
|
||||
id="${archive.id}"
|
||||
class="flex flex-col items-start border border-grey-light bg-white p-3">
|
||||
<p class="w-full">
|
||||
<img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
|
||||
<input
|
||||
type="image"
|
||||
class="float-right self-center text-white"
|
||||
alt="Delete"
|
||||
src="${assets.get('close-16.svg')}"
|
||||
onclick=${del}/>
|
||||
<h1 class="text-base font-semibold">${archive.name}</h1>
|
||||
<div class="text-sm font-light">${bytes(archive.size)}</div>
|
||||
</p>
|
||||
<div class="text-xs text-grey-dark w-full mt-2 mb-2">
|
||||
${expiryInfo(state.translate, archive)}
|
||||
</div>
|
||||
${archiveDetails(state.translate, archive)}
|
||||
<hr class="w-full border-t">
|
||||
<button
|
||||
class="text-blue self-end"
|
||||
onclick=${copy}>
|
||||
<img src="${assets.get('copy-16.svg')}" class="mr-1"/>
|
||||
${state.translate('copyUrlHover')}
|
||||
</button>
|
||||
</article>`;
|
||||
|
||||
function copy(event) {
|
||||
event.stopPropagation();
|
||||
copyToClipboard(archive.url);
|
||||
}
|
||||
|
||||
function del(event) {
|
||||
event.stopPropagation();
|
||||
emit('delete', { file: archive, location: 'success-screen' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.wip = function(state, emit) {
|
||||
return html`
|
||||
<article class="relative flex flex-col bg-white border border-grey-light p-2 z-20">
|
||||
${list(state.archive.files.map(f => fileInfo(f, remove(f))), 'list-reset')}
|
||||
<div class="border border-dashed border-blue-light mb-2">
|
||||
<input
|
||||
id="file-upload"
|
||||
class="hidden"
|
||||
type="file"
|
||||
multiple
|
||||
onchange=${add} />
|
||||
<label
|
||||
for="file-upload"
|
||||
class="flex flex-row items-center w-full h-full text-blue p-2"
|
||||
title="${state.translate('addFilesButton')}">
|
||||
<img src="${assets.get('addfile.svg')}" class="w-6 h-6 mr-2"/>
|
||||
${state.translate('addFilesButton')}
|
||||
</label>
|
||||
</div>
|
||||
${expiryOptions(state, emit)}
|
||||
<button
|
||||
class="border rounded bg-blue text-white mt-2 py-2 px-6"
|
||||
title="${state.translate('uploadFilesButton')}"
|
||||
onclick=${upload}>
|
||||
${state.translate('uploadFilesButton')}
|
||||
</button>
|
||||
</article>`;
|
||||
|
||||
function upload(event) {
|
||||
event.preventDefault();
|
||||
event.target.disabled = true;
|
||||
if (!state.uploading) {
|
||||
emit('upload', {
|
||||
type: 'click',
|
||||
dlimit: state.downloadCount || 1,
|
||||
password: state.password
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function add(event) {
|
||||
event.preventDefault();
|
||||
const newFiles = Array.from(event.target.files);
|
||||
|
||||
emit('addFiles', { files: newFiles });
|
||||
}
|
||||
|
||||
function remove(file) {
|
||||
return html`
|
||||
<input
|
||||
type="image"
|
||||
class="self-center text-white"
|
||||
alt="Delete"
|
||||
src="${assets.get('close-16.svg')}"
|
||||
onclick=${del}/>`;
|
||||
function del(event) {
|
||||
event.stopPropagation();
|
||||
emit('removeUpload', file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.uploading = function(state, emit) {
|
||||
const progress = state.transfer.progressRatio;
|
||||
const progressPercent = percent(progress);
|
||||
const archive = state.archive;
|
||||
return html`
|
||||
<article
|
||||
id="${archive.id}"
|
||||
class="relative z-20 flex flex-col items-start border border-grey-light bg-white p-3">
|
||||
<p class="w-full">
|
||||
<img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
|
||||
<h1 class="text-base font-semibold">${archive.name}</h1>
|
||||
<div class="text-sm font-light">${bytes(archive.size)}</div>
|
||||
</p>
|
||||
<div class="text-xs text-grey-dark w-full mt-2 mb-2">
|
||||
${expiryInfo(state.translate, {
|
||||
dlimit: state.downloadCount || 1,
|
||||
dtotal: 0,
|
||||
expiresAt: Date.now() + 500 + state.timeLimit * 1000
|
||||
})}
|
||||
</div>
|
||||
<div class="text-blue text-sm font-medium">${progressPercent}</div>
|
||||
<progress class="mb-1" value="${progress}">${progressPercent}</progress>
|
||||
<button
|
||||
class="text-blue self-end"
|
||||
onclick=${cancel}>
|
||||
${state.translate('uploadingPageCancel')}
|
||||
</button>
|
||||
</article>`;
|
||||
|
||||
function cancel(event) {
|
||||
event.stopPropagation();
|
||||
event.target.disabled = true;
|
||||
emit('cancel');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.empty = function(state, emit) {
|
||||
return html`
|
||||
<article class="flex flex-col items-center justify-center border border-dashed border-blue-light p-8">
|
||||
<div class="p-1">${state.translate('uploadDropDragMessage')}</div>
|
||||
<input
|
||||
id="file-upload"
|
||||
class="hidden"
|
||||
type="file"
|
||||
multiple
|
||||
onchange=${add} />
|
||||
<label
|
||||
for="file-upload"
|
||||
class="border rounded bg-blue text-white py-2 px-6"
|
||||
title="${state.translate('addFilesButton')}">
|
||||
${state.translate('addFilesButton')}
|
||||
</label>
|
||||
</article>`;
|
||||
|
||||
function add(event) {
|
||||
event.preventDefault();
|
||||
const newFiles = Array.from(event.target.files);
|
||||
|
||||
emit('addFiles', { files: newFiles });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.preview = function(state, emit) {
|
||||
const archive = state.fileInfo;
|
||||
return html`
|
||||
<article class="relative flex flex-col bg-white border border-grey-light p-2 z-20">
|
||||
<p class="w-full mb-4">
|
||||
<img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
|
||||
<h1 class="text-base font-semibold">${archive.name}</h1>
|
||||
<div class="text-sm font-light">${bytes(archive.size)}</div>
|
||||
</p>
|
||||
${archiveDetails(state.translate, archive)}
|
||||
<hr class="w-full border-t">
|
||||
<button
|
||||
class="border rounded bg-blue text-white mt-2 py-2 px-6"
|
||||
title="${state.translate('downloadButtonLabel')}"
|
||||
onclick=${download}>
|
||||
${state.translate('downloadButtonLabel')}
|
||||
</button>
|
||||
</article>`;
|
||||
|
||||
function download(event) {
|
||||
event.preventDefault();
|
||||
event.target.disabled = true;
|
||||
emit('download', archive);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.downloading = function(state, emit) {
|
||||
const archive = state.fileInfo;
|
||||
const progress = state.transfer.progressRatio;
|
||||
const progressPercent = percent(progress);
|
||||
return html`
|
||||
<article class="relative flex flex-col bg-white border border-grey-light p-2 z-20">
|
||||
<p class="w-full mb-4">
|
||||
<img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
|
||||
<h1 class="text-base font-semibold">${archive.name}</h1>
|
||||
<div class="text-sm font-light">${bytes(archive.size)}</div>
|
||||
</p>
|
||||
<div class="text-blue text-sm font-medium">${progressPercent}</div>
|
||||
<progress class="" value="${progress}">${progressPercent}</progress>
|
||||
<button
|
||||
class="border rounded bg-grey-dark text-white mt-2 py-2 px-6"
|
||||
title="${state.translate('downloadCancel')}"
|
||||
onclick=${cancel}>
|
||||
${state.translate('downloadCancel')}
|
||||
</button>
|
||||
</article>`;
|
||||
|
||||
function cancel(event) {
|
||||
event.preventDefault();
|
||||
event.target.disabled = true;
|
||||
emit('download', archive);
|
||||
}
|
||||
};
|
5
app/ui/blank.js
Normal file
5
app/ui/blank.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const html = require('choo/html');
|
||||
|
||||
module.exports = function() {
|
||||
return html`<main class="main"></main>`;
|
||||
};
|
103
app/ui/download.js
Normal file
103
app/ui/download.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
/* global downloadMetadata */
|
||||
const html = require('choo/html');
|
||||
const archiveTile = require('./archiveTile');
|
||||
|
||||
function password(state, emit) {
|
||||
const fileInfo = state.fileInfo;
|
||||
const invalid = fileInfo.password === null;
|
||||
|
||||
const visible = invalid ? 'visible' : 'invisible';
|
||||
const invalidBtn = invalid ? '' : '';
|
||||
|
||||
const div = html`
|
||||
<div class="">
|
||||
<label
|
||||
class="${visible}"
|
||||
for="password-input">
|
||||
${state.translate('passwordTryAgain')}
|
||||
</label>
|
||||
|
||||
<form class="" onsubmit=${checkPassword} data-no-csrf>
|
||||
<input id="password-input"
|
||||
class=""
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||
oninput=${inputChanged}
|
||||
type="password" />
|
||||
|
||||
<input type="submit"
|
||||
id="password-btn"
|
||||
class="${invalidBtn}"
|
||||
value="${state.translate('unlockInputLabel')}"/>
|
||||
</form>
|
||||
</div>`;
|
||||
|
||||
if (!(div instanceof String)) {
|
||||
setTimeout(() => document.getElementById('password-input').focus());
|
||||
}
|
||||
|
||||
function inputChanged() {
|
||||
//TODO
|
||||
const input = document.querySelector('.passwordForm__error');
|
||||
input.classList.remove('visible');
|
||||
const btn = document.getElementById('password-btn');
|
||||
btn.classList.remove('unlockBtn--error');
|
||||
}
|
||||
|
||||
function checkPassword(event) {
|
||||
event.preventDefault();
|
||||
const password = document.getElementById('password-input').value;
|
||||
if (password.length > 0) {
|
||||
document.getElementById('password-btn').disabled = true;
|
||||
state.fileInfo.url = window.location.href;
|
||||
state.fileInfo.password = password;
|
||||
emit('getMetadata');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
function createFileInfo(state) {
|
||||
return {
|
||||
id: state.params.id,
|
||||
secretKey: state.params.key,
|
||||
nonce: downloadMetadata.nonce,
|
||||
requiresPassword: downloadMetadata.pwd
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
let content = '';
|
||||
if (!state.fileInfo) {
|
||||
state.fileInfo = createFileInfo(state);
|
||||
}
|
||||
|
||||
if (!state.transfer && !state.fileInfo.requiresPassword) {
|
||||
emit('getMetadata');
|
||||
}
|
||||
|
||||
if (state.transfer) {
|
||||
switch (state.transfer.state) {
|
||||
case 'downloading':
|
||||
case 'decrypting':
|
||||
content = archiveTile.downloading(state, emit);
|
||||
break;
|
||||
case 'complete':
|
||||
content = ''; //TODO
|
||||
break;
|
||||
default:
|
||||
content = archiveTile.preview(state, emit);
|
||||
}
|
||||
} else if (state.fileInfo.requiresPassword && !state.fileInfo.password) {
|
||||
content = password(state, emit);
|
||||
}
|
||||
return html`
|
||||
<main class="main">
|
||||
<section class="relative h-full w-full my-4 px-6">
|
||||
${content}
|
||||
</section>
|
||||
</main>`;
|
||||
};
|
18
app/ui/error.js
Normal file
18
app/ui/error.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
return html`
|
||||
<main class="main">
|
||||
<div class="flex flex-col items-center text-center bg-white m-6 p-4 border border-grey-light md:border-none md:px-12">
|
||||
<h1 class="">${state.translate('errorPageHeader')}</h1>
|
||||
<img class="my-8" src="${assets.get('illustration_error.svg')}"/>
|
||||
<p class="">
|
||||
${state.translate('uploadPageExplainer')}
|
||||
</p>
|
||||
<a class="text-blue mt-4" href="/">
|
||||
${state.translate('sendYourFilesLink')}
|
||||
</a>
|
||||
</div>
|
||||
</main>`;
|
||||
};
|
73
app/ui/expiryOptions.js
Normal file
73
app/ui/expiryOptions.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
/* globals DEFAULTS */
|
||||
const html = require('choo/html');
|
||||
const raw = require('choo/html/raw');
|
||||
const { secondsToL10nId } = require('../utils');
|
||||
const selectbox = require('./selectbox');
|
||||
const signupDialog = require('./signupDialog');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const el = html`
|
||||
<div class="">
|
||||
${raw(
|
||||
state.translate('frontPageExpireInfo', {
|
||||
downloadCount: '<select id="dlCount"></select>',
|
||||
timespan: '<select id="timespan"></select>'
|
||||
})
|
||||
)}
|
||||
</div>`;
|
||||
if (el.__encoded) {
|
||||
// we're rendering on the server
|
||||
return el;
|
||||
}
|
||||
|
||||
const counts = DEFAULTS.DOWNLOAD_COUNTS.filter(
|
||||
i => state.capabilities.account || i <= state.user.maxDownloads
|
||||
);
|
||||
|
||||
const dlCountSelect = el.querySelector('#dlCount');
|
||||
el.replaceChild(
|
||||
selectbox(
|
||||
state.downloadCount || 1,
|
||||
counts,
|
||||
num => state.translate('downloadCount', { num }),
|
||||
value => {
|
||||
const max = state.user.maxDownloads;
|
||||
if (value > max) {
|
||||
state.modal = signupDialog();
|
||||
value = max;
|
||||
}
|
||||
state.downloadCount = value;
|
||||
emit('render');
|
||||
}
|
||||
),
|
||||
dlCountSelect
|
||||
);
|
||||
|
||||
const expires = DEFAULTS.EXPIRE_TIMES_SECONDS.filter(
|
||||
i => state.capabilities.account || i <= state.user.maxExpireSeconds
|
||||
);
|
||||
|
||||
const timeSelect = el.querySelector('#timespan');
|
||||
el.replaceChild(
|
||||
selectbox(
|
||||
state.timeLimit || 86400,
|
||||
expires,
|
||||
num => {
|
||||
const l10n = secondsToL10nId(num);
|
||||
return state.translate(l10n.id, l10n);
|
||||
},
|
||||
value => {
|
||||
const max = state.user.maxExpireSeconds;
|
||||
if (value > max) {
|
||||
state.modal = signupDialog();
|
||||
value = max;
|
||||
}
|
||||
state.timeLimit = value;
|
||||
emit('render');
|
||||
}
|
||||
),
|
||||
timeSelect
|
||||
);
|
||||
|
||||
return el;
|
||||
};
|
52
app/ui/footer.js
Normal file
52
app/ui/footer.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
const html = require('choo/html');
|
||||
const version = require('../../package.json').version;
|
||||
const { browserName } = require('../utils');
|
||||
|
||||
module.exports = function(state) {
|
||||
const browser = browserName();
|
||||
const feedbackUrl = `https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}&browser=${browser}`;
|
||||
const footer = html`<footer class="flex-none m-2 font-medium text-xs text-grey-dark">
|
||||
<ul class="list-reset flex flex-col md:flex-row items-start md:items-center md:justify-end">
|
||||
<li class="m-2"><a
|
||||
href="https://www.mozilla.org/about/legal">
|
||||
${state.translate('footerLinkLegal')}
|
||||
</a></li>
|
||||
<li class="m-2"><a
|
||||
href="https://testpilot.firefox.com/about">
|
||||
${state.translate('footerLinkAbout')}
|
||||
</a></li>
|
||||
<li class="m-2"><a
|
||||
href="/legal">
|
||||
${state.translate('footerLinkTerms')}
|
||||
</a></li>
|
||||
<li class="m-2"><a
|
||||
href="https://www.mozilla.org/privacy/websites/#cookies">
|
||||
${state.translate('footerLinkCookies')}
|
||||
</a></li>
|
||||
<li class="m-2"><a
|
||||
href="https://www.mozilla.org/about/legal/report-infringement/">
|
||||
${state.translate('reportIPInfringement')}
|
||||
</a></li>
|
||||
<li class="m-2"><a
|
||||
href="https://github.com/mozilla/send">GitHub
|
||||
</a></li>
|
||||
<li class="m-2"><a
|
||||
href="https://twitter.com/FxTestPilot">Twitter
|
||||
</a></li>
|
||||
<li class="m-2"><a href="${feedbackUrl}"
|
||||
rel="noreferrer noopener"
|
||||
class="feedback-link"
|
||||
alt="Feedback"
|
||||
target="_blank">
|
||||
${state.translate('siteFeedback')}
|
||||
</a></li>
|
||||
</ul>
|
||||
</footer>`;
|
||||
// HACK
|
||||
// We only want to render this once because we
|
||||
// toggle the targets of the links with utils/openLinksInNewTab
|
||||
footer.isSameNode = function(target) {
|
||||
return target && target.nodeName && target.nodeName === 'FOOTER';
|
||||
};
|
||||
return footer;
|
||||
};
|
19
app/ui/fxPromo.js
Normal file
19
app/ui/fxPromo.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function() {
|
||||
return html`
|
||||
<div class="flex-none flex flex-row items-center content-center justify-center text-sm bg-grey-light text-grey-darkest h-12 px-4">
|
||||
<div class="flex items-center mx-auto">
|
||||
<img
|
||||
src="${assets.get('firefox_logo-only.svg')}"
|
||||
class="w-6"
|
||||
alt="Firefox"/>
|
||||
<span class="ml-3">Send is brought to you by the all-new Firefox.
|
||||
<a
|
||||
class="text-blue"
|
||||
href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com">Download Firefox now ≫</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
21
app/ui/header.js
Normal file
21
app/ui/header.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
const html = require('choo/html');
|
||||
const account = require('./account');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const header = html`
|
||||
<header class="flex-none flex flex-row items-center bg-white w-full px-4 h-12 shadow-md justify-between">
|
||||
<a
|
||||
class="header-logo"
|
||||
href="/">
|
||||
<h1 class="text-black font-normal">Firefox <b>Send</b></h1>
|
||||
</a>
|
||||
${account(state, emit)}
|
||||
</header>`;
|
||||
// HACK
|
||||
// We only want to render this once because we
|
||||
// toggle the targets of the links with utils/openLinksInNewTab
|
||||
// header.isSameNode = function(target) {
|
||||
// return target && target.nodeName && target.nodeName === 'HEADER';
|
||||
// };
|
||||
return header;
|
||||
};
|
33
app/ui/legal.js
Normal file
33
app/ui/legal.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
const html = require('choo/html');
|
||||
const raw = require('choo/html/raw');
|
||||
|
||||
module.exports = function(state) {
|
||||
return html`
|
||||
<main class="main">
|
||||
<div class="flex flex-col items-center bg-white m-6 p-4 border border-grey-light md:border-none md:px-12">
|
||||
<h1 class="">${state.translate('legalHeader')}</h1>
|
||||
${raw(
|
||||
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
||||
'https://testpilot.firefox.com/terms',
|
||||
'https://testpilot.firefox.com/privacy',
|
||||
'https://testpilot.firefox.com/experiments/send'
|
||||
])
|
||||
)}
|
||||
${raw(
|
||||
replaceLinks(state.translate('legalNoticeMozilla'), [
|
||||
'https://www.mozilla.org/privacy/websites/',
|
||||
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
||||
])
|
||||
)}
|
||||
</div>
|
||||
</main>`;
|
||||
};
|
||||
|
||||
function replaceLinks(str, urls) {
|
||||
let i = 0;
|
||||
const s = str.replace(
|
||||
/<a>([^<]+)<\/a>/g,
|
||||
(m, v) => `<a class="text-blue" href="${urls[i++]}">${v}</a>`
|
||||
);
|
||||
return `<p class="m-4">${s}</p>`;
|
||||
}
|
15
app/ui/modal.js
Normal file
15
app/ui/modal.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
return html`
|
||||
<div class="fixed pin flex items-center justify-center overflow-hidden z-40 bg-shades" onclick=${close}>
|
||||
<div class="rounded max-w-md bg-white" onclick=${e => e.stopPropagation()}>
|
||||
${state.modal(state, emit, close)}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
function close(event) {
|
||||
state.modal = null;
|
||||
emit('render');
|
||||
}
|
||||
};
|
18
app/ui/notFound.js
Normal file
18
app/ui/notFound.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
return html`
|
||||
<main class="main">
|
||||
<div class="flex flex-col items-center text-center bg-white m-6 p-4 border border-grey-light md:border-none md:px-12">
|
||||
<h1 class="text-pink-dark">${state.translate('expiredPageHeader')}</h1>
|
||||
<img src="${assets.get('illustration_expired.svg')}" id="expired-img">
|
||||
<p class="">
|
||||
${state.translate('uploadPageExplainer')}
|
||||
</p>
|
||||
<a class="text-blue mt-4" href="/">
|
||||
${state.translate('sendYourFilesLink')}
|
||||
</a>
|
||||
</div>
|
||||
</main>`;
|
||||
};
|
25
app/ui/selectbox.js
Normal file
25
app/ui/selectbox.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(selected, options, translate, changed) {
|
||||
let x = selected;
|
||||
|
||||
return html`
|
||||
<select class="appearance-none cursor-pointer border rounded-sm bg-blue-lightest p-1" onchange=${choose}>
|
||||
${options.map(
|
||||
i =>
|
||||
html`<option value="${i}" ${
|
||||
i === selected ? 'selected' : ''
|
||||
}>${translate(i)}</option>`
|
||||
)}
|
||||
</select>`;
|
||||
|
||||
function choose(event) {
|
||||
const target = event.target;
|
||||
const value = +target.value;
|
||||
|
||||
if (x !== value) {
|
||||
x = value;
|
||||
changed(value);
|
||||
}
|
||||
}
|
||||
};
|
56
app/ui/signupDialog.js
Normal file
56
app/ui/signupDialog.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
/* global LIMITS */
|
||||
const html = require('choo/html');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function() {
|
||||
return function(state, emit, close) {
|
||||
return html`
|
||||
<div class="flex flex-col p-4">
|
||||
<p class="p-8">
|
||||
${state.translate('accountBenefitTitle')}
|
||||
<ul class="my-2">
|
||||
<li>${state.translate('accountBenefitLargeFiles', {
|
||||
size: bytes(LIMITS.MAX_FILE_SIZE)
|
||||
})}</li>
|
||||
<li>${state.translate('accountBenefitExpiry')}</li>
|
||||
<li>${state.translate('accountBenefitSync')}</li>
|
||||
</ul>
|
||||
</p>
|
||||
<form
|
||||
onsubmit=${submitEmail}
|
||||
data-no-csrf>
|
||||
<input
|
||||
id="email-input"
|
||||
type="text"
|
||||
class="border rounded w-full px-2 text-lg text-grey-darker leading-loose"
|
||||
placeholder=${state.translate('emailEntryPlaceholder')}/>
|
||||
<input
|
||||
class="hidden"
|
||||
id="email-submit"
|
||||
type="submit"/>
|
||||
</form>
|
||||
<label class="border rounded bg-blue text-white leading-loose text-center my-2" for="email-submit">
|
||||
${state.translate('signInMenuOption')}
|
||||
</label>
|
||||
<button
|
||||
class=""
|
||||
title="${state.translate('deletePopupCancel')}"
|
||||
onclick=${close}>${state.translate('deletePopupCancel')}
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
function submitEmail(event) {
|
||||
event.preventDefault();
|
||||
const el = document.getElementById('email-input');
|
||||
const email = el.value;
|
||||
if (email) {
|
||||
// just check if it's the right shape
|
||||
const a = email.split('@');
|
||||
if (a.length === 2 && a.every(s => s.length > 0)) {
|
||||
return emit('login', email);
|
||||
}
|
||||
}
|
||||
el.value = '';
|
||||
}
|
||||
};
|
||||
};
|
65
app/ui/unsupported.js
Normal file
65
app/ui/unsupported.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
let strings = {};
|
||||
let why = '';
|
||||
let url = '';
|
||||
let buttonAction = '';
|
||||
|
||||
if (state.params.reason !== 'outdated') {
|
||||
strings = unsupportedStrings(state);
|
||||
why = html`
|
||||
<a
|
||||
class="text-blue" href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">
|
||||
${state.translate('notSupportedLink')}
|
||||
</a>`;
|
||||
url =
|
||||
'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com';
|
||||
buttonAction = html`
|
||||
<p class="ml-3 font-bold">
|
||||
Firefox<br><span class="font-light">${strings.button}</span>
|
||||
</p>`;
|
||||
} else {
|
||||
strings = outdatedStrings(state);
|
||||
url = 'https://support.mozilla.org/kb/update-firefox-latest-version';
|
||||
buttonAction = html`
|
||||
<p class="ml-3">
|
||||
${strings.button}
|
||||
</p>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<main class="main">
|
||||
<div class="flex flex-col items-center bg-white m-6 p-4 border border-grey-light md:border-none md:px-12">
|
||||
<h1 class="text-center">${strings.header}</h1>
|
||||
<p class="my-16">
|
||||
${strings.description}
|
||||
</p>
|
||||
${why}
|
||||
<a href="${url}" class="border border-green-light rounded bg-green flex items-center justify-center text-2xl text-white my-16 p-2">
|
||||
<img
|
||||
src="${assets.get('firefox_logo-only.svg')}"
|
||||
class="w-16"
|
||||
alt="Firefox"/>
|
||||
${buttonAction}
|
||||
</a>
|
||||
</div>
|
||||
</main>`;
|
||||
};
|
||||
|
||||
function outdatedStrings(state) {
|
||||
return {
|
||||
header: state.translate('notSupportedHeader'),
|
||||
description: state.translate('notSupportedOutdatedDetail'),
|
||||
button: state.translate('updateFirefox')
|
||||
};
|
||||
}
|
||||
|
||||
function unsupportedStrings(state) {
|
||||
return {
|
||||
header: state.translate('notSupportedHeader'),
|
||||
description: state.translate('notSupportedDetail'),
|
||||
button: state.translate('downloadFirefoxButtonSub')
|
||||
};
|
||||
}
|
6
app/ui/welcome.js
Normal file
6
app/ui/welcome.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
const html = require('choo/html');
|
||||
const archiveList = require('./archiveList');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
return html`<main class="main">${archiveList(state, emit)}</main>`;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue