add error handling, toasts, and loading spinner
This commit is contained in:
parent
9d9e6716d5
commit
ee1251467a
41
package-lock.json
generated
41
package-lock.json
generated
|
@ -1321,6 +1321,11 @@
|
|||
"stream-shift": "1.0.0"
|
||||
}
|
||||
},
|
||||
"eases-jsnext": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/eases-jsnext/-/eases-jsnext-1.0.10.tgz",
|
||||
"integrity": "sha512-1bO1+FIuqtOZpcyoIJuTnw8PU9X+RHHA248mZ1m+CPiiKFGCiNLWecITlhO4DXe7whZmBoJyfKwUoMW0KK5mNw=="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
|
||||
|
@ -6779,6 +6784,11 @@
|
|||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-1.51.0.tgz",
|
||||
"integrity": "sha512-lqa9eAZ4ZQLMWsoyynAogUtib7HhHnrJJaS93uRgZU5cfXquBVR+FkKVK41LdlwffmOfOjbUin6pT8e/LZUwjA=="
|
||||
},
|
||||
"svelte-extras": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-extras/-/svelte-extras-1.6.0.tgz",
|
||||
"integrity": "sha512-0yzXHJdnaX3+KiLrDu9Hl6V7+idfKrUkYqhpbdnxCEJos2FSxtpos6cjAt+A2vVrdcNjFqtXYs6xS+rFWeg1yA=="
|
||||
},
|
||||
"svelte-loader": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-loader/-/svelte-loader-2.3.3.tgz",
|
||||
|
@ -6788,6 +6798,37 @@
|
|||
"tmp": "0.0.31"
|
||||
}
|
||||
},
|
||||
"svelte-transitions": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-transitions/-/svelte-transitions-1.1.1.tgz",
|
||||
"integrity": "sha1-AaLpVPOnTXH8dtOn3Sn90VVRCBA=",
|
||||
"requires": {
|
||||
"svelte-transitions-fade": "1.0.0",
|
||||
"svelte-transitions-fly": "1.0.2",
|
||||
"svelte-transitions-slide": "1.0.0"
|
||||
}
|
||||
},
|
||||
"svelte-transitions-fade": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-transitions-fade/-/svelte-transitions-fade-1.0.0.tgz",
|
||||
"integrity": "sha1-2+FSDfH1tTcL1hr+/Gfy0v/2MwM="
|
||||
},
|
||||
"svelte-transitions-fly": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-transitions-fly/-/svelte-transitions-fly-1.0.2.tgz",
|
||||
"integrity": "sha1-CP02aUG0uSmpL5Y1uQJDpYbCuNc=",
|
||||
"requires": {
|
||||
"eases-jsnext": "1.0.10"
|
||||
}
|
||||
},
|
||||
"svelte-transitions-slide": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-transitions-slide/-/svelte-transitions-slide-1.0.0.tgz",
|
||||
"integrity": "sha1-FQ3Zy455+p4vJQ4ZjH1plgvyGZQ=",
|
||||
"requires": {
|
||||
"eases-jsnext": "1.0.10"
|
||||
}
|
||||
},
|
||||
"svgo": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz",
|
||||
|
|
|
@ -34,7 +34,9 @@
|
|||
"serve-static": "^1.13.1",
|
||||
"style-loader": "^0.19.1",
|
||||
"svelte": "^1.50.0",
|
||||
"svelte-extras": "^1.6.0",
|
||||
"svelte-loader": "^2.3.3",
|
||||
"svelte-transitions": "^1.1.1",
|
||||
"uglifyjs-webpack-plugin": "^1.1.5",
|
||||
"url-search-params": "^0.10.0",
|
||||
"webpack": "^3.10.0",
|
||||
|
|
61
routes/_components/LoadingMask.html
Normal file
61
routes/_components/LoadingMask.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
<div class="loading-container">
|
||||
{{#if show}}
|
||||
<div transition:fade class="loading-mask">
|
||||
<svg>
|
||||
<use xlink:href="#fa-spinner" />
|
||||
</svg>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<style>
|
||||
.loading-container {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
.loading-mask {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--mask-bg);
|
||||
opacity: 0.6;
|
||||
pointer-events: auto;
|
||||
}
|
||||
svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
fill: var(--mask-svg-fill);
|
||||
animation: spin 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { fade } from 'svelte-transitions'
|
||||
|
||||
export default {
|
||||
transitions: { fade }
|
||||
}
|
||||
</script>
|
96
routes/_components/Toast.html
Normal file
96
routes/_components/Toast.html
Normal file
|
@ -0,0 +1,96 @@
|
|||
<div class="toast-modal {{shown ? 'shown' : ''}}">
|
||||
<div class="toast-container">
|
||||
{{text}}
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.toast-modal {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 333ms linear;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
max-width: 600px;
|
||||
max-height: 20vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 2px solid var(--toast-border);
|
||||
background: var(--toast-bg);
|
||||
border-radius: 5px;
|
||||
margin: 0 40px;
|
||||
padding: 20px;
|
||||
font-size: 1.3em;
|
||||
color: var(--toast-text);
|
||||
}
|
||||
|
||||
.toast-modal.shown {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.toast-container {
|
||||
max-width: 80vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { splice, push } from 'svelte-extras'
|
||||
|
||||
const TIME_TO_SHOW_TOAST = 5000
|
||||
const DELAY_BETWEEN_TOASTS = 1000
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
this._queue = Promise.resolve()
|
||||
this.observe('messages', (messages) => {
|
||||
console.log('messages', messages)
|
||||
if (messages.length) {
|
||||
this.onNewToast(messages[0])
|
||||
this.splice('messages', 0, 1)
|
||||
}
|
||||
})
|
||||
},
|
||||
ondestroy () {
|
||||
},
|
||||
data: () => ({
|
||||
text: '',
|
||||
shown: false,
|
||||
messages: []
|
||||
}),
|
||||
methods: {
|
||||
push,
|
||||
splice,
|
||||
say(text) {
|
||||
this.push('messages', text)
|
||||
},
|
||||
onNewToast(text) {
|
||||
this._queue = this._queue.then(() => {
|
||||
this.set({
|
||||
'text': text,
|
||||
shown: true
|
||||
})
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, TIME_TO_SHOW_TOAST)
|
||||
})
|
||||
}).then(() => {
|
||||
this.set({
|
||||
shown: false
|
||||
})
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, DELAY_BETWEEN_TOASTS)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
18
routes/_utils/toast.js
Normal file
18
routes/_utils/toast.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import Toast from '../_components/Toast.html'
|
||||
|
||||
let toast
|
||||
|
||||
if (process.browser) {
|
||||
toast = new Toast({
|
||||
target: document.querySelector('#toast')
|
||||
})
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
window.toast = toast // for debugging
|
||||
}
|
||||
} else {
|
||||
toast = {
|
||||
say: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
export { toast }
|
|
@ -4,8 +4,15 @@
|
|||
<style>
|
||||
ul {
|
||||
list-style: none;
|
||||
width: 80%;
|
||||
width: 100%;
|
||||
border: 1px solid var(--settings-list-item-border);
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
ul {
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -13,16 +13,15 @@
|
|||
|
||||
<style>
|
||||
ul {
|
||||
margin: 0;
|
||||
margin: 5px 10px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 10px 0;
|
||||
margin: 5px 0;
|
||||
font-size: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
li::after {
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
justify-content: right;
|
||||
}
|
||||
.instance-actions button {
|
||||
margin: 0 20px;
|
||||
margin: 0 5px;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
<SettingsLayout page='settings/instances/add' label="Add an Instance">
|
||||
<h1>Add an Instance</h1>
|
||||
|
||||
<LoadingMask show="{{loading}}"/>
|
||||
|
||||
{{#if $isUserLoggedIn}}
|
||||
<p>Connect to an instance to log in.</p>
|
||||
{{else}}
|
||||
|
@ -15,7 +17,7 @@
|
|||
<form class="add-new-instance" on:submit='onSubmit(event)'>
|
||||
<label for="instanceInput">Instance name:</label>
|
||||
<input type="text" id="instanceInput" bind:value='$instanceNameInSearch' placeholder=''>
|
||||
<button class="primary" type="submit" id="submitButton">Add instance</button>
|
||||
<button class="primary" type="submit" id="submitButton" disabled="{{!$instanceNameInSearch}}">Add instance</button>
|
||||
</form>
|
||||
|
||||
{{#if !$isUserLoggedIn}}
|
||||
|
@ -53,6 +55,9 @@
|
|||
import { store } from '../../_utils/store'
|
||||
import { goto } from 'sapper/runtime.js'
|
||||
import { switchToTheme } from '../../_utils/themeEngine'
|
||||
import { toast } from '../../_utils/toast'
|
||||
import LoadingMask from '../../_components/LoadingMask'
|
||||
import { fade } from 'svelte-transitions'
|
||||
|
||||
const REDIRECT_URI = (typeof location !== 'undefined' ?
|
||||
location.origin : 'https://pinafore.social') + '/settings/instances/add'
|
||||
|
@ -70,17 +75,37 @@
|
|||
},
|
||||
components: {
|
||||
Layout,
|
||||
SettingsLayout
|
||||
SettingsLayout,
|
||||
LoadingMask
|
||||
},
|
||||
store: () => store,
|
||||
transitions: {
|
||||
fade
|
||||
},
|
||||
methods: {
|
||||
onSubmit: async function(event) {
|
||||
event.preventDefault()
|
||||
this.set({loading: true})
|
||||
try {
|
||||
await this.redirectToOauth()
|
||||
} catch (err) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.error(err)
|
||||
}
|
||||
toast.say(`Error: ${err.message || err.name}. Is this a valid Mastodon instance?`)
|
||||
} finally {
|
||||
this.set({loading: false})
|
||||
}
|
||||
},
|
||||
redirectToOauth: async function() {
|
||||
let instanceName = this.store.get('instanceNameInSearch')
|
||||
let loggedInInstances = this.store.get('loggedInInstances')
|
||||
instanceName = instanceName.replace(/^https?:\/\//, '').replace('/$', '')
|
||||
// TODO: show toast error if you're already logged into this instance
|
||||
if (Object.keys(loggedInInstances).includes(instanceName)) {
|
||||
toast.say(`You've already logged in to ${instanceName}`)
|
||||
return
|
||||
}
|
||||
let instanceData = await registerApplication(instanceName, REDIRECT_URI)
|
||||
// TODO: handle error
|
||||
this.store.set({
|
||||
currentRegisteredInstanceName: instanceName,
|
||||
currentRegisteredInstance: instanceData
|
||||
|
@ -94,6 +119,16 @@
|
|||
document.location.href = oauthUrl
|
||||
},
|
||||
onReceivedOauthCode: async function(code) {
|
||||
try {
|
||||
this.set({loading: true})
|
||||
await this.registerNewInstance(code)
|
||||
} catch (err) {
|
||||
toast.say(`Error: ${err.message || err.name}. Failed to connect to instance.`)
|
||||
} finally {
|
||||
this.set({loading: false})
|
||||
}
|
||||
},
|
||||
registerNewInstance: async function (code) {
|
||||
let currentRegisteredInstanceName = this.store.get('currentRegisteredInstanceName')
|
||||
let currentRegisteredInstance = this.store.get('currentRegisteredInstance')
|
||||
let instanceData = await getAccessTokenFromAuthCode(
|
||||
|
@ -103,7 +138,6 @@
|
|||
code,
|
||||
REDIRECT_URI
|
||||
)
|
||||
// TODO: handle error
|
||||
let loggedInInstances = this.store.get('loggedInInstances')
|
||||
let loggedInInstancesInOrder = this.store.get('loggedInInstancesInOrder')
|
||||
let instanceThemes = this.store.get('instanceThemes')
|
||||
|
@ -124,7 +158,7 @@
|
|||
this.store.save()
|
||||
switchToTheme('default')
|
||||
goto('/')
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -24,7 +24,6 @@ main {
|
|||
background: var(--main-bg);
|
||||
border: 1px solid var(--main-border);
|
||||
border-radius: 1px;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
|
|
|
@ -47,4 +47,11 @@
|
|||
--settings-list-item-border: $border-color;
|
||||
--settings-list-item-bg-active: darken($main-bg-color, 10%);
|
||||
--settings-list-item-bg-hover: darken($main-bg-color, 2%);
|
||||
|
||||
--toast-bg: $toast-bg;
|
||||
--toast-border: $toast-border;
|
||||
--toast-text: $secondary-text-color;
|
||||
|
||||
--mask-bg: $toast-bg;
|
||||
--mask-svg-fill: $secondary-text-color;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ $main-text-color: #333;
|
|||
$border-color: #dadada;
|
||||
$main-bg-color: white;
|
||||
$secondary-text-color: white;
|
||||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ $main-text-color: #333;
|
|||
$border-color: #dadada;
|
||||
$main-bg-color: white;
|
||||
$secondary-text-color: white;
|
||||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ $main-text-color: #333;
|
|||
$border-color: #dadada;
|
||||
$main-bg-color: white;
|
||||
$secondary-text-color: white;
|
||||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ $main-text-color: #333;
|
|||
$border-color: #dadada;
|
||||
$main-bg-color: white;
|
||||
$secondary-text-color: white;
|
||||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ $main-text-color: #333;
|
|||
$border-color: #dadada;
|
||||
$main-bg-color: white;
|
||||
$secondary-text-color: white;
|
||||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ $main-text-color: #333;
|
|||
$border-color: #dadada;
|
||||
$main-bg-color: white;
|
||||
$secondary-text-color: white;
|
||||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -94,11 +94,20 @@
|
|||
<path d="M576 736v192q0 40-28 68t-68 28H288q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm512 0v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm512 0v192q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="fa-spinner" viewBox="0 0 1792 1792">
|
||||
<title>Spinner</title>
|
||||
<path d="M526 1394q0 53-37.5 90.5T398 1522q-52 0-90-38t-38-90q0-53 37.5-90.5T398 1266t90.5 37.5T526 1394zm498 206q0 53-37.5 90.5T896 1728t-90.5-37.5T768 1600t37.5-90.5T896 1472t90.5 37.5 37.5 90.5zM320 896q0 53-37.5 90.5T192 1024t-90.5-37.5T64 896t37.5-90.5T192 768t90.5 37.5T320 896zm1202 498q0 52-38 90t-90 38q-53 0-90.5-37.5T1266 1394t37.5-90.5 90.5-37.5 90.5 37.5 37.5 90.5zM558 398q0 66-47 113t-113 47-113-47-47-113 47-113 113-47 113 47 47 113zm1170 498q0 53-37.5 90.5T1600 1024t-90.5-37.5T1472 896t37.5-90.5T1600 768t90.5 37.5T1728 896zm-640-704q0 80-56 136t-136 56-136-56-56-136 56-136T896 0t136 56 56 136zm530 206q0 93-66 158.5T1394 622q-93 0-158.5-65.5T1170 398q0-92 65.5-158t158.5-66q92 0 158 66t66 158z"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
</svg>
|
||||
<!-- The application will be rendered inside this element,
|
||||
because `templates/main.js` references it -->
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
|
||||
<!-- Toast.html gets rendered here -->
|
||||
<div id="toast"></div>
|
||||
|
||||
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
||||
and anything else it needs to hydrate the app and
|
||||
initialise the router -->
|
||||
|
|
Loading…
Reference in a new issue