IMPR: GraphQL page browsing with History API + Minor logical Improvements

This commit is contained in:
Tony Air 2021-02-16 13:46:19 +07:00
parent cd305e05cb
commit 025cb00bc1
7 changed files with 632 additions and 0 deletions

View File

@ -0,0 +1,19 @@
import { InMemoryCache } from '@apollo/client';
//import { IonicStorageModule } from '@ionic/storage';
//import { persistCache, IonicStorageWrapper } from 'apollo3-cache-persist';
import { persistCacheSync, LocalStorageWrapper } from 'apollo3-cache-persist';
const cache = new InMemoryCache();
// await before instantiating ApolloClient, else queries might run before the cache is persisted
//await persistCache({
persistCacheSync({
cache,
storage: new LocalStorageWrapper(window.localStorage),
key: 'web-persist',
maxSize: 1048576, // 1Mb
//new IonicStorageWrapper(IonicStorageModule),
});
export { cache };

View File

@ -0,0 +1,208 @@
/*
* Lightbox window
*/
import { Component } from 'react';
import Events from '../_events';
import { client } from './_apollo';
import { gql } from '@apollo/client';
class Page extends Component {
state = {
type: [],
shown: false,
loading: false,
error: false,
current: null,
ID: null,
URLSegment: null,
ClassName: 'Page',
CSSClass: null,
Title: null,
Summary: null,
Link: null,
URL: null,
Elements: [],
page: null,
};
componentDidUpdate() {
const ui = this;
if (ui.state.Title) {
document.title = ui.state.Title;
if (ui.state.URL) {
window.history.pushState(
{ page: JSON.stringify(ui.state) },
ui.state.Title,
ui.state.URL,
);
}
}
if (ui.state.Elements.length) {
window.dispatchEvent(new Event(Events.AJAX));
}
}
constructor(props) {
super(props);
const ui = this;
ui.name = ui.constructor.name;
console.log(`${ui.name}: init`);
}
reset = () => {
const ui = this;
ui.setState({
type: [],
shown: false,
loading: false,
error: false,
ID: null,
Title: null,
URL: null,
Elements: [],
});
};
load = (link) => {
const ui = this;
const query = gql(`
query Pages {
readPages(URLSegment: "home", limit: 1, offset: 0) {
edges {
node {
__typename
_id
ID
Title
ClassName
CSSClass
Summary
Link
URLSegment
Elements {
edges {
node {
__typename
_id
ID
Title
Render
}
}
pageInfo {
hasNextPage
hasPreviousPage
totalCount
}
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
totalCount
}
}
}
`);
ui.reset();
ui.setState({
Title: 'Loading ...',
loading: true,
});
console.log(client.readQuery({ query }));
client
.query({
query: query,
})
.then((resp) => {
const page = resp.data.readPages.edges[0].node;
// write to cache
client.writeQuery({ query, data: { resp } });
console.log(client.readQuery({ query }));
ui.setState({
ID: page.ID,
Title: page.Title,
ClassName: page.ClassName,
URLSegment: page.URLSegment,
CSSClass: page.CSSClass,
Summary: page.Summary,
Link: page.Link,
Elements: page.Elements.edges,
URL: page.Link || link,
loading: false,
});
})
.catch((error) => {
console.error(error);
let msg = '';
if (error.response) {
switch (error.response.status) {
case 404:
msg = 'Not Found.';
break;
case 500:
msg = 'Server issue, please try again latter.';
break;
default:
msg = 'Something went wrong.';
break;
}
} else if (error.request) {
msg = 'No response received';
} else {
console.warn('Error', error.message);
}
ui.setState({ error: msg });
});
};
getHtml = (html) => {
const decodeHtmlEntity = (input) => {
var doc = new DOMParser().parseFromString(input, 'text/html');
return doc.documentElement.textContent;
};
return { __html: decodeHtmlEntity(html) };
};
render() {
const ui = this;
const name = ui.name;
const className = `elemental-area page-${ui.state.CSSClass} url-${ui.state.URLSegment}`;
const ElementItem = (props) => (
<div dangerouslySetInnerHTML={props.html}></div>
);
let html = '';
if (ui.state.Elements.length) {
ui.state.Elements.map((el) => {
html += el.node.Render;
});
} else {
html += '<div class="loading">Loading ...</div>';
}
return (
<div
className={className}
dangerouslySetInnerHTML={ui.getHtml(html)}
></div>
);
}
}
export default Page;

View File

@ -0,0 +1,54 @@
// browser tab visibility state detection
import Events from '../_events';
import Consts from '../_consts';
export default ((W) => {
const NAME = '_main.css-screen-size';
const D = document;
const BODY = D.body;
const detectCSSScreenSize = () => {
const el = D.createElement('div');
el.className = 'env-test';
BODY.appendChild(el);
const envs = [...Consts.ENVS].reverse();
let curEnv = envs.shift();
BODY.classList.remove(...envs);
for (let i = 0; i < envs.length; ++i) {
const env = envs[i];
el.classList.add(`d-${env}-none`);
if (W.getComputedStyle(el).display === 'none') {
curEnv = env;
BODY.classList.add(`${curEnv}`);
break;
}
}
let landscape = true;
if (W.innerWidth > W.innerHeight) {
BODY.classList.add('landscape');
BODY.classList.remove('portrait');
} else {
landscape = false;
BODY.classList.add('portrait');
BODY.classList.remove('landscape');
}
console.log(
`${NAME}: screen size detected ${curEnv} | landscape ${landscape}`,
);
BODY.removeChild(el);
return curEnv;
};
W.addEventListener(`${Events.LOADED}`, detectCSSScreenSize);
W.addEventListener(`${Events.RESIZE}`, detectCSSScreenSize);
})(window);

View File

@ -0,0 +1,146 @@
// browser tab visibility state detection
import Events from '../_events';
import Consts from '../_consts';
import Page from './_page.jsx';
const MainUILinks = ((W) => {
const NAME = '_main.links';
const D = document;
const BODY = D.body;
class MainUILinks {
static init() {
const ui = this;
ui.GraphPage = null;
console.log(`${NAME}: init`);
ui.loaded();
// history state switch
W.addEventListener('popstate', (e) => {
ui.popState(e);
});
}
static loaded() {
const ui = this;
D.querySelectorAll('.graphql-page').forEach((el, i) => {
const el_id = el.getAttribute('href');
el.setAttribute(`data-${ui.name}-id`, el_id);
el.addEventListener('click', ui.loadClick);
});
}
static reset() {
// reset focus
D.activeElement.blur();
// remove active and loading classes
D.querySelectorAll('.graphql-page,.nav-item').forEach((el2) => {
el2.classList.remove('active');
el2.classList.remove('loading');
});
}
static popState(e) {
const ui = this;
if (!ui.GraphPage) {
console.log(
`${NAME}: [popstate] GraphPage is missing. Have to render it first`,
);
ui.GraphPage = ReactDOM.render(
<Page />,
document.getElementById('MainContent'),
);
}
if (e.state && e.state.page) {
console.log(`${NAME}: [popstate] load`);
const state = JSON.parse(e.state.page);
state.current = null;
state.popstate = true;
ui.reset();
D.querySelectorAll(`[data-${ui.name}-id="${e.state.link}"]`).forEach(
(el) => {
el.classList.add('active');
},
);
ui.GraphPage.setState(state);
} else if (e.state && e.state.landing) {
console.log(`${NAME}: [popstate] go to landing`);
W.location.href = e.state.landing;
} else {
console.warn(`${NAME}: [popstate] state is missing`);
console.log(e);
}
}
// link specific event {this} = current link, not MainUILinks
static loadClick(e) {
console.groupCollapsed(`${NAME}: load on click`);
const ui = MainUILinks;
ui.reset();
e.preventDefault();
if (!ui.GraphPage) {
ui.GraphPage = ReactDOM.render(
<Page />,
document.getElementById('MainContent'),
);
}
const el = e.currentTarget;
const link = el.getAttribute('href') || el.getAttribute('data-href');
ui.GraphPage.state.current = el;
el.classList.add('loading');
ui.GraphPage.load(link)
.then((response) => {
el.classList.remove('loading');
el.classList.add('active');
if (ui.GraphPage.state.Link) {
window.history.pushState(
{
page: JSON.stringify(ui.GraphPage.state),
link: el.getAttribute(`data-${ui.name}-id`),
},
ui.GraphPage.state.Title,
ui.GraphPage.state.Link,
);
}
console.groupEnd(`${NAME}: load on click`);
})
.catch((e) => {
console.log(e);
el.classList.remove('loading');
el.classList.add('not-found');
console.groupEnd(`${NAME}: load on click`);
});
}
}
W.addEventListener(`${Events.LOADED}`, () => {
MainUILinks.init();
});
W.addEventListener(`${Events.AJAX}`, () => {
MainUILinks.loaded();
});
})(window);
export default MainUILinks;

View File

@ -0,0 +1,101 @@
// ping online/offline state switch and detection
import Events from '../_events';
import Consts from '../_consts';
const axios = require('axios');
export default ((W) => {
const NAME = '_main.online';
const D = document;
const BODY = D.body;
let pingInterval;
const PING_META = D.querySelector('meta[name="ping"]');
let update_online_status_lock = false;
const UPDATE_ONLINE_STATUS = (online) => {
if (update_online_status_lock) {
return;
}
update_online_status_lock = true;
if (online) {
if (BODY.classList.contains('is-offline')) {
console.log(`${NAME}: back Online`);
W.dispatchEvent(new Event(Events.BACKONLINE));
} else {
console.log(`${NAME}: Online`);
W.dispatchEvent(new Event(Events.ONLINE));
}
BODY.classList.add('is-online');
BODY.classList.remove('is-offline');
if (PING_META && !pingInterval) {
console.log(`${NAME}: SESSION_PING is active`);
pingInterval = setInterval(SESSION_PING, 300000); // 5 min in ms
}
} else {
console.log(`${NAME}: Offline`);
BODY.classList.add('is-offline');
BODY.classList.remove('is-online');
clearInterval(pingInterval);
pingInterval = null;
W.dispatchEvent(new Event(Events.OFFLINE));
}
update_online_status_lock = false;
};
// session ping
let session_ping_lock = false;
const SESSION_PING = () => {
if (session_ping_lock || BODY.classList.contains('is-offline')) {
return;
}
const PING_URL = PING_META.getAttribute('content');
console.log(`${NAME}: session ping`);
session_ping_lock = true;
axios
.post(PING_URL, {})
.then((resp) => {
session_ping_lock = false;
UPDATE_ONLINE_STATUS(true);
})
.catch((error) => {
console.error(error);
console.warn(`${NAME}: SESSION_PING failed`);
session_ping_lock = false;
UPDATE_ONLINE_STATUS(false);
});
};
// current browser online state
if (typeof navigator.onLine !== 'undefined') {
if (!navigator.onLine) {
UPDATE_ONLINE_STATUS(false);
} else {
UPDATE_ONLINE_STATUS(true);
}
}
W.addEventListener(`${Events.OFFLINE}`, () => {
UPDATE_ONLINE_STATUS(false);
});
W.addEventListener(`${Events.ONLINE}`, () => {
UPDATE_ONLINE_STATUS(true);
});
W.addEventListener(`${Events.LOADED}`, () => {
UPDATE_ONLINE_STATUS(true);
});
})(window);

View File

@ -0,0 +1,70 @@
// touch/mouse detection
import Events from '../_events';
import Consts from '../_consts';
export default ((W) => {
const NAME = '_main.touch';
const D = document;
const BODY = D.body;
let prev_touch_event_name;
let touch_timeout;
const SET_TOUCH_SCREEN = (bool, event_name) => {
if (touch_timeout || event_name === prev_touch_event_name) {
return;
}
if (bool) {
console.log(`${NAME}: Touch screen enabled`);
BODY.classList.add('is-touch');
BODY.classList.remove('is-mouse');
W.dispatchEvent(new Event(Events.TOUCHENABLE));
} else {
console.log(`${NAME}: Touch screen disabled`);
BODY.classList.add('is-mouse');
BODY.classList.remove('is-touch');
W.dispatchEvent(new Event(Events.TOUCHDISABLED));
}
prev_touch_event_name = event_name;
// prevent firing touch and mouse events together
if (!touch_timeout) {
touch_timeout = setTimeout(() => {
clearTimeout(touch_timeout);
touch_timeout = null;
}, 500);
}
};
SET_TOUCH_SCREEN(
'ontouchstart' in W ||
navigator.MaxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0 ||
W.matchMedia('(hover: none)').matches,
'init',
);
D.addEventListener('touchend', (e) => {
let touch = false;
if (e.type !== 'click') {
touch = true;
}
SET_TOUCH_SCREEN(touch, 'click-touchend');
});
// disable touch on mouse events
D.addEventListener('click', (e) => {
let touch = false;
if (e.type !== 'click') {
touch = true;
}
SET_TOUCH_SCREEN(touch, 'click-touchend');
});
})(window);

View File

@ -0,0 +1,34 @@
// browser tab visibility state detection
import Events from '../_events';
import Consts from '../_consts';
export default ((W) => {
const NAME = '_main.visibility';
const D = document;
const BODY = D.body;
// update visibility state
// get browser window visibility preferences
// Opera 12.10, Firefox >=18, Chrome >=31, IE11
const HiddenName = 'hidden';
const VisibilityChangeEvent = 'visibilitychange';
D.addEventListener(VisibilityChangeEvent, () => {
if (D.visibilityState === HiddenName) {
console.log(`${NAME}: Tab: hidden`);
BODY.classList.add('is-hidden');
BODY.classList.remove('is-focused');
W.dispatchEvent(new Event(Events.TABHIDDEN));
} else {
console.log(`${NAME}: Tab: focused`);
BODY.classList.add('is-focused');
BODY.classList.remove('is-hidden');
W.dispatchEvent(new Event(Events.TABFOCUSED));
}
});
})(window);