Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blog: Added filter by tags #466

Merged
merged 30 commits into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
069c89a
Added Filter by Tag feature
NLTN Apr 30, 2022
481b9c8
Added TagField component
NLTN May 5, 2022
ec3022f
Updated TagField on:change event & Fixed minor bugs
NLTN May 9, 2022
148fef5
Updated TagField + Added discernTags() + Modified fetchNewsletters()
NLTN May 11, 2022
259d7fd
Optimized discernTags()
NLTN May 12, 2022
98affdb
Update src/lib/components/utils/acm-tagfield.svelte
NLTN May 12, 2022
2021d42
Update src/routes/blog/index.xml.ts
NLTN May 12, 2022
32a7dc2
Fix bugs and improved code quality.
NLTN May 13, 2022
a843815
Created a unit test for discernTags() function
NLTN May 13, 2022
baaac58
Added Svelte Slot component to TagField component
NLTN May 13, 2022
4c9e390
Code refactoring. Change the word "Tag" with "Label".
NLTN May 13, 2022
ff25d04
Optimized filterPosts()
NLTN May 15, 2022
3abe409
Changed slot name.
NLTN May 27, 2022
111206b
Update src/lib/components/utils/acm-labelfield.svelte
NLTN May 28, 2022
1d52644
Update src/routes/blog/index.svelte
NLTN May 28, 2022
2cafd31
Remove unnecessary code
NLTN May 28, 2022
3e9f8c4
Update src/routes/blog/index.json.ts
NLTN May 28, 2022
94fec94
Merge branch 'main' into fix/277
NLTN Jun 6, 2022
2dd72cf
Fixed lint errors
NLTN Jun 7, 2022
2a1fea3
Hide LabelField if there is no posts
NLTN Jun 20, 2022
90489f5
Merge branch 'main' into fix/277
EthanThatOneKid Aug 29, 2022
c0b4f53
Fixed a mistake made when merging conflicts
EthanThatOneKid Aug 29, 2022
49c6146
Added support for the "no posts" UI state.
EthanThatOneKid Aug 29, 2022
549716f
Removed unused import to pass the tests!
EthanThatOneKid Aug 29, 2022
92ed359
Merge branch 'main' into fix/277
EthanThatOneKid Aug 29, 2022
27aae8a
Refactored `fetchNewsletters` to handle DEBUG mode logic
EthanThatOneKid Aug 30, 2022
42086af
Made blog page to work with/without Javascript.
NLTN Aug 30, 2022
1afde39
Merge branch 'main' into fix/277
EthanThatOneKid Sep 1, 2022
c729607
Revert "Revert "Refactored caching logic for individual blog posts (#…
EthanThatOneKid Sep 1, 2022
5fb3e7d
Fixed typo in blog page title
EthanThatOneKid Sep 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/lib/common/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test, expect } from 'vitest';
import * as utils from './utils';

test('discerns no labels from empty input', () => {
const input = [];
const expected = [];
const output = utils.discernLabels(input);
expect(output).toEqual(expected);
});

test('discerns one label from length-1 input', () => {
const input = [{ labels: ['a'] }];
const expected = ['a'];
const output = utils.discernLabels(input);
expect(output).toEqual(expected);
});

test('discerns unique labels from list in alphabetical order', () => {
const input = [
{ labels: [] },
{ labels: ['abc def'] },
{ labels: ['d', 'c', 'c', 'c'] },
{ labels: ['e', 'd'] },
{ labels: ['c', 'a', 'b'] },
];
const expected = ['a', 'abc def', 'b', 'c', 'd', 'e'];
const output = utils.discernLabels(input);
expect(output).toEqual(expected);
});
8 changes: 4 additions & 4 deletions src/lib/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ interface WithLabels {
labels: string[];
}

export function discernTags(posts: WithLabels[]): string[] {
const tags = new Set([]);
export function discernLabels(posts: WithLabels[]): string[] {
const labels = new Set([]);
for (const post of posts) {
post.labels.forEach((label) => tags.add(label));
post.labels.forEach((label) => labels.add(label));
}
return Array.from(tags).sort();
return Array.from(labels).sort();
}
Original file line number Diff line number Diff line change
@@ -1,95 +1,102 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';

export let tags: string[] = [];
export let selectedTags: string[] = [];
export let label = '';
export let resetButton = '';
export let labels: string[] = [];
export let selectedLabels: string[] = [];
export let urlSearchParamKey = '';

const dispatch = createEventDispatcher();
let hasSelectedTags = selectedTags.length > 0;
let hasSelectedLabels = selectedLabels.length > 0;

function selectTag(event: MouseEvent) {
function selectLabel(event: MouseEvent) {
event.preventDefault();

if (selectedTags.includes(this.innerText)) {
selectedTags = selectedTags.filter((t) => t !== this.innerText);
if (selectedLabels.includes(this.innerText)) {
selectedLabels = selectedLabels.filter((t) => t !== this.innerText);
} else {
selectedTags.push(this.innerText);
selectedLabels.push(this.innerText);
}

this.classList.toggle('selected');
hasSelectedTags = selectedTags.length > 0;
dispatch('change', selectedTags);
hasSelectedLabels = selectedLabels.length > 0;
dispatch('change', selectedLabels);
}

function deselectAll(event: MouseEvent) {
event.preventDefault();
selectedTags = [];
hasSelectedTags = false;
dispatch('change', selectedTags);
selectedLabels = [];
hasSelectedLabels = false;
dispatch('change', selectedLabels);
}

function createTagURL(tag: string) {
function createLabelURL(label: string) {
if (urlSearchParamKey === '') {
return '';
}
const nextTags = selectedTags.includes(tag)
? selectedTags.filter((t) => t !== tag)
: selectedTags.concat([tag]);
const params = new URLSearchParams([[urlSearchParamKey, nextTags.join(',')]]);
const nextLabels = selectedLabels.includes(label)
? selectedLabels.filter((t) => t !== label)
: selectedLabels.concat([label]);
const params = new URLSearchParams([[urlSearchParamKey, nextLabels.join(',')]]);
return '?' + params.toString();
}
</script>

<div class="tag-box">
<div class="tag-title" class:hidden={hasSelectedTags}>{label}</div>
<a href="?" class="tag-clear-button" class:hidden={!hasSelectedTags} on:click={deselectAll}>
{resetButton}
</a>

<div class="tag-list">
{#each tags as tag}
<a
href={createTagURL(tag)}
class="tag"
class:selected={selectedTags.includes(tag)}
on:click={selectTag}
>
{tag}
</a>
{/each}
{#if labels.length > 0}
<div class="label-box">
<div class="title" class:hidden={hasSelectedLabels}>
<slot name="title" />
</div>
<a href="?" class="reset-button" class:hidden={!hasSelectedLabels} on:click={deselectAll}>
<slot name="reset-button" />
</a>

<div class="label-list">
{#each labels as label}
<a
href={createLabelURL(label)}
class="label"
class:selected={selectedLabels.includes(label)}
on:click={selectLabel}
>
{label}
</a>
{/each}
</div>
</div>
</div>
{/if}

<style lang="scss">
.tag-box {
.label-box {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.2em;
.tag-title {

.title {
font-size: 1em;
font-weight: bold;
margin-right: 1em;
cursor: default;
}

.hidden {
display: none;
}
.tag-clear-button {

.reset-button {
margin-right: 1em;
font-size: small;
text-decoration: underline;
cursor: pointer;
}
.tag-list {

.label-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 0.2em;
.tag {

.label {
margin-bottom: 0.2em;
margin-right: var(--size-sm);
padding: 0.25em 0.8em 0.25em 0.8em;
Expand All @@ -100,27 +107,33 @@
cursor: pointer;
transition: 0.25s ease-in-out;
text-decoration: none;

&:hover {
border-color: #b0bec5;
}

&.selected {
transition: 1s ease-in-out;
background-color: #81d4fa;
border-color: #4fc3f7;

&:before {
content: '✓ ';
}

&:hover {
border-color: #2196f3;
}
}
}
}

@media screen and (max-width: 900px) {
.tag-title {
.title {
display: none;
}
.tag-clear-button {

.reset-button {
display: none;
}
}
Expand Down
20 changes: 11 additions & 9 deletions src/routes/blog/_query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Officer } from '$lib/constants';
import { OFFICERS } from '$lib/constants';
import { discernTags } from '$lib/common/utils';
import { discernLabels } from '$lib/common/utils';

export interface BlogOutput {
tags: string[];
labels: string[];
posts: Newsletter[];
}

Expand Down Expand Up @@ -113,14 +113,16 @@ export async function fetchNewsletters(options?: NewsletterFetchOptions): Promis
body: JSON.stringify({ query: newslettersQuery }),
});

let newsletters = formatNewsletters(await response.json());
const tags = discernTags(newsletters);
let posts = formatNewsletters(await response.json());
const labels = discernLabels(posts);

if (options && options.labels.length > 0) {
newsletters = newsletters.filter((post) =>
post.labels.some((item) => options.labels.includes(item))
);
if (options?.labels.length > 0) {
posts = posts.filter((post) => post.labels.some((item) => options.labels.includes(item)));
}

return { tags: tags, posts: newsletters };
posts = posts.sort((a, b) => {
return new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf();
});

return { labels, posts };
}
25 changes: 19 additions & 6 deletions src/routes/blog/index.json.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import type { RequestHandlerOutput, RequestEvent } from '@sveltejs/kit/types/internal';
import { fetchNewsletters, NewsletterFetchOptions } from './_query';
import { DEBUG } from '$lib/constants';
import { posts } from './_testdata/posts';

const SAMPLE_POSTS = JSON.stringify(posts);

export async function get(event: RequestEvent): Promise<RequestHandlerOutput> {
const fetchOptions: NewsletterFetchOptions = { labels: [] };
const rawLabels = event.url.searchParams.get('l');

if (event.url.searchParams.has('l') === true && event.url.searchParams.get('l').length > 0) {
fetchOptions.labels = event.url.searchParams.get('l').split(',');
if (rawLabels?.length > 0) {
fetchOptions.labels = rawLabels.split(',');
}

return new Response(JSON.stringify(await fetchNewsletters(fetchOptions)), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
// Uses sample data when DEBUG = 1 or env variables are not satisfied.
const isSatisfied =
import.meta.env.VITE_GH_ACCESS_TOKEN !== undefined &&
import.meta.env.VITE_GH_DISCUSSION_CATEGORY_ID !== undefined;

return new Response(
DEBUG || !isSatisfied ? SAMPLE_POSTS : JSON.stringify(await fetchNewsletters(fetchOptions)),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
jaasonw marked this conversation as resolved.
Show resolved Hide resolved
}