Browse Source

Finally merged the code from the xor.academy work back into the project template.

master
Zed A. Shaw 1 month ago
parent
commit
9ac78dc7d9
  1. 7
      api/login.js
  2. 20
      api/media.js
  3. 13
      api/password_reset.js
  4. 9
      api/register.js
  5. 4
      api/user/payments.js
  6. 4
      api/user/profile.js
  7. 18
      client/Footer.svelte
  8. 25
      client/Header.svelte
  9. 12
      client/Layout.svelte
  10. 25
      client/api.js
  11. 2
      client/bando/Bandolier.svelte
  12. 26
      client/bando/Components.svelte
  13. 4
      client/bando/demos/Accordion.svelte
  14. 7
      client/bando/demos/Accordion.svelte.md
  15. 28
      client/bando/demos/AspectRatio.svelte
  16. 12
      client/bando/demos/AspectRatio.svelte.md
  17. 8
      client/bando/demos/Badge.svelte
  18. 3
      client/bando/demos/Badge.svelte.md
  19. 1
      client/bando/demos/ButtonGroup.svelte.md
  20. 1
      client/bando/demos/Calendar.svelte.md
  21. 4
      client/bando/demos/Callout.svelte
  22. 8
      client/bando/demos/Callout.svelte.md
  23. 1
      client/bando/demos/Cards.svelte.md
  24. 1
      client/bando/demos/Carousel.svelte.md
  25. 13
      client/bando/demos/Chat.svelte
  26. 1
      client/bando/demos/Chat.svelte.md
  27. 25
      client/bando/demos/Code.svelte.md
  28. 1
      client/bando/demos/Countdown.svelte.md
  29. 21
      client/bando/demos/Darkmode.svelte.md
  30. 3
      client/bando/demos/DataTable.svelte.md
  31. 14
      client/bando/demos/FairPay.svelte
  32. 16
      client/bando/demos/FairPay.svelte.md
  33. 7
      client/bando/demos/Flipper.svelte
  34. 13
      client/bando/demos/Flipper.svelte.md
  35. 2
      client/bando/demos/Form.svelte
  36. 10
      client/bando/demos/Form.svelte.md
  37. 7
      client/bando/demos/LiveStream.svelte
  38. 5
      client/bando/demos/LiveStream.svelte.md
  39. 13
      client/bando/demos/LoggedIn.svelte
  40. 16
      client/bando/demos/LoggedIn.svelte.md
  41. 5
      client/bando/demos/Login.svelte.md
  42. 2
      client/bando/demos/Markdown.svelte
  43. 8
      client/bando/demos/Modal.svelte
  44. 8
      client/bando/demos/Modal.svelte.md
  45. 1
      client/bando/demos/Pagination.svelte.md
  46. 3
      client/bando/demos/PlaceHolder.svelte.md
  47. 4
      client/bando/demos/Progress.svelte
  48. 13
      client/bando/demos/Progress.svelte.md
  49. 8
      client/bando/demos/Sidebar.svelte
  50. 6
      client/bando/demos/Sidebar.svelte.md
  51. 8
      client/bando/demos/SnapImage.svelte
  52. 29
      client/bando/demos/SnapImage.svelte.md
  53. 8
      client/bando/demos/Spinner.svelte
  54. 16
      client/bando/demos/Spinner.svelte.md
  55. 41
      client/bando/demos/StackLayer.svelte
  56. 25
      client/bando/demos/StackLayer.svelte.md
  57. 8
      client/bando/demos/Switch.svelte
  58. 4
      client/bando/demos/Switch.svelte.md
  59. 5
      client/bando/demos/Tabs.svelte.md
  60. 2
      client/bando/demos/Tiles.svelte
  61. 1
      client/bando/demos/Tiles.svelte.md
  62. 7
      client/bando/demos/Toast.svelte
  63. 4
      client/bando/demos/Toast.svelte.md
  64. 12
      client/bando/demos/Tooltip.svelte.md
  65. 47
      client/bando/demos/Video.svelte
  66. 5
      client/bando/demos/Video.svelte.md
  67. 35
      client/bando/demos/WTVideo.svelte
  68. 24
      client/bando/demos/WTVideo.svelte.md
  69. 2
      client/components/BTCPay.svelte
  70. 5
      client/components/Carousel.svelte
  71. 11
      client/components/DataTable.svelte
  72. 1
      client/components/Icon.svelte
  73. 21
      client/components/IconImage.svelte
  74. 25
      client/components/LoggedIn.svelte
  75. 19
      client/components/Login.svelte
  76. 20
      client/components/Markdown.svelte
  77. 2
      client/components/Paypal.svelte
  78. 74
      client/components/SnapImage.svelte
  79. 41
      client/components/Spinner.svelte
  80. 25
      client/components/TOS.svelte
  81. 38
      client/components/Video.svelte
  82. 26
      client/components/WTVideo.svelte
  83. 2
      client/config.js
  84. 11
      client/pages/Home.svelte
  85. 66
      client/pages/Register.svelte
  86. 2
      client/pages/ResetPassword.svelte
  87. 16
      client/pages/TOS.svelte
  88. 16
      client/pages/admin/Create.svelte
  89. 1
      client/pages/admin/Table.svelte
  90. 2
      client/pages/admin/index.svelte
  91. 6
      client/routes.js
  92. 26
      client/wt.js
  93. 5
      lib/auth.js
  94. 25
      lib/models.js
  95. 53
      lib/ormish.js
  96. 17
      lib/testing.js
  97. 11
      migrations/20210823001710_media_needs_poster.cjs
  98. 16
      migrations/20210828133355_site_metadata.cjs
  99. 1216
      package-lock.json
  100. 11
      package.json

7
api/login.js

@ -5,12 +5,16 @@ export const options = (req, res) => {
}
export const get = (req, res) => {
// NOTE: we could also set authenticated in models.User.auth but this is more of a
// UI level variable than an authentication variable. If the code gets to this
// point then the get.authenticated passed and the user has to be authenticated.
const reply = {
id: req.user.id,
initials: req.user.initials,
full_name: req.user.full_name,
admin: req.user.admin,
email: req.user.email,
authenticated: true
}
res.status(200).json(reply);
}
@ -23,7 +27,8 @@ export const post = (req, res) => {
initials: req.user.initials,
full_name: req.user.full_name,
admin: req.user.admin,
email: req.user.email
email: req.user.email,
authenticated: true
}
res.status(200).json(reply);
}

20
api/media.js

@ -0,0 +1,20 @@
import logging from "../lib/logging.js";
import assert from "assert";
import { API } from "../lib/api.js";
import { knex } from "../lib/ormish.js";
const log = logging.create(import.meta.url);
export const get = async (req, res) => {
const api = new API(req, res);
try {
const media = await knex("media").whereNotNull("torrent_url");
api.reply(200, media);
} catch (error) {
log.error(error);
api.error(500, error.message || "Internal Server Error");
}
}
get.authenticated = false;

13
api/password_reset.js

@ -4,8 +4,12 @@ import assert from "assert";
import { User } from "../lib/models.js";
import { v4 as uuidv4} from "uuid";
import { send_reset, send_reset_finished } from '../lib/queues.js';
import differenceInHours from 'date-fns/differenceInHours/index.js';
const log = logging.create("/api/password_reset.js");
const RESET_HOURS_MAX = 10;
// this doesn't change so just do it here
const RESET_TIME_ERROR = `Reset already sent. You can only send one every ${RESET_HOURS_MAX} hours.`
export const post = async (req, res) => {
const api = new API(req, res);
@ -23,7 +27,7 @@ export const post = async (req, res) => {
// they have a code submitted, so check it
if(user.reset_code !== code) {
return api.error(400, {message: "Reset code doesn't match."});
return api.error(400, "Reset code doesn't match.");
} else {
user.password = User.encrypt_password(password);
await User.update({email}, {password: user.password, reset_code: null, reset_sent_at: null});
@ -32,6 +36,9 @@ export const post = async (req, res) => {
return api.reply(200, {message: "OK"});
}
} else if(user.reset_sent_at && differenceInHours(Date.now(), user.reset_sent_at) < RESET_HOURS_MAX) {
// prevent repeated high speed reset attempts by using the advanced technolog of...time
return api.error(400, RESET_TIME_ERROR);
} else {
// new request, send out a code
user.reset_code = uuidv4();
@ -39,10 +46,10 @@ export const post = async (req, res) => {
send_reset(user);
return res.reply(200, {message: "OK"});
return api.reply(200, {message: "OK"});
}
} catch(error) {
log.error(error);
return api.error(400, {message: error.message});
return api.error(500, error.message || "Internal Server Error");
}
}

9
api/register.js

@ -1,13 +1,15 @@
import { User } from "../lib/models.js";
// TODO: rewrite this to use lib/api.js
import { User, Site } from "../lib/models.js";
import * as queues from "../lib/queues.js";
export const post = async (req, res) => {
// TODO: this is kind of weird so find a way to validate but allow one or more fields
let count = await Site.get("registered_count");
let password_repeat = req.body.password_repeat;
delete req.body.password_repeat;
let [valid, errors] = await User.validate(req.body);
let [valid, errors] = User.validate(req.body);
if(!valid) {
return res.status(400).json({message: `Invalid user attributes: ${JSON.stringify(errors)}`});
@ -17,6 +19,7 @@ export const post = async (req, res) => {
let good = await User.register(req.body);
if(good) {
await Site.increment("registered_count", 1);
delete good.password;
queues.send_welcome(good);
return res.status(200).json({message: "OK"});

4
api/user/payments.js

@ -15,7 +15,7 @@ export const get = async (req, res) => {
api.reply(200, reply_data);
} catch (error) {
log.error(error);
api.error(500, error);
api.error(500, error.message || "Internal Server Error");
}
}
@ -52,7 +52,7 @@ export const post = async (req, res) => {
return api.reply(200, reply_data);
} catch (error) {
log.error(error);
return api.error(500, error);
return api.error(500, error.message || "Internal Server Error");
}
}

4
api/user/profile.js

@ -22,7 +22,7 @@ export const post = async (req, res) => {
}
// now we validate it matches the schema
let [valid, errors] = await User.validate(req.body);
let [valid, errors] = User.validate(req.body);
if(valid) {
// TODO: need a way to share the validation
@ -37,7 +37,7 @@ export const post = async (req, res) => {
}
} catch (error) {
log.error(error);
return api.error(500, error);
return api.error(500, error.message || "Internal Server Error");
}
}

18
client/Footer.svelte

@ -1,20 +1,16 @@
<footer>
<section>
<aside>
<h3>Contact</h3>
<p>
<h4>Contact</h4>
<span>
You can always message me on Twitter at <a href="https://twitter.com/lzsthw">@lzsthw</a>.
</p>
</span>
</aside>
<aside>
<h3>About</h3>
<p>This project is a work in progress project for <a href="https://learnjsthehardway.com">Learn JavaScript the Hard Way</a>. It will be the foundation for all of the demonstration projects.</p>
<h4>About</h4>
<span>This is a starter project for students of <a href="https://learnjsthehardway.com">Learn JavaScript the Hard Way</a>.</span>
</aside>
<aside>
<h3>Code</h3>
<p>You can look at the code in the <a href="https://git.learnjsthehardway.com/zedshaw/ljsthw-project-template">Learn JS the Hard Way Git Repository.</a> This is <b>not</b> open source software and you have no license to run it.</p>
<h4>Code</h4>
<span>You can look at the code in the <a href="https://git.learnjsthehardway.com/zedshaw/ljsthw-project-template">LJSTHW Project Template</a> This is <b>not</b> open source software and you have no license to run it.</span>
</aside>
</section>
</footer>

25
client/Header.svelte

@ -1,26 +1,33 @@
<script>
import { logout_user } from '$/client/api';
import LoggedIn from '$/client/components/LoggedIn.svelte';
import Icon from '$/client/components/Icon.svelte';
import Darkmode from '$/client/components/Darkmode.svelte';
import { onMount } from 'svelte';
import {push, link} from 'svelte-spa-router';
import {link} from 'svelte-spa-router';
import { user } from "$/client/stores.js";
$: console.log("USER IS", $user);
</script>
<header>
<nav>
<a href="/" use:link><Icon name="home" size="48" /></a>
<LoggedIn>
<ul slot="yes">
{#if $user.authenticated}
<a href="/" use:link><Icon name="home" size="48" /></a>
<ul>
<li><a on:click|preventDefault={ logout_user } data-testid="logout-link">Logout</a></li>
<li><a href="/profile/" use:link><Icon name="settings" /></a></li>
{#if $user.admin }
<li><a href="/admin/table/" use:link><Icon name="database" /></a></li>
{/if}
<li><Darkmode /></li>
</ul>
<ul slot="no">
</ul>
{:else}
<!-- don't use:link because we want them to go to the landing page if they aren't logged in. -->
<a href="/"><Icon name="home" size="48" /></a>
<ul>
<li><a href="/register/" use:link>Register</a></li>
<li><a href="/login/" use:link>Login</a></li>
<li><Darkmode /></li>
</ul>
</LoggedIn>
{/if}
</nav>
</header>

12
client/Layout.svelte

@ -1,7 +1,8 @@
<script>
import LoggedIn from './components/LoggedIn.svelte';
import Footer from './Footer.svelte';
import Header from './Header.svelte';
import LoggedIn from '$/client/components/LoggedIn.svelte';
import Spinner from '$/client/components/Spinner.svelte';
import Footer from '$/client/Footer.svelte';
import Header from '$/client/Header.svelte';
export let footer = true;
export let header = true;
@ -42,10 +43,13 @@
<Header />
{/if}
<LoggedIn redirect="/login" show_required_page={ true }>
<LoggedIn redirect="/login" show_required_page={ false }>
<main class:fullscreen={fullscreen} class:centered={ centered } slot="yes" data-testid={ testid }>
<slot></slot>
</main>
<main class:fullscreen={fullscreen} class:centered={ centered } slot="no" data-testid={ testid }>
<Spinner />
</main>
</LoggedIn>
{#if footer}

25
client/api.js

@ -2,6 +2,9 @@ const MOCK_ROUTES = {};
import { user } from "./stores.js";
// use these when you do your own fetch
const fetch_opts = { credentials: 'same-origin',};
export const mock = (config) => {
for(let [route, value] of Object.entries(config)) {
MOCK_ROUTES[route] = value;
@ -76,15 +79,25 @@ export const raw = async (url, method, body, unauthed_action) => {
}
}
export const get = async (url) => await raw(url, 'GET');
export const get = async (url, unauthed_action) => {
return await raw(url, 'GET', undefined, unauthed_action);
}
export const post = async (url, data) => await raw(url, 'POST', data);
export const post = async (url, data, unauthed_action) => {
return await raw(url, 'POST', data, unauthed_action);
}
export const put = async (url, data) => await raw(url, 'PUT', data);
export const put = async (url, data, unauthed_action) => {
return await raw(url, 'PUT', data, unauthed_action);
}
export const del = async (url) => await raw(url, "DELETE");
export const del = async (url, unauthed_action) => {
return await raw(url, 'DELETE', undefined, unauthed_action);
}
export const options = async (url) => await raw(url, "OPTIONS");
export const options = async (url, unauthed_action) => {
return await raw(url, 'OPTIONS', undefined, unauthed_action);
}
export const logout_user = async () => {
user.update(() => {
@ -98,5 +111,5 @@ export const logout_user = async () => {
}
export default {
post, get, put, del, mock, options
post, get, put, del, mock, options, fetch_opts, logout_user
}

2
client/bando/Bandolier.svelte

@ -133,7 +133,7 @@
left: 0;
right: 0;
padding: 0;
background-color: #fff;
background-color: var(--color-bg);
border: 3px solid var(--color-accent);
display: flex;
flex-direction: row;

26
client/bando/Components.svelte

@ -20,7 +20,8 @@
import LiveStream from "./demos/LiveStream.svelte";
import LoggedIn from "./demos/LoggedIn.svelte";
import Login from "./demos/Login.svelte";
import Markdown from "./demos/Markdown.svelte";
import Markdown from "$/client/components/Markdown.svelte";
import MarkdownDemo from "./demos/Markdown.svelte";
import Modal from "./demos/Modal.svelte";
import Pagination from "./demos/Pagination.svelte";
import PlaceHolder from "./demos/PlaceHolder.svelte";
@ -37,6 +38,8 @@
import Video from "./demos/Video.svelte";
import WTVideo from "./demos/WTVideo.svelte";
import Code from "../components/Code.svelte";
import StackLayer from "./demos/StackLayer.svelte";
import AspectRatio from "./demos/AspectRatio.svelte";
import Layout from "../Layout.svelte";
import { link } from "svelte-spa-router";
@ -45,7 +48,8 @@
* or set active: true on them it will require a login.
*/
let panels = [
{title: "Accordion", active: true, icon: "align-justify", component: Accordion},
{title: "Accordion", active: false, icon: "align-justify", component: Accordion},
{title: "AspectRatio", active: false, icon: "copy", component: AspectRatio},
{title: "Badge", active: false, icon: "award", component: Badge},
{title: "ButtonGroup", active: false, icon: "server", component: ButtonGroup},
{title: "Calendar", active: false, icon: "calendar", component: Calendar},
@ -65,7 +69,7 @@
{title: "LiveStream", active: false, icon: "cast", component: LiveStream},
{title: "LoggedIn", active: false, icon: "log-out", component: LoggedIn},
{title: "Login", active: false, icon: "log-in", component: Login},
{title: "Markdown", active: false, icon: "file", component: Markdown},
{title: "Markdown", active: false, icon: "file", component: MarkdownDemo},
{title: "Modal", active: false, icon: "maximize", component: Modal},
{title: "Pagination", active: false, icon: "skip-forward", component: Pagination},
{title: "PlaceHolder", active: false, icon: "image", component: PlaceHolder},
@ -73,20 +77,27 @@
{title: "Sidebar", active: false, icon: "sidebar", component: SidebarDemo},
{title: "SnapImage", active: false, icon: "camera", component: SnapImage},
{title: "Spinner", active: false, icon: "rotate-cw", component: Spinner},
{title: "StackLayer", active: false, icon: "layers", component: StackLayer},
{title: "Switch", active: false, icon: "check-square", component: Switch},
{title: "Tabs", active: false, icon: "folder", component: Tabs},
{title: "Tiles", active: false, icon: "camera", component: Tiles},
{title: "Toast", active: false, icon: "message-square", component: Toast},
{title: "Tooltip", active: false, icon: "help-circle", component: Tooltip},
{title: "WTVideo", active: false, icon: "video", component: WTVideo},
{title: "Video", active: false, icon: "video", component: Video},
{title: "WTVideo", active: false, icon: "video", component: WTVideo},
];
panels.forEach(p => (p.code = p.code || `/bando/demos/${p.title}.svelte`));
let selected = panels[0];
selected.active = true;
let show_code = false;
const load_docs = async (from) => {
const res = await fetch(`${from.code}.md`);
return res.status == 200 ? res.text() : undefined;
}
const sidebar_select = (event) => {
const {index, item} = event.detail;
show_code = false;
@ -171,6 +182,13 @@
<Code src={ selected.code } />
{:else}
<svelte:component this={selected.component} />
{#await load_docs(selected) then docs }
{#if docs}
<h2>Documentation</h2>
<Markdown content={ docs } />
{/if}
{/await}
{/if}
</component>
</contents>

4
client/bando/demos/Accordion.svelte

@ -18,7 +18,3 @@
</script>
<Tabs panels={ panels } on:select={ tab_select } bind:selected vertical={true} />
<hr/>
<p>An Accordion is just the <b>Tabs</b> component with <b>vertical</b> set to <b>true</b>. </p>

7
client/bando/demos/Accordion.svelte.md

@ -0,0 +1,7 @@
An accordion is just the `Tabs` component with `vertical` set to true:
```
<Tabs panels={ panels } on:select={ tab_select } bind:selected vertical={true} />
```
See the documentation for `Tabs` for how to use it.

28
client/bando/demos/AspectRatio.svelte

@ -0,0 +1,28 @@
<style>
aspect-test {
display: flex;
flex-direction: column;
}
.top-box p {
display: flex;
justify-content: center;
align-items: center;
background-color: var(--value5);
font-size: 2.5vw;
padding: 1rem;
}
</style>
<h1>Aspect Ratio</h1>
<aspect-test>
<div class="top-box" style="--aspect-ratio: 16/9;">
<p>Every single visual experience that has a bounding plane has to deal with aspect ratios, whether that's a photo through a 1:1 ratio medium format camera or a 16:9 ratio 4k movie. If there's a visual bounding box around the scene then everything in the scene must deal with the ratio of the width to the height.</p>
</div>
<div style="--aspect-ratio: 10/2; background-color: var(--value7); font-size: 1.5vw;">
<p>CSS is one of the only visual systems that has no concept of aspect ratios despite the performance benefits of knowing the aspect of a block before the contents of the block are available. If you know the aspect ratio of a block, and you know the width of the page, then you can render the block immediately because you only need <b>one</b> dimension to render both.</p>
</div>
</aspect-test>

12
client/bando/demos/AspectRatio.svelte.md

@ -0,0 +1,12 @@
Because CSS doesn't really support aspect ratios (despite a standard that says it does) you'll have a difficult time positioning and sizing many elements on the page. The `--aspect-ratio` hack I've put into `static/global.css` does a decent job of creating a fake aspect ratio system without too many changes to your HTML. You only need to add:
```
style="--aspect-ratio: 16/9"
```
And the magic CSS should do what you want. It's a trick I've taken from [an article on CSS tricks](https://css-tricks.com/aspect-ratio-boxes/#using-custom-properties) that gives an excellent solution that doesn't require crazy changed to your CSS or HTML. You can also use `16/9` or decimal notation `1.4`.
Limitations
===
As you change the size of this demo you'll see the limitation of this method. As the boxes shrink the text will explode out of the "box" that CSS has, or the boxes will cover other boxes. It's best to have only 1 or 2 elements that use `--aspect-ration` and test it at various sizes.

8
client/bando/demos/Badge.svelte

@ -28,24 +28,24 @@
<p>Top left position with <b>class="top-left"</b>:</p>
<box>
<h3>Top Left</h3>
<b>Top Left</b>
<badge class="top-left"><Icon name="inbox" size="14px" width="1px" /></badge>
</box>
<p>Bottom left position with <b>class="bottom-left"</b>:</p>
<box>
<h3>Bottom Left</h3>
<b>Bottom Left</b>
<badge class="bottom-left"><Icon name="inbox" size="14px" width="1px" /></badge>
</box>
<p>Top right position with <b>class="top-right"</b>:</p>
<box>
<h3>Top Right</h3>
<b>Top Right</b>
<badge class="top-right"><Icon name="inbox" size="14px" width="1px" /></badge>
</box>
<p>Bottom right position with <b>class="bottom-right"</b>:</p>
<box>
<h3>Bottom Right</h3>
<b>Bottom Right</b>
<badge class="bottom-right"><Icon name="inbox" size="14px" width="1px" /></badge>
</box>

3
client/bando/demos/Badge.svelte.md

@ -0,0 +1,3 @@
Badges are simple little notification icons placed on elements. The ones implemented here are _very_ minimalist, with just enough space to work with the [feather icons](https://feathericons.com) included with the project.
You can place most single character elements inside a badge but test that your font will scale appropriately on different screen sizes.

1
client/bando/demos/ButtonGroup.svelte.md

@ -0,0 +1 @@
ButtonGroup will bundle a strip of buttons together with one or more marked "active". It will correctly curve the corners of the first and last button, and can be run horizontally or vertically.

1
client/bando/demos/Calendar.svelte.md

@ -0,0 +1 @@
This is a _very_ basic implementation of a calendar that you might use in a fancy date picker. It does _not_ take into account the myriad of different date systems in the world, but it wouldn't be hard to change it to meet your needs. Consider it a starting point for your own Calendar if you need one.

4
client/bando/demos/Callout.svelte

@ -25,6 +25,10 @@
<span>This is an alert callout example.</span>
</callout>
<callout class="info">
<span>This is an info callout example.</span>
</callout>
<h2>Callout with Badges</h2>
<callout class="alert">

8
client/bando/demos/Callout.svelte.md

@ -0,0 +1,8 @@
The Callout is simply a way to focus the user on a piece of text they should see. There's different levels of callout you can use:
* `class="alert"` -- An alert style you can use to tell of impending doom. Change the variable `--red` or `--color-error` in the CSS to change it's color from the default monochrome.
* `class="success"` -- For success messages. Change `--color-good` or `--green` to change it in the CSS.
* `class="warning"` -- For warning messages. Change `--color-warning` or `--orange`.
* `class="info"` -- For info messages. Change `--color-info` or `--yellow`.
When I say "change color" I mean go into the file `static/monochrome.css` or `static/color.css` to change how it looks.

1
client/bando/demos/Cards.svelte.md

@ -0,0 +1 @@
Cards are used everywhere on the web, many times without you even knowing it. They allow you to show an image or similar media, some text, then some buttons. A variant of `Cards` is `Tiles` that are a horizontal layout of the same information. I generally use `Card` when I want a large display of the image or video, and `Tile` when I want a small display with a thumbnail or icon.

1
client/bando/demos/Carousel.svelte.md

@ -0,0 +1 @@
The `Carousel` is found mostly on landing pages and even then it's considered not very useful. The problem with this UI component is it makes it difficult to navigate the images, and if it's automated then users will have a hard time accessing something you show them when it passes by. If you're using it I suggest using it only on the landing page, and then if it auto-rotates have it stop when users touch it for the first time.

13
client/bando/demos/Chat.svelte

@ -12,16 +12,3 @@
<div>
<Chat />
</div>
<hr/>
<Markdown>
This is a simple chat panel that lets users watch the chat, but they can't
reply until they log in. The use case is on the LiveStream (see the
``Video`` component) where you want people to chat during the steam or watch
the chat.
The component talks to a backend handler in ``socket/chat.js`` which specifies what
to do on each message received. You should study both that code and ``client/components/Chat.svelte``.
</Markdown>

1
client/bando/demos/Chat.svelte.md

@ -0,0 +1 @@
The `Chat` is a small mini chat you might find on live streaming websites. It's not full featured but the client and backend are simple enough that you can change them to do what you want. To make it work you may need to look at `socket/chat.js` and make sure that's running correctly. If you don't use `Chat` on your site then consider removing `socket/chat.js` and edit `client/stores.js` to remove all [socket-io](https://socket.io) references. This will save you about 50k in the downloaded size of your app.

25
client/bando/demos/Code.svelte.md

@ -0,0 +1,25 @@
The `Code` component simplifies displaying code with line numbers and letting people copy the code to their clipboard. It will get the code from a URL, wrap it in a `<pre><code>..</code></pre>` block, and then convert each line to an inner `<span>` so it will receive a line number using CSS.
The CSS that makes this magic happen is:
```
pre code span::before {
counter-increment: line;
content: counter(line);
display: inline-block;
padding: 0 0.3rem;
margin-right: 0.5rem;
border-right: 1px solid var(--color-inactive);
min-width: 3ch;
text-align: right;
color: var(--color-inactive);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
```
This uses the `::before`` selector to add a line number counter to the front of each span, and then create a thin line. This is why you can use your mouse to select the lines of code but the _line numbers_ won't be selected (even if some browsers show you they are being selected).
To make it easier on people it will also just copy the code when you click on it and then do a `Toad` alert.

1
client/bando/demos/Countdown.svelte.md

@ -0,0 +1 @@
In my `LiveStream` usage I found I needed to tell people when the stream was starting with a countdown timer. This will implement a simple panel that you can layer over the video using the `Stacked/Layer` CSS.

21
client/bando/demos/Darkmode.svelte.md

@ -0,0 +1,21 @@
This simple little component will create a dark or light switch that changes the CSS and remembers the users setting. The technique uses this line from `static/colors.css` or `static/monochrome.css`:
```
[data-theme="dark"] {
```
This changes the CSS using the variables in that block to alter their colors to darker versions. Then in the JavaScript the theme is stored in the browser's local storage to remember it:
```
const set_theme = () => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
```
When the user comes back their theme is loaded with:
```
theme = localStorage.getItem('theme') ? localStorage.getItem('theme') : 'light';
set_theme()
```

3
client/bando/demos/DataTable.svelte.md

@ -0,0 +1,3 @@
The `DataTable` simply takes an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of objects and displays them using a typical table with rows and columns. It also provides pagination and search features by default.
It's intended to be a starting point for your own table display if you need one, and it's used in the `admin/` management system.

14
client/bando/demos/FairPay.svelte

@ -58,6 +58,7 @@
const payment_error = (event) => {
const { system, status } = event.detail;
console.log("Payment failed notice, tell Zed:", system, status);
payment_failed = true;
}
@ -69,12 +70,11 @@
if(!form.valid) return;
// posting to /api/user/payments is how you do a free purchase
let [status, data] = await api.post("/api/user/payments", { amount }, api.logout_user);
let [status, data] = await api.post("/api/user/payments", { amount: form.amount }, api.logout_user);
if(status === 200) {
paid_in_full = true;
} else {
console.error("status", status, "data", data);
payment_failed = true;
form.errors.main = data.message || data.error || "Payment Error.";
}
@ -146,7 +146,7 @@
<middle>
<h1>Welcome!</h1>
<p>Thank you for your purchase. You can now enjoy the entire
<p>Thank you for your purchase. You can now enjoy the entire
course and attend the live lessons.
</p>
</middle>
@ -164,7 +164,7 @@
<middle>
<p style="font-size: 1.5em">There was an error processing your payment. Please try again later.</p>
<p>You can email <a href="mailto:help@xor.academy">help@xor.academy</a> to get help with this purchase.</p>
<p>You can email <a href="mailto:help@yourbusiness.com">help@yourbusiness.com</a> to get help with this purchase.</p>
</middle>
<bottom>
@ -187,7 +187,7 @@
<middle>
<FormField form={ form } field="amount" label="{ form.valid ? quips[form.amount] : 'Bad Value!' }">
<label class="slider"
<label class="slider"
for="amount" style="--amount: { form.amount }"
on:click={ () => change_amount() }>
</label>
@ -198,7 +198,7 @@
<p>Click on the slider to change <b>how much you think this course is worth</b>, then
<b>select your payment method</b>.</p>
{:else}
<p><b>You've caused an error in this form. Please email help@xor.academy and tell them how you did this.</b></p>
<p><b>You've caused an error in this form. Please email help@yourbusiness.com and tell them how you did this.</b></p>
{/if}
</middle>
@ -231,5 +231,3 @@
<hr/>
<callout class="warning">You will need to be logged in for the payments to actually finish.</callout>

16
client/bando/demos/FairPay.svelte.md

@ -0,0 +1,16 @@
The FairPay component is more of a demo than an actual component. It shows how to use the
`Paypal.svelte` and `BTCPay.svelte` components. The `FairPay.svelte` implements a "pay what you
want" style of payments.
Why the $10 increments? There's a form of credit card crime called "carding" where people with
stolen numbers test their cards on any purchase that is $1. They'll get large numbers of possibly
valid credit cards and they need to test them. The best way to test them is to attempt a very small
purchase, and if the purchase works then they mark the card valid.
If you allow people to pay any amount as a donation then carders will raid your purchase form and
rack up $1 fraudulent charges. That may not seem to bad, but your payment processor will then
charge _you_ $15 or more to handle the charge back. Even if you offer to refund the purchase and
they do no work. This means if you receive $100 of fraudulent charges you would owe $1500 in fees.
The easiest way to prevent this is to simply only allow increments of $10. $10 is too high for most
carder testers, so this will deter them (at least until everyone does this).

7
client/bando/demos/Flipper.svelte

@ -1,12 +1,11 @@
<style>
card {
width: 400px;
border: 1px solid black;
}
</style>
<script>
import PlaceHolder from "$/client/components/PlaceHolder.svelte";
import IconImage from "$/client/components/IconImage.svelte";
let flipped = false;
</script>
@ -15,7 +14,7 @@
<inner>
<card class="front">
<top>
<PlaceHolder width={ 16 * 30 } height={ 9 * 30 } />
<IconImage name="arrow-left" />
</top>
<middle>
@ -33,7 +32,7 @@
</card>
<card class="back">
<top>
<PlaceHolder width={ 16 * 60 } height={ 9 * 60 } />
<IconImage name="arrow-right" />
</top>
<middle>

13
client/bando/demos/Flipper.svelte.md

@ -0,0 +1,13 @@
The `Flipper` shows two panels on the front and back of and lets you "flip" them around. This is a
decent way to restrict the user interface between two options, but it will most likely have problems
with accessibility without some additional `aria` attributes.
The technique uses the `transform-style: preserve-3d` to make sure that the browser treats it like a
3D space. Then it uses `transform: rotateY(180deg)` to do the flipping action.
You finally need these two lines to maintain the back and front:
```
-webkit-backface-visibility: hidden; /* Safari */
backface-visibility: hidden;
```

2
client/bando/demos/Form.svelte

@ -61,7 +61,7 @@
</FormField>
<FormField form={ form } field="subscribe" label="Want Notifications?">
<input type="checkbox" id="subscribe" name="subscribe" bind:value={ form.subscribe }>
<input type="checkbox" id="subscribe" name="subscribe" bind:checked={ form.subscribe }>
</FormField>
</middle>

10
client/bando/demos/Form.svelte.md

@ -0,0 +1,10 @@
This demonstrates how to use the `FormField` component to construct forms with nicer error messages. There are a lot of things you need to do for nice error messages on forms, and `FormField` handles most of them. You need to set variables for it, then place your input inside:
```
<FormField form={ form } field="subscribe" label="Want Notifications?">
<input type="checkbox" id="subscribe" name="subscribe" bind:checked={ form.subscribe }>
</FormField>
```
All of the input fields work mostly the same but with Svelte you need to use `bind:checked={
variable }` instead of `bind:value`.

7
client/bando/demos/LiveStream.svelte

@ -25,10 +25,3 @@
<Video poster={ stream.poster } background_color={video_background } source={ stream.source } />
</container>
<Markdown>
You can use the ``Video`` component to stream any HLS stream. On Apple's Safari this works natively and doesn't require any additional code. On other platforms you have to use the [HLS.js library](https://github.com/video-dev/hls.js/) in ``static/js/hls.js`` to decode and play the HLS protocol.
The ``Video`` component automatically negotiates the complex dance of figuring out when to load HLS.js.
This demo won't work without you modifying it with an HLS stream (or other stream supported by the ``&lt;video&gt;`` tag. Change the ``stream`` variable in ``client/bando/demos/LiveStream.svelte`` to enable one.
</Markdown>

5
client/bando/demos/LiveStream.svelte.md

@ -0,0 +1,5 @@
You can use the `Video` component to stream any HLS stream. On Apple's Safari this works natively and doesn't require any additional code. On other platforms you have to use the [HLS.js library](https://github.com/video-dev/hls.js/) in `static/js/hls.js` to decode and play the HLS protocol.
The `Video` component automatically negotiates the complex dance of figuring out when to load HLS.js.
This demo won't work without you modifying it with an HLS stream (or other stream supported by the `<video>` tag. Change the stream variable in `client/bando/demos/LiveStream.svelte` to enable one.

13
client/bando/demos/LoggedIn.svelte

@ -15,16 +15,3 @@
<button on:click={ () => $user.authenticated = !$user.authenticated }>Change Login</button>
<p>The LoggedIn component checks if the user is logged in by doing a GET to "/api/login" and if that API endpoint returns 401 or 403 it will perform redirects for you. It updates the <b>$user.authenticated</b> Svelte store (from <b>client/stores.js</b>) and then as your app changes that setting it will adapt and display different content.</p>
<p>Use this component to:</p>
<ol>
<li>Show different text to authenticated or not authenticated users.</li>
<li>Block a page's content from unauthenticated users.</li>
<li>Redirect users to your a new location if they are not authenticated.</li>
<li>Show a standard redirect page when it's redirecting.</li>
<li>Add as a Hydrate component to <b>rendered/pages/</b> pages to show login/logout state on static pages.</li>
</ol>
<p>In this test you are simply changing the $user.authenticated to see how the LoggedIn component handles it dynamically. You should still add a check for <b>$user.authenticated</b> in your own <b>onMount</b> since svelte will run them no matter what.
</p>

16
client/bando/demos/LoggedIn.svelte.md

@ -0,0 +1,16 @@
The LoggedIn component checks if the user is logged in by doing a GET to "/api/login" and if that API endpoint returns 401 or 403 it will perform redirects for you. It updates the `$user.authenticated` Svelte store (from `client/stores.js`) and then as your app changes that setting it will adapt and display different content.
Use this component to:
1. Block a page's content from unauthenticated users.
2. Redirect users to your a new location if they are not authenticated.
3. Show a standard redirect page when it's redirecting.
In this test you are simply changing the $user.authenticated to see how the LoggedIn component handles it dynamically. You should still add a check for `$user.authenticated` in your own `onMount` since svelte will run them no matter what.
<callout class="warning">
<span>
Currently you should only use LoggedIn as a major page blocking component rather than fine grained individual elements. It makes a network request to `/api/login` to confirm the user's logged in status, and if you have more than one each instance will do this check. If you see repeated calls to `/api/login` then check how your using this component on your pages.
</span>
</callout>

5
client/bando/demos/Login.svelte.md

@ -0,0 +1,5 @@
The `Login` is both in `client/pages/Login.svelte` and also in `client/components/Login.svelte`. The
one in `pages` is more like this demo and simply uses the `component`. The reason for the split is
so you can prompt for a login with a `Modal` in situations where tearing the user away from what
they're viewing might be wrong. A good example of this is if people are watching a video but need
to log in to comment or chat.

2
client/bando/demos/Markdown.svelte

@ -47,5 +47,5 @@ template syntax.
<h2>Security Warning</h2>
<p>There is <b>NO</b> HTML sanitization on this output, so do <b>NOT</b> render
user inupt without adding some form of sanitization like with <a href="https://github.com/cure53/DOMPurify">dompurify</a>. Consult the <a href="https://marked.js.org">marked</a> documentation for more information.
user inupt without adding some form of sanitization like with <a href="https://github.com/cure53/DOMPurify">dompurify</a>. Consult the <a href="https://github.com/developit/snarkdown">snardown</a> documentation for more information.
</p>

8
client/bando/demos/Modal.svelte

@ -27,10 +27,6 @@
</test-panel>
</Modal>
{:else}
<p>The Modal component is useful for short notices or UI elements when you don't want to
remove the user from a view they need to continue seeing. A good example usage is logging in
while they watch a video. Transitioning to an entirely new page just to ask for username/password
ruins their video viewing experience.
</p>
<button on:click={ () => modal_open = true }>OPEN MODAL</button>
<button on:click={ () => modal_open = true }>OPEN MODAL</button>
{/if}

8
client/bando/demos/Modal.svelte.md

@ -0,0 +1,8 @@
The `Modal` component is useful for short notices or UI elements when you don't want to remove the
user from a view they need to continue seeing. A good example usage is logging in while they watch
a video. Transitioning to an entirely new page just to ask for username/password ruins their video
viewing experience.
Try changing this `client/bando/demos/Model.svelte` file to not use the `{#if}` when the model
opens. You'll see that the button to open the modal stays around until you hover off. Just a quirk
of how the modal is layered and probably fixed with `z-index` if you run into it.

1
client/bando/demos/Pagination.svelte.md

@ -0,0 +1 @@
A reasonably complete `Pagination` system that will work with the [knex-paginate by felixmosh](https://github.com/felixmosh/knex-paginate) module. It tries to replicate the behavior of pagination system found on many websites that abreviates the pages when you reach the middle but still allow for jumping to the first or last.

3
client/bando/demos/PlaceHolder.svelte.md

@ -0,0 +1,3 @@
The `PlaceHolder` is a simple JavaScript generated placeholder image for when you're starting your
design. I prefer the `ImageIcon` instead of this one since it gives a better start and feedback on
the design.

4
client/bando/demos/Progress.svelte

@ -13,7 +13,7 @@
}
input {
width: 10ch;
width: 5ch;
}
</style>
@ -28,6 +28,6 @@
<p>Since those tags are already taken, ours will be called...<b>progress-meter</b>!</p>
<ProgressMeter percent={ progress } />
<br/>
<input placeholder="Percent Progress" type="numeric" bind:value={ progress } />

13
client/bando/demos/Progress.svelte.md

@ -0,0 +1,13 @@
The `Progress` demo uses either the standard tags found in browsers, or the
`client/components/Progress.svelte` component if you want a simple custom progress meter.
Your OS (or browser) should have native controls for progress, but there are two HTML tags
available:
1. `<progress>` -- It shows a ... progress.
2. `<mete>` -- It shows a ... meter which is apparently...totally different?
Go with whatever works for you, and if you want to altern the look then use the `<Progress/>` tag.
In the `static/global.css` file this is listed as a `<progress-meter>` tag to void conflicts with
`<progress>` or `<meter>`.

8
client/bando/demos/Sidebar.svelte

@ -8,14 +8,6 @@
{title: "Countdown", active: false, icon: "clock"},
{title: "Darkmode", active: false, icon: "sunrise"},
{title: "Form", active: false, icon: "database"},
{title: "Icon", active: false, icon: "feather"},
{title: "Login", active: false, icon: "log-in"},
{title: "Modal", active: false, icon: "maximize"},
{title: "Panels", active: false, icon: "columns"},
{title: "SnapImage", active: false, icon: "camera"},
{title: "Spinner", active: false, icon: "rotate-cw"},
{title: "Tabs", active: false, icon: "folder"},
{title: "Tiles", active: false, icon: "camera"},
{title: "Video", active: false, icon: "video"},
{title: "Sidebar", active: false, icon: "sidebar"},
];

6
client/bando/demos/Sidebar.svelte.md

@ -0,0 +1,6 @@
This `Sidebar` is a very simple version of the ones you find online. It doesn't support nested
accordion style structures and only a simple list of Icon+Title elements.
Look in `client/bando/Components.svelte` to see a real use of the sidebar to load components
dynamically. It uses `<svelte:component>` to render the chosen component, which comes up quite
often in applications.

8
client/bando/demos/SnapImage.svelte

@ -1,16 +1,16 @@
<script>
import SnapImage from "$/client/components/SnapImage.svelte";
const image = "/thumbs/valheim/thumb0025.jpeg";
const image = "https://zedshaw.games/thumbs/valheim/thumb0025.jpeg";
let source = "";
const colors = [[44,77,26],[127,187,60],[189,160,115],[88,153,44],[25,31,14],[124,151,61],[89,115,43],[65,122,34],[108,149,173],[89,119,109]];
const colors = [[44, 77, 26], [127, 187, 60], [189, 160, 115], [88, 153, 44], [25, 31, 14], [124, 151, 61], [89, 115, 43], [65, 122, 34], [108, 149, 173], [89, 119, 109]];
</script>
<SnapImage width="1600" height="800" colors={ colors } src={ source } />
<br />
<button on:click={ () => source = image }>Click to Load</button>
<p>The SnapImage generates an SVG from colors extracted from the image and injects that before the full image loads. It blurs this randomized color pre-loading image and when the real image is loaded it swaps the colors out for the loaded image. Normally you wouldn't have a delay like this, but the button <button on:click={ () => source = image }>Click to Load</button> is here so you can see how the loading works.
</p>

29
client/bando/demos/SnapImage.svelte.md

@ -0,0 +1,29 @@
`SnapImage` creates a temporary image using SVG that looks _kind of_ like the incoming image, and
when that image finally loads fades it into view. This helps with _perceived_ render speed in the
following ways:
1. The aspect ratio of the image is placed before the page loads, reducing or eliminating render shift.
2. A block of responsive SVG loads instantly which tricks the browser into fully rendering the whole page making it seem faster.
3. The rendered colors look enough like the image for the brief time they're present that people percieve the image as loading faster even when it's not.
By default `SnapImage` will use a set of random monochrome colors if none are given. You can change the options used to generate the random colors by setting `random_options` like this:
```
random_options = {
hue: "monochrome",
"format": "rgbArray",
"luminosity": "random",
count: 10
}
```
These options come from the [randomColor project by davidmerfield](https://github.com/davidmerfield/randomColor) which is used in the project for all random color generation.
Generating Image Colors
===
To generate your own colors there's a tiny script I'm using:
<callout class="warning">
<span>I need to find this code actually. Stay tuned.</span>
</callout>

8
client/bando/demos/Spinner.svelte

@ -2,5 +2,13 @@
import Spinner from "$/client/components/Spinner.svelte";
</script>
<h2>Basic Spinner</h2>
<p>Fairly basic, just an SVG that's rotated using a simple CSS animation.</p>
<Spinner />
<h2>Using aspect_ratio</h2>
<p>You can give an aspect ratio and the spinner will create a <code>&lt;div&gt;</code> with the <code>--aspect-ratio:</code> set from the <code>static/gobal.css</code> CSS. This will make it work as a compliment to the <code>.stacked</code> and <code>.layer</code> CSS so you can have a simple loading spinner over another component.
</p>
<Spinner aspect_ratio="16/9" />

16
client/bando/demos/Spinner.svelte.md

@ -0,0 +1,16 @@
The internet is slow, but Google says your website should be fast. Google can't meet their own
standards but that doesn't matter. All that matters is there's now a cost added to creating a
website because Google will punish _your_ content for loading slowly while _their_ content (and the
content of their buddies) can load slowly.
The Spinner helps tell users that a page is loading and it's typically used with `{#await}` and the
`client/helpers.js:defer()` function.
<callout class="warning">
<span>Find or create examples in the project for how to do this.
</span>
</callout>
Adding an `aspect_ratio` also helps you with page load speed because the page renders, it has less
layout shift, and there's a notification that it's done. Best of all the spinning is done in CSS
which doesn't count toward "execution" in Lighthouse.

41
client/bando/demos/StackLayer.svelte

@ -0,0 +1,41 @@
<script>
let first_top = true;
</script>
<style>
display {
width: 100%;
border: 1px solid var(--value0);
display: flex;
flex-direction: row;
}
first {
display: flex;
justify-content: center;
align-items: center;
background-color: var(--red);
}
second {
display: flex;
justify-content: center;
align-items: center;
background-color: var(--yellow);
}
</style>
<h2>Stack/Layers</h2>
<display class="stacked" style="--aspect-ratio: 10/3;">
<first class="layer" class:top={ first_top }>
<h1>I Am On Top</h1>
</first>
<second class="layer" class:top={ !first_top }>
<h1>I Am On Bottom</h1>
</second>
</display>
<br/>
<button on:click={() => first_top = !first_top}>Swap Layers</button>

25
client/bando/demos/StackLayer.svelte.md

@ -0,0 +1,25 @@
Layers with different opacity are a staple of digital compositing systems but weirdly CSS is terrible at it. The stack/layer CSS in `static/global.csS` implements a simple method using `display: grid` to place elements into a single cell. The CSS grid system will then correctly layer each element in a stack. The elements also are set to `position:relative` so you can change their `z-index` to shuffle them around. Alternatively, you can simply set one of the children to `class="top"` and that one element will be on top.
If you don't set a `background-color` color then the layers will be transparent. You can also use `opacity` to hide or fade the layers in the stack.
The CSS that is making this work is below (but look in `static/global.css` to confirm it's still done this way.
```
.stacked &lbrace;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 1fr;
grid-template-areas: "cover";
}
.stacked .layer &lbrace;
width: 100%;
position: relative;
grid-area: cover;
}
.stacked .top &lbrace;
z-index: 10;
}
```

8
client/bando/demos/Switch.svelte

@ -31,18 +31,18 @@
<p>Demo with Icons in rather than text.</p>
<input class="switch" type="checkbox" id="demo-5">
<label class="switch" for="demo-5">
<active><Icon name="eye" light={ true } size="18px" /></active>
<inactive><Icon name="eye-off" size="18px" /></inactive>
<active><Icon name="eye" size="18px" /></active>
<inactive><Icon name="eye-off" size="18px" /></inactive>
</label>
<input class="switch" type="checkbox" id="demo-6">
<label class="switch medium" for="demo-6">
<active><Icon name="eye" light={ true } size="24px" /></active>
<active><Icon name="eye" size="24px" /></active>
<inactive><Icon name="eye-off" size="24px" /></inactive>
</label>
<input class="switch" type="checkbox" id="demo-7">
<label class="switch large" for="demo-7">
<active><Icon name="eye" light={ true } size="36px" /></active>
<active><Icon name="eye" size="36px" /></active>
<inactive><Icon name="eye-off" size="36px" /></inactive>
</label>

4
client/bando/demos/Switch.svelte.md

@ -0,0 +1,4 @@
I generally think you shouldn't use this `Switch` as it's probably not going to work with screen
readers and will mostly just confuse users. I have it here as a demonstration of _how_ to alter an
element by _hiding_ it, but using its state to change the CSS of another element. This is a deep
CSS trick, which I hope you won't really need that often.

5
client/bando/demos/Tabs.svelte.md

@ -0,0 +1,5 @@
`Tabs` give you the common tabbed layout found in many applications for decades. The `Accordion`
demo just uses the `Tab` component with `vertical={true}`. It needs other Svelte components to
place into the tab panels, but it's implemented mostly in CSS. That means if you need to do a
different presentatino or can't use components for the panels then you can use the `<tab>` from
`static/global.css`.

2
client/bando/demos/Tiles.svelte

@ -30,5 +30,3 @@
</tile>
</content>
<p>This is a CSS only component that simply provides a classic "tile" layout. A tile seems to be organized with a left side that has an image/icon, a middle with content, and a right with buttons or other things. You don't need Svelte to make this work, and actually trying to use Svelte ends up making it more complex than just using CSS. To see how you use this just look in <b>client/bando/demos/Tiles.svelte</b>.
</p>

1
client/bando/demos/Tiles.svelte.md

@ -0,0 +1 @@
This is a CSS only component that simply provides a classic "tile" layout. A tile seems to be organized with a left side that has an image/icon, a middle with content, and a right with buttons or other things. You don't need Svelte to make this work, and actually trying to use Svelte ends up making it more complex than just using CSS. To see how you use this just look in `client/bando/demos/Tiles.svelte`.

7
client/bando/demos/Toast.svelte

@ -1,14 +1,14 @@
<script>
import Icon from "$/client/components/Icon.svelte";
import { fade } from "svelte/transition";
let bottom=false;
let bottom=true;
let top=false;
let right=false;
let right=true;
let left=false;
let active=true;
let reverse = false;
let toasts = [ "Toast 1" ];
let toasts = ["Toast 1"];
const add_toast = () => {
toasts.push(`Toast ${toasts.length + 1}`);
@ -24,7 +24,6 @@
<span on:click={ () => reverse = !reverse }><Icon name={ reverse ? "chevrons-up" : "chevrons-down" } size="48" /></span>
<p>A toast is a little pop-up that slides or fades in from the edge of the screen. It's used to notify of events without getting in the way of the UI. Use the arrow buttons to change the location of the <b>toast-list</b> and the plus to add more toasts. Use the chevrons to change the direction toasts are filled in.</p>
<toast-list class:bottom class:top
class:left class:right class:active class:reverse>

4
client/bando/demos/Toast.svelte.md

@ -0,0 +1,4 @@
A toast is a little pop-up that slides or fades in from the edge of the screen. It's used to notify of events without getting in the way of the UI. Use the arrow buttons to change the location of the `toast-list` and the plus to add more toasts. Use the chevrons to change the direction toasts are filled in.
This demo starts with the toasts in the most common position on the bottom-right. You can use the
arrows to test out different combinations and then hit _+_ to add random toasts.

12
client/bando/demos/Tooltip.svelte.md

@ -0,0 +1,12 @@
`Tooltip` simply displays a little overlay message on a component. It's best to not use this too
much as it can clutter your UI significantly. Try to find a way to not need the tooltip.
You can put a tooltip on a word using:
```
<word>tooltip word test<tooltip>I hover</tooltip></word>
```
Which works like this:
<word>tooltip word test<tooltip>I hover</tooltip></word>

47
client/bando/demos/Video.svelte

@ -1,51 +1,12 @@
<script>
import Video from "$/client/components/Video.svelte";
let source = "/videos/sample.mp4";
let poster = "/images/header.svg";
let source = "/videos/TNT_1_Calibrate_Your_Lights.720.mp4";
let poster = "/video_stills/TNT_1_Calibrate_Your_Lights.jpg";
let video_background = "rgba(0,0,0,0)";
</script>
<style>
container {
display: flex;
flex-direction: column;
width: 100%;
min-width: 770px;
margin-right: 0.5rem;
}
</style>
<h1>Video</h1>
<container class="pattern-lines-sm">
<Video poster={ poster } background_color={video_background } source={ source } />
<Video poster={ poster } background_color={video_background } source={ source } starts_on={ new Date(Date.now() + 10000) } />
</container>
<p>In order to maintain the aspect ratio of the video at 16:9 you have to create a container with this:</p>
<pre>
<code>
container &lcub;
display: flex;
flex-direction: column;
width: 100%;
min-width: 770px;
margin-right: 0.5rem;
&rcub;
</code>
</pre>
<p>If you don't the video is actually being displayed off screen because I'm using this stupid trick:</p>
<pre>
<code>
video-player &lcub;
overflow: hidden;
height: 0;
padding-top: calc(9 / 16 * 100%);
position: relative;
box-sizing: border-box;
&rcub;
</code>
</pre>
<p>That <code>padding-top: calc(9 / 16 * 100%);</code> is what keeps the aspect ratio. No, the actual <b>aspect</b> CSS setting does nothing since it's not really supported by anything despite out essential aspect ratios are.</p>

5
client/bando/demos/Video.svelte.md

@ -0,0 +1,5 @@
The `Video` component can handle both static video downloads and streaming using HLS. On platforms that support HLS video it will use the native `<video>` tag, but if the platform doesn't support it then it will fall back to [HLS.js](https://github.com/video-dev/hls.js/). Internally it uses the `client/fsm.js` code to keep everything straight while it juggles all the byzantine errors you have with videos.
Most of the options exported by `Video.svelte` make sense but the `aspect_ratio` setting is probably one you'll want to adjust if your videos aren't 16:9. This setting uses the CSS in `static/global.css` that uses `--aspect-ratio` to make sure the container holding the video stays at the correct aspect ratio. Without this setting the video's container will do weird things at different screen sizes.
This demo also shows how the countdown timer works with `starts_on` which is useful for live streaming situations where shows start at specific times.

35
client/bando/demos/WTVideo.svelte

@ -1,11 +1,10 @@
<script>
import WTVideo from "$/client/components/WTVideo.svelte";
import Markdown from "$/client/components/Markdown.svelte";
let media = {
"src": "/videos/sample.mp4",
"filename": "sample.mp4",
"poster": "/images/header.svg",
"poster": "/video_stills/sample.jpg",
"preload": "none",
"torrent_url": "/torrents/sample.torrent"
}
@ -18,38 +17,8 @@
margin-right: 0.5rem;
}
</style>
<h1>WebTorrent Video</h1>
<container class="pattern-lines-sm">
<WTVideo media={ media } download={ true }/>
</container>
<Markdown>
WebTorrent Video
==
The [WebTorrent](https://webtorrent.io) project provides a way for websites to
share the bandwidth load with the users viewing content. It works on almost any
content, but the first thing I I implemented is for Video.
This will most likely *only* work if you have your server on ``localhost:5001`` since the torrent file is configured for that. Also, the default ``npm run dev`` will run the simple tracker implementation found in ``services/tracker.js``. If you want to generate the torrents again then run:
```
sh scripts/mktorrents.sh sample.mp4
```
It uses the [mktorrents](https://github.com/pobrn/mktorrent) to generate the torrent since it is faster. Be careful of the WebTorrent specific options I use in the ``services/mktorrents.sh`` that are needed for WebTorrent to understand the torrent:
* ``-p`` -- Sets the private flag. This isn't needed for WebTorrent but is
needed if you want to add extra protections to people accessing your content
only after they log in.
* ``-l 15`` -- Sets the piece length to 15.
* ``-w localhost:5001`` -- Sets an initial web URL to use as a source. You
should remove this if you want them to only go through the tracker and other
users.
Download Link
===
If you want to let people download the video then add the ``download={ true }`` property. This demo sets that option so you can see how it works.
</Markdown>

24
client/bando/demos/WTVideo.svelte.md

@ -0,0 +1,24 @@
The [WebTorrent](https://webtorrent.io) project provides a way for websites to
share the bandwidth load with the users viewing content. It works on almost any
content, but the first thing I I implemented is for Video.
This will most likely *only* work if you have your server on `localhost:5001` since the torrent file is configured for that. Also, the default `npm run dev` will run the simple tracker implementation found in `services/tracker.js`. If you want to generate the torrents again then run:
```
sh scripts/mktorrents.sh sample.mp4
```
It uses the [mktorrents](https://github.com/pobrn/mktorrent) to generate the torrent since it is faster. Be careful of the WebTorrent specific options I use in the ``services/mktorrents.sh`` that are needed for WebTorrent to understand the torrent:
* `-p` -- Sets the private flag. This isn't needed for WebTorrent but is
needed if you want to add extra protections to people accessing your content
only after they log in.
* `-l 15` -- Sets the piece length to 15.
* `-w localhost:5001` -- Sets an initial web URL to use as a source. You
should remove this if you want them to only go through the tracker and other
users.
Download Link
===
If you want to let people download the video then add the `download={ true }` property. This demo sets that option so you can see how it works.

2
client/components/BTCPay.svelte