Commit 8bbbda14 authored by Aral Balkan's avatar Aral Balkan
Browse files

Merge branch 'svelte-spa-router'

parents 895d40bb 7754a57c
<script>
import Router from 'svelte-spa-router'
import Navigation from './Navigation.svelte'
import SetupSection from './sections/Setup.svelte'
import HomeSection from './sections/Home.svelte'
import AboutSection from './sections/About.svelte'
import SlidingSection from './sections/Sliding.svelte'
import SwitchSection from './sections/Switch.svelte'
import PrivateSection from './sections/Private.svelte'
import SignInSection from './sections/SignIn.svelte'
import SignOutSection from './sections/SignOut.svelte'
import NotFound from './sections/NotFound.svelte'
import { routes } from './sections'
import { onMount, getContext } from 'svelte'
import { get } from 'svelte/store'
// Initialise state.
import { initialise, state } from './state.js'
initialise()
// Initialise context.
import { initialiseContext, state, signedIn} from './state.js'
initialiseContext()
const { publicSocket } = getContext(state)
const { publicSocket, privateToken, privateSocket, privateMessages } = getContext(state)
// Normal closure code for web sockets.
const NORMAL_CLOSURE = 1000
onMount(() => {
// Make the public socket connection.
$publicSocket = new WebSocket(`wss://${location.hostname}/public`)
$publicSocket.onmessage = message => {
alert(message.data)
console.log(`Public message: ${message.data}`)
}
// When signed in, make the private socket connection.
signedIn.subscribe(isSignedIn => {
if (isSignedIn) {
console.log('Attempting to connect using private token', get(privateToken))
// Create a web socket connection.
$privateSocket = new WebSocket(`wss://${location.hostname}/private/${get(privateToken)}`)
$privateSocket.onmessage = message => {
$privateMessages = [...$privateMessages, message]
}
} else {
if ($privateSocket !== null) {
if ($privateSocket.readyState === 0 || $privateSocket.readyState === 1) {
console.log('Signed out, closing private socket.')
$privateSocket.send('Client is about to close.')
$privateSocket.close(NORMAL_CLOSURE, 'Person signed out.')
$privateToken = null
$privateMessages = []
}
}
}
})
})
</script>
......@@ -36,13 +56,5 @@
<Navigation />
<main>
<SetupSection />
<HomeSection />
<AboutSection />
<SlidingSection />
<SwitchSection />
<PrivateSection />
<SignInSection />
<SignOutSection />
<NotFound />
<Router {routes}/>
</main>
<script>
import { getContext } from 'svelte'
import { state } from './state.js'
import { signedIn } from './state.js'
import sections from './sections.js'
import { link } from 'svelte-spa-router'
import active from 'svelte-spa-router/active'
let { signedIn, currentSection } = getContext(state)
let selectedSection = $currentSection
function handleNavigation (event) {
selectedSection = event.target.hash.replace('#', '')
if (selectedSection !== $currentSection) {
$currentSection = selectedSection
}
}
function shouldDisplay(path) {
function shouldDisplay(section, isSignedIn) {
// Display either the Sign In or Private section based on sign in status.
return !(!$signedIn && path === '/private') && !($signedIn && path === '/sign-in') && !(!$signedIn && path === '/sign-out')
const isInNavigation = !section.notInNavigation
const isPublic = !section.isPrivate
const isPrivateAndPersonIsSignedIn = section.isPrivate && isSignedIn
const isNotSignInLinkWhenAlreadySignedIn = !(section.path == '/sign-in' && isSignedIn)
return isInNavigation && (isPublic || isPrivateAndPersonIsSignedIn) && isNotSignInLinkWhenAlreadySignedIn
}
</script>
......@@ -26,20 +20,20 @@
li { display: inline-block; }
li:not(:first-child):before { content: '|'; padding-right: 0.5em;}
li:not(:last-child) { padding-right: 0.5em; }
:global(a.active) {
color: black;
text-decoration: none;
}
</style>
<nav>
<ul>
{#each sections as section}
<!-- Including $currentSection here to trigger reactivity. -->
{#if !section.notInNavigation && $currentSection && shouldDisplay(section.path)}
<!-- Including $signedIn here to trigger reactivity. -->
{#if !section.notInNavigation && shouldDisplay(section, $signedIn)}
<li>
<!-- Display the current section without a hyperlink; all other sections with a link. -->
{#if section.path !== $currentSection}
<a href='/#{section.path}' on:click={handleNavigation}>{section.title}</a>
{:else}
{section.title}
{/if}
<a href='{section.path}' use:link use:active>{section.title}</a>
</li>
{/if}
{/each}
......
body {
font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
input, button, label {
font-size: 1.25em;
}
input, button {
background: white;
color: black;
border: 2px solid grey;
border-radius: 0.25em;
padding: 0.5em;
}
input:focus, button:focus {
outline: none;
box-shadow: 0px 0px 2px #0066ff;
}
......@@ -3,6 +3,7 @@
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<link rel='icon' href='data:,'> <!-- no favicon request -->
<title>Henry: Small Web Reference Client</title>
<script src='./index.js' type='module'></script>
</head>
......
......@@ -14,6 +14,7 @@
"buffer": "^6.0.3",
"session25519": "^1.1.0",
"svelte-loading-spinners": "^0.1.4",
"svelte-spa-router": "^3.1.0",
"svelte-switch": "0.0.4",
"tweetnacl": "^1.0.3",
"tweetnacl-sealedbox-js": "^1.2.0",
......@@ -526,6 +527,14 @@
"url": "https://opencollective.com/postcss/"
}
},
"node_modules/regexparam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz",
"integrity": "sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==",
"engines": {
"node": ">=6"
}
},
"node_modules/require-relative": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
......@@ -625,6 +634,17 @@
"resolved": "https://registry.npmjs.org/svelte-loading-spinners/-/svelte-loading-spinners-0.1.4.tgz",
"integrity": "sha512-r/0Hs9cxtRLh2RwBwwEat1V1M1kypaGFPllM7XdZSH299JTed994B6zMHJ1Z5Ug8B9Tl2bs+VOy8Vz5EFWy5Ww=="
},
"node_modules/svelte-spa-router": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.1.0.tgz",
"integrity": "sha512-jlM/xwjn57mylr+pzHYCOOy+IPQauT46gOucNGTBu6jHcFXu3F+oaojN4PXC1LYizRGxFB6QA0qnYbZnRfX7Sg==",
"dependencies": {
"regexparam": "1.3.0"
},
"funding": {
"url": "https://github.com/sponsors/ItalyPaleAle"
}
},
"node_modules/svelte-switch": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/svelte-switch/-/svelte-switch-0.0.4.tgz",
......@@ -1025,6 +1045,11 @@
"source-map": "^0.6.1"
}
},
"regexparam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz",
"integrity": "sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g=="
},
"require-relative": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
......@@ -1105,6 +1130,14 @@
"resolved": "https://registry.npmjs.org/svelte-loading-spinners/-/svelte-loading-spinners-0.1.4.tgz",
"integrity": "sha512-r/0Hs9cxtRLh2RwBwwEat1V1M1kypaGFPllM7XdZSH299JTed994B6zMHJ1Z5Ug8B9Tl2bs+VOy8Vz5EFWy5Ww=="
},
"svelte-spa-router": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.1.0.tgz",
"integrity": "sha512-jlM/xwjn57mylr+pzHYCOOy+IPQauT46gOucNGTBu6jHcFXu3F+oaojN4PXC1LYizRGxFB6QA0qnYbZnRfX7Sg==",
"requires": {
"regexparam": "1.3.0"
}
},
"svelte-switch": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/svelte-switch/-/svelte-switch-0.0.4.tgz",
......
export default [
{ path: '/', title: 'Home'},
{ path: '/about', title: 'About'},
{ path: '/sliding', title: 'Sliding'},
{ path: '/switch', title: 'Switch'},
{ path: '/sign-in', title: 'Sign in'},
{ path: '/sign-out', title: 'Sign out'},
{ path: '/private', title: 'Private'},
{ path: '/setup', title: 'Setup', notInNavigation: true},
{ path: '/404', title: '404: not found', notInNavigation: true}
import SetupSection from './sections/Setup.svelte'
import HomeSection from './sections/Home.svelte'
import AboutSection from './sections/About.svelte'
import SlidingSection from './sections/Sliding.svelte'
import SwitchSection from './sections/Switch.svelte'
import PrivateSection from './sections/Private.svelte'
import SignInSection from './sections/SignIn.svelte'
import SignOutSection from './sections/SignOut.svelte'
import NotFound from './sections/NotFound.svelte'
import wrap from 'svelte-spa-router/wrap'
import { replace } from 'svelte-spa-router'
import { signedIn } from './state.js'
let isSignedIn
signedIn.subscribe(value => {
console.log(`Sections: signed in changed to ${value}`)
isSignedIn = value
})
export const sections = [
{ path: '/', title: 'Home', route: HomeSection},
{ path: '/about', title: 'About', route: AboutSection},
{ path: '/sliding', title: 'Sliding', route: SlidingSection},
{ path: '/switch', title: 'Switch', route: SwitchSection},
{ path: '/sign-in', title: 'Sign in', route: SignInSection},
{ path: '/sign-out', title: 'Sign out', route: SignOutSection, isPrivate: true},
{ path: '/private', title: 'Private', route: PrivateSection, isPrivate: true},
{ path: '/setup', title: 'Setup', notInNavigation: true, route: SetupSection},
{ path: '*', title: '404: not found', notInNavigation: true, route: NotFound}
]
const isPrivate = () => {
if (!isSignedIn) {
replace('/sign-in')
}
return isSignedIn
}
// Create the routes map.
export let routes = new Map()
sections.forEach(section => {
const wrappedRoute = wrap({
component: section.route,
props: { title: section.title }
})
if (section.isPrivate) {
wrappedRoute.conditions = [ isPrivate ]
}
routes.set(section.path, wrappedRoute)
})
export default sections
......@@ -2,36 +2,15 @@
import Section from './Section.svelte'
import { getContext } from 'svelte'
import { get } from 'svelte/store'
import { state } from '../state.js'
const { privateToken, privateSocket } = getContext(state)
// NOTE: All this socket-related code should be taken out of the view.
let messages = []
function onShow() {
if ($privateSocket === null || $privateSocket.readyState !== 1) {
console.log('Attempting to connect using private token', get(privateToken))
// Create a web socket connection.
$privateSocket = new WebSocket(`wss://${location.hostname}/private/${get(privateToken)}`)
$privateSocket.onmessage = message => {
messages = [...messages, message]
}
} else {
console.log('Socket is open, not creating a new one.')
}
// Return an empty string so nothing is rendered as this is called from the view.
return ''
}
const { privateMessages } = getContext(state)
</script>
<Section path='/private' title='Private'>
{ onShow() }
<Section title='Private'>
<p>This is private.</p>
<ul>
{#each messages as message}
{#each $privateMessages as message}
<li>{message.data}</li>
{/each}
</ul>
......
......@@ -4,9 +4,6 @@
import { state } from '../state.js'
import { fade } from 'svelte/transition'
let { currentSection } = getContext(state)
export let path
export let title
</script>
......@@ -17,10 +14,7 @@
}
</style>
<!-- Only display a section if it is the current one. -->
{#if $currentSection === path}
<section transition:fade="{{duration: 300}}">
<h2>{title}</h2>
<slot></slot>
</section>
{/if}
<script>
import { getContext } from 'svelte'
import { state } from '../state.js'
import { state, signedIn } from '../state.js'
import Section from './Section.svelte'
import { authenticate } from '../authentication.js'
import { BarLoader } from 'svelte-loading-spinners'
import { push } from 'svelte-spa-router'
const path = '/sign-in'
let { signedIn, privateToken, currentSection } = getContext(state)
let { privateToken } = getContext(state)
let passphraseField
let _privateToken = undefined
......@@ -24,8 +24,9 @@
// it will return here and find the successful sign in.)
_privateToken = undefined
$signedIn = true
$currentSection = '/private'
location.hash = '/private'
// Navigate to the private route.
push('/private')
// Returning an empty string as we’re called
// from within the template’s await.
......@@ -37,7 +38,7 @@
}
</script>
<Section {path} title='Sign in'>
<Section title='Sign in'>
<form on:submit|preventDefault={handleSignIn} id='signInForm' name='signInForm'>
<label for='passphrase'>Password</label>
<input bind:this={passphraseField} name='passphrase' type='password'/>
......@@ -58,5 +59,4 @@
{:catch error}
<p style='color: red'>{error.message}</p>
{/await}
</Section>
<script>
import { getContext, tick } from 'svelte'
import { state } from '../state.js'
import { getContext } from 'svelte'
import { state, signedIn } from '../state.js'
import Section from './Section.svelte'
const NORMAL_CLOSURE = 1000
let { signedIn, privateToken, privateSocket, currentSection } = getContext(state)
import { push } from 'svelte-spa-router'
async function handleSignOut () {
console.log('private socket', $privateSocket)
$privateSocket.send('about to close')
$privateSocket.close(NORMAL_CLOSURE, 'Person signed out.')
$privateToken = null
$signedIn = false
$currentSection = '/sign-in'
location.hash = '/sign-in'
push('/sign-in')
}
</script>
<Section path='/sign-out' title='Sign out'>
<Section title='Sign out'>
<p>Are you sure?</p>
<button on:click|preventDefault={handleSignOut}>Yes, sign me out.</button>
</Section>
import { setContext } from 'svelte'
import { writable, get } from 'svelte/store'
import sections from './sections.js'
const sectionPaths = sections.map(value => value.path)
import { writable } from 'svelte/store'
// Context key.
export const state = {}
// Set initial application state, including the current section based on the hash.
export function initialise () {
const signedIn = writable(false)
const privateToken = writable(null)
const privateSocket = writable(null)
const publicSocket = writable(null)
const currentSection = writable(currentSectionFromHash(false))
setContext(state, { currentSection, signedIn, publicSocket, privateToken, privateSocket })
window.addEventListener('hashchange', () => {
const _signedIn = get(signedIn)
currentSection.update(value => value = currentSectionFromHash(_signedIn))
})
}
function currentSectionFromHash (signedIn) {
let section = location.hash.replace('#', '')
export const signedIn = writable(false)
export const privateMessages = writable([])
export const privateToken = writable(null)
export const privateSocket = writable(null)
export const publicSocket = writable(null)
// Redirect to Sign In page if trying to access private area while not signed in.
// (This is just client-side validation, not security.)
if (!signedIn && section.startsWith('/private') || !signedIn && section.startsWith('/sign-out')) {
section = '/sign-in'
// In a real app, you would also save a redirect path in a store in the context.
}
// Normalise all paths so they do not end in a slash.
if (section !== '/' && section.endsWith('/')) { section = section.slice(0, section.length - 1) }
// Normalise index path.
if (section === '') { section = '/'; }
// If we’ve updated the section path, update the hash also.
if (section !== location.hash.replace('#', '')) { location.hash = section }
// Check if this is a known path and show 404 page if it’s not.
// (Note: we do not update the hash here on purpose so the original missing path is shown.)
if (!sectionPaths.includes(section)) { section = '/404' }
return section
// Set initial application state, including the current section based on the hash.
export function initialiseContext () {
setContext(state, { signedIn, publicSocket, privateToken, privateSocket, privateMessages })
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment