kopia lustrzana https://github.com/learn-awesome/learndb
First commit
commit
94cdb77f7f
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Learndb
|
||||
|
||||
Non-social version of https://learnawesome.org/
|
||||
|
||||
Run `datasette . -o` in the top-level directory.
|
||||
|
||||
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)
|
||||
|
||||
Additional pages:
|
||||
- Syllabus page per topic
|
||||
- Format page
|
||||
|
||||
To generate the sqlite database from the source files, run `generatedb.sh`
|
|
@ -0,0 +1,2 @@
|
|||
topic,name,description,sequence
|
||||
mathematics,Learn math in 21 minutes,,
|
|
|
@ -0,0 +1,2 @@
|
|||
name,description,category,photo,social_urls
|
||||
Bill Gates,techbro,founder,,@billgates
|
|
|
@ -0,0 +1,2 @@
|
|||
name,description,image,links,topics,creators,year,difficulty,cost,extra_data
|
||||
sapiens,,https://learn-awesome.github.io/assets/book_covers/23692271.jpg,book|https://learnawesome.org/items/6c2ef4c8-a018-430a-9173-3868310a03ea-sapiens-a-brief-history-of-humankind,history
|
|
|
@ -0,0 +1 @@
|
|||
id,item_id,by,rating,blurb,url
|
|
|
@ -0,0 +1,6 @@
|
|||
id,display_name,image,first_parent_topic_id,second_parent_topic_id
|
||||
physics,Physics
|
||||
mathematics,Maths,,physics,
|
||||
language.english,English
|
||||
programming.java,Java,,,,
|
||||
history,History,,language.english,
|
|
|
@ -0,0 +1,6 @@
|
|||
rm learn.db
|
||||
sqlite-utils insert learn.db creators db/creators.csv --csv
|
||||
sqlite-utils insert learn.db topics db/topics.csv --csv
|
||||
sqlite-utils insert learn.db items db/items.csv --csv
|
||||
sqlite-utils insert learn.db reviews db/reviews.csv --csv
|
||||
sqlite-utils insert learn.db courses db/courses.csv --csv
|
Plik binarny nie jest wyświetlany.
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"title": "LearnAwesome",
|
||||
"description": "Curated collection of learning resources",
|
||||
"license": "ISC",
|
||||
"license_url": "",
|
||||
"source": "LearnAwesome.org",
|
||||
"source_url": "https://learnawesome.org/"
|
||||
}
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "learndb",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"start": "sirv public --no-clear"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||
"rollup": "^2.3.4",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.0.0",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"svelte": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"sirv-cli": "^2.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import svelte from 'rollup-plugin-svelte';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
function serve() {
|
||||
let server;
|
||||
|
||||
function toExit() {
|
||||
if (server) server.kill(0);
|
||||
}
|
||||
|
||||
return {
|
||||
writeBundle() {
|
||||
if (server) return;
|
||||
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
shell: true
|
||||
});
|
||||
|
||||
process.on('SIGTERM', toExit);
|
||||
process.on('exit', toExit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
input: 'src/main.js',
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'static/bundle.js'
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
compilerOptions: {
|
||||
// enable run-time checks when not in production
|
||||
dev: !production
|
||||
}
|
||||
}),
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
css({ output: 'bundle.css' }),
|
||||
|
||||
// If you have external dependencies installed from
|
||||
// npm, you'll most likely need these plugins. In
|
||||
// some cases you'll need additional configuration -
|
||||
// consult the documentation for details:
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ['svelte']
|
||||
}),
|
||||
commonjs(),
|
||||
|
||||
// In dev mode, call `npm run start` once
|
||||
// the bundle has been generated
|
||||
!production && serve(),
|
||||
|
||||
// Watch the `public` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload('public'),
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser()
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false
|
||||
}
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
let query = '';
|
||||
let results = [];
|
||||
|
||||
$: query && fetch(`/learn/items.json?_shape=array&name__contains=${query}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
results = data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
|
||||
<div class="overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<!--
|
||||
Command palette, show/hide based on modal state.
|
||||
|
||||
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">
|
||||
<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">
|
||||
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<input bind:value={query} type="text" class="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-800 placeholder-gray-400 focus:ring-0 sm:text-sm" placeholder="Search..." role="combobox" aria-expanded="false" aria-controls="options">
|
||||
</div>
|
||||
|
||||
{#if !query }
|
||||
<!-- Default state, show/hide based on command palette state -->
|
||||
<div class="border-t border-gray-100 py-14 px-6 text-center text-sm sm:px-14">
|
||||
<!-- Heroicon name: outline/globe -->
|
||||
<svg class="mx-auto h-6 w-6 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p class="mt-4 font-semibold text-gray-900">Search for items, topics and creators</p>
|
||||
<p class="mt-2 text-gray-500">Quickly look for resources by running a global search.</p>
|
||||
</div>
|
||||
|
||||
{:else if results.length > 0}
|
||||
|
||||
<!-- Results, show/hide based on command palette state -->
|
||||
<ul class="max-h-80 scroll-pt-11 scroll-pb-2 space-y-2 overflow-y-auto pb-2" id="options" role="listbox">
|
||||
<li>
|
||||
<h2 class="bg-gray-100 py-2.5 px-4 text-xs font-semibold text-gray-900">Items</h2>
|
||||
<ul class="mt-2 text-sm text-gray-800">
|
||||
{#each results as item}
|
||||
<li><a href="#/item/{item.rowid}" class="block cursor-default select-none px-4 py-2 hover:bg-indigo-600 hover:text-white cursor-pointer" id="option-1" role="option" tabindex="-1">{item.name}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2 class="bg-gray-100 py-2.5 px-4 text-xs font-semibold text-gray-900">Topics</h2>
|
||||
<ul class="mt-2 text-sm text-gray-800">
|
||||
{#each results as topic}
|
||||
<li><a href="#/topic/{topic.name}" class="block cursor-default select-none px-4 py-2 hover:bg-indigo-600 hover:text-white cursor-pointer" id="option-1" role="option" tabindex="-1">{topic.name}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{:else}
|
||||
|
||||
<!-- Empty state, show/hide based on command palette state -->
|
||||
<div class="border-t border-gray-100 py-14 px-6 text-center text-sm sm:px-14">
|
||||
<!-- Heroicon name: outline/emoji-sad -->
|
||||
<svg class="mx-auto h-6 w-6 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p class="mt-4 font-semibold text-gray-900">No results found</p>
|
||||
<p class="mt-2 text-gray-500">We couldn’t find anything with that term. Please try again.</p>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,64 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import * as TailwindUI from "./tailwindui/index"
|
||||
|
||||
import Home from "./Home.svelte"
|
||||
import TopicList from "./TopicList.svelte"
|
||||
import TopicDetail from "./TopicDetail.svelte"
|
||||
import FormatList from "./FormatList.svelte"
|
||||
import FormatDetail from "./FormatDetail.svelte"
|
||||
import CourseList from "./CourseList.svelte"
|
||||
import ItemDetail from "./ItemDetail.svelte"
|
||||
import AdvancedSearch from "./AdvancedSearch.svelte"
|
||||
|
||||
let sidebarItems = [
|
||||
{text: "Topics", link: "#/topics", icon: "home"},
|
||||
{text: "Formats", link: "#/formats", icon: "home"},
|
||||
{text: "Random item", link: "#/item/1", icon: "home"},
|
||||
{text: "Search", link: "#/search", icon: "home"},
|
||||
{text: "Datasette", link: "/learn", icon: "home"}
|
||||
]
|
||||
|
||||
let currentView = "/topics";
|
||||
|
||||
function handleTabChanged(event) {
|
||||
currentView = event.detail.tab;
|
||||
}
|
||||
|
||||
async function hashchange() {
|
||||
// the poor man's router!
|
||||
const path = window.location.hash.slice(1);
|
||||
|
||||
if (path.length > 0) {
|
||||
currentView = path
|
||||
} else {
|
||||
window.location.hash = '/home';
|
||||
currentView = '/home'
|
||||
}
|
||||
}
|
||||
|
||||
onMount(hashchange);
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:window on:hashchange={hashchange}/>
|
||||
|
||||
<TailwindUI.AppShell {sidebarItems}>
|
||||
{#if currentView === "/home" || currentView === "/"}
|
||||
<Home/>
|
||||
{:else if currentView === "/topics"}
|
||||
<TopicList/>
|
||||
{:else if currentView.startsWith("/topic/")}
|
||||
<TopicDetail topic={currentView.split("/")[2]}/>
|
||||
{:else if currentView === "/formats"}
|
||||
<FormatList/>
|
||||
{:else if currentView.startsWith("/format/")}
|
||||
<FormatDetail format={currentView.split("/")[2]}/>
|
||||
{:else if currentView === "/courses"}
|
||||
<CourseList/>
|
||||
{:else if currentView.startsWith("/item/")}
|
||||
<ItemDetail itemid={currentView.split("/")[2]}/>
|
||||
{:else if currentView === "/search"}
|
||||
<AdvancedSearch/>
|
||||
{/if}
|
||||
</TailwindUI.AppShell>
|
|
@ -0,0 +1,3 @@
|
|||
<script>
|
||||
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
<script>
|
||||
|
||||
</script>
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import ItemCard from "./ItemCard.svelte"
|
||||
export let format;
|
||||
let items = [];
|
||||
|
||||
$: fetch(`/learn/items.json?_shape=array&links__contains=${format}|`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
items = data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="md:flex md:items-center md:justify-between mb-8">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">{format}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each items as item}
|
||||
<ItemCard {item}/>
|
||||
{/each}
|
|
@ -0,0 +1,34 @@
|
|||
<script>
|
||||
let dataPromise = getData();
|
||||
async function getData() {
|
||||
const res = await fetch(`/learn.json?_shape=array&sql=select+distinct(substr(links%2C1%2Cinstr(links%2C'|')-1))+as+name+from+items`)
|
||||
if(res.ok){
|
||||
return await res.json();
|
||||
} else {
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#await dataPromise}
|
||||
<p>Fetching data...</p>
|
||||
{:then formats}
|
||||
|
||||
<div class="mt-6" style="columns: 6 240px; column-gap: 1rem;">
|
||||
{#each formats as format}
|
||||
<div tabindex="0" class="inline-block w-full mt-4 bg-white rounded-lg mt-4 px-4 py-4 shadow-lg focus:outline-none">
|
||||
<a href="#/format/{format.name}"><h4 class="mt-1 p-1 text-gray-900 font-semibold text-lg">{ format.name }</h4></a>
|
||||
|
||||
<div class="mt-2 flex flex-wrap text-sm text-gray-900">
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-right"><span>and 37 more.</span></p>
|
||||
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{:catch error}
|
||||
<p>{error.message}</p>
|
||||
{/await}
|
|
@ -0,0 +1,37 @@
|
|||
<main class="mt-16 mx-auto max-w-7xl px-4 sm:mt-24 sm:px-6 lg:mt-32">
|
||||
<div class="lg:grid lg:grid-cols-12 lg:gap-8">
|
||||
<div class="sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left">
|
||||
<h1>
|
||||
<span class="block text-sm font-semibold uppercase tracking-wide text-gray-500 sm:text-base lg:text-sm xl:text-base">Coming soon</span>
|
||||
<span class="mt-1 block text-4xl tracking-tight font-extrabold sm:text-5xl xl:text-6xl">
|
||||
<span class="block text-gray-900">Data to enrich your</span>
|
||||
<span class="block text-indigo-600">online business</span>
|
||||
</span>
|
||||
</h1>
|
||||
<p class="mt-3 text-base text-gray-500 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat commodo. Elit sunt amet fugiat veniam occaecat fugiat aliqua ad ad non deserunt sunt.</p>
|
||||
</div>
|
||||
<div class="mt-12 relative sm:max-w-lg sm:mx-auto lg:mt-0 lg:max-w-none lg:mx-0 lg:col-span-6 lg:flex lg:items-center">
|
||||
<svg class="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-8 scale-75 origin-top sm:scale-100 lg:hidden" width="640" height="784" fill="none" viewBox="0 0 640 784" aria-hidden="true">
|
||||
<defs>
|
||||
<pattern id="4f4f415c-a0e9-44c2-9601-6ded5a34a13e" x="118" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||
<rect x="0" y="0" width="4" height="4" class="text-gray-200" fill="currentColor" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect y="72" width="640" height="640" class="text-gray-50" fill="currentColor" />
|
||||
<rect x="118" width="404" height="784" fill="url(#4f4f415c-a0e9-44c2-9601-6ded5a34a13e)" />
|
||||
</svg>
|
||||
<div class="relative mx-auto w-full rounded-lg shadow-lg lg:max-w-md">
|
||||
<button type="button" class="relative block w-full bg-white rounded-lg overflow-hidden focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<span class="sr-only">Watch our video to learn more</span>
|
||||
<img class="w-full" src="https://images.unsplash.com/photo-1556740758-90de374c12ad?ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80" alt="">
|
||||
<div class="absolute inset-0 w-full h-full flex items-center justify-center" aria-hidden="true">
|
||||
<svg class="h-20 w-20 text-indigo-500" fill="currentColor" viewBox="0 0 84 84">
|
||||
<circle opacity="0.9" cx="42" cy="42" r="42" fill="white" />
|
||||
<path d="M55.5039 40.3359L37.1094 28.0729C35.7803 27.1869 34 28.1396 34 29.737V54.263C34 55.8604 35.7803 56.8131 37.1094 55.9271L55.5038 43.6641C56.6913 42.8725 56.6913 41.1275 55.5039 40.3359Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
|
@ -0,0 +1,9 @@
|
|||
<script>
|
||||
export let item;
|
||||
</script>
|
||||
|
||||
<a href="#/item/{item.rowid}">
|
||||
<img class="h-64 w-44 mr-6 mb-6 float-left border border-purple-200 rounded-md shadow-md md:shadow-xl transform transition ease-out duration-300 hover:scale-105"
|
||||
src={item.image} alt="">
|
||||
</a>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<script>
|
||||
export let itemid;
|
||||
|
||||
let item;
|
||||
|
||||
$: fetch(`/learn/items/${itemid}.json?_shape=object`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
item = data[itemid];
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if item}
|
||||
<h1>{item.name}</h1>
|
||||
|
||||
<a href="#/item/{item.rowid}">
|
||||
<img class="h-64 w-44 mr-6 mb-6 float-left border border-purple-200 rounded-md shadow-md md:shadow-xl transform transition ease-out duration-300 hover:scale-105"
|
||||
src={item.image} alt="">
|
||||
</a>
|
||||
{:else}
|
||||
<p class="loading">loading...</p>
|
||||
{/if}
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
|
||||
</script>
|
||||
|
||||
review
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
export let topic;
|
||||
import Icon from "./tailwindui/Icon.svelte"
|
||||
|
||||
$: abbr = topic.display_name.slice(0,2).toUpperCase()
|
||||
</script>
|
||||
|
||||
<li class="col-span-1 flex shadow-sm rounded-md">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-16 bg-pink-600 text-white text-sm font-medium rounded-l-md">{abbr}</div>
|
||||
<div class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate">
|
||||
<div class="flex-1 px-4 py-2 text-sm truncate">
|
||||
<a href={"/topic/" + topic.id} class="text-gray-900 font-medium hover:text-gray-600">{topic.display_name}</a>
|
||||
<p class="text-gray-500">{topic.rowid} items</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 pr-2">
|
||||
<button type="button" class="w-8 h-8 bg-white inline-flex items-center justify-center text-gray-400 rounded-full bg-transparent hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<span class="sr-only">Open options</span>
|
||||
<Icon kind="dots"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
|
@ -0,0 +1,23 @@
|
|||
<script>
|
||||
import ItemCard from "./ItemCard.svelte"
|
||||
|
||||
export let topic;
|
||||
let items = [];
|
||||
|
||||
$: fetch(`/learn/items.json?_shape=array&topics__contains=${topic}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
items = data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="md:flex md:items-center md:justify-between mb-8">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">{topic}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{#each items as item}
|
||||
<ItemCard {item}/>
|
||||
{/each}
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import TopicCard from "./TopicCard.svelte"
|
||||
|
||||
let dataPromise = getData();
|
||||
async function getData() {
|
||||
const res = await fetch(`/learn/topics.json?_shape=array`)
|
||||
if(res.ok){
|
||||
return await res.json();
|
||||
} else {
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
function hierarchy(topics){
|
||||
return topics.reduce((map, topic) => {
|
||||
if(!topic.first_parent_topic_id) {
|
||||
map.set(topic, []);
|
||||
} else {
|
||||
let parent = topics.find(t => t.id == topic.first_parent_topic_id)
|
||||
map.set(parent, [...map.get(parent), topic])
|
||||
}
|
||||
return map;
|
||||
}, new Map())
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await dataPromise}
|
||||
<p>Fetching data...</p>
|
||||
{:then topics}
|
||||
|
||||
<div class="mt-6" style="columns: 6 240px; column-gap: 1rem;">
|
||||
{#each [...hierarchy(topics).keys()] as parent}
|
||||
<div tabindex="0" class="inline-block w-full mt-4 bg-white rounded-lg mt-4 px-4 py-4 shadow-lg focus:outline-none">
|
||||
<h4 class="mt-1 p-1 text-gray-900 font-semibold text-lg">{ parent.display_name }</h4>
|
||||
|
||||
<div class="mt-2 flex flex-wrap text-sm text-gray-900">
|
||||
{#each hierarchy(topics).get(parent) as child}
|
||||
<a href={"#/topic/" + child.id} class="text-purple-600 no-underline hover:underline hover:text-purple-900 px-2">{child.display_name}</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-right"><span>and 37 more.</span></p>
|
||||
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{:catch error}
|
||||
<p>{error.message}</p>
|
||||
{/await}
|
|
@ -0,0 +1,5 @@
|
|||
import App from './App.svelte';
|
||||
|
||||
new App({
|
||||
target: document.querySelector('#app'),
|
||||
});
|
|
@ -0,0 +1,134 @@
|
|||
<script>
|
||||
import Icon from "./Icon.svelte"
|
||||
import MenuButton from "./MenuButton.svelte"
|
||||
import SearchForm from "./SearchForm.svelte"
|
||||
export let sidebarItems = [];
|
||||
let isNavDrawerOpen = false
|
||||
export let showNotificationBell = false;
|
||||
export let showProfileMenu = false;
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<!-- Off-canvas menu for mobile, show/hide based on off-canvas menu state. -->
|
||||
{#if isNavDrawerOpen}
|
||||
<div class="relative z-40 md:hidden" role="dialog" aria-modal="true">
|
||||
<!--
|
||||
Off-canvas menu backdrop, show/hide based on off-canvas menu state.
|
||||
|
||||
Entering: "transition-opacity ease-linear duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "transition-opacity ease-linear duration-300"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="fixed inset-0 bg-gray-600 bg-opacity-75"></div>
|
||||
|
||||
<div class="fixed inset-0 flex z-40">
|
||||
<!--
|
||||
Off-canvas menu, show/hide based on off-canvas menu state.
|
||||
|
||||
Entering: "transition ease-in-out duration-300 transform"
|
||||
From: "-translate-x-full"
|
||||
To: "translate-x-0"
|
||||
Leaving: "transition ease-in-out duration-300 transform"
|
||||
From: "translate-x-0"
|
||||
To: "-translate-x-full"
|
||||
-->
|
||||
<div class="relative flex-1 flex flex-col max-w-xs w-full pt-5 pb-4 bg-indigo-700">
|
||||
<!--
|
||||
Close button, show/hide based on off-canvas menu state.
|
||||
|
||||
Entering: "ease-in-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in-out duration-300"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="absolute top-0 right-0 -mr-12 pt-2">
|
||||
<button on:click={e => isNavDrawerOpen = false} type="button" class="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<!-- Heroicon name: outline/x -->
|
||||
<svg class="h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0 flex items-center px-4">
|
||||
<a href="#/" class="">LearnAwesome</a>
|
||||
</div>
|
||||
<div class="mt-5 flex-1 h-0 overflow-y-auto">
|
||||
<nav class="px-2 space-y-1">
|
||||
<!-- Current: "bg-indigo-800 text-white", Default: "text-indigo-100 hover:bg-indigo-600" -->
|
||||
{#each sidebarItems as { text, link, icon }, i}
|
||||
<a href={link} class="w-full bg-indigo-800 text-white group flex items-center px-2 py-2 text-base font-medium rounded-md">
|
||||
<Icon kind={icon}/>
|
||||
{text}
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0 w-14" aria-hidden="true">
|
||||
<!-- Dummy element to force sidebar to shrink to fit close icon -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Static sidebar for desktop -->
|
||||
<div class="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0">
|
||||
<!-- Sidebar component, swap this element with another sidebar if you like -->
|
||||
<div class="flex flex-col flex-grow pt-5 bg-indigo-700 overflow-y-auto">
|
||||
<div class="flex items-center flex-shrink-0 px-4 bg-white">
|
||||
<a href="/" class="">LearnAwesome</a>
|
||||
</div>
|
||||
<div class="mt-5 flex-1 flex flex-col">
|
||||
<nav class="flex-1 px-2 pb-4 space-y-1">
|
||||
<!-- Current: "bg-indigo-800 text-white", Default: "text-indigo-100 hover:bg-indigo-600" -->
|
||||
{#each sidebarItems as { text, link, icon }, i}
|
||||
<a href={link} class="w-full bg-indigo-800 text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md">
|
||||
<Icon kind={icon}/>
|
||||
{text}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:pl-64 flex flex-col flex-1">
|
||||
<div class="sticky top-0 z-10 flex-shrink-0 flex h-16 bg-white shadow">
|
||||
<button on:click={e => isNavDrawerOpen = true} type="button" class="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 md:hidden">
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<Icon kind="menu"/>
|
||||
</button>
|
||||
<div class="flex-1 px-4 flex justify-between">
|
||||
{#if showNotificationBell || showProfileMenu}
|
||||
<div class="ml-4 flex items-center md:ml-6">
|
||||
{#if showNotificationBell}
|
||||
<button type="button" class="bg-white p-1 rounded-full text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<span class="sr-only">View notifications</span>
|
||||
<Icon kind="bell"/>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if showProfileMenu}<MenuButton />{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
export let kind;
|
||||
export let size;
|
||||
</script>
|
||||
|
||||
{#if kind === 'home'}
|
||||
<!-- Heroicon name: outline/home -->
|
||||
<svg class="mr-4 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
{:else if kind === "menu"}
|
||||
<!-- Heroicon name: outline/menu-alt-2 -->
|
||||
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
{:else if kind === "bell"}
|
||||
<!-- Heroicon name: outline/bell -->
|
||||
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
{:else if kind === "search"}
|
||||
<!-- Heroicon name: solid/search -->
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{:else if kind === "dots"}
|
||||
<!-- Heroicon name: solid/dots-vertical -->
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||||
</svg>
|
||||
{/if}
|
|
@ -0,0 +1,28 @@
|
|||
<!-- Profile dropdown -->
|
||||
<div class="ml-3 relative">
|
||||
<div>
|
||||
<button type="button" class="max-w-xs bg-white flex items-center text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Dropdown menu, show/hide based on menu state.
|
||||
|
||||
Entering: "transition ease-out duration-100"
|
||||
From: "transform opacity-0 scale-95"
|
||||
To: "transform opacity-100 scale-100"
|
||||
Leaving: "transition ease-in duration-75"
|
||||
From: "transform opacity-100 scale-100"
|
||||
To: "transform opacity-0 scale-95"
|
||||
-->
|
||||
<div class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
|
||||
<!-- Active: "bg-gray-100", Not Active: "" -->
|
||||
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-0">Your Profile</a>
|
||||
|
||||
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-1">Settings</a>
|
||||
|
||||
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</a>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,38 @@
|
|||
<script>
|
||||
import Icon from "./Icon.svelte"
|
||||
let query;
|
||||
let results = [];
|
||||
|
||||
function handleSubmit(ev){
|
||||
console.log({query})
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="w-full flex md:ml-0" on:submit|preventDefault={handleSubmit}>
|
||||
<label for="search-field" class="sr-only">Search</label>
|
||||
<div class="relative w-full text-gray-400 focus-within:text-gray-600">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pointer-events-none">
|
||||
<Icon kind="search"/>
|
||||
</div>
|
||||
<input bind:value={query} class="block w-full h-full pl-8 pr-3 py-2 border-transparent text-gray-900 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-0 focus:border-transparent sm:text-sm" placeholder="Search" type="search" name="search">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul class="max-h-80 scroll-pt-11 scroll-pb-2 space-y-2 overflow-y-auto pb-2" id="options" role="listbox">
|
||||
<li>
|
||||
<h2 class="bg-gray-100 py-2.5 px-4 text-xs font-semibold text-gray-900">Clients</h2>
|
||||
<ul class="mt-2 text-sm text-gray-800">
|
||||
<!-- Active: "bg-indigo-600 text-white" -->
|
||||
<li class="cursor-default select-none px-4 py-2" id="option-1" role="option" tabindex="-1">Workflow Inc.</li>
|
||||
<li class="cursor-default select-none px-4 py-2" id="option-2" role="option" tabindex="-1">Multinational LLC.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2 class="bg-gray-100 py-2.5 px-4 text-xs font-semibold text-gray-900">Projects</h2>
|
||||
<ul class="mt-2 text-sm text-gray-800">
|
||||
<!-- Active: "bg-indigo-600 text-white" -->
|
||||
<li class="cursor-default select-none px-4 py-2" id="option-3" role="option" tabindex="-1">Workflow Inc. / Website Redesign</li>
|
||||
<li class="cursor-default select-none px-4 py-2" id="option-3" role="option" tabindex="-1">Multinational LLC. / Animation</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
|
@ -0,0 +1,3 @@
|
|||
export {default as Icon} from './Icon.svelte'
|
||||
export {default as MenuButton} from './MenuButton.svelte'
|
||||
export {default as AppShell} from './AppShell.svelte'
|
|
@ -0,0 +1 @@
|
|||
h1.svelte-b0hmqh{background-color:yellow}
|
Plik diff jest za duży
Load Diff
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-gray-100">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="//cdn.tailwindcss.com/?plugins=forms,typography,aspect-ratio,line-clamp"></script>
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<script src="/static/bundle.js" defer></script>
|
||||
<link href="/static/bundle.css" rel="stylesheet" />
|
||||
<title>LearnAwesome</title>
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<div class="h-full" id="app"></div>
|
||||
</body>
|
||||
</html>
|
Ładowanie…
Reference in New Issue