pull/9/head^2
Nilesh 2022-05-30 12:18:19 +01:00
rodzic a282b05d16
commit ea4483604f
6 zmienionych plików z 90 dodań i 49 usunięć

Wyświetl plik

@ -1,21 +1,16 @@
# Learndb
# LearnAwesome
Non-social version of https://learnawesome.org/
An offline-browsable collection of learning resources organized by topics, formats, difficulty level etc.
Run `datasette . -o` in the top-level directory.
## Users
Schema:
- Format: inline string like book, course, video, audio, podcast, newsletter, game, toy, website, article etc
- Topic (id - using slash or dot separator for hierarchy, display_name, image)
- Why not an inline string?
- Need to support Special characters (dot, hyphen etc), preserve capitalization etc
- Hierarchy may change over time
- Item (id, name, description, image, []{format, URL/hash}, rating, topic_id: [], creator_ids: [], year, difficulty, cost, quality_tags, extra_data: {})
- Creator (id, name, description, category, social_urls_or_ids, photo)
- Review/Recommendation (id, item_id, by: item_id/creator_id, rating, blurb, URL, quality_tags)
Run `datasette . -o` in the top-level directory which opens the Datasette default view. Click on "home" in the top-left to open the custom UI which is much nicer.
Additional pages:
- Syllabus page per topic
- Format page
## Developers
To generate the sqlite database from the source files, run `generatedb.sh`
When you modify the *.csv files in `db/`, generate the sqlite database with `./generatedb.sh`.
Run `npm run dev` to keep building the JS bundle as you edit the source code.
## Details
The dataset here is identical to https://learnawesome.org/. But this runs on your computer so there are no user accounts, no social features like learning feeds or ActivityPub. Your bookmarks will be saved in browser's localStorage.

Wyświetl plik

@ -1,4 +1,8 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let showSearch = false;
let query = '';
let result_items = [];
let result_topics = [];
@ -16,20 +20,18 @@
});
</script>
<div class="relative">
<div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="overflow-y-auto p-4 sm:p-6 md:p-20">
<!--
Command palette, show/hide based on modal state.
{#if showSearch}
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
{/if}
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
-->
<div class="mx-auto max-w-xl transform overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
{#if showSearch}
<div class="fixed z-10 inset-0 overflow-y-auto" on:click="{e => dispatch('closed',{}) }">
<div class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0">
<div class="mx-auto w-xl transform overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all" on:click|stopPropagation>
<div class="relative">
<!-- Heroicon name: solid/search -->
<svg class="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@ -86,4 +88,6 @@
{/if}
</div>
</div>
</div>
</div>
{/if}
</div>

Wyświetl plik

@ -17,6 +17,7 @@
let currentView = "/topics";
let randomItemId;
let alltopics = [];
let showSearch = false;
function getRandomItemId(){
fetch('/learn.json?_shape=array&sql=select+rowid+from+items+order+by+random()+limit+1').then(r => r.json())
@ -70,13 +71,13 @@
<ItemDetail itemid={currentView.split("/")[2]}/>
{:else if currentView == "/random"}
{#if randomItemId}<ItemDetail itemid={randomItemId}/>{/if}
{:else if currentView === "/search"}
<AdvancedSearch/>
{:else if currentView === "/wanttolearn"}
<ItemList kind={0}/>
{:else if currentView === "/finishedlearning"}
<ItemList kind={1}/>
{/if}
<AdvancedSearch {showSearch} on:closed="{e => showSearch = false}"/>
</svelte:fragment>
<svelte:fragment slot="nav">
@ -92,10 +93,10 @@
<SearchIcon class=" flex-shrink-0 h-6 w-6"/>
<h3 class="text-center"> Random Item</h3>
</a>
<a href="#/search" class={(currentView === "/search" ? 'bg-lightSecondary1 text-lightSecondary2 dark:bg-darkPrimaryBg dark:text-darkSecondary2' : '') + " text-lightSecondary1 w-full hover:bg-lightSecondary1 hover:text-lightSecondary2 hover:dark:text-darkSecondary2 hover:dark:bg-darkPrimaryBg group flex flex-col items-center py-2 text-sm font-medium"}>
<button on:click="{e => showSearch = true}" class="text-lightSecondary1 w-full hover:bg-lightSecondary1 hover:text-lightSecondary2 hover:dark:text-darkSecondary2 hover:dark:bg-darkPrimaryBg group flex flex-col items-center py-2 text-sm font-medium">
<SearchIcon class=" flex-shrink-0 h-6 w-6"/>
<h3 class="text-center"> Search</h3>
</a>
</button>
<a href="#/wanttolearn" class={(currentView === "/wanttolearn" ? 'bg-lightSecondary1 text-lightSecondary2 dark:bg-darkPrimaryBg dark:text-darkSecondary2' : '') + " text-lightSecondary1 w-full hover:bg-lightSecondary1 hover:text-lightSecondary2 hover:dark:text-darkSecondary2 hover:dark:bg-darkPrimaryBg group flex flex-col items-center py-2 text-sm font-medium"}>
<BookmarkIcon class=" flex-shrink-0 h-6 w-6 "/>

Wyświetl plik

@ -1,15 +1,43 @@
<script>
import ItemCard from "./ItemCard.svelte"
import SearchForm from "./SearchForm.svelte"
export let format;
export let alltopics;
let items = [];
let filteredItems = [];
let query = {
text: "",
topic: "",
format: "",
level: "",
quality: "",
sortby: "rating"
};
$: fetch(`/learn/items.json?_shape=array&links__contains=${format}|`)
.then(r => r.json())
.then(data => {
items = data;
});
function handleQueryChanged(event){
console.log("queryChanged: ", event.detail);
query = event.detail;
}
$: filteredItems = items.filter(item => {
if(query.text && !item.name.toLowerCase().includes(query.text.toLowerCase())){ return false; }
if(query.format && !item.links.includes(query.format)) { return false; }
if(query.level && item.difficulty != query.level){ return false; }
// TODO Apply quality filter
return true;
}).sort((a,b) => {
if(query.sortby == 'rating') { return (a.rating - b.rating) };
if(query.sortby == 'year') { return (a.year - b.year)};
if(query.sortby == 'name') { return a.name.localeCompare(b.name)};
});
</script>
<div class="md:flex md:items-center md:justify-between mb-8">
@ -18,17 +46,17 @@
</div>
</div>
<SearchForm {alltopics} on:queryChanged={handleQueryChanged} hideFormat={true}/>
{#if format == 'book'}
<div class="mt-12 grid gap-5 grid-cols-2 sm:grid-cols-3 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 justify-items-center">
{#each items as item}
{#each filteredItems as item}
<ItemCard {item} displayType={format}/>
{/each}
</div>
{:else}
<div class="mt-12 max-w-lg mx-auto grid gap-5 lg:grid-cols-2 lg:max-w-none xl:grid-cols-3">
{#each items as item}
{#each filteredItems as item}
<ItemCard {item} displayType={format}/>
{/each}
</div>

Wyświetl plik

@ -3,12 +3,16 @@
import { createEventDispatcher } from 'svelte';
export let alltopics;
export let hideFormat = false;
export let hideTopic = false;
export let hideQuality = true;
let query = {
text: "",
topic: "",
format: "",
level: "",
quality: "",
sortby: "rating"
};
@ -23,14 +27,18 @@
<sl-icon name="search" slot="prefix"></sl-icon>
</sl-input>
{#if !hideTopic}
<ComboBox options={alltopics.map(t => { return {label: t.display_name, value: t.name}; }).sort((a,b) => a.label.localeCompare(b.label))} selected={null}/>
{/if}
{#if !hideFormat}
<sl-select class="ml-2 w-44" on:sl-change="{e => query.format = e.target.value}" value={query.format}>
<sl-menu-item value="">Any format</sl-menu-item>
<sl-menu-item value="book">Books</sl-menu-item>
<sl-menu-item value="video">Videos</sl-menu-item>
<sl-menu-item value="audio">Podcasts</sl-menu-item>
</sl-select>
{/if}
<sl-select class="ml-2 w-44" on:sl-change="{e => query.level = e.target.value}" value={query.level}>
<sl-menu-item value="">Any level</sl-menu-item>
@ -41,7 +49,16 @@
<sl-menu-item value="research">Research</sl-menu-item>
</sl-select>
<sl-select class="ml-2 w-44" on:sl-change="{e => query.sortby = e.target.value}" value={query.sortby}>
{#if !hideQuality}
<sl-select class="ml-2 w-44" on:sl-change="{e => query.quality = e.target.value}" value={query.quality}>
<sl-menu-item value="">Any quality</sl-menu-item>
<sl-menu-item value="visual">Visual</sl-menu-item>
<sl-menu-item value="interactive">Interactive</sl-menu-item>
<sl-menu-item value="entertaining">Entertaining</sl-menu-item>
</sl-select>
{/if}
<sl-select class="ml-2 w-52" on:sl-change="{e => query.sortby = e.target.value}" value={query.sortby}>
<sl-icon name="sort-down-alt" slot="prefix"></sl-icon>
<sl-menu-item value="rating">Sort by Rating</sl-menu-item>
<sl-menu-item value="year">Sort by Year</sl-menu-item>

Wyświetl plik

@ -18,6 +18,7 @@
topic: "",
format: "",
level: "",
quality: "",
sortby: "rating"
};
@ -37,6 +38,7 @@
if(query.text && !item.name.toLowerCase().includes(query.text.toLowerCase())){ return false; }
if(query.format && !item.links.includes(query.format)) { return false; }
if(query.level && item.difficulty != query.level){ return false; }
// TODO: apply quality filter
return true;
}).sort((a,b) => {
if(query.sortby == 'rating') { return (a.rating - b.rating) };
@ -50,23 +52,17 @@
<TopicMasonryGrid {topicname} {alltopics}/>
<SearchForm {alltopics} on:queryChanged={handleQueryChanged}/>
<!-- <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
{#each items as item}
<ItemCard {item}/>
{/each}
</div> -->
<SearchForm {alltopics} on:queryChanged={handleQueryChanged} hideTopic={true} hideFormat={true}/>
<div class="mt-10">
<div class="">
<sl-tab-group placement="start">
{#each formats.filter(f => items.filter(x => x.links.includes(f.id + '|')).length > 0) as format}
<sl-tab slot="nav" panel="{format.id}" class="">{format.name}</sl-tab>
{#each formats.filter(f => items.filter(x => x.links.includes(f.id + '|')).length > 0) as format, i}
<sl-tab slot="nav" panel={format.id} active={i == 0}>{format.name}</sl-tab>
{#if format.id == 'book'}
<sl-tab-panel name="{format.id}">
<sl-tab-panel name={format.id} active={i == 0}>
<div class="grid gap-5 grid-cols-2 sm:grid-cols-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 justify-items-center">
{#each filteredItems.filter(x => x.links.includes(format.id + '|')) as item}
<BookCard {item}/>
@ -75,7 +71,7 @@
</sl-tab-panel>
{:else if format.id == 'video'}
<sl-tab-panel name="{format.id}">
<sl-tab-panel name={format.id} active={i == 0}>
<div class="max-w-lg mx-auto grid gap-5 lg:grid-cols-2 lg:max-w-none xl:grid-cols-3">
{#each filteredItems.filter(x => x.links.includes(format.id + '|')) as item}
<VideoCard {item}/>
@ -84,7 +80,7 @@
</sl-tab-panel>
{:else}
<sl-tab-panel name="{format.id}">
<sl-tab-panel name={format.id} active={i == 0}>
<div class="max-w-lg mx-auto grid gap-5 lg:grid-cols-2 lg:max-w-none xl:grid-cols-3">
{#each filteredItems.filter(x => x.links.includes(format.id + '|')) as item}
<GenericCard {item}/>