Skip to content
This repository has been archived by the owner on Nov 13, 2022. It is now read-only.

Commit

Permalink
Initializing repo.
Browse files Browse the repository at this point in the history
  • Loading branch information
nlundquist committed Jan 8, 2020
0 parents commit 3e9f814
Show file tree
Hide file tree
Showing 37 changed files with 2,422 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.idea
.DS_Store
node_modules
dist
package-lock.json
68 changes: 68 additions & 0 deletions components/advisor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { html } from 'lit-html';
import { component, useEffect, useRef } from 'haunted';
import { syringeServiceRoot } from "../helpers/page-state.js";
import useFetch from '../helpers/use-fetch.js'

function Advisor({host}) {
const syringeServicePrefix = host ? host+'/syringe' : syringeServiceRoot;
const awesompleteRef = useRef(null);
const allLessonRequest = useFetch(`${syringeServicePrefix}/exp/lesson`);
const lessonOptions = allLessonRequest.succeeded
? allLessonRequest.data.lessons.map((l) => ({
label: l.LessonName,
value: l.LessonId
}))
: [];

if (lessonOptions.length > 0) {
useEffect(() => {
const input = this.shadowRoot.querySelector('input');
awesompleteRef.current = new Awesomplete(input, {
list: lessonOptions,
minChars: 0
});
}, []);
}

function select(ev) {
const lessonId = ev.text.value;
location.href = `${host || ''}/advisor/courseplan.html?lessonId=${lessonId}`;
}

return html`
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/nlundquist/nre-styles@latest/dist/styles.css" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.css" rel="stylesheet "/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.base.css" rel="stylesheet" />
<style>
.awesomplete > ul:before,
.input-wrapper .awesomplete:before{
display: none;
}
</style>
<div class="advisor canister secondary">
<h1>
<span>NRE Labs Advisor</span>
<span class="subtitle">Get a customized lesson path</span>
</h1>
<div class="input-wrapper">
<input type="text" placeholder="I want to learn..."
@awesomplete-select=${select}
class="awesomeplete" />
</div>
<button class="btn secondary">Search Lesson Content</button>
<aside class="small">
Use the box above to say what you want to learn, and we’ll work with you
to build a relevant learning path. Try “Python” or “StackStorm”!
</aside>
</div>
`;
}

Advisor.observedAttributes = ['host'];

customElements.define('antidote-advisor', component(Advisor));

export default Advisor;
33 changes: 33 additions & 0 deletions components/catalog-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import '../contexts.js'; // make sure all contexts are defined
import { html } from 'lit-html';
import { component, useState } from 'haunted';
import { syringeServiceRoot } from "../helpers/page-state.js";
import useFetch from '../helpers/use-fetch.js'

customElements.define('antidote-catalog-context', component(() => {
const allLessonRequest = useFetch(`${syringeServiceRoot}/exp/lesson`);
const [filteringState, setFilteringState] = useState({
searchString: null,
Category: null,
Duration: null,
Difficulty: null,
Tags: []
});

return html`
<style>
:host,
antidote-all-lesson-context-provider,
antidote-lesson-filtering-context-provider {
display: block;
height: 100%;
width: 100%;
}
</style>
<antidote-all-lesson-context-provider .value=${allLessonRequest}>
<antidote-lesson-filtering-context-provider .value=${[filteringState, setFilteringState]}>
<slot></slot>
</antidote-lesson-filtering-context-provider>
</antidote-all-lesson-context-provider>
`
}));
72 changes: 72 additions & 0 deletions components/catalog-filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { html } from 'lit-html';
import { component, useContext } from 'haunted';
import { AllLessonContext, LessonFilteringContext } from "../contexts.js";
import debounce from "../helpers/debounce.js";

function getOptionSetsFromLessons(lessons) {
const categories = new Set();
const tags = new Set();
lessons.forEach((l) => {
categories.add(l.Category);
(l.Tags || []).forEach((t) => tags.add(t));
});
return [Array.from(categories), Array.from(tags)];
}

function CatalogFilters() {
const allLessonRequest = useContext(AllLessonContext);
const [filterState, setFilterState] = useContext(LessonFilteringContext);
const [categories, tags] = allLessonRequest.succeeded
? getOptionSetsFromLessons(allLessonRequest.data.lessons)
: [[], []];

function setFilter(filterName) {
return debounce(function() {
const value = filterName === 'Tags'
? filterState.Tags = this.value.split(',').map((t) => t.trim()).filter((s) => s.length > 0)
: this.value || null;

filterState[filterName] = value;
setFilterState(filterState);
}, 200);
}

return html`
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/nlundquist/nre-styles@latest/dist/styles.css" />
<style>
:host {
display: flex;
}
:host > label {
flex-grow: 1;
}
:host > label:not(:first-of-type) {
margin-left: 30px;
}
</style>
<label>
<span>Category</span>
<div>
<antidote-select
placeholder="Label"
.options=${categories}
.change=${setFilter('Category')} />
</div>
</label>
<label>
<span>Tags</span>
<div>
<antidote-select
placeholder="Label, Label"
multi="true"
.options=${tags}
.change=${setFilter('Tags')} />
</div>
</label>
`;
}

customElements.define('antidote-catalog-filters', component(CatalogFilters));

export default CatalogFilters;
29 changes: 29 additions & 0 deletions components/catalog-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { html } from 'lit-html';
import { component, useContext } from 'haunted';
import { LessonFilteringContext } from "../contexts.js";
import debounce from "../helpers/debounce.js";

function CatalogSearch() {
const [filterState, setFilterState] = useContext(LessonFilteringContext);

const change = debounce(function change() {
filterState.searchString = this.value.length > 0 ? this.value.toLowerCase() : null;
setFilterState(filterState);
}, 200);

return html`
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/nlundquist/nre-styles@latest/dist/styles.css" />
<style>
:host {
display: block;
}
</style>
<label>
<span>Search</span>
<input type="text" placeholder="Lesson Title"
@keyup=${change} @change=${change} />
</label>
`;
}

customElements.define('antidote-catalog-search', component(CatalogSearch));
83 changes: 83 additions & 0 deletions components/catalog-table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { html } from 'lit-html';
import { component, useContext} from 'haunted';
import { AllLessonContext, LessonFilteringContext } from "../contexts.js";

function doFiltering(lessons, filteringState) {
const filterEntries = Object.entries(filteringState);

return lessons.filter((lesson) => {
return filterEntries.reduce((acc, [filterProp, filterValue]) => {
if (filterValue !== null) {
if (filterProp === 'Tags') {
return acc && filterValue.every((tag) => (lesson.Tags || []).includes(tag));
} else if (filterProp === 'searchString') {
return acc && lesson.LessonName.toLowerCase().indexOf(filterValue) > -1;
} else {
return acc && lesson[filterProp] === filterValue;
}
} else {
return acc;
}
}, true);
});
}

function CatalogTable() {
const allLessonRequest = useContext(AllLessonContext);
const [filteringState] = useContext(LessonFilteringContext);
const lessons = allLessonRequest.succeeded
? doFiltering(allLessonRequest.data.lessons, filteringState)
: [];

return html`
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/nlundquist/nre-styles@latest/dist/styles.css" />
<style>
/*todo: move to nre theme?*/
/*todo: remove row hover ughhhhh */
.tag {
display: inline-flex;
align-items: center;
padding: 2px 5px;
word-spacing: normal;
border: 2px solid #0096c3;
color: #0096c3;
background-color: white;
margin-top: 10px;
}
.tags {
word-spacing: 10px;
padding: 0 10px 10px 8px;
}
</style>
<table class="catalog">
<thead>
<tr>
<th>Lesson</th>
<th>Description</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
${lessons.map((lesson) => html`
<tr>
<td class="title">
<a href="/labs/?lessonId=${lesson.LessonId}&lessonStage=1">
${lesson.LessonName}
</a>
</td>
<td>${lesson.Description}</td>
<td class="tags">
${(lesson.Tags || []).map((tag) => html`
<span class="tag">${tag}</span>
`)}
</td>
</tr>
`)}
</tbody>
</table>
`;
}

customElements.define('antidote-catalog-table', component(CatalogTable));

export default CatalogTable;
64 changes: 64 additions & 0 deletions components/collection-details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import '../contexts.js'; // make sure all contexts are defined
import { html } from 'lit-html';
import { component, useState } from 'haunted';
import { syringeServiceRoot, collectionId } from "../helpers/page-state.js";
import useFetch from '../helpers/use-fetch.js'

function CollectionDetails() {
const request = useFetch(`${syringeServiceRoot}/exp/collection/${collectionId}`);

return html`
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/nlundquist/nre-styles@latest/dist/styles.css" />
<style>
h1 {
text-align: center;
}
</style>
${request.succeeded ? html`
<h1>${request.data.Title}</h1>
<img src="${request.data.Image}" />
<p>${request.data.LongDescription}</p>
<table>
<tr><td>Type</td><td>${request.data.Type}</td></tr>
<tr>
<td>Website</td>
<td>
<a href="${request.data.Website}">${request.data.Website}</a>
</td>
</tr>
<tr>
<td>Email</td>
<td>
<a href="mailto:${request.data.ContactEmail}">${request.data.ContactEmail}</a>
</td>
</tr>
</table>
<div class="canister medium-gray">
${request.data.Lessons ? html`
<h3>Lessons</h3>
${request.data.Lessons.map((lesson, i) => html`
<div>
<a href="/labs/?lessonId=${lesson.lessonId}&lessonStage=1">
${lesson.lessonName}
</a>
<p>
${lesson.lessonDescription}
</p>
</div>
${request.data.Lessons.length !== i + 1 ? html`<hr/>` : ''}
`)}
` : html `
<h3>Coming Soon!</h3>
`}
</div>
` : ''}
`
}

customElements.define('antidote-collection-details', component(CollectionDetails));

export default CollectionDetails;
30 changes: 30 additions & 0 deletions components/collections-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import '../contexts.js'; // make sure all contexts are defined
import { html } from 'lit-html';
import { component, useState } from 'haunted';
import { syringeServiceRoot } from "../helpers/page-state.js";
import useFetch from '../helpers/use-fetch.js'

customElements.define('antidote-collections-context', component(() => {
const allCollectionRequest = useFetch(`${syringeServiceRoot}/exp/collection`);
const [filteringState, setFilteringState] = useState({
searchString: null,
Type: null,
});

return html`
<style>
:host,
antidote-all-collection-context-provider,
antidote-collection-filtering-context-provider {
display: block;
height: 100%;
width: 100%;
}
</style>
<antidote-all-collection-context-provider .value=${allCollectionRequest}>
<antidote-collection-filtering-context-provider .value=${[filteringState, setFilteringState]}>
<slot></slot>
</antidote-collection-filtering-context-provider>
</antidote-all-collection-context-provider>
`
}));
Loading

0 comments on commit 3e9f814

Please sign in to comment.