diff --git a/server.js b/server.js
index 766deb6..aaa8da5 100644
--- a/server.js
+++ b/server.js
@@ -26,8 +26,9 @@ const SERVER_ERROR = {status: "error", error: "server error"};
initDb();
// list boards
-app.get("/api/boards", (req, res) => {
+app.get("/api/boards", async (req, res) => {
try{
+ await sleep(1000);
let boards = db.prepare("SELECT * FROM boards").all();
for( let i = 0; i < boards.length; ++i ){
diff --git a/static/app.js b/static/app.js
new file mode 100644
index 0000000..a2b9f1d
--- /dev/null
+++ b/static/app.js
@@ -0,0 +1,192 @@
+Reef.debug(true);
+
+// force a re-render
+app.addSetter("render", (data) => {
+ appComponent.render();
+});
+
+app.addSetter("loader.show", (data) => {
+ data.loading++;
+});
+
+app.addSetter("loader.hide", (data) => {
+ data.loading--;
+});
+
+app.addSetter("load.boards", async (data) => {
+
+ store.do("loader.show");
+
+ let res = await fetch("/api/boards");
+ data.boards = await res.json();
+
+ store.do("loader.hide");
+});
+
+app.addSetter('load.board', async (data) => {
+ store.do("loader.show");
+
+ if ( !data.board || data.board.id != data.hash.board ){
+ let res = await fetch("/api/boards/" + data.hash.board);
+ data.board = await res.json();
+ }
+
+ store.do("loader.hide");
+});
+
+app.addSetter("hash.update", (data) => {
+ console.log("hash update");
+ data.hash = parseQueryString(window.location.hash.substr(1));
+
+ if ( data.hash.board ){
+ store.do('load.board');
+ } else {
+ data.board = null;
+
+ data.pinZoomModal.active = false;
+ data.addPinModal.active = false;
+ data.aboutModal.active = false;
+ }
+});
+
+let store = new Reef.Store({
+ data: {
+ hash: {
+ board: null
+ },
+ loading: 0,
+ boards: [],
+ board: null,
+ addPinModal: {
+ active: false,
+ boardId: "",
+ newBoardName: null,
+ imageUrl: "",
+ previewImageUrl: null,
+ siteUrl: "",
+ description: "",
+ saveInProgress: false
+ },
+ pinZoomModal: {
+ active: false,
+ pin: null,
+ fullDescriptionOpen: false
+ },
+ aboutModal: {
+ active: false
+ },
+ editBoardModal: {
+ active: false,
+ name: ""
+ }
+ },
+ getters: app.getGetters(),
+ setters: app.getSetters()
+});
+
+
+app.freeze();
+
+// init the app component
+const appComponent = new Reef("#app", {
+ store: store,
+ template: (data) => {
+ return /*html*/`
+
+
+
+
+
+
+
+
+ `
+ //
+ }
+});
+
+// attach all the child components
+for (const [name, f] of Object.entries(app.getComponents())) {
+ let c = f(store);
+ if ( !c ){
+ throw(new Error(`component ${name} did not return a Reef component`));
+ } else {
+ appComponent.attach(c);
+ }
+}
+
+
+document.addEventListener('click', (el) => {
+ let target = el.target.closest('[data-onclick]');
+ if (target) {
+ let action = target.getAttribute('data-onclick');
+ if (action) {
+ try{
+ store.do(action, target);
+ } catch (err){
+ console.error(`Error invoking ${action}:`, err);
+ }
+ }
+ }
+});
+
+// focusout bubbles while 'blur' does not.
+document.addEventListener('focusout', (el) => {
+ let target = el.target.closest('[data-onblur]');
+ if ( target ){
+ let method = target.getAttribute('data-onblur');
+ if ( method ) {
+ store.do(method, target);
+ }
+ }
+});
+
+document.addEventListener('keyup', (el) => {
+
+ if ( store.data.pinZoomModal.active ){
+ if ( el.key == "Escape" ){
+ store.do('pinZoomModal.close');
+
+ } else if ( el.key == "ArrowLeft" ){
+ store.do('pinZoomModal.moveLeft');
+ } else if ( el.key == "ArrowRight" ){
+ store.do('pinZoomModal.moveRight');
+ }
+ }
+
+ if ( store.data.addPinModal.active ){
+ if ( el.key == "Escape" ){
+ store.do('addPinModal.close');
+ }
+ }
+
+ if ( store.data.aboutModal.active ){
+ if ( el.key == "Escape" ){
+ store.do('aboutModal.close');
+ }
+ }
+
+});
+
+window.addEventListener("hashchange", () => {
+ store.do("hash.update");
+});
+
+window.addEventListener('resize', (evt) => {
+ store.do("render");
+});
+
+Reef.databind(appComponent);
+
+store.do('load.boards');
+store.do('hash.update');
+
+appComponent.render();
\ No newline at end of file
diff --git a/static/client.css b/static/client.css
index df18c1d..fe0722e 100644
--- a/static/client.css
+++ b/static/client.css
@@ -1,8 +1,8 @@
-.brick-wall {
+.brickwall {
display: flex;
}
-.brick-wall-column {
+.brickwall-column {
flex: 1;
width: 100%;
}
@@ -19,7 +19,7 @@
background-color: #ccc;
}
-#brick-wall-container {
+#brickwall-container {
max-width: 95%;
}
@@ -45,18 +45,18 @@
object-fit: cover;
}
-#pin-zoom-modal .modal-content {
+#pinZoomModal .modal-content {
/* height: 90%; */
height: calc(100% - 120px);
min-height: 50px;
width: 90%;
}
-#pin-zoom-modal .modal-content > p {
+#pinZoomModal .modal-content > p {
height: 100%;
}
-#pin-zoom-modal .modal-content > p > img {
+#pinZoomModal .modal-content > p > img {
height: 100%;
width: 100%;
margin-left: auto;
@@ -64,29 +64,29 @@
object-fit: contain;
}
-#add-pin-modal-board-name {
+#addPinModal-boardName {
font-weight: bold;
}
-#add-pin-modal .add-pin-flex {
+#addPinModal .add-pin-flex {
display: flex;
}
-#add-pin-modal .add-pin-flex-left {
+#addPinModal .add-pin-flex-left {
flex: 1;
margin: 10px;
margin-top: 40px;
}
-#add-pin-modal .add-pin-flex-left img{
+#addPinModal .add-pin-flex-left img{
border-radius: 12px;
width: 100%;
height: auto;
display: block;
}
-#add-pin-modal .add-pin-flex-right {
+#addPinModal .add-pin-flex-right {
flex: 2;
margin: 10px;
}
@@ -106,49 +106,7 @@
}
-/*
- * loader - https://loading.io/css/
- */
- .lds-ring {
- display: inline-block;
- position: relative;
- width: 80px;
- height: 80px;
- }
- .lds-ring div {
- box-sizing: border-box;
- display: block;
- position: absolute;
- width: 64px;
- height: 64px;
- margin: 8px;
- border: 8px solid #fff;
- border-radius: 50%;
- animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
- border-color: #fff transparent transparent transparent;
- }
- .lds-ring div:nth-child(1) {
- animation-delay: -0.45s;
- }
- .lds-ring div:nth-child(2) {
- animation-delay: -0.3s;
- }
- .lds-ring div:nth-child(3) {
- animation-delay: -0.15s;
- }
- @keyframes lds-ring {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
-
-
-
-
-.pin-zoom-modal-delete {
+.pinZoomModal-delete {
position: fixed;
width: 24px;
height: 24px;
@@ -160,11 +118,11 @@
border: none;
opacity: 0.8;
}
-.pin-zoom-modal-delete:hover{
+.pinZoomModal-delete:hover{
opacity: 1;
}
-.pin-zoom-modal-site-link {
+.pinZoomModal-site-link {
position: fixed;
width: 24px;
height: 24px;
@@ -177,11 +135,11 @@
opacity: 0.8;
}
-.pin-zoom-modal-site-link:hover{
+.pinZoomModal-site-link:hover{
opacity: 1;
}
-.pin-zoom-modal-edit {
+.pinZoomModal-edit {
position: fixed;
width: 24px;
height: 24px;
@@ -194,16 +152,13 @@
opacity: 0.8;
}
-.pin-zoom-modal-edit {
+.pinZoomModal-edit {
opacity: 1;
}
-#loader:after {
- border-left-color: #3273dc;
- border-bottom-color: #3273dc;
-}
-#pin-zoom-modal .pin-zoom-modal-description {
+
+#pinZoomModal .pinZoomModal-description {
position: fixed;
height: 24px;
left: 20px;
@@ -215,15 +170,15 @@
text-overflow: ellipsis;
}
-#pin-zoom-modal .pin-zoom-modal-description:hover {
+#pinZoomModal .pinZoomModal-description:hover {
cursor: pointer;
}
-#pin-zoom-modal .pin-zoom-modal-full-description {
+#pinZoomModal .pinZoomModal-full-description {
display: none;
}
-#pin-zoom-modal .pin-zoom-modal-hide-full-description {
+#pinZoomModal .pinZoomModal-hide-full-description {
display: block;
height: 16px;
width: 16px;
@@ -234,11 +189,37 @@
margin-bottom: 8px;
}
-#pin-zoom-modal .pin-zoom-modal-full-description.pin-zoom-modal-full-description-open {
+#pinZoomModal .pinZoomModal-full-description.pinZoomModal-full-description-open {
display: block;
position: fixed;
left: 0px;
bottom: 0px;
background-color: #eeeeee;
padding: 20px;
+}
+
+
+#loader:after {
+ border-left-color: #3273dc;
+ border-bottom-color: #3273dc;
+}
+
+#loader-mobile {
+ display: none;
+}
+
+@media (max-width: 1023px) {
+ #loader-desktop {
+ display: none;
+ }
+
+ #burger-mobile {
+ margin-left: 0;
+ }
+
+ #loader-mobile {
+ display: block;
+ position: relative;
+ margin-left: auto;
+ }
}
\ No newline at end of file
diff --git a/static/client.js b/static/client.js
index 7f70a11..d5e9e79 100644
--- a/static/client.js
+++ b/static/client.js
@@ -24,7 +24,7 @@ const store = new Reef.Store({
pin: null,
fullDescriptionOpen: false
},
- about: {
+ aboutModal: {
active: false
},
editBoard: {
@@ -47,22 +47,55 @@ const store = new Reef.Store({
return false;
}
- return true;
- },
- isEditBoardValid: (data) => {
- if (!data.editBoard.name){
- return false;
- }
-
- if ( data.editBoard.name.trim().length < 1 ){
- return false;
- }
-
return true;
}
}
});
+// since we can't dynamically set setters/getters in Reef,
+// we'll create our own outside 'store'
+const actions = new Proxy(new function(){
+
+ const _actions = {};
+
+ this.add = (actionName, f) => {
+ if ( _actions[actionName] ){
+ console.error(`action ${actionName} is already defined.`);
+ } else {
+ console.log(`Added action ${actionName}`);
+ _actions[actionName] = f;
+ }
+ };
+
+ this.do = (actionName, target) => {
+ console.log(_actions);
+ if (!_actions[actionName]){
+ console.error(`action ${actionName} is not defined.`);
+ } else {
+ console.log(`running action ${actionName}`);
+ _actions[actionName](store.data, target);
+ }
+ }
+
+ set = () => {
+ console.error("Use actions.do(name, function).");
+ }
+
+}, {
+ get(target, name, receiver){
+ console.log("target");
+ console.log(target);
+ console.log("name");
+ console.log(name);
+ console.log("receiver");
+ console.log(receiver);
+ return Reflect.get(target, name, receiver);
+ },
+ set(target, name, receiver){
+ console.error("Direct modification of actions is not allowed. Use actions.do(name, function) instead.");
+ }
+});
+
function getBoardIndexById(id){
let idx = -1;
for ( let i = 0; i < store.data.boards.length; ++i ){
@@ -91,243 +124,39 @@ function getPinById(id){
return store.data.board.pins[getPinIndexById(id)];
}
-const actions = {
- openAddPinModal: () => {
-
- if ( store.data.board ){
- store.data.addPin.boardId = store.data.board.id;
- } else if ( store.data.boards && store.data.boards.length > 0 ){
- store.data.addPin.boardId = store.data.boards[0].id;
- } else {
- store.data.addPin.boardId = "new";
- }
+// const actions = {
+
+// deletePin: async () => {
+// if ( confirm("Are you sure you want to delete this pin?") ){
- store.data.addPin.active = true;
- },
- closeAddPinModal: () => {
- store.data.addPin.active = false;
- store.data.addPin.imageUrl = "";
- store.data.addPin.previewImageUrl = "";
- store.data.addPin.siteUrl = "";
- store.data.addPin.description = "";
- store.data.addPin.newBoardName = "";
- store.data.addPin.saveInProgress = false;
- },
- saveAddPin: async () => {
+// store.data.loading++;
- store.data.addPin.saveInProgress = true;
+// let pinId = store.data.pinZoom.pin.id;
- let boardId = store.data.addPin.boardId;
+// let idx = getPinIndexById(pin.id);
+// if ( idx >= 0 ){
+// store.data.board.pins.splice(idx,1);
+// }
- let newBoard = null;
+// actions.closePinZoomModal();
- if ( boardId == "new" ){
- let res = await fetch('api/boards', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- "name": store.data.addPin.newBoardName
- })
- });
+// let res = await fetch(`/api/pins/${pinId}`, {
+// method: "DELETE"
+// });
- if ( res.status == 200 ){
- newBoard = await res.json();
- boardId = newBoard.id;
- store.data.boards.push(newBoard);
- }
- }
+// if ( res.status == 200 ){
+// console.log(`deleted pin#${pinId}`);
+// } else {
+// console.error(`error deleting pin#${pinId}`);
+// }
- let postData = {
- boardId: boardId,
- imageUrl: store.data.addPin.imageUrl,
- siteUrl: store.data.addPin.siteUrl,
- description: store.data.addPin.description
- };
-
- let res = await fetch('api/pins', {
- method: 'POST',
- headers: {
- 'Content-Type': "application/json"
- },
- body: JSON.stringify(postData)
- });
-
- if ( res.status == 200 ){
-
- let body = await res.json();
- if ( store.data.board && store.data.board.id == boardId ){
- store.data.board.pins.push(body);
- }
-
- if ( newBoard ){
- newBoard.titlePinId = body.id;
- }
-
- actions.closeAddPinModal();
- }
-
- },
- updateAddPinPreview: () => {
- if ( store.data.addPin.imageUrl.startsWith("http") ){
- ( async() => {
- let res = await fetch(store.data.addPin.imageUrl, {
- mode: 'no-cors',
- method: "HEAD"
- });
- if ( res.status = 200 ){
- store.data.addPin.previewImageUrl = store.data.addPin.imageUrl;
- }
- })();
- } else {
- store.data.addPin.previewImageUrl = null;
- }
- },
- openPinZoomModal: (el) => {
-
- let pinId = el.getAttribute("data-pinid");
-
- if( pinId ){
- store.data.pinZoom.pin = getPinById(pinId);
- store.data.pinZoom.active = true;
- }
-
- },
- closePinZoomModal: () => {
- store.data.pinZoom.active = false;
- store.data.pinZoom.pinId = null;
- store.data.pinZoom.fullDescriptionOpen = false;
- },
- movePinZoomModalLeft: () => {
-
- let idx = getPinIndexById(store.data.pinZoom.pin.id);
-
- if ( idx > 0 ){
- store.data.pinZoom.pin = store.data.board.pins[idx-1];
- }
-
- },
- movePinZoomModalRight: () => {
-
- let idx = getPinIndexById(store.data.pinZoom.pin.id);
-
- if ( idx >= 0 && (idx < store.data.board.pins.length-1) ){
- store.data.pinZoom.pin = store.data.board.pins[idx+1];
- }
- },
- deletePin: async () => {
- if ( confirm("Are you sure you want to delete this pin?") ){
-
- store.data.loading++;
-
- let pinId = store.data.pinZoom.pin.id;
-
- let idx = getPinIndexById(pin.id);
- if ( idx >= 0 ){
- store.data.board.pins.splice(idx,1);
- }
-
- actions.closePinZoomModal();
-
- let res = await fetch(`/api/pins/${pinId}`, {
- method: "DELETE"
- });
-
- if ( res.status == 200 ){
- console.log(`deleted pin#${pinId}`);
- } else {
- console.error(`error deleting pin#${pinId}`);
- }
-
- store.data.loading--;
+// store.data.loading--;
- }
- },
- showAboutModal: () => {
- store.data.about.active = true;
- },
- closeAboutModal: () => {
- store.data.about.active = false;
- },
- openEditBoardModal: () => {
- store.data.editBoard.name = store.data.board.name;
- store.data.editBoard.active = true;
- },
- closeEditBoardModal: () => {
- store.data.editBoard.name = "";
- store.data.editBoard.active = false;
- },
- saveEditBoard: async () => {
+// }
+// },
+
- store.data.loading++
-
- let boardId = store.data.board.id;
- let name = store.data.editBoard.name;
-
- let idx = getBoardIndexById(boardId);
- console.log("idx=" + idx);
- if ( idx >= 0 ){
- store.data.boards[idx].name = name;
- store.data.board.name = name;
- }
-
- let res = await fetch(`/api/boards/${boardId}`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- name: name
- })
- });
-
- if ( res.status == 200 ){
- console.log(`updated board#${boardId}`);
- store.data.editBoard.active = false;
- } else {
- console.error(`error updating board#${boardId}`);
- }
-
-
- store.data.loading--;
- },
- editBoardDelete: async () => {
-
- if ( !confirm("Are you sure you want to delete this board and all pins on it?") ){
- return;
- }
-
- store.data.loading++;
-
- let boardId = store.data.board.id;
-
-
- let idx = getBoardIndexById(boardId);
- if ( idx >= 0 ){
- store.data.boards.splice(idx, 1);
- }
- store.data.editBoard.active = false;
- window.location.hash = "";
-
-
- let res = await fetch(`/api/boards/${boardId}`, {
- method: 'DELETE'
- });
-
- if ( res.status == 200 ){
- console.log(`deleted board#${boardId}`);
- } else {
- console.log(`error deleting board#${boardId}`);
- }
-
- store.data.loading--;
- },
- pinZoomShowFullDescription: () => {
- store.data.pinZoom.fullDescriptionOpen = true;
- },
- pinZoomHideFullDescription: () => {
- store.data.pinZoom.fullDescriptionOpen = false;
- }
-}
+// }
const app = new Reef("#app", {
store: store,
@@ -340,388 +169,28 @@ const app = new Reef("#app", {