diff --git a/01 - JavaScript Drum Kit/index.js b/01 - JavaScript Drum Kit/index.js
new file mode 100644
index 0000000000..bb775a0237
--- /dev/null
+++ b/01 - JavaScript Drum Kit/index.js
@@ -0,0 +1,25 @@
+const ks = [...document.querySelectorAll('.key')];
+
+// add event to k
+ks.forEach((k) => {
+ k.addEventListener('transitionend', (e) => {
+ if (e.propertyName !== 'transform') return;
+ e.target.classList.remove('playing');
+ console.log(e);
+ });
+});
+
+window.addEventListener('keydown', (e) => playSound(e.keyCode));
+
+function playSound(code) {
+ const btn = document.querySelector(`div[data-key='${code}']`);
+ const audio = document.querySelector(`audio[data-key='${code}']`);
+ if (!audio) return;
+
+ btn.classList.add('playing');
+ audio.currentTime = 0;
+ audio.play();
+
+ // setTimeout(() => btn.classList.remove('playing'), 500);
+ // console.log(code, btn, audio);
+}
diff --git a/01 - JavaScript Drum Kit/index0.html b/01 - JavaScript Drum Kit/index0.html
new file mode 100644
index 0000000000..421f2353e9
--- /dev/null
+++ b/01 - JavaScript Drum Kit/index0.html
@@ -0,0 +1,60 @@
+
+
+
+
+ JS Drum Kit
+
+
+
+
+
+ A
+ clap
+
+
+ S
+ hihat
+
+
+ D
+ kick
+
+
+ F
+ openhat
+
+
+ G
+ boom
+
+
+ H
+ ride
+
+
+ J
+ snare
+
+
+ K
+ tom
+
+
+ L
+ tink
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/02 - JS and CSS Clock/index.js b/02 - JS and CSS Clock/index.js
new file mode 100644
index 0000000000..eef455e456
--- /dev/null
+++ b/02 - JS and CSS Clock/index.js
@@ -0,0 +1,23 @@
+const data = {
+ digitclock: document.querySelector('#digitclock'),
+ shand: document.querySelector('.second-hand'),
+ mhand: document.querySelector('.min-hand'),
+ hhand: document.querySelector('.hour-hand'),
+};
+
+function showtime({ digitclock, shand, mhand, hhand }) {
+ const t = new Date(),
+ sdeg = t.getSeconds() * (360 / 60) + 90,
+ mdeg =
+ t.getMinutes() * (360 / 60) + 90 + ((t.getSeconds() / 60) * 360) / 60,
+ hdeg = t.getHours() * (360 / 12) + 90 + t.getMinutes() * 0.5;
+
+ digitclock.textContent = t.toLocaleTimeString();
+ shand.style.transform = `rotate(${sdeg}deg)`;
+ mhand.style.transform = `rotate(${mdeg}deg)`;
+ hhand.style.transform = `rotate(${hdeg}deg)`;
+}
+
+const iPnt = setInterval(() => showtime(data), 1000);
+
+// showtime(data);
diff --git a/02 - JS and CSS Clock/index0.html b/02 - JS and CSS Clock/index0.html
new file mode 100644
index 0000000000..17d9b52c4e
--- /dev/null
+++ b/02 - JS and CSS Clock/index0.html
@@ -0,0 +1,83 @@
+
+
+
+
+ JS + CSS Clock
+
+
+
+
+ time
+
+
+
+
+
+
diff --git a/03 - CSS Variables/index0.html b/03 - CSS Variables/index0.html
new file mode 100644
index 0000000000..83eeddd553
--- /dev/null
+++ b/03 - CSS Variables/index0.html
@@ -0,0 +1,91 @@
+
+
+
+
+ Scoped CSS Variables and JS
+
+
+ Update CSS Variables with JS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/05 - Flex Panel Gallery/index0.html b/05 - Flex Panel Gallery/index0.html
new file mode 100644
index 0000000000..89e5a7776c
--- /dev/null
+++ b/05 - Flex Panel Gallery/index0.html
@@ -0,0 +1,133 @@
+
+
+
+
+ Flex Panels 💪
+
+
+
+
+
+
+
+
Give
+
Take
+
Receive
+
+
+
Experience
+
It
+
Today
+
+
+
+
+
+
+
+
diff --git a/06 - Type Ahead/app/app.js b/06 - Type Ahead/app/app.js
new file mode 100644
index 0000000000..8b2989fe85
--- /dev/null
+++ b/06 - Type Ahead/app/app.js
@@ -0,0 +1,185 @@
+import { Suggestions } from './components.js';
+// Utilities Start ====================
+const pr = console.log;
+// Utilities End ====================
+
+// App Start ====================
+//
+export class EventRegistry {
+ constructor() {
+ this.map = new Map();
+ }
+
+ fire(event) {
+ for (const cb of this.map.get(event.name) || []) {
+ cb(event);
+ }
+ }
+
+ listen(eventName, cb) {
+ const cbs = this.map.get(eventName) || [];
+ cbs.push(cb);
+ this.map.set(eventName, cbs);
+ }
+ unListen(eventName, cb) {
+ const cbs = this.map.get(eventName) || [];
+ this.map.set(
+ eventName,
+ cbs.filter((x) => x != cb),
+ );
+ }
+}
+
+export class Place {
+ constructor(city, st) {
+ this.city = city;
+ this.st = st;
+ }
+}
+
+export const App = new EventRegistry();
+App.filter = '';
+
+App.setupDispatch = () => {
+ pr('App.setupDispatch App:', App);
+ App.db.onmessage = (e) => {
+ const { op, error } = e.data;
+ pr(op, 'data:', e.data);
+ switch (op) {
+ case 'addDone':
+ if (!error) {
+ App.getPlaces(App.filter);
+ App.fire({ name: 'addCity success', data: e.data });
+ } else {
+ App.fire({ name: 'addCity error', error: e.data.error.message });
+ }
+ break;
+ case 'delDone':
+ if (!error) {
+ App.getPlaces(App.filter);
+ } else {
+ App.fire({ name: 'delCity error', error: e.data.error.message });
+ }
+ break;
+ case 'getDone':
+ App.fire({ name: 'placesChange', data: e.data });
+ break;
+ default:
+ pr('App.onmessage.default');
+ }
+ };
+};
+
+App.getPlaces = (pattern) => {
+ const msg = { op: 'get', path: 'place', pattern: pattern };
+ pr('App.getPlaces msg:', msg);
+ App.db.postMessage(msg);
+};
+
+App.filterChange = (pattern) => {
+ App.filter = pattern;
+ App.getPlaces(pattern);
+ pr('App.filterChange pattern:', pattern);
+ App.fire({ name: 'filterChange', filter: pattern });
+};
+
+App.addCity = (str) => {
+ const [city, st] = str
+ .toLowerCase()
+ .split(',')
+ .map((x) => x.trim());
+ if (city && st) {
+ App.db.postMessage({ op: 'add', path: 'place', obj: new Place(city, st) });
+ }
+};
+
+App.delCity = (city, st) => {
+ App.db.postMessage({ op: 'del', path: 'place', obj: new Place(city, st) });
+};
+
+App.init = () => {
+ App.db = new Worker('store-worker.js');
+ App.setupDispatch();
+
+ const suggestionsDiv = document.querySelector('.suggestions');
+ App.listen('placesChange', ({ data }) => {
+ const places = data.data;
+ pr('suggestionsDiv, placesChange, data:', data, places);
+ const component = Suggestions({
+ places: places,
+ onDel: (city, st) => App.delCity(city, st),
+ onEdit: (city, st) => App.editCity(city, st),
+ });
+ render(suggestionsDiv, component);
+ });
+
+ // filters
+ // TODO: throttle keyup events for 200ms //
+ document.querySelectorAll('.filter').forEach((el) => {
+ el.addEventListener('keyup', (e) => App.filterChange(e.target.value));
+ App.listen('filterChange', ({ filter }) => {
+ if (el.value !== filter) el.value = filter;
+ });
+ });
+
+ // add city
+ const addInp = document.querySelector('.add');
+ addInp.addEventListener('change', (e) => App.addCity(e.target.value));
+ App.listen('addCity success', () => {
+ addInp.value = '';
+ });
+ App.listen('addCity error', (e) => {
+ addInp.value = e.error;
+ });
+
+ // const { place, err } = App.addCity(e.target.value);
+ // if (err) {
+ // pr('Error', err);
+ // return;
+ // }
+ // const event = { name: 'PlacesChange', place: place };
+ // App.eReg.fireEvent(event);
+ // e.target.value = '';
+ // });
+};
+
+// Element, Element -> DOM Effect
+function render(root, component) {
+ root.replaceChildren(component);
+}
+
+// App End ====================
+
+// App Run ====================
+App.init();
+
+// e// // str, str -> err: str
+// delCity(city, st) {
+// pr(city, st);
+// this._places.delete(this.key(city, st));
+// this.eReg.fireEvent({
+// name: 'placesChange',
+// city: city,
+// st: st,
+// details: 'delete',
+// });
+// }
+
+// // str, str -> err: str
+// editCity(city, st) {
+// pr(city, st);
+// }
+// }
+
+// export const app = new App(new Worker('store-worker.js'));
+// App End ====================
+
+// store-worker Start ====================
+// const store = new Worker('store-worker.js');
+
+// window.onload = (e) => {
+// pr('onload', e);
+// store.postMessage({ op: 'get', path: 'place', pattern: '' });
+// };
+
+// store-worker End ====================
diff --git a/06 - Type Ahead/app/components.js b/06 - Type Ahead/app/components.js
new file mode 100644
index 0000000000..784bc414e3
--- /dev/null
+++ b/06 - Type Ahead/app/components.js
@@ -0,0 +1,76 @@
+// Utilitis
+const pr = console.table;
+const Element = (tag) => document.createElement(tag);
+
+// {places, ...[onChangeFn]} -> Element('ul')
+export function Suggestions({ places, onDel, onEdit }) {
+ pr('Suggestions places:', places);
+ const lst = places.map(({ city, st }) => {
+ const li = Element('li'),
+ c = Element('span'),
+ s = Element('span'),
+ del = Element('button'),
+ ed = Element('button'),
+ sbtn = Element('span');
+ c.append(city);
+ s.append(st);
+ del.append('Del');
+ del.addEventListener('click', () => {
+ onDel(city, st);
+ });
+ ed.append('Edit');
+ ed.addEventListener('click', () => {
+ onEdit(city, st);
+ });
+ sbtn.append(ed, del);
+ li.append(c, s, sbtn);
+ return li;
+ });
+ const ul = Element('ul');
+ ul.append(...lst);
+ return ul;
+}
+// ==========
+
+// const suggestions = document.querySelector('.suggestions');
+// // str, Element ->
+// function showSuggestions(pattern, rootEl) {
+// const ps = App.places(pattern),
+// lis = placesList({
+// places: ps,
+// onDel: (city, st) => App.delCity(city, st),
+// onEdit: (city, st) => App.editCity(city, st),
+// });
+// rootEl.replaceChildren(...lis);
+// }
+
+// App.eReg.listen('SearchChange', ({ pattern }) =>
+// showSuggestions(pattern, suggestions),
+// );
+
+// App.eReg.listen('PlacesChange', () => {
+// const pattern = search.value;
+// showSuggestions(pattern, suggestions);
+// });
+
+// showSuggestions('', suggestions);
+// ==========
+
+// const search = document.querySelector('.search');
+// search.addEventListener('keyup', (e) => {
+// const event = { name: 'SearchChange', pattern: e.target.value };
+// App.eReg.fireEvent(event);
+// });
+// // ==========
+
+// const add = document.querySelector('.add');
+// add.addEventListener('change', (e) => {
+// const { place, err } = App.addCity(e.target.value);
+// if (err) {
+// pr('Error', err);
+// return;
+// }
+// const event = { name: 'PlacesChange', place: place };
+// App.eReg.fireEvent(event);
+// e.target.value = '';
+// });
diff --git a/06 - Type Ahead/app/index.js b/06 - Type Ahead/app/index.js
new file mode 100644
index 0000000000..24187679de
--- /dev/null
+++ b/06 - Type Ahead/app/index.js
@@ -0,0 +1,164 @@
+// Utilitis
+const pr = console.table;
+const Element = (tag) => document.createElement(tag);
+
+class EventRegistry {
+ constructor() {
+ this.map = new Map();
+ }
+
+ fireEvent(event) {
+ for (const cb of this.map.get(event.name) || []) {
+ cb(event);
+ }
+ }
+
+ listen(eventName, cb) {
+ const cbs = this.map.get(eventName) || [];
+ cbs.push(cb);
+ this.map.set(eventName, cbs);
+ }
+ unListen(eventName, cb) {
+ const cbs = this.map.get(eventName) || [];
+ this.map.set(
+ eventName,
+ cbs.filter((x) => x != cb),
+ );
+ }
+}
+
+const App = {
+ eReg: new EventRegistry(),
+ _places: new Map(),
+
+ init() {
+ // init places
+ const _cs = ['Madera,ca', 'Fresno, CA', 'boulder, co', 'austin,tx'];
+ for (const s of _cs) {
+ this.addCity(s);
+ }
+ },
+
+ // str -> [place]
+ places(pattern) {
+ const r = new RegExp(pattern, 'gi'),
+ res = [];
+ this._places.forEach((v, k) => {
+ if (k.match(r)) res.push(v);
+ });
+ return res;
+ },
+
+ // str, str -> str
+ key(city, st) {
+ return `${city},${st}`;
+ },
+
+ // str -> {place: obj, err: str}
+ addCity(str) {
+ const [city, st] = str
+ .toLowerCase()
+ .split(',')
+ .map((x) => x.trim());
+ const key = this.key(city, st),
+ place = { city: city, st: st };
+ if (this._places.has(key)) return { place: null, err: 'key exists' };
+ this._places.set(key, place);
+ this.eReg.fireEvent({ name: 'PlacesChange', city: city, st: st });
+ return { place: place, err: '' };
+ },
+
+ // str, str -> err: str
+ delCity(city, st) {
+ pr(city, st);
+ this._places.delete(this.key(city, st));
+ this.eReg.fireEvent({
+ name: 'PlacesChange',
+ city: city,
+ st: st,
+ details: 'delete',
+ });
+ },
+
+ // str, str -> err: str
+ editCity(city, st) {
+ pr(city, st);
+ },
+};
+
+App.init();
+// ==========
+
+// Componets
+//
+// [places], rootElment -> [Element('li')]
+function placesList({ places, onDel, onEdit }) {
+ const lst = places.map(({ city, st }) => {
+ const li = Element('li'),
+ c = Element('span'),
+ s = Element('span'),
+ del = Element('button'),
+ ed = Element('button'),
+ sbtn = Element('span');
+ c.append(city);
+ s.append(st);
+ del.append('Del');
+ del.addEventListener('click', () => {
+ onDel(city, st);
+ });
+ ed.append('Edit');
+ ed.addEventListener('click', () => {
+ onEdit(city, st);
+ });
+ sbtn.append(ed, del);
+ li.append(c, s, sbtn);
+ return li;
+ });
+ return lst;
+}
+// ==========
+
+const suggestions = document.querySelector('.suggestions');
+// str, Element ->
+function showSuggestions(pattern, rootEl) {
+ const ps = App.places(pattern),
+ lis = placesList({
+ places: ps,
+ onDel: (city, st) => App.delCity(city, st),
+ onEdit: (city, st) => App.editCity(city, st),
+ });
+ rootEl.replaceChildren(...lis);
+}
+
+App.eReg.listen('SearchChange', ({ pattern }) =>
+ showSuggestions(pattern, suggestions),
+);
+
+App.eReg.listen('PlacesChange', () => {
+ const pattern = search.value;
+ showSuggestions(pattern, suggestions);
+});
+
+showSuggestions('', suggestions);
+// ==========
+
+const search = document.querySelector('.search');
+search.addEventListener('keyup', (e) => {
+ const event = { name: 'SearchChange', pattern: e.target.value };
+ App.eReg.fireEvent(event);
+});
+// ==========
+
+const add = document.querySelector('.add');
+add.addEventListener('change', (e) => {
+ const { place, err } = App.addCity(e.target.value);
+ if (err) {
+ pr('Error', err);
+ return;
+ }
+ const event = { name: 'PlacesChange', place: place };
+ App.eReg.fireEvent(event);
+ e.target.value = '';
+});
+
+// END Componets ===============
diff --git a/06 - Type Ahead/app/main.html b/06 - Type Ahead/app/main.html
new file mode 100644
index 0000000000..43ea172970
--- /dev/null
+++ b/06 - Type Ahead/app/main.html
@@ -0,0 +1,36 @@
+
+
+
+
+ Type Ahead 👀
+
+
+
+
+
+
+
+
+
+
+
+ - Filter for a city
+ - or a state
+
+
+
+
+
+
diff --git a/06 - Type Ahead/app/mocha b/06 - Type Ahead/app/mocha
new file mode 120000
index 0000000000..67d7fb6daa
--- /dev/null
+++ b/06 - Type Ahead/app/mocha
@@ -0,0 +1 @@
+/Users/v/code/learning/js/mocha
\ No newline at end of file
diff --git a/06 - Type Ahead/app/store-worker.js b/06 - Type Ahead/app/store-worker.js
new file mode 100644
index 0000000000..5a930ce1e2
--- /dev/null
+++ b/06 - Type Ahead/app/store-worker.js
@@ -0,0 +1,130 @@
+// import { Place } from './app.js';
+const pr = console.log;
+let db;
+
+const connection = indexedDB.open('cities', 1);
+
+connection.onerror = (e) => pr('why not allow to use indexedDB?');
+
+connection.onupgradeneeded = (e) => {
+ pr('in connection.onupgradeneeded');
+ db = e.target.result;
+ db.onerror = (e) => console.error('Error loading db');
+
+ // create objectStore
+ const objectStore = db.createObjectStore('place', { keyPath: 'key' });
+ // define data items
+ // objectStore.createIndex('zip', 'zip', {unique: false})
+
+ //populate with initial data
+ objectStore.transaction.oncomplete = (e) => {
+ pr('onupgradeneeded oncomplete, e', e);
+ add(db, {
+ op: 'add',
+ path: 'place',
+ obj: { city: 'example city', st: 'ny' },
+ });
+ };
+};
+
+connection.onsuccess = (e) => {
+ pr('in connection.onsuccess');
+ db = e.target.result;
+ // setupDispatch(db);
+ // get all places
+ get(db, {
+ op: 'get',
+ path: 'place',
+ pattern: '',
+ });
+};
+
+// setupDispatch
+onmessage = (event) => {
+ const { op } = event.data;
+ switch (op) {
+ case 'get':
+ get(db, event.data);
+ break;
+
+ case 'add':
+ add(db, event.data);
+ break;
+
+ case 'del':
+ del(db, event.data);
+ break;
+ default:
+ pr('worker.setupDispatch.default', event.data);
+ }
+};
+
+function key({ city, st }) {
+ return `${city},${st}`;
+}
+
+function add(db, data) {
+ const { path, obj } = data;
+ if (path === 'place') {
+ obj.key = key(obj);
+ }
+
+ const transaction = db.transaction([path], 'readwrite');
+ const objectStore = transaction.objectStore(path);
+ const req = objectStore.add(obj);
+
+ req.onsuccess = (e) => {
+ const res = { data: e.target.result, op: 'addDone', error: '' };
+ postMessage(res);
+ };
+ req.onerror = (e) => {
+ const res = { data: e.target.result, op: 'addDone', error: e.target.error };
+ postMessage(res);
+ };
+}
+
+function get(db, data) {
+ const { path, pattern } = data;
+ const r = new RegExp(pattern, 'gi'),
+ trans = db.transaction([path]),
+ items = [];
+
+ objectStore = trans.objectStore(path);
+
+ objectStore.openCursor().onsuccess = (e) => {
+ const cursor = e.target.result;
+ if (!cursor) {
+ pr('lookup is done');
+ const res = { op: 'getDone', data: items, pattern: pattern, error: '' };
+ postMessage(res);
+ return;
+ }
+ const item = cursor.value;
+ if (item.key.match(r)) items.push(item);
+ cursor.continue();
+ };
+}
+
+function del(db, data) {
+ const { path, obj } = data;
+ if (path === 'place') {
+ obj.key = key(obj);
+ }
+
+ const transaction = db.transaction([path], 'readwrite');
+ const objectStore = transaction.objectStore(path);
+ const req = objectStore.delete(obj.key);
+
+ req.onsuccess = (e) => {
+ const res = { data: e.target.result, op: 'delDone', error: '' };
+ postMessage(res);
+ };
+ req.onerror = (e) => {
+ const res = {
+ data: e.target.result,
+ op: 'delDone',
+ error: e.target.error,
+ };
+ postMessage(res);
+ };
+}
diff --git a/06 - Type Ahead/app/style.css b/06 - Type Ahead/app/style.css
new file mode 100644
index 0000000000..2e62f4e331
--- /dev/null
+++ b/06 - Type Ahead/app/style.css
@@ -0,0 +1,84 @@
+html {
+ box-sizing: border-box;
+ background: #ffc600;
+ font-family: 'helvetica neue';
+ font-size: 20px;
+ font-weight: 200;
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+div.utils {
+ border: 2px dotted black;
+}
+.utils input {
+ width: 30%;
+ height: 20px;
+}
+
+input {
+ width: 100%;
+ padding: 20px;
+}
+
+.search-form {
+ max-width: 400px;
+ margin: 50px auto;
+}
+
+input.search {
+ margin: 0;
+ text-align: center;
+ outline: 0;
+ border: 10px solid #f7f7f7;
+ width: 120%;
+ left: -10%;
+ position: relative;
+ top: 10px;
+ z-index: 2;
+ border-radius: 5px;
+ font-size: 40px;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.12), inset 0 0 2px rgba(0, 0, 0, 0.19);
+}
+
+.suggestions {
+ margin: 0;
+ padding: 0;
+ position: relative;
+ /*perspective: 20px;*/
+}
+
+.suggestions li {
+ background: white;
+ list-style: none;
+ border-bottom: 1px solid #d8d8d8;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.14);
+ margin: 0;
+ padding: 20px;
+ transition: background 0.2s;
+ display: flex;
+ justify-content: space-between;
+ text-transform: capitalize;
+}
+
+.suggestions li:nth-child(even) {
+ transform: perspective(100px) rotateX(3deg) translateY(2px) scale(1.001);
+ background: linear-gradient(to bottom, #ffffff 0%, #efefef 100%);
+}
+
+.suggestions li:nth-child(odd) {
+ transform: perspective(100px) rotateX(-3deg) translateY(3px);
+ background: linear-gradient(to top, #ffffff 0%, #efefef 100%);
+}
+
+span.population {
+ font-size: 15px;
+}
+
+.hl {
+ background: #ffc600;
+}
diff --git a/06 - Type Ahead/app/t.spec.js b/06 - Type Ahead/app/t.spec.js
new file mode 100644
index 0000000000..6ea37cea6f
--- /dev/null
+++ b/06 - Type Ahead/app/t.spec.js
@@ -0,0 +1,82 @@
+const assert = chai.assert;
+import { Place, App, EventRegistry } from './app.js';
+
+const pr = console.log;
+
+describe('function', () => {
+ it('desctructure arguments', () => {
+ function add({ a, b }) {
+ return a + b;
+ }
+ const props = { a: 1, b: 3 };
+ assert.equal(4, add(props));
+ });
+});
+
+describe('class', () => {
+ it('desctructure class obj', () => {
+ const place = new Place('a', 'ca');
+ const { city, st } = place;
+ assert.equal('a', city);
+ });
+ it('extends', () => {
+ class A {
+ constructor(x) {
+ this.x = x;
+ }
+ add(y) {
+ return this.x + y;
+ }
+ sub(y) {
+ return this.x - y;
+ }
+ }
+ class B extends A {
+ constructor(x) {
+ super(x);
+ }
+ add(y) {
+ return this.x * y;
+ }
+ }
+ const a = new A(1),
+ b = new B(1);
+
+ assert.equal(a.add(1), 2);
+ assert.equal(a.sub(1), 0);
+ assert.equal(b.sub(1), 0);
+ assert.equal(b.add(1), 1);
+ });
+});
+
+describe('App', () => {
+ it('extends EventRegistry', () => {
+ class Reg extends EventRegistry {
+ constructor(x) {
+ super();
+ this.x = x;
+ this.listen('change a', (e) => {
+ // pr('in Reg.constructor, e', e);
+ });
+ }
+ f(x) {
+ this.fire({ name: 'change a', data: x });
+ }
+ }
+
+ const reg = new Reg(1),
+ a = { a: 1, b: 2 },
+ b = { a: 1, b: 2 };
+
+ reg.listen('change a', (e) => {
+ // pr('in describe App, e', e);
+ b.a = e.data + b.a;
+ });
+ assert.deepEqual({ a: 1, b: 2 }, b);
+ reg.fire({ name: 'change a', data: 1 });
+ assert.deepEqual({ a: 2, b: 2 }, b);
+
+ reg.f(2);
+ assert.deepEqual({ a: 4, b: 2 }, b);
+ });
+});
diff --git a/06 - Type Ahead/app/tests.html b/06 - Type Ahead/app/tests.html
new file mode 100644
index 0000000000..0766a8b57c
--- /dev/null
+++ b/06 - Type Ahead/app/tests.html
@@ -0,0 +1,30 @@
+
+
+
+
+ Mocha Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/06 - Type Ahead/cities.json b/06 - Type Ahead/cities.json
new file mode 100644
index 0000000000..259103a7b8
--- /dev/null
+++ b/06 - Type Ahead/cities.json
@@ -0,0 +1 @@
+[{ "1": 2 }, { "1": 2 }]
diff --git a/06 - Type Ahead/events.html b/06 - Type Ahead/events.html
new file mode 100644
index 0000000000..ca531685c7
--- /dev/null
+++ b/06 - Type Ahead/events.html
@@ -0,0 +1,21 @@
+
+
+
+
+ Type Ahead 👀
+
+
+
+
+
+
+ - Filter for a city
+ - or a state
+
+
+
+
+
diff --git a/06 - Type Ahead/events.js b/06 - Type Ahead/events.js
new file mode 100644
index 0000000000..24187679de
--- /dev/null
+++ b/06 - Type Ahead/events.js
@@ -0,0 +1,164 @@
+// Utilitis
+const pr = console.table;
+const Element = (tag) => document.createElement(tag);
+
+class EventRegistry {
+ constructor() {
+ this.map = new Map();
+ }
+
+ fireEvent(event) {
+ for (const cb of this.map.get(event.name) || []) {
+ cb(event);
+ }
+ }
+
+ listen(eventName, cb) {
+ const cbs = this.map.get(eventName) || [];
+ cbs.push(cb);
+ this.map.set(eventName, cbs);
+ }
+ unListen(eventName, cb) {
+ const cbs = this.map.get(eventName) || [];
+ this.map.set(
+ eventName,
+ cbs.filter((x) => x != cb),
+ );
+ }
+}
+
+const App = {
+ eReg: new EventRegistry(),
+ _places: new Map(),
+
+ init() {
+ // init places
+ const _cs = ['Madera,ca', 'Fresno, CA', 'boulder, co', 'austin,tx'];
+ for (const s of _cs) {
+ this.addCity(s);
+ }
+ },
+
+ // str -> [place]
+ places(pattern) {
+ const r = new RegExp(pattern, 'gi'),
+ res = [];
+ this._places.forEach((v, k) => {
+ if (k.match(r)) res.push(v);
+ });
+ return res;
+ },
+
+ // str, str -> str
+ key(city, st) {
+ return `${city},${st}`;
+ },
+
+ // str -> {place: obj, err: str}
+ addCity(str) {
+ const [city, st] = str
+ .toLowerCase()
+ .split(',')
+ .map((x) => x.trim());
+ const key = this.key(city, st),
+ place = { city: city, st: st };
+ if (this._places.has(key)) return { place: null, err: 'key exists' };
+ this._places.set(key, place);
+ this.eReg.fireEvent({ name: 'PlacesChange', city: city, st: st });
+ return { place: place, err: '' };
+ },
+
+ // str, str -> err: str
+ delCity(city, st) {
+ pr(city, st);
+ this._places.delete(this.key(city, st));
+ this.eReg.fireEvent({
+ name: 'PlacesChange',
+ city: city,
+ st: st,
+ details: 'delete',
+ });
+ },
+
+ // str, str -> err: str
+ editCity(city, st) {
+ pr(city, st);
+ },
+};
+
+App.init();
+// ==========
+
+// Componets
+//
+// [places], rootElment -> [Element('li')]
+function placesList({ places, onDel, onEdit }) {
+ const lst = places.map(({ city, st }) => {
+ const li = Element('li'),
+ c = Element('span'),
+ s = Element('span'),
+ del = Element('button'),
+ ed = Element('button'),
+ sbtn = Element('span');
+ c.append(city);
+ s.append(st);
+ del.append('Del');
+ del.addEventListener('click', () => {
+ onDel(city, st);
+ });
+ ed.append('Edit');
+ ed.addEventListener('click', () => {
+ onEdit(city, st);
+ });
+ sbtn.append(ed, del);
+ li.append(c, s, sbtn);
+ return li;
+ });
+ return lst;
+}
+// ==========
+
+const suggestions = document.querySelector('.suggestions');
+// str, Element ->
+function showSuggestions(pattern, rootEl) {
+ const ps = App.places(pattern),
+ lis = placesList({
+ places: ps,
+ onDel: (city, st) => App.delCity(city, st),
+ onEdit: (city, st) => App.editCity(city, st),
+ });
+ rootEl.replaceChildren(...lis);
+}
+
+App.eReg.listen('SearchChange', ({ pattern }) =>
+ showSuggestions(pattern, suggestions),
+);
+
+App.eReg.listen('PlacesChange', () => {
+ const pattern = search.value;
+ showSuggestions(pattern, suggestions);
+});
+
+showSuggestions('', suggestions);
+// ==========
+
+const search = document.querySelector('.search');
+search.addEventListener('keyup', (e) => {
+ const event = { name: 'SearchChange', pattern: e.target.value };
+ App.eReg.fireEvent(event);
+});
+// ==========
+
+const add = document.querySelector('.add');
+add.addEventListener('change', (e) => {
+ const { place, err } = App.addCity(e.target.value);
+ if (err) {
+ pr('Error', err);
+ return;
+ }
+ const event = { name: 'PlacesChange', place: place };
+ App.eReg.fireEvent(event);
+ e.target.value = '';
+});
+
+// END Componets ===============
diff --git a/06 - Type Ahead/worker/app.js b/06 - Type Ahead/worker/app.js
new file mode 100644
index 0000000000..54b2904e56
--- /dev/null
+++ b/06 - Type Ahead/worker/app.js
@@ -0,0 +1,6 @@
+import { Lst } from './lst.js';
+
+const pr = console.log;
+
+pr('hi');
+pr(Lst.map((x) => x.name + x.val));
diff --git a/06 - Type Ahead/worker/index.html b/06 - Type Ahead/worker/index.html
new file mode 100644
index 0000000000..20e0408efc
--- /dev/null
+++ b/06 - Type Ahead/worker/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+ Type Ahead 👀
+
+
+
+
+
+
+ - Filter for a city
+ - or a state
+
+
+
+
+
diff --git a/06 - Type Ahead/worker/lst.js b/06 - Type Ahead/worker/lst.js
new file mode 100644
index 0000000000..0917c41274
--- /dev/null
+++ b/06 - Type Ahead/worker/lst.js
@@ -0,0 +1,5 @@
+export const Lst = [
+ { name: 'a', val: 'aa' },
+ { name: 'b', val: 'bb' },
+ { name: 'c', val: 'cc' },
+];
diff --git a/27 - Click and Drag/index-FINISHED.html b/27 - Click and Drag/index-FINISHED.html
index 52eb86628c..aefeedcddc 100644
--- a/27 - Click and Drag/index-FINISHED.html
+++ b/27 - Click and Drag/index-FINISHED.html
@@ -1,71 +1,69 @@
-
-
- Click and Drag
-
-
-
-
-
01
-
02
-
03
-
04
-
05
-
06
-
07
-
08
-
09
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
+
+
+ Click and Drag
+
+
+
+
+
01
+
02
+
03
+
04
+
05
+
06
+
07
+
08
+
09
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
-
+ slider.addEventListener('mouseup', () => {
+ isDown = false;
+ slider.classList.remove('active');
+ });
+ slider.addEventListener('mousemove', (e) => {
+ if (!isDown) return; // stop the fn from running
+ e.preventDefault();
+ const x = e.pageX - slider.offsetLeft;
+ const walk = (x - startX) * 3;
+ slider.scrollLeft = scrollLeft - walk;
+ });
+
diff --git a/data/cities.json b/data/cities.json
new file mode 100644
index 0000000000..259103a7b8
--- /dev/null
+++ b/data/cities.json
@@ -0,0 +1 @@
+[{ "1": 2 }, { "1": 2 }]
diff --git a/lab1/PT_Part2_Music_Generation.ipynb b/lab1/PT_Part2_Music_Generation.ipynb
new file mode 100644
index 0000000000..7c9722391d
--- /dev/null
+++ b/lab1/PT_Part2_Music_Generation.ipynb
@@ -0,0 +1,1438 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "view-in-github",
+ "colab_type": "text"
+ },
+ "source": [
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "uoJsVjtCMunI"
+ },
+ "source": [
+ "\n",
+ "\n",
+ "# Copyright Information"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "bUik05YqMyCH"
+ },
+ "outputs": [],
+ "source": [
+ "# Copyright 2025 MIT Introduction to Deep Learning. All Rights Reserved.\n",
+ "#\n",
+ "# Licensed under the MIT License. You may not use this file except in compliance\n",
+ "# with the License. Use and/or modification of this code outside of MIT Introduction\n",
+ "# to Deep Learning must reference:\n",
+ "#\n",
+ "# © MIT Introduction to Deep Learning\n",
+ "# http://introtodeeplearning.com\n",
+ "#"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "O-97SDET3JG-"
+ },
+ "source": [
+ "# Lab 1: Intro to PyTorch and Music Generation with RNNs\n",
+ "\n",
+ "# Part 2: Music Generation with RNNs\n",
+ "\n",
+ "In this portion of the lab, we will explore building a Recurrent Neural Network (RNN) for music generation using PyTorch. We will train a model to learn the patterns in raw sheet music in [ABC notation](https://en.wikipedia.org/wiki/ABC_notation) and then use this model to generate new music."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "rsvlBQYCrE4I"
+ },
+ "source": [
+ "## 2.1 Dependencies\n",
+ "First, let's download the course repository, install dependencies, and import the relevant packages we'll need for this lab.\n",
+ "\n",
+ "We will be using [Comet ML](https://www.comet.com/docs/v2/) to track our model development and training runs. First, sign up for a Comet account [at this link](https://www.comet.com/signup?utm_source=mit_dl&utm_medium=partner&utm_content=github\n",
+ ") (you can use your Google or Github account). You will need to generate a new personal API Key, which you can find either in the first 'Get Started with Comet' page, under your account settings, or by pressing the '?' in the top right corner and then 'Quickstart Guide'. Enter this API key as the global variable `COMET_API_KEY`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "id": "riVZCVK65QTH"
+ },
+ "outputs": [],
+ "source": [
+ "!pip install comet_ml > /dev/null 2>&1\n",
+ "import comet_ml\n",
+ "# TODO: ENTER YOUR API KEY HERE!! instructions above\n",
+ "COMET_API_KEY = \"rom6izyKhaALsVmPeV0NjkAmQ\"\n",
+ "\n",
+ "# Import PyTorch and other relevant libraries\n",
+ "import torch\n",
+ "import torch.nn as nn\n",
+ "import torch.optim as optim\n",
+ "\n",
+ "# Download and import the MIT Introduction to Deep Learning package\n",
+ "!pip install mitdeeplearning --quiet\n",
+ "import mitdeeplearning as mdl\n",
+ "\n",
+ "# Import all remaining packages\n",
+ "import numpy as np\n",
+ "import os\n",
+ "import time\n",
+ "import functools\n",
+ "from IPython import display as ipythondisplay\n",
+ "from tqdm import tqdm\n",
+ "from scipy.io.wavfile import write\n",
+ "!apt-get install abcmidi timidity > /dev/null 2>&1\n",
+ "\n",
+ "\n",
+ "# Check that we are using a GPU, if not switch runtimes\n",
+ "# using Runtime > Change Runtime Type > GPU\n",
+ "assert torch.cuda.is_available(), \"Please enable GPU from runtime settings\"\n",
+ "assert COMET_API_KEY != \"\", \"Please insert your Comet API Key\"\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "_ajvp0No4qDm"
+ },
+ "source": [
+ "## 2.2 Dataset\n",
+ "\n",
+ "\n",
+ "\n",
+ "We've gathered a dataset of thousands of Irish folk songs, represented in the ABC notation. Let's download the dataset and inspect it:\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "id": "P7dFnP5q3Jve",
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "outputId": "ab4ed7d0-b691-4988-ac61-b64d7493e69f"
+ },
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "Found 817 songs in text\n",
+ "\n",
+ "Example song: \n",
+ "X:1\n",
+ "T:Alexander's\n",
+ "Z: id:dc-hornpipe-1\n",
+ "M:C|\n",
+ "L:1/8\n",
+ "K:D Major\n",
+ "(3ABc|dAFA DFAd|fdcd FAdf|gfge fefd|(3efe (3dcB A2 (3ABc|!\n",
+ "dAFA DFAd|fdcd FAdf|gfge fefd|(3efe dc d2:|!\n",
+ "AG|FAdA FAdA|GBdB GBdB|Acec Acec|dfaf gecA|!\n",
+ "FAdA FAdA|GBdB GBdB|Aceg fefd|(3efe dc d2:|!\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Download the dataset\n",
+ "songs = mdl.lab1.load_training_data()\n",
+ "\n",
+ "# Print one of the songs to inspect it in greater detail!\n",
+ "example_song = songs[0]\n",
+ "print(\"\\nExample song: \")\n",
+ "print(example_song)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "hKF3EHJlCAj2"
+ },
+ "source": [
+ "We can easily convert a song in ABC notation to an audio waveform and play it back. Be patient for this conversion to run, it can take some time."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "id": "11toYzhEEKDz",
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 76
+ },
+ "outputId": "4e357c3a-5930-4202-bf85-5b8d8138a9ee"
+ },
+ "outputs": [
+ {
+ "output_type": "execute_result",
+ "data": {
+ "text/plain": [
+ ""
+ ],
+ "text/html": [
+ "\n",
+ "