initial commit with collapsible filters and filtering on categories

This commit is contained in:
Dave Lane 2024-09-19 16:23:30 +12:00
parent a15ad889d1
commit d2aab294bd
6 changed files with 150 additions and 145 deletions

View file

@ -59,11 +59,18 @@ and then run
docker-compose build docker-compose build
### You can push your new container to a docker container repository (I use my Forgejo instance!) You can push your new container to a docker container repository (I use my Forgejo instance!)
docker push forge.magnificent.nz/lightweight/webservices-app:latest docker push forge.magnificent.nz/lightweight/webservices-app:latest
### To start it up (and tail the logs - CTRL-C to quit out of the logs without affecting running container)
### To deploy the new container
At the location you're running production, pull the updated container
docker-compose pull
To start it up (and tail the logs - CTRL-C to quit out of the logs without affecting running container)
docker-compose up -d && docker-compose logs -f docker-compose up -d && docker-compose logs -f

View file

@ -0,0 +1,43 @@
<script>
// Ref: https://svelte.dev/repl/a5f4d395b15a44d48a6b2239ef705fc4?version=3.35.0
// based on suggestions from:
// Inclusive Components by Heydon Pickering https://inclusive-components.design/collapsible-sections/
export let headerText;
let expanded = false
</script>
<div class="collapsible">
<h3>
<button aria-expanded={expanded} on:click={() => expanded = !expanded}>{headerText}
<svg viewBox="0 0 20 20" fill="none" >
<path class="vert" d="M10 1V19" stroke="black" stroke-width="2"/>
<path d="M1 10L19 10" stroke="black" stroke-width="2"/>
</svg>
</button>
</h3>
<div class='contents' hidden={!expanded}>
<slot></slot>
</div>
</div>
<style>
.collapsible { border-bottom: 1px solid var(--gray-light, #eee); }
h3 { margin: 0; }
button {
background-color: var(--background, #fff);
color: var(--gray-darkest, #282828);
display: flex;
justify-content: space-between;
width: 100%;
border: none;
margin: 0;
padding: 1em 0.5em;
}
button[aria-expanded="true"] { border-bottom: 1px solid var(--gray-light, #eee); }
button[aria-expanded="true"] .vert { display: none; }
button:focus svg { outline: 2px solid; }
button [aria-expanded="true"] rect { fill: currentColor; }
svg { height: 0.7em; width: 0.7em; }
</style>

View file

@ -23,7 +23,7 @@
console.log('joinActive'); console.log('joinActive');
console.log('filterValues: ', filterValues); console.log('filterValues: ', filterValues);
if (isActive.length) return formatter.format(isActive); if (isActive.length) return formatter.format(isActive);
return 'None'; return 'No';
} }
function getActive($f) { function getActive($f) {
@ -88,11 +88,11 @@
<span class="filter {context}" class:active={filter.active} <span class="filter {context}" class:active={filter.active}
on:click="{() => { filter.active = !filter.active; updateFilterValues(filter);}}" on:click="{() => { filter.active = !filter.active; updateFilterValues(filter);}}"
on:keypress="{() => {filter.active = !filter.active; updateFilterValues(filter);}}" on:keypress="{() => {filter.active = !filter.active; updateFilterValues(filter);}}"
name="{filter.id}" role="button" tabindex={tabindex + filter.id}>{filter.name} ({filter.id})</span><wbr/> name="{filter.id}" role="button" tabindex={tabindex + filter.id}>{filter.name}</span><wbr/>
{/each} {/each}
</div> </div>
<p>{activeFiltersString} {filterText} active.</p> <!--<p>{activeFiltersString} {filterText} active.</p>-->
<style> <style>
h2 { h2 {
@ -122,6 +122,7 @@
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));*/ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));*/
} }
.filter { .filter {
font-size: 80%;
width: 80px; width: 80px;
height: auto; height: auto;
border: 1px #aaa solid; border: 1px #aaa solid;

View file

@ -0,0 +1,19 @@
import { writable } from 'svelte/store';
import { getContext, setContext } from 'svelte';
export function setFilter(context) {
let contextFilter = writable(0);
console.log('setFilter: ', contextFilter);
contextFilter.subscribe((value) => {
console.log('initial value: ', value);
});
setContext(context, contextFilter);
console.log('contextFilter (' + context + '): ', contextFilter);
}
export function getFilter(context) {
console.log('looking for writable for context ', context);
let f = getContext(context);
console.log('returning filter writable: ', f);
return f;
}

View file

@ -75,80 +75,5 @@ export const references = {
'Webserver', 'Webserver',
'Website Analytics' 'Website Analytics'
], ],
copyleft_list: ['AGPL-3', 'AGPL-3+', 'GPL', 'GPL-2', 'GPL-2+', 'GPL-3', 'GPL-3+', 'LGPL-3'], copyleft_list: ['AGPL-3', 'AGPL-3+', 'GPL', 'GPL-2', 'GPL-2+', 'GPL-3', 'GPL-3+', 'LGPL-3']
category_filter_list: [
'App Ecosystem',
'Application Design',
'Asset Management',
'Association Management',
'Association Management System',
'Blog Syndication',
'Book Reviews',
'Calendar',
'Collaborative',
'Collaborative Markdown Editing',
'Collaborative Wiki',
'Content Management Systems',
'Digital Media Sales',
'Document Management',
'Email Marketing Automation',
'Email Services',
'Enterprise Resource Planning',
'Event Management',
'Facial Recognition',
'Facility Booking',
'Federated Messaging',
'Fediverse',
'File Synchronisation',
'Forum',
'Framasoft',
'Home Automation',
'Home Security',
'Image Backup',
'Image Gallery',
'Kanban Project Management',
'Learning Management System',
'Link Ranking',
'Link Sharing',
'Link Shortener',
'Membership Management',
'Messaging Cache',
'Micro-blogging',
'Multi-Domain Server',
'Multimedia',
'Music Discovery',
'Music Streaming',
'Network Infrastructure Monitoring',
'Newsletters',
'Online Forms',
'Password Manager',
'Photos',
'Portfolio Management',
'Privacy',
'Productivity',
'Reading List',
'Remote Desktop Sharing',
'Remote Incremental Encrypted System Backups',
'Reverse Proxy',
'Rich Messaging Client',
'Rich Messaging Server',
'Scheduling',
'Server Monitoring',
'Share Secrets Management',
'Single Sign-On',
'Social Bookmarking',
'Social Media',
'Software Development Forge',
'Software Interface Designer',
'Streaming',
'Surveys',
'TURN/STUN Server',
'Video',
'Video Conferencing',
'Video Surveillance',
'Visualisation of Time Series Data',
'Webmail',
'Webserver',
'Website Analytics'
]
}; };

View file

@ -6,8 +6,11 @@
import { references } from '$lib/references.js'; import { references } from '$lib/references.js';
import { colours } from '$lib/colours.js'; import { colours } from '$lib/colours.js';
// the filters
import { setFilter, getFilter } from '$lib/components/filter.js'; import { setFilter, getFilter } from '$lib/components/filter.js';
import Filterable from '$lib/components/Filterable.svelte'; import Filterable from '$lib/components/Filterable.svelte';
// collapsible sections
import CollapsibleSection from '$lib/components/CollapsibleSection.svelte'
//console.log('colours: ', colours); //console.log('colours: ', colours);
@ -93,15 +96,9 @@
hosts[key] = host; hosts[key] = host;
} }
} }
//console.log('webservices.affiliates: ', webservices.affiliates);
for (let key in webservices.affiliates) { for (let key in webservices.affiliates) {
//console.log('key: ', key);
let affiliate = webservices.affiliates[key]; let affiliate = webservices.affiliates[key];
//console.log('affiliate assignment: ', affiliate);
affiliates[key] = affiliate; affiliates[key] = affiliate;
// if (affiliate.hasOwnProperty('name')) {
// affiliate_data[key] = affiliate;
// }
} }
return { return {
@ -242,29 +239,38 @@
const set2 = new Set(b); const set2 = new Set(b);
const intersection = [...set1].filter((element) => set2.has(element)); const intersection = [...set1].filter((element) => set2.has(element));
//console.log('intersection = ', intersection); console.log('intersection = ', intersection);
return intersection; return intersection;
} }
// converts a filter with id, name, and active fields into an array of names
function flattenFilter(filter) {
let flat = [];
if (filter.constructor === Array) {
filter.forEach(function(e) { if (e.active) flat.push(e.name) });
}
return flat;
}
// filter technologies based on a list of Categories // filter technologies based on a list of Categories
function filterTechnologiesByCategoryList(technologies, list) { function filterTechnologiesByCategoryList(technologies, list) {
console.log('looking for tech in categories: ', list); console.log('looking for tech in categories: ', list);
const included = []; const included = [];
technologies.forEach(function (tech, index) { technologies.forEach(function (tech) {
//console.log('tech(' + index + '): ', tech);
if (hasInstances(tech)) { if (hasInstances(tech)) {
const intersection = inCommon(tech.categories, list); const intersection = inCommon(tech.categories, list);
//console.log('categories: ', tech.categories); //console.log('categories: ', tech.categories);
//console.log('list: ', list); //console.log('list: ', list);
//console.log('intersection: ', intersection); console.log('intersection: ', intersection);
if (intersection && intersection.constructor === Array) { if (intersection && intersection.constructor === Array) {
//console.log('intersection length: '. intersection.length); //console.log('found intersection!', intersection.constructor);
console.log('intersection length: ', intersection.length);
if (intersection.length > 0) { if (intersection.length > 0) {
//console.log('including tech: ', tech); //console.log('including tech: ', tech);
included.push(tech); included.push(tech);
} }
} else { } else {
//console.log('intersection not array'); console.log('intersection not array');
} }
} }
}); });
@ -273,7 +279,6 @@
} }
const results = processData(webservices); const results = processData(webservices);
//console.log('results: ', results);
//const technologies = webservices.technologies; //const technologies = webservices.technologies;
//console.log(technologies); //console.log(technologies);
@ -312,29 +317,20 @@
const affiliate_colours = affiliateColours($affiliateFilter, colours, affiliates); const affiliate_colours = affiliateColours($affiliateFilter, colours, affiliates);
console.log('affiliate_colours:',affiliate_colours); console.log('affiliate_colours:',affiliate_colours);
//console.log('categories array: ', results.tech_tags.categories); let filteredTechnologies;
//console.log('categories keys: ', categories); let technologies;
//console.log('references: ', references); // reactive stuff...
/*console.log('category_list: ', category_list); $: {
//console.log('analogue_list: ', analogue_list); console.log('about to filterTechnologiesByCategoryList');
console.log('license_list: ', license_list); filteredTechnologies = filterTechnologiesByCategoryList(results.active_services, flattenFilter($categoryFilter));
console.log('status_list: ', status_list); const technologies = sortTechnologies(filteredTechnologies);
console.log('affiliate_list: ', affiliate_list); }
console.log('hosts_list: ', host_list);*/
/*function flipOn(list, id) {
const filtered_technologies = filterTechnologiesByCategoryList(
results.active_services,
references.full_category_list,
// references.category_filter_list
);
const technologies = sortTechnologies(filtered_technologies);
function flipOn(list, id) {
console.log('list: ' + list + ', id: ' + id); console.log('list: ' + list + ', id: ' + id);
return true; return true;
} }*/
</script> </script>
@ -354,40 +350,54 @@
<li>Total number of services: {results.total_instances}</li> <li>Total number of services: {results.total_instances}</li>
</ul> </ul>
</div> </div>
<CollapsibleSection headerText={'Show or Hide Filters'}>
<div class="filters"> <div class="filters">
<CollapsibleSection headerText={'Show or Hide Software Catergory Filter'}>
<div class="filter categories"> <div class="filter categories">
<Filterable filterValues={categoryFilter} context={'categories'}> <Filterable filterValues={categoryFilter} context={'categories'}>
<h2 slot="h2">Categories</h2> <h2 slot="h2">Categories</h2>
</Filterable> </Filterable>
</div> </div>
</CollapsibleSection>
<CollapsibleSection headerText={'Show or Hide Software Analogue Filter'}>
<div class="filter analogues"> <div class="filter analogues">
<Filterable filterValues={analogueFilter} context={'analogues'}> <Filterable filterValues={analogueFilter} context={'analogues'}>
<h2 slot="h2">Analogues</h2> <h2 slot="h2">Analogues</h2>
</Filterable> </Filterable>
</div> </div>
</CollapsibleSection>
<CollapsibleSection headerText={'Show or Hide License Filter'}>
<div class="filter licenses"> <div class="filter licenses">
<Filterable filterValues={licenseFilter} context={'licenses'}> <Filterable filterValues={licenseFilter} context={'licenses'}>
<h2 slot="h2">Open Source & Copyleft Licenses</h2> <h2 slot="h2">Open Source & Copyleft Licenses</h2>
</Filterable> </Filterable>
</div> </div>
</CollapsibleSection>
<CollapsibleSection headerText={'Show or Hide Service Status Filter'}>
<div class="filter statuses"> <div class="filter statuses">
<Filterable filterValues={statusFilter} context={'statuses'}> <Filterable filterValues={statusFilter} context={'statuses'}>
<h2 slot="h2">Statuses</h2> <h2 slot="h2">Statuses</h2>
</Filterable> </Filterable>
</div> </div>
</CollapsibleSection>
<CollapsibleSection headerText={'Show or Hide Service Affiliates Filter'}>
<div class="filter affilates"> <div class="filter affilates">
<Filterable filterValues={affiliateFilter} context={'affiliates'}> <Filterable filterValues={affiliateFilter} context={'affiliates'}>
<h2 slot="h2">Affiliates</h2> <h2 slot="h2">Affiliates</h2>
</Filterable> </Filterable>
</div> </div>
</CollapsibleSection>
<CollapsibleSection headerText={'Show or Hide Service Host filter'}>
<div class="filter hosts"> <div class="filter hosts">
<Filterable filterValues={hostFilter} context={'hosts'}> <Filterable filterValues={hostFilter} context={'hosts'}>
<h2 slot="h2">Hosts</h2> <h2 slot="h2">Hosts</h2>
</Filterable> </Filterable>
</div> </div>
</CollapsibleSection>
</div> </div>
</CollapsibleSection>
<div class="tiles"> <div class="tiles">
{#each filtered_technologies as technology} {#each filteredTechnologies as technology}
<div class="tile technology"> <div class="tile technology">
<div class="links"> <div class="links">
{#if technology.repository}<span class="repository" {#if technology.repository}<span class="repository"