Browse Source

A much better active tag filter system.

pull/23/head
Zed A. Shaw 1 month ago
parent
commit
7caff6b9d1
9 changed files with 182 additions and 89 deletions
  1. +1
    -0
      db/blog/posts/01-first-blog-post.md
  2. +1
    -0
      db/blog/posts/02-svelte-and-sapper-so-far.md
  3. +34
    -0
      src/components/TagFilter.svelte
  4. +19
    -0
      src/node_modules/tags.js
  5. +34
    -22
      src/routes/blog/index.svelte
  6. +40
    -65
      src/routes/modules/index.svelte
  7. +1
    -1
      static/feed.atom
  8. +1
    -1
      static/feed.rss
  9. +51
    -0
      tests/utils.js

+ 1
- 0
db/blog/posts/01-first-blog-post.md View File

@@ -3,6 +3,7 @@
"date": "Apr 10, 2020",
"icon": "award",
"color": "gray",
"tag": "Announcement",
"pattern": "triangles-sm",
"summary": "A new kind of blog post for a new kind of blog."
}

+ 1
- 0
db/blog/posts/02-svelte-and-sapper-so-far.md View File

@@ -2,6 +2,7 @@
"author": "Zed A. Shaw",
"date": "Apr 11, 2020",
"has_image": true,
"tag": "Demo",
"summary": "How is Svelte and Sapper working for this website."
}


+ 34
- 0
src/components/TagFilter.svelte View File

@@ -0,0 +1,34 @@
<style lang="scss">
@import 'sass/_variables';

.filter-empty {
color: $gray-color !important;
background-color: $gray-color-light !important;
}

</style>

<script>
export let tags = [];
export let active = new Set();
</script>


<div class="filter">
<input type="radio" id="tag-0" class="filter-tag" name="filter-radio" hidden checked>
{#each tags as tag, i}
<input type="radio" id="tag-{i+1}" class="filter-tag" name="filter-radio" disabled="{!active.has(tag)}" hidden>
{/each}

<div class="filter-nav">
<label class="chip" for="tag-0">All</label>
{#each tags as tag, i}
<label class="chip" for="tag-{i+1}" class:filter-empty="{!active.has(tag)}">{tag}</label>
{/each}
</div>

<div class="filter-body">
<slot>
</slot>
</div>
</div>

+ 19
- 0
src/node_modules/tags.js View File

@@ -0,0 +1,19 @@
export const generate_tag_map = (tag_list) => {
let tag_map = {};
// remember we have to start at 1 because All is tag-0
tag_list.forEach((x, i) => tag_map[x] = `tag-${i+1}`);
return tag_map;
}

export const map_tags = (items, tag_map) => {
let tag_set = new Set();

let new_items = items.map(i => {
i.tag_id = tag_map[i.tag] || '';
tag_set.add(i.tag);
return i;
});

return [new_items, tag_set];
}


+ 34
- 22
src/routes/blog/index.svelte View File

@@ -3,16 +3,26 @@
</svelte:head>

<script context="module">
import { generate_tag_map, map_tags } from 'tags';

const tag_list = ["Opinion", "Demo", "Theory", "Announcement", "Misc"];
const tag_map = generate_tag_map(tag_list);

export async function preload({ params, query }) {
let res = await this.fetch(`api/blog/index.json`);
let blog = await res.json();
return { posts: blog.posts.map(x => x.metadata).reverse() };
let [posts, tag_set] = map_tags(blog.posts.map(x => x.metadata).reverse(), tag_map);

return { posts, tag_set };
}
</script>

<script>
import IconImage from '../../components/IconImage.svelte';
import TagFilter from '../../components/TagFilter.svelte';

export let posts;
export let tag_set;
</script>

<style lang="scss">
@@ -27,29 +37,31 @@
<div class="columns">
<div class="column col-12">
<h1>Learn JavaScript The Blog Way</h1>
<div class="grid">
{#each posts as post, index}
<div class="card grid-tall" id="post">
{#if post.image}
<div class="card-image">
<img class="img-responsive" src="{post.image}" alt="holder"/>
</div>
{:else}
<div class="card-image">
<IconImage name="{post.icon || 'mic'}" size="300" pattern="pattern-{post.pattern || 'diagonal-lines-sm'}" pattern_color="{post.color || 'yellow'}" width="1" />
</div>
{/if}
<div class="card-header">
<div class="card-title h5"> <a data-testid="blog-post-{index}" rel="prefetch" href="blog/{post.slug}">{post.title}</a></div>
{#if post.subtitle }<div class="card-subtitle text-gray">{ post.subtitle }</div>{/if}
<TagFilter tags="{tag_list}" active="{tag_set}">
<div class="container grid">
{#each posts as post, index}
<div class="filter-item card grid-tall" id="post" data-tag="{post.tag_id}">
{#if post.image}
<div class="card-image">
<img class="img-responsive" src="{post.image}" alt="holder"/>
</div>
<div class="card-body" data-testid="blog-summary">
{@html post.summary}
{:else}
<div class="card-image">
<IconImage name="{post.icon || 'mic'}" size="300" pattern="pattern-{post.pattern || 'diagonal-lines-sm'}" pattern_color="{post.color || 'yellow'}" width="1" />
</div>
<div class="card-footer text-right"> <a class="btn btn-primary btn-action" data-testid="blog-post" rel="prefetch" href="blog/{post.slug}"> <i class="icon icon-forward">&gt;</i></a></div>
</div>
{/each}
</div>
{/if}
<div class="card-header">
<div class="card-title h5"> <a data-testid="blog-post-{index}" rel="prefetch" href="blog/{post.slug}">{post.title}</a></div>
{#if post.subtitle }<div class="card-subtitle text-gray">{ post.subtitle }</div>{/if}
</div>
<div class="card-body" data-testid="blog-summary">
{@html post.summary}
</div>
<div class="card-footer text-right"> <a class="btn btn-primary btn-action" data-testid="blog-post" rel="prefetch" href="blog/{post.slug}"> <i class="icon icon-forward">&gt;</i></a></div>
</div>
{/each}
</div>
</TagFilter>
</div>
</div>
</div>

+ 40
- 65
src/routes/modules/index.svelte View File

@@ -1,23 +1,19 @@
<svelte:head>
<title>Learn JavaScript The Hard Way -- Modules</title>
</svelte:head>

<script context="module">
import { generate_tag_map, map_tags } from 'tags';

const tag_list = ["JS", "OS", "GUI", "Backend", "Misc"];
const tag_map = generate_tag_map(tag_list);

export async function preload({ params, query }) {
let res = await this.fetch(`api/modules/index.json`);
const tag_map = {
"All": "tag-0",
"OS": "tag-1",
"JS": "tag-2",
"GUI": "tag-3",
"Backend": "tag-4",
"Misc": "tag-5",
}

let json = await res.json();
let [modules, tag_set ] = map_tags(json, tag_map);

let modules = json.map(m => {
m.tag_id = tag_map[m.tag] || 'tag-5';
return m;
});

return { modules };
return { modules, tag_set };
}
</script>

@@ -25,10 +21,12 @@
import Icon from '../../components/Icon.svelte';
import Sidebar from '../../components/Sidebar.svelte';
import IconImage from '../../components/IconImage.svelte';
import TagFilter from '../../components/TagFilter.svelte';

let active = true;

export let modules;
export let tag_set;
</script>

<style lang="scss">
@@ -39,11 +37,6 @@
}
</style>

<svelte:head>
<title>Learn JavaScript The Hard Way -- Modules</title>
</svelte:head>


<Sidebar>
<ul class="nav" slot="nav">
<li class="nav-item active">
@@ -62,55 +55,37 @@
</ul>

<div slot="content" class="content" data-testid="modules-listing-page">
<div class="filter">
<input type="radio" id="tag-0" class="filter-tag" name="filter-radio" hidden checked>
<input type="radio" id="tag-1" class="filter-tag" name="filter-radio" hidden>
<input type="radio" id="tag-2" class="filter-tag" name="filter-radio" hidden>
<input type="radio" id="tag-3" class="filter-tag" name="filter-radio" hidden>
<input type="radio" id="tag-4" class="filter-tag" name="filter-radio" hidden>
<input type="radio" id="tag-5" class="filter-tag" name="filter-radio" hidden>

<div class="filter-nav">
<label class="chip" for="tag-0">All</label>
<label class="chip" for="tag-1">OS</label>
<label class="chip" for="tag-2">JS</label>
<label class="chip" for="tag-3">GUI</label>
<label class="chip" for="tag-4">Backend</label>
<label class="chip" for="tag-5">Misc</label>
</div>

<div class="filter-body">
<div class="container grid">
{#each modules as module, i}
<div class="filter-item card grid-tall" data-tag="{module.tag_id}">
<div class="card-image">
<a href="/modules/{module.slug}/" data-testid="module-link-{i}">
<IconImage name="{module.icon || 'book-open'}" size="300" pattern_color="{module.color || 'gray'}"
pattern="pattern-{module.pattern || 'dots-md'}" />
</a>
</div>
<TagFilter tags="{tag_list}" active="{tag_set}">
<div class="container grid">
{#each modules as module, i}
<div class="filter-item card grid-tall" data-tag="{module.tag_id}">
<div class="card-image">
<a href="/modules/{module.slug}/" data-testid="module-link-{i}">
<IconImage name="{module.icon || 'book-open'}" size="300" pattern_color="{module.color || 'gray'}"
pattern="pattern-{module.pattern || 'dots-md'}" />
</a>
</div>

<div class="card-header">
<div class="card-title h5"><a href="/modules/{module.slug}/">{ module.title }</a></div>
<div class="card-subtitle text-gray">{module.subtitle}</div>
</div>
<div class="card-header">
<div class="card-title h5"><a href="/modules/{module.slug}/">{ module.title }</a></div>
<div class="card-subtitle text-gray">{module.subtitle}</div>
</div>

<div class="card-body">
{ module.summary }
</div>
<div class="card-body">
{ module.summary }
</div>

<div class="card-footer">
{#if module.reserved }
<button class="btn btn-secondary">Return</button>
<a href="/modules/{module.slug}/" class="btn btn-success badge" data-badge="{module.completed}/{module.total}">Continue</a>
{:else}
<a href="/modules/{module.slug}/" class="btn btn-secondary">Start Course</a>
{/if}
</div>
<div class="card-footer">
{#if module.reserved }
<button class="btn btn-secondary">Return</button>
<a href="/modules/{module.slug}/" class="btn btn-success badge" data-badge="{module.completed}/{module.total}">Continue</a>
{:else}
<a href="/modules/{module.slug}/" class="btn btn-secondary">Start Course</a>
{/if}
</div>
{/each}
</div>
</div>
{/each}
</div>
</div>
</TagFilter>
</div>
</Sidebar>

+ 1
- 1
static/feed.atom View File

@@ -2,7 +2,7 @@
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://learnjsthehardway.com/</id>
<title>Learn JavaScript the Hard Way</title>
<updated>2020-04-26T19:49:05.273Z</updated>
<updated>2020-04-26T23:24:11.792Z</updated>
<generator>LJSTHW Custom Generator</generator>
<author>
<name>Zed A. Shaw</name>

+ 1
- 1
static/feed.rss View File

@@ -4,7 +4,7 @@
<title>Learn JavaScript the Hard Way</title>
<link>https://learnjsthehardway.com/</link>
<description>The blog for the Learn JavaScript the Hard Way course.</description>
<lastBuildDate>Sun, 26 Apr 2020 19:49:05 GMT</lastBuildDate>
<lastBuildDate>Sun, 26 Apr 2020 23:24:11 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>LJSTHW Custom Generator</generator>
<language>en</language>

+ 51
- 0
tests/utils.js View File

@@ -0,0 +1,51 @@
const t = require('@lib/testing');
const {User, Auth, knex} = require('@lib/models');

/* Common operations common to this test suite. */

exports.login = async (user, start) => {
let [browser, page] = await t.begin(start);

await page.waitForSelector(t.sel('nav-login'));
await page.click(t.sel('nav-login'));

// wait for the login page to show up then submit email
await page.waitForSelector(t.sel('email'));
await page.type(t.sel('email'), user.email);
await page.click(t.sel('email-button'));

// make sure that they get told they were sent an email
await t.has_content(page, t.sel('email-sent-msg'), "You've been sent");

// go in through the backend and get the token
let token = await Auth.token_by_email(user.email);
expect(token).toBeDefined();

// now submit it pretending we got it over an email
await page.waitForSelector(t.sel('token'));

// submit the token to log in
await page.type(t.sel('token'), token);
await page.click(t.sel('token-button'));

return [browser, page];
}


exports.register = async (main_user, page) => {
await t.has_content(page, t.sel('register-page'), "Please Register");

await page.type(t.sel('full-name'), main_user.name);
await page.waitForSelector(t.sel('tos-agree'));
await page.click(t.sel('send-emails'));
await page.click(t.sel('tos-agree'));
await page.click(t.sel('register-submit'));

await page.waitForSelector(t.sel('modules-listing-page'));

let {user, is_valid} = await User.find_and_validate(main_user.email, true);

expect(is_valid).toBe(true);

return [user, is_valid];
}

Loading…
Cancel
Save