Merge remote-tracking branch 'upstream/main'

pull/25/head
cssbubble 2022-06-03 18:03:01 +01:00
commit 94f61d253f
13 zmienionych plików z 120 dodań i 74 usunięć

Wyświetl plik

@ -12,11 +12,17 @@ In conjunction with this, we're also building an online game where this is prese
[Visit https://learnawesome.vercel.app/](https://learnawesome.vercel.app/)
This is the exact same version. Your bookmarks will still be saved in localStorage so be assured that no personal data is being tracked or saved on this site.
<img width="1689" alt="image" src="https://user-images.githubusercontent.com/19304/171865328-b8c22482-15d7-476e-9b91-a1c482809f29.png">
But if you'd like faster performance or to self-host this, say in your company's intranet, you need a general-purpose computer (that means Linux/Windows/Mac but not crippled OSes like Android or iOS) with Datasette (which is an exploratory tool for SQLite databases) installed. You can find [installation instructions specific to your operating system here](https://docs.datasette.io/en/stable/installation.html).
<img width="1697" alt="image" src="https://user-images.githubusercontent.com/19304/171865412-e3bec0b3-3205-4f89-8d95-4c0af8476c7a.png">
After cloning this git repository on your local machine, run `datasette . -o` in the top-level directory to start the datasette serve and open the app in your browser.
<img width="1687" alt="image" src="https://user-images.githubusercontent.com/19304/171865546-9cd68586-a096-4754-a78f-9aaebe6164ae.png">
Your bookmarks are saved in localStorage so be assured that no personal data is being tracked or saved on this site.
But if you'd like faster performance or to self-host this, you need a general-purpose computer (that means Linux/Windows/Mac) with Datasette (which is an exploratory tool for SQLite databases) installed. You can find [installation instructions specific to your operating system here](https://docs.datasette.io/en/stable/installation.html).
After cloning this git repository on your local machine, run `npm run start` in the top-level directory to start the datasette server and open the app in your browser.
## To contribute:
@ -26,7 +32,7 @@ This is a Wikipedia-scale project and we could use all kind of help:
- To donate funds, [visit our OpenCollective](https://opencollective.com/learnawesome)
- To report bugs, [create an issue](https://github.com/learn-awesome/learndb/issues)
- To improve our topic taxonomy (improve sub-topics / prerequisites etc), [raise a PR on our Github with changes in `db/topics.csv` file](https://github.com/learn-awesome/learndb/tree/main/db)
- To improve the data about learning resources, [raise a PR on our Github with changes in `db/items.csv` file](https://github.com/learn-awesome/learndb/tree/main/db)
- To improve the data about learning resources, first read [db/README.md](db/README.md) and [raise a PR on our Github with changes in `db/items.csv` file](https://github.com/learn-awesome/learndb/tree/main/db)
- To improve design and suggest features, [start a discussion](https://github.com/learn-awesome/learndb/discussions)
- To fix technical bugs, [propose solutions on the issues](https://github.com/learn-awesome/learndb/issues)
- For anything else, [start a discussion](https://github.com/learn-awesome/learndb/discussions)
@ -34,8 +40,7 @@ This is a Wikipedia-scale project and we could use all kind of help:
## To develop:
When you modify the *.csv files in `db/`, you should re-generate the sqlite database with `./generatedb.sh`.
Run `npm run dev` to keep live-building the JS bundle as you edit the source code.
And then run `datasette . -o` to open the app in your browser.
Run `npm run dev` to keep live-building the JS bundle as you edit the source code. This automatically runs `datasette . -o` to open the app in your browser.
You can install Datasette's Vercel plugin with: `datasette install datasette-publish-vercel`.
To publish this, we first run `npm run build` followed by `npm run publish`.
@ -44,7 +49,8 @@ To publish this, we first run `npm run build` followed by `npm run publish`.
The dataset here is identical to https://learnawesome.org/. But there are no user accounts, no social features like learning feeds or ActivityPub. Users' bookmarks are saved in browser's localStorage.
The source data is in `db/*.csv` files. This is imported into a sqlite database with `./generatedb.sh`.
The source data is in `db/*.csv` files. The schema is described in [db/README.md](db/README.md).
These CSV files get imported into a sqlite database with `./generatedb.sh`.
We then rely on datasette to load this file and offer JSON APIs over HTTP.
Settings and metadata are specified in `settings.json` and `metadata.json` which datasette uses.

38
db/README.md 100644
Wyświetl plik

@ -0,0 +1,38 @@
# CSV format
## topics.csv
`name` is used as primary key and therefore, must be unique and avoid uppercase and special characters other than hyphen and slash. Here are some examples: `physics`, `linear-algebra`, `nations/india`, `programming-languages/objective-c`.
`display_name` is used as human-readable name and can preserve uppercase. For eg: `ADHD`.
`parent_id` should be the name of the parent topic. This makes it possible to show a hierarchical view. If a topic does not have `parent_id`, it would be at the top-level but if it doesn't have children topics of its own, it will be clubbed under a dummy top-level topic called `Misc`.
`sort_index` is an integer that's used for controlling the ordering in which topics are displayed.
## items.csv
`iid` should be a unique UUID. It is needed because `reviews.csv` needs to refer to items and there is no other natural primary key. Later, if we'd want to build collections of items, the same `iid` key would be helpful.
`description` can contain markdown with multiple lines.
`links` is an array value separated by `;`. Each item in this array a pair of `format` and `url` separated by `|`. For eg, `links` can have a value like this: `summary|https://sivers.org/book/Decisive;book|https://www.goodreads.com/book/show/15798078-decisive;summary|https://fourminutebooks.com/decisive-summary/`.
We are considering including other fields like `ipfsHash` and `image` in each value of `links`. This decision is yet to be made.
`topics` is a array value of topic names separated by `;`. These should exactly match `topics` table's `name` column.
`creators` is arbitrary string for now. For eg: `Charles Darwin`. In future, this might become a full record on its own including fields like `name`,`website`,`twitter`,`email`. In that case, we will have to somehow figure out unique key for each creator that could serve the role of primary key and foreign key.
`difficulty` must be empty or one of these: `childlike`, `beginner`, `intermediate`, `advanced`, `research`.
`rating` is on a 5.0 point scale with up to two decimal places allowed. This is a curated value and should not be simply copied from external sources.
`tags` can describe quality: `visual`, `entertaining`, `challenging`, `inspirational`, `interactive`.
## reviews.csv
`item_id` is a foreign key to `items.csv`.
`by` is the name of the person or item.
`blurb` is small description in markdown format.

Wyświetl plik

@ -6,7 +6,7 @@
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public --no-clear",
"start": "datasette . -o",
"publish": "datasette publish vercel learn.db --project learnawesome -m metadata.json --template-dir templates --static static:static --setting max_returned_rows 20000"
},
"devDependencies": {

Wyświetl plik

@ -20,7 +20,7 @@ function serve() {
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
server = require('child_process').spawn('npm', ['run', 'start'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});

Wyświetl plik

@ -97,10 +97,6 @@
<LibraryIcon class=" flex-shrink-0 h-6 w-6"/>
</NavButtonWithLabel>
<NavButtonWithLabel isActive={currentView === "/map"} target="#/map" label="TreeMap">
<LibraryIcon class=" flex-shrink-0 h-6 w-6"/>
</NavButtonWithLabel>
<NavButtonWithLabel isActive={currentView === "/formats"} target="#/formats" label="Formats">
<ViewGridIcon class=" flex-shrink-0 h-6 w-6"/>
</NavButtonWithLabel>
@ -127,11 +123,6 @@
<NavButtonWithLabel isActive={currentView === "/finishedlearning"} target="#/finishedlearning" label="Finished learning">
<BookmarkAltIcon class=" flex-shrink-0 h-6 w-6"/>
</NavButtonWithLabel>
<a href="/learn" target="_blank" class="text-indigo-100 hover:bg-lightSecondary1 hover:text-lightSecondary2 hover:dark:bg-darkPrimaryBg w-full group flex justify-start gap-3 items-center py-5 text-sm font-medium hover:dark:text-darkSecondary2 mb-5 pl-4">
<CogIcon class=" flex-shrink-0 h-6 w-6 "/>
<h3 class="text-center"> Datasette</h3>
</a>
</svelte:fragment>
</TailwindUI.AppShell>

Wyświetl plik

@ -10,28 +10,27 @@
let query = {
text: "",
topic: "",
format: "",
level: "",
tags: "",
tag: "",
sortby: "rating"
};
$: fetch(`/learn/items.json?_shape=array&links__contains=${format}|`)
$: query && fetch(`/learn/items.json?_shape=array&_size=100&links__contains=${format}|&topics__contains=${query.topic}`)
.then(r => r.json())
.then(data => {
items = data;
});
function handleQueryChanged(event){
console.log("queryChanged: ", event.detail);
// 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.topic && !item.topics.includes(query.topic)){ return false; }
if(query.level && item.difficulty != query.level){ return false; }
// TODO Apply tags filter
if(query.tag && !item.tags.includes(query.tag)){ return false; }
return true;
}).sort((a,b) => {
if(query.sortby == 'rating') { return (a.rating - b.rating) };

Wyświetl plik

@ -3,16 +3,12 @@
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",
tag: ""
};
@ -24,24 +20,19 @@
</script>
<form class="w-full p-2 inline-flex flex-wrap" on:submit|preventDefault>
<sl-input type="search" placeholder="Type something to search items by keywords" size="medium" clearable class="flex-1 border-0 p-0 focus:ring-0" value={query.text} on:sl-input="{e => query.text = e.target.value}">
<sl-input type="search" placeholder="Search by keywords" size="medium" clearable class="flex-1 border-0 p-0 focus:ring-0" value={query.text} on:sl-input="{e => query.text = e.target.value}">
<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)).slice(0,10)} selected={null}/>
<fluent-combobox autocomplete="both" placeholder="Any topic" class="ml-2 mt-1 outline-none border-2 border-grey-600" on:change="{e => query.topic = e.target.value}">
{#each alltopics.sort((a,b) => a.display_name.localeCompare(b.display_name)) as topic}
<fluent-option value={topic.name}>{topic.display_name}</fluent-option>
{/each}
</fluent-combobox>
{/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.tag = e.target.value}" value={query.tag}>
<sl-select class="ml-2 w-52" on:sl-change="{e => query.tag = e.target.value}" value={query.tag}>
<sl-menu-item value="">Any tag</sl-menu-item>
<sl-menu-item value="inspirational">Inspirational</sl-menu-item>
<sl-menu-item value="educational">Educational</sl-menu-item>
@ -49,6 +40,7 @@
<sl-menu-item value="entertaining">Entertaining</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="oer">Open (no login or pay)</sl-menu-item>
</sl-select>
<sl-select class="ml-2 w-44" on:sl-change="{e => query.level = e.target.value}" value={query.level}>
@ -60,15 +52,6 @@
<sl-menu-item value="research">Research</sl-menu-item>
</sl-select>
{#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>

Wyświetl plik

@ -14,9 +14,8 @@
let query = {
text: "",
topic: "",
format: "",
level: "",
tags: "",
tag: "",
sortby: "rating"
};
@ -28,15 +27,14 @@
});
function handleQueryChanged(event){
console.log("queryChanged: ", event.detail);
// 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 tags filter
if(query.tag && !item.tags.includes(query.tag)){ return false; }
return true;
}).sort((a,b) => {
if(query.sortby == 'rating') { return (a.rating - b.rating) };

Wyświetl plik

@ -4,4 +4,7 @@
import SearchForm from "./SearchForm.svelte"
</script>
<div class="flex justify-end">
<a href="#/map" class="font-semibold bg-lightTertiary text-lightBg rounded-lg hover:scale-110 px-3 py-2 hover:ease-in-out transition duration-200">Explore Map</a>
</div>
<TopicMasonryGrid {alltopics}/>

Wyświetl plik

@ -84,7 +84,7 @@
</sl-breadcrumb>
</div>
<div class="gap-8 columns-1 sm:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5 mb-8">
<div class="gap-8 columns-1 sm:columns-2 lg:columns-3 xl:columns-4 3xl:columns-5 mb-8">
{#each [...map.entries()].sort((t1,t2) => (t1[0].sort_index || 100) - (t2[0].sort_index || 100)) as parent}
<a href={"#/topic/" + parent[0].name}>
<div class="rounded-lg shadow-md p-4 break-inside-avoid mb-4 border-8 border-lightPrimCont dark:border-darkPrimCont hover:bg-lightPrimCont dark:bg-darkPrimCont ">

Wyświetl plik

@ -1,15 +1,24 @@
<script>
export let options = [];
let userInput = '3';
export let userInput = '';
$: filteredOptions = options.filter(x => x.label.startsWith(userInput))
$: console.log(userInput)
$: console.log({options})
$: console.log(filteredOptions)
$: console.log({userInput})
</script>
<sl-dropdown>
<sl-input slot="trigger" on:change="{e => userInput = e.target.value}" value={userInput}></sl-input>
<sl-dropdown class="ml-2">
<sl-input
slot="trigger"
class="p-0 m-0 border-0"
on:sl-input="{e => userInput = e.target.value}"
value={userInput}
placeholder="select topic">
</sl-input>
<sl-menu>
{#each filteredOptions as opt (opt.value)}
<sl-menu-item>{opt.label}</sl-menu-item>
<sl-menu-item on:click="{e => userInput = opt.value}">{opt.label}</sl-menu-item>
{/each}
</sl-menu>
</sl-dropdown>

Wyświetl plik

@ -6,6 +6,7 @@
<title>TreeMap</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style>
body {
margin: 0px;
@ -21,7 +22,9 @@ body {
}
text {
pointer-events: none;
cursor: pointer;
fill: #7a28cb;
}
.grandparent text {
@ -39,11 +42,11 @@ rect.parent,
}
.grandparent rect {
fill: rgb(232, 255, 0);
fill: #f4b393;
}
.grandparent:hover rect {
fill: rgb(202, 225, 0);
fill: #f4d393;
}
.children rect.parent,
@ -51,21 +54,30 @@ rect.parent,
cursor: pointer;
}
.leaf rect.parent {
cursor: pointer;
fill: #ffdbd1;
}
.leaf rect.parent:hover {
cursor: pointer;
fill: #ffcfd4;
}
.children rect.parent {
fill: #99F6E4;
fill: #ceec97;
fill-opacity: .5;
}
.children:hover rect.child {
fill: #D7CBFA;
fill: #deec97;
}
</style>
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300' rel='stylesheet' type='text/css'>
</head>
<body>
<p id="chart"></p>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript">
function hierarchy(topic_array, parent_id){
@ -78,8 +90,8 @@ function hierarchy(topic_array, parent_id){
.map(item => ({ ...item, children: nest(items, item.name), value: 100 }))
.map(item => {
return item.children.length == 0 ?
{id: item.name, name: item.name.split("/").reverse()[0], value: 100} :
{id: item.name, name: item.name.split("/").reverse()[0], children: item.children}
{id: item.name, name: item.name, value: 100} :
{id: item.name, name: item.name, children: item.children}
});
let topnodes = nest(topic_array, ''); // includes childless nodes
@ -88,13 +100,13 @@ function hierarchy(topic_array, parent_id){
let misc = {id: 'misc', name: 'Misc', children: topnodes.filter(t => t.value).slice(0,15)};
return {name: "·", children: [...topnodes.filter(t => t.children), misc]};
return {name: "", children: [...topnodes.filter(t => t.children), misc]};
}
d3.json("/learn/topics.json?_shape=array&_size=5000", function(alltopics){
// console.log({alltopics});
var root = hierarchy(alltopics, "");
console.log({root});
// console.log({root});
initialize(root);
accumulate(root);
layout(root);
@ -203,7 +215,11 @@ grandparent.append("text")
.classed("children", true)
.on("click", transition);
g.selectAll(".child")
g.filter(function(d) { return !d._children; })
.classed("leaf", true)
.on("click",(n) => { window.parent.location.href = "/#/topic/" + n.name;});
g.filter(function(d) { return d._children; }).selectAll(".child")
.data(function(d) { return d._children || [d]; })
.enter().append("rect")
.attr("class", "child")
@ -217,7 +233,8 @@ grandparent.append("text")
g.append("text")
.attr("dy", ".75em")
.text(function(d) { return d.name.split('{')[0].split('(')[0]
.on("click", (n) => { window.parent.location.href = "/#/topic/" + n.name;})
.text(function(d) { return d.name.split('/').reverse()[0].split('{')[0].split('(')[0]
.split('[')[0]; })
.call(text);
@ -260,7 +277,8 @@ grandparent.append("text")
function text(text) {
text.attr("x", function(d) { return x(d.x) + 10; })
.attr("y", function(d) { return y(d.y) + 10; });
.attr("y", function(d) { return y(d.y) + 10; })
.attr("text-decoration","underline");
}
function rect(rect) {

Wyświetl plik

@ -35,6 +35,7 @@
<link href="/static/bundle.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.73/dist/themes/light.css" />
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.73/dist/shoelace.js"></script>
<script type="module" src="https://unpkg.com/@fluentui/web-components"></script>
<title>LearnAwesome</title>
<!-- fonts -->