├── packages
├── src
│ ├── pwa
│ │ ├── components
│ │ │ ├── index.js
│ │ │ ├── Icons
│ │ │ │ ├── Menu.js
│ │ │ │ ├── Schedule.js
│ │ │ │ ├── Facebook.js
│ │ │ │ ├── Twitter.js
│ │ │ │ ├── Instagram.js
│ │ │ │ ├── Bullhorn.js
│ │ │ │ ├── Hyperlink.js
│ │ │ │ ├── Comments.js
│ │ │ │ ├── Utensils.js
│ │ │ │ └── Users.js
│ │ │ ├── Theme
│ │ │ │ ├── Title.js
│ │ │ │ ├── Favicon.js
│ │ │ │ └── AppleMeta.js
│ │ │ ├── LazyFacebookIframe
│ │ │ │ └── index.js
│ │ │ ├── Menu
│ │ │ │ ├── MenuList.js
│ │ │ │ ├── MenuLinks.js
│ │ │ │ ├── MenuButton.js
│ │ │ │ ├── MenuLink.js
│ │ │ │ ├── MenuIcon.js
│ │ │ │ ├── MenuHeader.js
│ │ │ │ ├── index.js
│ │ │ │ ├── MenuRoute.js
│ │ │ │ └── MenuNotifications.js
│ │ │ ├── Home
│ │ │ │ ├── NowNext.js
│ │ │ │ ├── Nav.js
│ │ │ │ ├── CardsList.js
│ │ │ │ ├── CardSpeakers.js
│ │ │ │ ├── ScheduleItemSpeakers.js
│ │ │ │ ├── VenueLink.js
│ │ │ │ ├── CardTitle.js
│ │ │ │ ├── index.js
│ │ │ │ ├── ScheduleItemTitle.js
│ │ │ │ ├── FavoriteButton.js
│ │ │ │ ├── NavItem.js
│ │ │ │ ├── Schedule.js
│ │ │ │ ├── FilterFavorites.js
│ │ │ │ ├── ScheduleItem.js
│ │ │ │ └── ScheduleList.js
│ │ │ ├── Manifest
│ │ │ │ └── index.js
│ │ │ ├── Pages
│ │ │ │ ├── index.js
│ │ │ │ └── Page.js
│ │ │ ├── Venues
│ │ │ │ ├── index.js
│ │ │ │ └── Venue.js
│ │ │ ├── Table
│ │ │ │ └── index.js
│ │ │ ├── Posts
│ │ │ │ ├── index.js
│ │ │ │ └── Post.js
│ │ │ ├── Sessions
│ │ │ │ ├── index.js
│ │ │ │ └── FavoriteButton.js
│ │ │ ├── Speakers
│ │ │ │ ├── index.js
│ │ │ │ └── SessionCard.js
│ │ │ ├── TopBar
│ │ │ │ ├── Logo.js
│ │ │ │ ├── CloseButton.js
│ │ │ │ └── index.js
│ │ │ ├── Nav
│ │ │ │ ├── Button.js
│ │ │ │ ├── index.js
│ │ │ │ └── Share.js
│ │ │ ├── Media
│ │ │ │ ├── index.js
│ │ │ │ └── image.js
│ │ │ ├── Spinner
│ │ │ │ └── index.js
│ │ │ ├── Announcements
│ │ │ │ ├── index.js
│ │ │ │ ├── NextPage.js
│ │ │ │ ├── Card.js
│ │ │ │ └── Refresh.js
│ │ │ ├── HtmlToReactConverter
│ │ │ │ ├── filter.js
│ │ │ │ └── htmlMap.js
│ │ │ ├── LazyIframe
│ │ │ │ └── index.js
│ │ │ ├── Link
│ │ │ │ └── index.js
│ │ │ ├── LazyYoutube
│ │ │ │ └── index.js
│ │ │ ├── LazyAudio
│ │ │ │ └── index.js
│ │ │ ├── LazyVideo
│ │ │ │ └── index.js
│ │ │ ├── Anchor
│ │ │ │ └── index.js
│ │ │ └── LazyTweet
│ │ │ │ └── index.js
│ │ ├── converters
│ │ │ ├── removeTagStyle.js
│ │ │ ├── table.js
│ │ │ ├── removeHidden.js
│ │ │ ├── audio.js
│ │ │ ├── removeScript.js
│ │ │ ├── anchor.js
│ │ │ ├── iframe.js
│ │ │ ├── index.js
│ │ │ ├── video.js
│ │ │ ├── twitter.js
│ │ │ ├── youtube.js
│ │ │ ├── facebookIframe.js
│ │ │ ├── instagram.js
│ │ │ └── image.js
│ │ ├── stores
│ │ │ ├── favorites.js
│ │ │ ├── menu.js
│ │ │ ├── notifications.js
│ │ │ ├── schedule.js
│ │ │ ├── speaker.js
│ │ │ ├── track.js
│ │ │ └── session.js
│ │ ├── client.js
│ │ ├── server.js
│ │ ├── processors
│ │ │ ├── removeInlineStyle.js
│ │ │ ├── removeAmpIds.js
│ │ │ ├── dmGallery.js
│ │ │ ├── myrTiledGallery.js
│ │ │ ├── myrGallery.js
│ │ │ ├── index.js
│ │ │ ├── tzGallery.js
│ │ │ └── anchor.js
│ │ ├── styles
│ │ │ └── lightbox.js
│ │ ├── flows
│ │ │ └── client.js
│ │ ├── consts
│ │ │ └── index.js
│ │ ├── utils
│ │ │ └── index.js
│ │ └── contexts
│ │ │ └── index.js
│ └── server
│ │ ├── index.js
│ │ └── manifest.js
├── .gitignore
├── README.md
├── package.json
└── LICENSE
├── .gitignore
├── core
├── packages
│ ├── settings
│ │ ├── shared
│ │ │ ├── selectors
│ │ │ │ └── index.js
│ │ │ ├── actionTypes
│ │ │ │ └── index.js
│ │ │ ├── actions
│ │ │ │ └── index.js
│ │ │ ├── reducers
│ │ │ │ └── index.js
│ │ │ └── selectorCreators
│ │ │ │ └── index.js
│ │ ├── amp
│ │ │ └── index.js
│ │ └── pwa
│ │ │ └── index.js
│ ├── customCss
│ │ ├── amp
│ │ │ └── index.js
│ │ ├── pwa
│ │ │ └── index.js
│ │ └── shared
│ │ │ └── components
│ │ │ └── index.js
│ ├── analytics
│ │ ├── amp
│ │ │ ├── index.js
│ │ │ └── components
│ │ │ │ ├── ComScore.js
│ │ │ │ └── GoogleAnalytics.js
│ │ ├── pwa
│ │ │ ├── index.js
│ │ │ ├── sagas
│ │ │ │ └── client.js
│ │ │ └── components
│ │ │ │ ├── ComScore.js
│ │ │ │ ├── GoogleTagManager.js
│ │ │ │ └── index.js
│ │ └── shared
│ │ │ ├── helpers
│ │ │ └── index.js
│ │ │ └── stores
│ │ │ └── index.js
│ ├── iframes
│ │ ├── amp
│ │ │ └── index.js
│ │ ├── pwa
│ │ │ └── index.js
│ │ └── shared
│ │ │ ├── selectors
│ │ │ └── index.js
│ │ │ └── components
│ │ │ └── index.js
│ ├── ads
│ │ ├── shared
│ │ │ ├── actionTypes
│ │ │ │ └── index.js
│ │ │ ├── actions
│ │ │ │ └── index.js
│ │ │ ├── components
│ │ │ │ ├── index.js
│ │ │ │ └── LazyUnload
│ │ │ │ │ └── index.js
│ │ │ ├── selectors
│ │ │ │ └── index.js
│ │ │ ├── selectorCreators
│ │ │ │ └── index.js
│ │ │ └── reducers
│ │ │ │ └── index.js
│ │ ├── amp
│ │ │ └── index.js
│ │ └── pwa
│ │ │ └── index.js
│ └── build
│ │ ├── shared
│ │ ├── reducers
│ │ │ ├── __tests__
│ │ │ │ └── index.tests.js
│ │ │ └── index.js
│ │ ├── actionTypes
│ │ │ └── index.js
│ │ ├── stores
│ │ │ └── index.js
│ │ ├── selectors
│ │ │ └── index.js
│ │ └── actions
│ │ │ └── index.js
│ │ ├── amp
│ │ └── index.js
│ │ └── pwa
│ │ └── index.js
├── client
│ └── public-path.js
├── vendors.js
├── components
│ ├── Universal.js
│ └── App.js
├── server
│ ├── settings.js
│ ├── requires.js
│ ├── utils.js
│ ├── amp-template.js
│ └── pwa-template.js
├── certs
│ ├── localhost.crt
│ └── localhost.key
├── store
│ └── index.js
├── scripts
│ ├── build.js
│ └── start.js
├── index.js
└── webpack
│ ├── server.prod.js
│ └── server.dev.js
├── watch.js
├── .eslintrc
├── LICENSE
└── .babelrc
/packages/src/pwa/components/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Theme';
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | buildClient
2 | buildServer
3 | node_modules
4 | *.log
5 | .build
6 | now.json
7 |
--------------------------------------------------------------------------------
/packages/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
5 |
--------------------------------------------------------------------------------
/core/packages/settings/shared/selectors/index.js:
--------------------------------------------------------------------------------
1 | export const getSiteId = state => state.settings.siteId;
2 |
--------------------------------------------------------------------------------
/core/packages/settings/shared/actionTypes/index.js:
--------------------------------------------------------------------------------
1 | export const SETTINGS_UPDATED = 'settings/SETTINGS_UPDATED';
2 |
--------------------------------------------------------------------------------
/core/packages/customCss/amp/index.js:
--------------------------------------------------------------------------------
1 | import CustomCss from '../shared/components';
2 |
3 | export default CustomCss;
4 |
--------------------------------------------------------------------------------
/core/packages/customCss/pwa/index.js:
--------------------------------------------------------------------------------
1 | import CustomCss from '../shared/components';
2 |
3 | export default CustomCss;
4 |
--------------------------------------------------------------------------------
/core/client/public-path.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase, no-undef, no-underscore-dangle */
2 | __webpack_public_path__ = window['wp-pwa'].publicPath;
3 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/removeTagStyle.js:
--------------------------------------------------------------------------------
1 | export default {
2 | test: ({ tagName }) => tagName === 'style',
3 | converter: () => null,
4 | };
5 |
--------------------------------------------------------------------------------
/packages/src/pwa/stores/favorites.js:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree';
2 |
3 | const Favorite = types.model('Favorite').props({
4 |
5 | })
6 |
--------------------------------------------------------------------------------
/core/packages/analytics/amp/index.js:
--------------------------------------------------------------------------------
1 | import components from './components';
2 | import Store from '../shared/stores';
3 |
4 | export default components;
5 | export { Store };
6 |
--------------------------------------------------------------------------------
/core/packages/iframes/amp/index.js:
--------------------------------------------------------------------------------
1 | import components from '../shared/components';
2 | import selectors from '../shared/selectors';
3 |
4 | export default components;
5 | export { selectors };
6 |
--------------------------------------------------------------------------------
/core/packages/iframes/pwa/index.js:
--------------------------------------------------------------------------------
1 | import components from '../shared/components';
2 | import selectors from '../shared/selectors';
3 |
4 | export default components;
5 | export { selectors };
6 |
--------------------------------------------------------------------------------
/packages/src/pwa/client.js:
--------------------------------------------------------------------------------
1 | import components from './components';
2 | import Store from './stores';
3 | import flow from './flows/client';
4 |
5 | export default components;
6 | export { Store, flow };
7 |
--------------------------------------------------------------------------------
/packages/src/pwa/server.js:
--------------------------------------------------------------------------------
1 | import components from './components';
2 | import flow from './flows/server';
3 | import Store from './stores';
4 |
5 | export default components;
6 | export { Store, flow };
7 |
--------------------------------------------------------------------------------
/packages/src/server/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router();
2 | const manifest = require('./manifest.js');
3 |
4 | router.get('/:siteId/manifest.json', manifest);
5 |
6 | module.exports = router
7 |
--------------------------------------------------------------------------------
/core/packages/analytics/pwa/index.js:
--------------------------------------------------------------------------------
1 | import components from './components';
2 | import clientSagas from './sagas/client';
3 | import Store from '../shared/stores';
4 |
5 | export default components;
6 | export { clientSagas, Store };
7 |
--------------------------------------------------------------------------------
/core/packages/ads/shared/actionTypes/index.js:
--------------------------------------------------------------------------------
1 | export const STICKY_HAS_SHOWN = 'theme/STICKY_HAS_SHOWN';
2 | export const STICKY_HAS_HIDDEN = 'theme/STICKY_HAS_HIDDEN';
3 | export const STICKY_UPDATE_TIMEOUT = 'theme/STICKY_UPDATE_TIMEOUT';
4 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/table.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Table from '../components/Table';
3 |
4 | export default {
5 | test: ({ tagName }) => tagName === 'table',
6 | converter: () => children =>
,
7 | };
8 |
--------------------------------------------------------------------------------
/core/packages/settings/shared/actions/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 | import * as actionTypes from '../actionTypes';
3 |
4 | export const settingsUpdated = ({ settings }) => ({ type: actionTypes.SETTINGS_UPDATED, settings });
5 |
--------------------------------------------------------------------------------
/packages/src/pwa/processors/removeInlineStyle.js:
--------------------------------------------------------------------------------
1 | export default {
2 | test: element => element && element.attributes && element.attributes.style,
3 | process: element => {
4 | delete element.attributes.style;
5 | return element;
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/core/packages/build/shared/reducers/__tests__/index.tests.js:
--------------------------------------------------------------------------------
1 | import { packages } from '../';
2 |
3 | test('package reducers should be initializated to empty object.', () => {
4 | const state = packages(undefined, {});
5 | expect(state).toEqual({});
6 | });
7 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Menu.js:
--------------------------------------------------------------------------------
1 | import Facebook from './Facebook';
2 | import Twitter from './Twitter';
3 | import Instagram from './Instagram';
4 | import Hyperlink from './Hyperlink';
5 |
6 | export default { Facebook, Twitter, Instagram, Hyperlink };
7 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Theme/Title.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Helmet } from 'react-helmet';
3 |
4 | const Title = () => (
5 |
6 | WordCamp Europe 2018
7 |
8 | );
9 |
10 | export default Title;
11 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/removeHidden.js:
--------------------------------------------------------------------------------
1 | export default {
2 | test: ({ attributes }) =>
3 | attributes &&
4 | attributes.style &&
5 | (attributes.style.display === 'none' || attributes.style.visibility === 'hidden'),
6 | converter: () => null
7 | };
8 |
--------------------------------------------------------------------------------
/packages/src/pwa/processors/removeAmpIds.js:
--------------------------------------------------------------------------------
1 | export default {
2 | test: ({ attributes }) => attributes.id === 'amp',
3 | process: (element, { state }) => {
4 | if (state.build.amp) {
5 | element.attributes.id = null;
6 | }
7 |
8 | return element;
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/src/pwa/processors/dmGallery.js:
--------------------------------------------------------------------------------
1 | export default {
2 | test: ({ tagName, attributes }) =>
3 | tagName === 'div' &&
4 | attributes &&
5 | attributes.className &&
6 | attributes.className.includes('gallery_regular'),
7 | process: element => {
8 | element.attributes.id = 'gallery-0';
9 | return element;
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/packages/src/pwa/processors/myrTiledGallery.js:
--------------------------------------------------------------------------------
1 | export default {
2 | test: ({ tagName, attributes }) =>
3 | tagName === 'div' &&
4 | attributes &&
5 | attributes.className &&
6 | attributes.className.includes('tiled-gallery'),
7 | process: element => {
8 | element.attributes.id = 'gallery-0';
9 | return element;
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/core/packages/build/amp/index.js:
--------------------------------------------------------------------------------
1 | import * as actions from '../shared/actions';
2 | import * as actionTypes from '../shared/actionTypes';
3 | import reducers from '../shared/reducers';
4 | import * as selectors from '../shared/selectors';
5 |
6 | const Build = () => null;
7 |
8 | export default Build;
9 | export { actions, actionTypes, reducers, selectors };
10 |
--------------------------------------------------------------------------------
/packages/src/pwa/processors/myrGallery.js:
--------------------------------------------------------------------------------
1 | export default {
2 | test: ({ tagName, attributes }) =>
3 | tagName === 'div' &&
4 | attributes &&
5 | attributes.className &&
6 | attributes.className.includes('td-slide-on-2-columns'),
7 | process: element => {
8 | element.attributes.id = 'gallery-0';
9 | return element;
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/packages/src/pwa/stores/menu.js:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree';
2 |
3 | export default types
4 | .model('Menu')
5 | .props({ isOpen: types.optional(types.boolean, false) })
6 | .actions(self => ({
7 | open() {
8 | if (!self.isOpen) self.isOpen = true;
9 | },
10 | close() {
11 | if (self.isOpen) self.isOpen = false;
12 | },
13 | }));
14 |
--------------------------------------------------------------------------------
/core/packages/settings/shared/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import * as actionTypes from '../actionTypes';
3 |
4 | export const collection = (state = {}, { type, settings }) => {
5 | if (type === actionTypes.SETTINGS_UPDATED) return { ...state, ...settings };
6 | return state;
7 | };
8 |
9 | export default () =>
10 | combineReducers({
11 | collection,
12 | });
13 |
--------------------------------------------------------------------------------
/core/vendors.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | 'react',
3 | 'prop-types',
4 | 'react-dom',
5 | 'react-helmet',
6 | 'react-emotion',
7 | 'emotion-theming',
8 | 'worona-deps',
9 | 'superagent',
10 | 'mobx',
11 | 'mobx-state-tree',
12 | 'mobx-react',
13 | 'redux',
14 | 'redux-saga',
15 | 'redux-saga/effects',
16 | 'reselect',
17 | 'react-redux',
18 | 'react-slot-fill',
19 | 'recompose',
20 | ];
21 |
--------------------------------------------------------------------------------
/core/packages/analytics/pwa/sagas/client.js:
--------------------------------------------------------------------------------
1 | import { fork, all } from 'redux-saga/effects';
2 | import gtmSagas from './gtm';
3 | import comScoreSagas from './comScore';
4 | import googleAnalyticsSagas from './analytics';
5 |
6 | export default function* analyticsClientSagas({ stores }) {
7 | yield all([
8 | fork(gtmSagas, stores),
9 | fork(comScoreSagas, stores),
10 | fork(googleAnalyticsSagas, stores),
11 | ]);
12 | }
13 |
--------------------------------------------------------------------------------
/watch.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const watch = require('watch');
3 |
4 | watch.watchTree('.', (f, curr, prev) => {
5 | if (typeof f === 'object' && prev === null && curr === null) {
6 | // Finished walking the tree
7 | } else if (prev === null) {
8 | console.log(`created: ${f}`);
9 | } else if (curr.nlink === 0) {
10 | console.log(`removed: ${f}`);
11 | } else {
12 | console.log(`changed: ${f}`);
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/core/packages/settings/amp/index.js:
--------------------------------------------------------------------------------
1 | import * as actions from '../shared/actions';
2 | import * as actionTypes from '../shared/actionTypes';
3 | import reducers from '../shared/reducers';
4 | import * as selectors from '../shared/selectors';
5 | import * as selectorCreators from '../shared/selectorCreators';
6 |
7 | const Settings = () => null;
8 |
9 | export default Settings;
10 | export { actions, actionTypes, reducers, selectors, selectorCreators };
11 |
--------------------------------------------------------------------------------
/core/packages/settings/pwa/index.js:
--------------------------------------------------------------------------------
1 | import * as actions from '../shared/actions';
2 | import * as actionTypes from '../shared/actionTypes';
3 | import reducers from '../shared/reducers';
4 | import * as selectors from '../shared/selectors';
5 | import * as selectorCreators from '../shared/selectorCreators';
6 |
7 | const Settings = () => null;
8 |
9 | export default Settings;
10 | export { actions, actionTypes, reducers, selectors, selectorCreators };
11 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/audio.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LazyAudio from '../components/LazyAudio';
3 | import { filter } from '../components/HtmlToReactConverter/filter';
4 |
5 | export default {
6 | test: ({ tagName }) => tagName === 'audio',
7 | converter: element => children => (
8 |
9 | {children}
10 |
11 | ),
12 | };
13 |
--------------------------------------------------------------------------------
/core/packages/ads/shared/actions/index.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actionTypes';
2 |
3 | export const hasShown = ({ timeout }) => ({
4 | type: actionTypes.STICKY_HAS_SHOWN,
5 | timeout,
6 | });
7 |
8 | export const hasHidden = ({ closedByUser }) => ({
9 | type: actionTypes.STICKY_HAS_HIDDEN,
10 | closedByUser,
11 | });
12 |
13 | export const updateTimeout = ({ timeout }) => ({
14 | type: actionTypes.STICKY_UPDATE_TIMEOUT,
15 | timeout,
16 | });
17 |
--------------------------------------------------------------------------------
/core/packages/build/pwa/index.js:
--------------------------------------------------------------------------------
1 | import * as actions from '../shared/actions';
2 | import * as actionTypes from '../shared/actionTypes';
3 | import reducers from '../shared/reducers';
4 | import * as selectors from '../shared/selectors';
5 | import Store from '../shared/stores';
6 | import { version } from '../../../../package.json';
7 |
8 | const Build = () => null;
9 |
10 | export default Build;
11 | export { actions, actionTypes, reducers, selectors, Store, version };
12 |
--------------------------------------------------------------------------------
/core/packages/build/shared/actionTypes/index.js:
--------------------------------------------------------------------------------
1 | export const BUILD_UPDATED = 'build/BUILD_UPDATED';
2 | export const CLIENT_STARTED = 'build/CLIENT_STARTED';
3 | export const CLIENT_RENDERED = 'build/CLIENT_RENDERED';
4 | export const CLIENT_SAGAS_INITIALIZED = 'build/CLIENT_SAGAS_INITIALIZED';
5 | export const SERVER_STARTED = 'build/SERVER_STARTED';
6 | export const SERVER_FINISHED = 'build/SERVER_FINISHED';
7 | export const SERVER_SAGAS_INITIALIZED = 'build/SERVER_SAGAS_INITIALIZED';
8 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/LazyFacebookIframe/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import LazyIframe from '../LazyIframe';
4 |
5 | const LazyFacebook = ({ height, attributes }) => (
6 |
7 | );
8 |
9 | LazyFacebook.propTypes = {
10 | height: PropTypes.string.isRequired,
11 | attributes: PropTypes.shape({}).isRequired,
12 | };
13 |
14 | export default LazyFacebook;
15 |
--------------------------------------------------------------------------------
/core/packages/ads/amp/index.js:
--------------------------------------------------------------------------------
1 | import AdsFills, * as components from '../shared/components';
2 | import * as actions from '../shared/actions';
3 | import * as actionTypes from '../shared/actionTypes';
4 | import reducers from '../shared/reducers';
5 | import * as selectors from '../shared/selectors';
6 | import * as selectorCreators from '../shared/selectorCreators';
7 |
8 | export default AdsFills;
9 | export { actions, actionTypes, reducers, selectors, selectorCreators, components };
10 |
--------------------------------------------------------------------------------
/core/packages/ads/pwa/index.js:
--------------------------------------------------------------------------------
1 | import AdsFills, * as components from '../shared/components';
2 | import * as actions from '../shared/actions';
3 | import * as actionTypes from '../shared/actionTypes';
4 | import * as selectors from '../shared/selectors';
5 | import * as selectorCreators from '../shared/selectorCreators';
6 | import reducers from '../shared/reducers';
7 |
8 | export default AdsFills;
9 | export { actions, actionTypes, reducers, selectors, selectorCreators, components };
10 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Menu/MenuList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'react-emotion';
3 | import MenuRoute from './MenuRoute';
4 |
5 | const routes = ['schedule', 'venue-map', 'announcements', 'menus', 'code-of-conduct'];
6 |
7 | const MenuList = () => (
8 | {routes.map(route => )}
9 | );
10 |
11 | export default MenuList;
12 |
13 | const Container = styled.div`
14 | display: flex;
15 | flex-direction: column;
16 | `;
17 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/NowNext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import CardsList from './CardsList';
4 | import Page from '../Pages/Page';
5 |
6 | const NowNext = ({ sessions }) =>
7 | sessions.some(s => s.type === 'page') ? (
8 |
9 | ) : (
10 |
11 | );
12 |
13 | NowNext.propTypes = {
14 | sessions: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
15 | };
16 |
17 | export default NowNext;
18 |
--------------------------------------------------------------------------------
/packages/src/pwa/processors/index.js:
--------------------------------------------------------------------------------
1 | // import anchor from './anchor';
2 | // import tzGallery from './tzGallery';
3 | // import dmGallery from './dmGallery';
4 | // import myrGallery from './myrGallery';
5 | // import myrTiledGallery from './myrTiledGallery';
6 | import removeInlineStyle from './removeInlineStyle';
7 | import removeAmpIds from './removeAmpIds';
8 |
9 | export default [
10 | removeInlineStyle,
11 | removeAmpIds,
12 | // anchor,
13 | // tzGallery,
14 | // dmGallery,
15 | // myrGallery,
16 | // myrTiledGallery,
17 | ];
18 |
--------------------------------------------------------------------------------
/core/packages/build/shared/stores/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { types, getParent, onAction } from 'mobx-state-tree';
3 |
4 | const dev = process.env.NODE_ENV !== 'production';
5 |
6 | const Build = types
7 | .model('Build')
8 | .props({})
9 | .views(self => ({
10 | get root() {
11 | return getParent(self);
12 | },
13 | }))
14 | .actions(self => ({
15 | afterCreate: () => {
16 | if (dev) onAction(self.root, action => console.log(action));
17 | },
18 | }));
19 |
20 | export default Build;
21 |
--------------------------------------------------------------------------------
/core/packages/settings/shared/selectorCreators/index.js:
--------------------------------------------------------------------------------
1 | import { find } from 'lodash';
2 |
3 | export const getSettings = namespace => state => state.settings.collection[namespace];
4 | export const getSetting = (namespace, setting) => state =>
5 | state.settings.collection[namespace][setting];
6 |
7 | export const getSettingsById = _id => state =>
8 | find(state.settings.collection, item => item._id === _id);
9 | export const getSettingById = (_id, setting) => state =>
10 | find(state.settings.collection, item => item._id === _id)[setting];
11 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Theme/Favicon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import { Helmet } from 'react-helmet';
5 |
6 | const Favicon = ({ logoUrl }) => (
7 |
8 |
9 |
10 | );
11 |
12 | Favicon.propTypes = {
13 | logoUrl: PropTypes.string.isRequired,
14 | };
15 |
16 | export default inject(({ settings }) => ({
17 | logoUrl: settings.theme.logoUrl,
18 | }))(Favicon);
19 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/removeScript.js:
--------------------------------------------------------------------------------
1 | // const scriptsToRemove = [
2 | // '//platform.twitter.com/widgets.js',
3 | // 'https://platform.twitter.com/widgets.js',
4 | // '//platform.instagram.com/en_US/embeds.js',
5 | // 'https://platform.instagram.com/en_US/embeds.js',
6 | // ];
7 |
8 | export default {
9 | test: ({ tagName, children }) =>
10 | tagName === 'script' ||
11 | (tagName === 'p' &&
12 | children[0].tagName === 'script'),
13 | // && scriptsToRemove.includes(children[0].attributes.src),
14 | converter: () => null
15 | };
16 |
--------------------------------------------------------------------------------
/packages/README.md:
--------------------------------------------------------------------------------
1 | # WordCamp Theme
2 |
3 | ## Installation
4 |
5 | This is the WCEU theme package
6 |
7 | Use `npm run start:pwa` to start the development environment.
8 |
9 | ## Changelog
10 |
11 | #### 1.0.4
12 |
13 | - Add links to venues
14 |
15 | #### 1.0.3
16 |
17 | - Last styling and bug fixes
18 | - Added iOS metatags
19 |
20 | #### 1.0.2
21 |
22 | - Several bugfixes
23 | - Push Notifications
24 |
25 | #### 1.0.1
26 |
27 | - Some small bugfixes
28 |
29 | #### 1.0.0
30 |
31 | - First version deployed!
32 |
33 | #### 0.2.0
34 |
35 | - First version
36 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/Nav.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'react-emotion';
3 | import NavItem from './NavItem';
4 |
5 | const labels = ['on-now', 'up-next', 'schedule'];
6 |
7 | const Nav = () => (
8 | {labels.map(label => )}
9 | );
10 |
11 | export default Nav;
12 |
13 | const Container = styled.div`
14 | position: fixed;
15 | bottom: 0;
16 | left: 0;
17 | width: 100vw;
18 | height: ${({ theme }) => theme.size.button};
19 | display: flex;
20 | background-color: ${({ theme }) => theme.color.lightGrey};
21 | `;
22 |
--------------------------------------------------------------------------------
/core/packages/analytics/pwa/components/ComScore.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-danger */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import { Helmet } from 'react-helmet';
5 |
6 | export const comScoreNoScript = id => (
7 |
10 | );
11 |
12 | const ComScore = ({ id }) => {comScoreNoScript(id)};
13 |
14 | ComScore.propTypes = {
15 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
16 | };
17 |
18 | export default ComScore;
19 |
--------------------------------------------------------------------------------
/core/packages/build/shared/selectors/index.js:
--------------------------------------------------------------------------------
1 | // import { createSelector } from 'reselect';
2 |
3 | export const getExtensions = state => state.build.extensions;
4 | export const getTheme = state => state.build.theme;
5 | export const getSsr = state => state.build.ssr;
6 | export const getInitialUrl = state => state.build.initialUrl;
7 | export const getPerPage = state => state.build.perPage;
8 |
9 | // export const getPackages = createSelector(getExtensions, getTheme, (extensions, theme) => [
10 | // ...Object.values(extensions),
11 | // theme,
12 | // ]);
13 |
14 | export const getPackages = state => Object.values(state.build.packages);
15 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/anchor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Anchor from '../components/Anchor';
3 |
4 | export default {
5 | test: ({ tagName, attributes }) =>
6 | tagName === 'a' && attributes.href && /^#(\S+)/.test(attributes.href),
7 | converter: (element, { extraProps }) => {
8 | const { attributes: { href, className } } = element;
9 |
10 | return children => (
11 |
17 | {children}
18 |
19 | );
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Manifest/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Helmet } from 'react-helmet';
4 | import { inject } from 'mobx-react';
5 |
6 | const Manifest = ({ siteId, dynamicUrl }) => (
7 |
8 |
9 |
10 | );
11 | Manifest.propTypes = {
12 | siteId: PropTypes.string.isRequired,
13 | dynamicUrl: PropTypes.string.isRequired,
14 | }
15 |
16 | export default inject(({ build }) => ({
17 | siteId: build.siteId,
18 | dynamicUrl: build.dynamicUrl,
19 | }))(Manifest);
20 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import Page from './Page';
5 | import TopBar from '../TopBar';
6 | import Nav from '../Nav';
7 |
8 | const Pages = ({ selectedItem }) => (
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
16 | Pages.propTypes = {
17 | selectedItem: PropTypes.shape({}).isRequired,
18 | };
19 |
20 | export default inject(({ connection }) => ({
21 | selectedItem: connection.selectedItem,
22 | }))(Pages);
23 |
--------------------------------------------------------------------------------
/core/packages/iframes/shared/selectors/index.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import { dep } from 'worona-deps';
3 |
4 | const emptyArray = [];
5 |
6 | export const getIframes = createSelector(
7 | state => state,
8 | state =>
9 | dep('settings', 'selectorCreators', 'getSetting')('theme', 'iframes')(state) || emptyArray,
10 | );
11 |
12 | export const getIframesForMobile = createSelector(
13 | state => getIframes(state),
14 | iframes => iframes.filter(({ device }) => device === 'mobile'),
15 | );
16 |
17 | export const getIframesForTablet = createSelector(
18 | state => getIframes(state),
19 | iframes => iframes.filter(({ device }) => device === 'tablet'),
20 | );
21 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/iframe.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LazyIframe from '../components/LazyIframe';
3 | import { filter } from '../components/HtmlToReactConverter/filter';
4 |
5 | export default {
6 | test: ({ tagName, ignore }) => tagName === 'iframe' && !ignore,
7 | converter: element => {
8 | const { attributes } = element;
9 |
10 | let height;
11 |
12 | if (attributes.height && attributes.width) {
13 | height = `${(attributes.height * 100) / attributes.width}vw`; // prettier-ignore
14 | } else {
15 | height = '120px';
16 | }
17 |
18 | return ;
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/packages/src/pwa/styles/lightbox.js:
--------------------------------------------------------------------------------
1 | import { injectGlobal } from 'react-emotion';
2 |
3 | export default injectGlobal`
4 | .lightbox {
5 |
6 | .ril-toolbar {
7 | height: 54px;
8 |
9 | .ril-toolbar-right,
10 | .ril-toolbar-right > li {
11 | padding: 0;
12 | height: 54px;
13 | width: 54px;
14 |
15 | .ril-close {
16 | box-sizing: border-box;
17 | height: 100%;
18 | width: 100%;
19 | display: flex;
20 | justify-content: center;
21 | align-items: center;
22 | z-index: 50;
23 | color: inherit;
24 | opacity: 1;
25 | }
26 | }
27 | }
28 | }
29 | `;
30 |
--------------------------------------------------------------------------------
/packages/src/pwa/flows/client.js:
--------------------------------------------------------------------------------
1 | import { onSnapshot, applySnapshot } from 'mobx-state-tree';
2 |
3 | export default self => () => {
4 | const favoritesMap = window.localStorage.getItem('frontity.favoritesMap');
5 | if (favoritesMap) {
6 | applySnapshot(self.theme.favoritesMap, JSON.parse(favoritesMap));
7 | }
8 |
9 | onSnapshot(self.theme.favoritesMap, snapshot => {
10 | window.localStorage.setItem('frontity.favoritesMap', JSON.stringify(snapshot));
11 | });
12 |
13 | // Starts to modify current time
14 | self.theme.toggleRealTime();
15 | // Sets global functions to change current time
16 | window.setTime = self.theme.setTime;
17 | window.toggleRealTime = self.theme.toggleRealTime;
18 | };
19 |
--------------------------------------------------------------------------------
/core/components/Universal.js:
--------------------------------------------------------------------------------
1 | import universal from 'react-universal-component';
2 |
3 | const promiseCallbacks = {};
4 |
5 | const Universal = universal(
6 | props => import(`../../packages/${props.name}/src/${process.env.MODE}/index`),
7 | {
8 | minDelay: 1200,
9 | onLoad: (module, { isServer }, { name, namespace }) => {
10 | if (!isServer && promiseCallbacks[name]) {
11 | promiseCallbacks[name]({ name, namespace, module });
12 | }
13 | },
14 | },
15 | );
16 |
17 | export const importPromises = ({ name, namespace }) =>
18 | new Promise(resolve => {
19 | promiseCallbacks[name] = resolve;
20 | Universal.preload({ name, namespace });
21 | });
22 |
23 | export default Universal;
24 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/index.js:
--------------------------------------------------------------------------------
1 | import image from './image';
2 | import audio from './audio';
3 | import video from './video';
4 | import youtube from './youtube';
5 | import twitter from './twitter';
6 | import instagram from './instagram';
7 | import facebookIframe from './facebookIframe';
8 | import iframe from './iframe';
9 | import removeHidden from './removeHidden';
10 | import removeTagStyle from './removeTagStyle';
11 | import anchor from './anchor';
12 | import table from './table';
13 |
14 | export default [
15 | removeHidden,
16 | removeTagStyle,
17 | anchor,
18 | image,
19 | audio,
20 | video,
21 | youtube,
22 | twitter,
23 | instagram,
24 | facebookIframe,
25 | iframe,
26 | table,
27 | ];
28 |
--------------------------------------------------------------------------------
/packages/src/pwa/stores/notifications.js:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree';
2 |
3 | export default types
4 | .model('Notifications')
5 | .props({
6 | areSupported: types.optional(types.boolean, false),
7 | areEnabled: types.optional(types.boolean, true),
8 | areRegistered: types.optional(types.boolean, false),
9 | })
10 | .actions(self => ({
11 | support() {
12 | if (!self.areSupported) self.areSupported = true;
13 | },
14 | enable() {
15 | if (!self.areEnabled) self.areEnabled = true;
16 | },
17 | disable() {
18 | if (self.areEnabled) self.areEnabled = false;
19 | },
20 | toggleRegistered() {
21 | self.areRegistered = !self.areRegistered;
22 | },
23 | }));
24 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Pages/Page.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Content from '../Content';
6 |
7 | const Page = ({ content }) => (
8 |
9 |
10 |
11 | );
12 |
13 | Page.propTypes = {
14 | content: PropTypes.string.isRequired,
15 | };
16 |
17 | export default inject((_, { entity }) => ({
18 | content: entity.content,
19 | }))(Page);
20 |
21 | const Container = styled.div`
22 | box-sizing: border-box;
23 | padding: 80px 0;
24 | width: 100vw;
25 | min-height: 100vh;
26 | display: flex;
27 | flex-direction: column;
28 | align-items: stretch;
29 | `;
30 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/video.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LazyVideo from '../components/LazyVideo';
3 | import { filter } from '../components/HtmlToReactConverter/filter';
4 |
5 | export default {
6 | test: ({ tagName }) => tagName === 'video',
7 | converter: element => {
8 | const { attributes } = element;
9 |
10 | let height;
11 |
12 | if (attributes.height && attributes.width) {
13 | height = `${(attributes.height * 100) / attributes.width}vw`; // prettier-ignore
14 | } else {
15 | height = '120px';
16 | }
17 |
18 | return children => (
19 |
20 | {children}
21 |
22 | );
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/core/packages/ads/shared/components/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { Fill } from 'react-slot-fill';
5 | import Ad from './Ad';
6 | import Sticky from './Sticky';
7 | import * as selectors from '../selectors';
8 |
9 | const Ads = ({ ads }) =>
10 | ads.map(({ name, ...adProps }) => (
11 |
12 |
13 |
14 | ));
15 |
16 | Ads.propTypes = {
17 | ads: PropTypes.arrayOf(
18 | PropTypes.shape({
19 | name: PropTypes.string.isRequired,
20 | }),
21 | ),
22 | };
23 |
24 | const mapStateToProps = state => ({
25 | ads: selectors.getFills(state),
26 | });
27 |
28 | export default connect(mapStateToProps)(Ads);
29 | export { Ad, Sticky };
30 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Schedule.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Bullhorn from './Bullhorn';
4 | import Comments from './Comments';
5 | import Utensils from './Utensils';
6 | import Users from './Users';
7 |
8 | const Icon = ({ title, size, inCard }) => {
9 | if (title === 'Lunch') return ;
10 | if (title === 'Open networking') return ;
11 | if (title === 'Registration opens') return ;
12 | return ;
13 | };
14 |
15 | Icon.propTypes = {
16 | title: PropTypes.string.isRequired,
17 | size: PropTypes.number,
18 | inCard: PropTypes.bool,
19 | };
20 |
21 | Icon.defaultProps = {
22 | size: 24,
23 | inCard: false,
24 | };
25 |
26 | export default Icon;
27 |
--------------------------------------------------------------------------------
/core/packages/analytics/pwa/components/GoogleTagManager.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Helmet } from 'react-helmet';
4 |
5 | export const gtmScript = gtmId => (
6 |
7 | );
8 |
9 | export const gtmNoScript = gtmId => (
10 |
18 | );
19 |
20 | const GoogleTagManager = ({ gtmId }) => (
21 |
22 | {gtmScript(gtmId)}
23 | {gtmNoScript(gtmId)}
24 |
25 | );
26 |
27 | GoogleTagManager.propTypes = {
28 | gtmId: PropTypes.string.isRequired,
29 | };
30 |
31 | export default GoogleTagManager;
32 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["airbnb", "prettier", "prettier/react"],
4 | "globals": {
5 | "window": true,
6 | "document": true,
7 | "test": true,
8 | "expect": true,
9 | "beforeEach": true,
10 | "describe": true,
11 | "jest": true
12 | },
13 | "rules": {
14 | "import/no-extraneous-dependencies": 0,
15 | "react/jsx-filename-extension": 0,
16 | "no-underscore-dangle": ["error", { "allow": ["_id"] }],
17 | "no-param-reassign": 0,
18 | "array-callback-return": 0,
19 | "jsx-a11y/anchor-is-valid": 0,
20 | "comma-dangle": [
21 | "error", {
22 | "arrays": "always-multiline",
23 | "objects": "always-multiline",
24 | "imports": "always-multiline",
25 | "exports": "always-multiline",
26 | "functions": "always-multiline"
27 | }
28 | ]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Venues/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import TopBar from '../TopBar';
4 | import Slider from '../Slider';
5 | import Venue from './Venue';
6 | import Nav from '../Nav';
7 |
8 | const Venues = ({ columns, selectedColumnIndex, handleOnTransitionEnd }) => (
9 |
10 |
11 | {columns.map(({ selectedItem }) => )}
12 |
13 |
14 |
15 |
16 | );
17 |
18 | Venues.propTypes = {
19 | columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
20 | selectedColumnIndex: PropTypes.number.isRequired,
21 | handleOnTransitionEnd: PropTypes.func.isRequired,
22 | };
23 |
24 | export default Venues;
25 |
--------------------------------------------------------------------------------
/packages/src/pwa/stores/schedule.js:
--------------------------------------------------------------------------------
1 | import { types, getParent } from 'mobx-state-tree';
2 | import Track from './track';
3 |
4 | export default types
5 | .model('Schedule')
6 | .props({
7 | selected: types.maybe(types.reference(Track)),
8 | isFiltered: types.optional(types.boolean, false),
9 | })
10 | .views(self => ({
11 | get tracks() {
12 | return getParent(self)
13 | .tracks.filter(track => track.name !== 'Networking')
14 | .sort((a, b) => a.id - b.id);
15 | },
16 | }))
17 | .actions(self => ({
18 | setSelected(track) {
19 | self.selected = track;
20 | },
21 | selectTrack(value) {
22 | const track = getParent(self).tracks.find(t => t.name === value);
23 | if (self.selected !== track) self.selected = track;
24 | },
25 | toggleFilter() {
26 | self.isFiltered = !self.isFiltered;
27 | },
28 | }));
29 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Table/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Table = ({ children }) => (
6 |
7 |
8 |
9 | );
10 |
11 | Table.propTypes = {
12 | children: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
13 | };
14 |
15 | export default Table;
16 |
17 | const Container = styled.div`
18 | box-sizing: border-box;
19 | width: calc(100% - 30px);
20 | margin: 15px;
21 | overflow: auto;
22 |
23 | tr:nth-child(even) {
24 | background-color: #eee;
25 | }
26 |
27 | tr:nth-child(odd) {
28 | background-color: #fafafa;
29 | }
30 |
31 | td {
32 | border-spacing: 0;
33 | padding: 10px;
34 | text-align: center;
35 | }
36 |
37 | &::-webkit-scrollbar {
38 | display: none;
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/core/packages/ads/shared/selectors/index.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import { dep } from 'worona-deps';
3 |
4 | const emptyArray = [];
5 |
6 | export const getConfig = state =>
7 | dep('settings', 'selectorCreators', 'getSetting')('theme', 'ads')(state);
8 |
9 | export const getFills = createSelector(
10 | getConfig,
11 | config => (!!config && config.fills) || emptyArray,
12 | );
13 |
14 | export const areLazy = createSelector(
15 | getConfig,
16 | config =>
17 | !!config && !!config.settings && typeof config.settings.areLazy === 'boolean'
18 | ? config.settings.areLazy
19 | : true,
20 | );
21 |
22 | export const doesStickyExist = createSelector(
23 | getConfig,
24 | config =>
25 | !!config &&
26 | !!config.formats &&
27 | config.formats.filter(({ options }) => options && options.sticky && options.sticky.display)
28 | .length > 0,
29 | );
30 |
--------------------------------------------------------------------------------
/packages/src/pwa/processors/tzGallery.js:
--------------------------------------------------------------------------------
1 | const idFormat = /^(?:\d+)-(?:\d+,)+(?:\d)+$/g;
2 |
3 | export default {
4 | test: ({ tagName, attributes }) =>
5 | tagName === 'div' &&
6 | attributes &&
7 | attributes.id &&
8 | idFormat.test(attributes.id) &&
9 | attributes.className &&
10 | attributes.className.find(name => /^mc-[\w|-]+gallery/.test(name)),
11 | process: ({ attributes: { id } }) => {
12 | const ids = new Set(id.match(/(\d+)/g));
13 | const children = Array.from(ids).map(mediaId => ({
14 | type: 'Element',
15 | tagName: 'img',
16 | attributes: {
17 | dataset: {
18 | attachmentId: parseInt(mediaId, 10),
19 | },
20 | },
21 | children: [],
22 | }));
23 |
24 | return {
25 | type: 'Element',
26 | tagName: 'div',
27 | attributes: { id: ['gallery-0'] },
28 | children,
29 | };
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/core/packages/customCss/shared/components/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { Helmet } from 'react-helmet';
5 | import { css } from 'react-emotion';
6 | import { dep } from 'worona-deps';
7 |
8 | const CustomCss = ({ customCss }) => {
9 | const className = css`
10 | ${customCss};
11 | `;
12 | return (
13 | // this do the trick
14 |
15 |
16 |
17 | ;
18 |
19 |
20 | );
21 | };
22 |
23 | CustomCss.propTypes = {
24 | customCss: PropTypes.string.isRequired,
25 | };
26 |
27 | const mapStateToProps = state => ({
28 | customCss: dep('settings', 'selectorCreators', 'getSetting')('theme', 'customCss')(state) || '',
29 | });
30 |
31 | export default connect(mapStateToProps)(CustomCss);
32 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Facebook.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Facebook = ({ size }) => (
6 |
12 | );
13 |
14 | Facebook.propTypes = {
15 | size: PropTypes.number.isRequired,
16 | };
17 |
18 | export default Facebook;
19 |
20 | const Svg = styled.svg`
21 | width: ${({ size }) => `${size}px`};
22 | height: ${({ size }) => `${size}px`};
23 | color: ${({ theme }) => theme.color.darkGrey};
24 | `;
25 |
--------------------------------------------------------------------------------
/core/packages/analytics/amp/components/ComScore.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-no-target-blank, react/no-danger */
2 | import React, { Fragment } from 'react';
3 | import PropTypes from 'prop-types';
4 | import { Helmet } from 'react-helmet';
5 |
6 | const ComScore = ({ id }) => (
7 |
8 |
9 |
14 |
15 |
16 |
26 |
27 |
28 | );
29 |
30 | ComScore.propTypes = {
31 | id: PropTypes.number.isRequired,
32 | };
33 |
34 | export default ComScore;
35 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/twitter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LazyTweet from '../components/LazyTweet';
3 | import { getTweetId } from '../utils';
4 |
5 | export default {
6 | test: ({ tagName, attributes, ignore }) =>
7 | tagName === 'blockquote' &&
8 | attributes.className &&
9 | attributes.className.includes('twitter-tweet') &&
10 | !ignore,
11 | converter: element => {
12 | const { ...rest } = element;
13 | const height = 'auto';
14 | const width = '100%';
15 |
16 | // Sets current element as its child
17 | element.children = [{ ...rest, ignore: true }];
18 | const tweetId = getTweetId(element.children);
19 |
20 | return children => (
21 |
28 | {children}
29 |
30 | );
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/CardsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 | import Card from './Card';
5 |
6 | const CardsList = ({ sessions }) => (
7 |
8 | {sessions.map(session => (
9 | [{ type, id, page }])}
13 | isFavorite={!!session.isFavorite}
14 | isSpecial={!session.hasSpeakers}
15 | />
16 | ))}
17 |
18 | );
19 |
20 | CardsList.propTypes = {
21 | sessions: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
22 | };
23 |
24 | export default CardsList;
25 |
26 | const Container = styled.div`
27 | box-sizing: border-box;
28 | min-height: 100vh;
29 | width: 100vw;
30 | display: flex;
31 | flex-direction: column;
32 | align-items: center;
33 | padding: ${({ theme }) => theme.padding.home};
34 | `;
35 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Menu/MenuLinks.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'react-emotion';
3 | import MenuLink from './MenuLink';
4 |
5 | const links = [
6 | {
7 | name: 'Facebook',
8 | url: 'https://www.facebook.com/WordCampEurope',
9 | },
10 | {
11 | name: 'Twitter',
12 | url: 'https://twitter.com/wceurope',
13 | },
14 | {
15 | name: 'Instagram',
16 | url: 'https://www.instagram.com/wceurope/',
17 | },
18 | {
19 | name: 'Hyperlink',
20 | url: 'https://2018.europe.wordcamp.org/',
21 | },
22 | ];
23 |
24 | const MenuLinks = () => (
25 |
26 | {links.map(({ name, url }) => )}
27 |
28 | );
29 |
30 | export default MenuLinks;
31 |
32 | const Container = styled.div`
33 | box-sizing: border-box;
34 | height: ${({ theme }) => theme.size.button};
35 | padding: 0 24px;
36 | box-shadow: inset 0 -1px 0 0 rgba(40, 36, 9, 0.1);
37 | display: flex;
38 | `;
39 |
--------------------------------------------------------------------------------
/packages/src/pwa/processors/anchor.js:
--------------------------------------------------------------------------------
1 | import { css } from 'react-emotion';
2 |
3 | export default {
4 | test: ({ tagName }) => tagName === 'a',
5 | process: (element, { state, theme }) => {
6 | let linkClass;
7 |
8 | if (state && state.settings.collection.theme.linkStyles) {
9 | const { linkStyles } = state.settings.collection.theme;
10 |
11 | linkClass = css`
12 | color: ${linkStyles.color};
13 | font-weight: ${linkStyles.bold ? 'bold' : 'normal'};
14 | text-decoration: ${linkStyles.underline ? 'underline' : 'none'};
15 | `;
16 | } else {
17 | linkClass = css`
18 | color: ${theme.colors.link};
19 | font-weight: normal;
20 | text-decoration: underline;
21 | `;
22 | }
23 |
24 | if (element.attributes.className) {
25 | element.attributes.className.push(linkClass);
26 | } else {
27 | element.attributes.className = [linkClass];
28 | }
29 |
30 | return element;
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/core/packages/iframes/shared/components/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { Fill } from 'react-slot-fill';
5 | import LazyIframe from './LazyIframe';
6 | import { getIframesForMobile } from '../selectors';
7 |
8 | const Iframes = ({ iframes }) =>
9 | iframes.map(props => (
10 |
11 |
12 |
13 | ));
14 |
15 | Iframes.propTypes = {
16 | iframes: PropTypes.arrayOf(
17 | PropTypes.shape({
18 | name: PropTypes.string.isRequired,
19 | src: PropTypes.string.isRequired,
20 | className: PropTypes.string,
21 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
22 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
23 | }),
24 | ),
25 | };
26 |
27 | const mapStateToProps = state => ({
28 | iframes: getIframesForMobile(state),
29 | });
30 |
31 | export default connect(mapStateToProps)(Iframes);
32 |
--------------------------------------------------------------------------------
/packages/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wordcamp-theme",
3 | "version": "1.0.4",
4 | "private": true,
5 | "description": "A Frontity theme for Wordcamps.",
6 | "scripts": {},
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/frontity/wordcamp-theme.git"
10 | },
11 | "keywords": [
12 | "wordcamp",
13 | "theme",
14 | "frontity",
15 | "package"
16 | ],
17 | "author": "eduardo@frontity.com, luis@frontity.com, david@frontity.com",
18 | "license": "MIT",
19 | "dependencies": {
20 | "adler-32": "^1.2.0",
21 | "he": "^1.1.1",
22 | "himalaya": "^1.1.0",
23 | "moment-timezone": "^0.5.17",
24 | "rc-switch": "^1.6.0",
25 | "react-image-lightbox": "^5.0.0",
26 | "react-select": "^2.0.0-beta.5",
27 | "react-share": "^2.1.1",
28 | "react-waypoint": "^8.0.1"
29 | },
30 | "devDependencies": {},
31 | "bugs": {
32 | "url": "https://github.com/frontity/wordcamp-theme/issues"
33 | },
34 | "homepage": "https://github.com/frontity/wordcamp-theme#readme"
35 | }
36 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/youtube.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LazyYoutube from '../components/LazyYoutube';
3 | import { filter } from '../components/HtmlToReactConverter/filter';
4 |
5 | export default {
6 | test: ({ tagName, attributes }) => tagName === 'iframe' && /youtube/.test(attributes.src),
7 | converter: element => {
8 | const { attributes } = element;
9 |
10 | let height;
11 |
12 | if (attributes.height && attributes.width) {
13 | height = `${(attributes.height * 100) / attributes.width}vw`; // prettier-ignore
14 | } else {
15 | height = '120px';
16 | }
17 |
18 | const match =
19 | attributes.src.match(/\/embed\/([\d\w]+)/) || attributes.src.match(/\/([\w-]+?)\?/);
20 |
21 | const youtubeId = match ? match[1] : null;
22 |
23 | return (
24 |
31 | );
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Menu/MenuButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import MenuIcon from './MenuIcon';
6 |
7 | const MenuButton = ({ openMenu }) => (
8 |
9 |
10 |
11 |
12 | );
13 |
14 | MenuButton.propTypes = {
15 | openMenu: PropTypes.func.isRequired,
16 | };
17 |
18 | export default inject(({ theme }) => ({
19 | openMenu: theme.menu.open,
20 | }))(MenuButton);
21 |
22 | const Container = styled.div`
23 | box-sizing: border-box;
24 | width: ${({ theme }) => theme.size.button};
25 | flex-shrink: 0;
26 | display: flex;
27 | flex-direction: column;
28 | justify-content: center;
29 | align-items: center;
30 | padding: 14px 16px;
31 | `;
32 |
33 | const Label = styled.div`
34 | padding-top: 4px;
35 | font-size: 10px;
36 | line-height: 10px;
37 | text-transform: uppercase;
38 | color: ${({ theme }) => theme.color.text};
39 | `;
40 |
--------------------------------------------------------------------------------
/core/server/settings.js:
--------------------------------------------------------------------------------
1 | import request from 'superagent';
2 | import { normalize, schema } from 'normalizr';
3 |
4 | const settingSchema = new schema.Entity(
5 | 'settings',
6 | {},
7 | { idAttribute: setting => setting.woronaInfo.namespace }
8 | );
9 | const settingsSchema = [settingSchema];
10 |
11 | // Fetch settings from the backend.
12 | export const getSettings = async ({ siteId, env }) => {
13 | const cdn = env === 'prod' ? 'cdn' : 'precdn';
14 | try {
15 | const { body } = await request(
16 | `https://${cdn}.worona.io/api/v1/settings/site/${siteId}/app/prod/live`,
17 | );
18 | const { entities: { settings } } = normalize(body, settingsSchema);
19 | return settings;
20 | } catch (error) {
21 | return null;
22 | }
23 | };
24 |
25 | // Extract activated packages array from settings.
26 | export const getActivatedPackages = async ({ settings }) =>
27 | Object.values(settings)
28 | .filter(pkg => pkg.woronaInfo.namespace !== 'generalSite')
29 | .reduce((obj, pkg) => ({ ...obj, [pkg.woronaInfo.namespace]: pkg.woronaInfo.name }), {});
30 |
--------------------------------------------------------------------------------
/core/packages/ads/shared/selectorCreators/index.js:
--------------------------------------------------------------------------------
1 | import * as selectors from '../selectors';
2 |
3 | export const getFormats = type => state => {
4 | const adsConfig = selectors.getConfig(state);
5 |
6 | if (!adsConfig || !adsConfig.formats) return {};
7 |
8 | const formatsList = adsConfig.formats;
9 |
10 | const typeFormats = formatsList.find(formats => {
11 | if (formats.type === type) return true;
12 | if (typeof formats.type === 'object' && formats.type.includes(type)) return true;
13 |
14 | return false;
15 | });
16 |
17 | return typeFormats || formatsList.find(options => options.type === 'default');
18 | };
19 |
20 | export const getOptions = type => state => {
21 | const formats = getFormats(type)(state);
22 |
23 | return formats.options;
24 | };
25 |
26 | export const getContentFormats = type => state => {
27 | const formats = getFormats(type)(state);
28 |
29 | return formats.content;
30 | };
31 |
32 | export const getStickyFormat = type => state => {
33 | const formats = getFormats(type)(state);
34 |
35 | return formats.sticky;
36 | };
37 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Posts/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Slider from '../Slider';
4 | import Post from './Post';
5 | import TopBar from '../TopBar';
6 | import Nav from '../Nav';
7 |
8 | const Posts = ({ columns, selectedColumnIndex, handleOnTransitionEnd }) => (
9 |
10 |
11 | {columns.map(({ index, selectedItem }) => {
12 | if (index > selectedColumnIndex + 1) return ;
13 | if (index < selectedColumnIndex - 1) return ;
14 |
15 | return ;
16 | })}
17 |
18 |
19 |
20 |
21 | );
22 |
23 | Posts.propTypes = {
24 | columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
25 | selectedColumnIndex: PropTypes.number.isRequired,
26 | handleOnTransitionEnd: PropTypes.func.isRequired,
27 | };
28 |
29 | export default Posts;
30 |
--------------------------------------------------------------------------------
/core/packages/ads/shared/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import * as actionTypes from '../actionTypes';
3 |
4 | const isOpen = (state = false, action) => {
5 | switch (action.type) {
6 | case actionTypes.STICKY_HAS_SHOWN:
7 | return true;
8 | case actionTypes.STICKY_HAS_HIDDEN:
9 | return false;
10 | default:
11 | return state;
12 | }
13 | };
14 |
15 | const timeout = (state = null, action) => {
16 | switch (action.type) {
17 | case actionTypes.STICKY_HAS_SHOWN:
18 | return action.timeout;
19 | case actionTypes.STICKY_UPDATE_TIMEOUT:
20 | return action.timeout;
21 | case actionTypes.STICKY_HAS_HIDDEN:
22 | return null;
23 | default:
24 | return state;
25 | }
26 | };
27 |
28 | const closedByUser = (state = false, action) => {
29 | switch (action.type) {
30 | case actionTypes.STICKY_HAS_HIDDEN:
31 | return action.closedByUser;
32 | default:
33 | return state;
34 | }
35 | };
36 |
37 | export default () =>
38 | combineReducers({
39 | isOpen,
40 | timeout,
41 | closedByUser,
42 | });
43 |
--------------------------------------------------------------------------------
/core/packages/build/shared/actions/index.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actionTypes';
2 |
3 | export const buildUpdated = ({
4 | packages,
5 | env,
6 | ssr,
7 | amp,
8 | siteId,
9 | perPage,
10 | device,
11 | dev,
12 | initialUrl,
13 | }) => ({
14 | type: actionTypes.BUILD_UPDATED,
15 | packages,
16 | env,
17 | ssr,
18 | amp,
19 | siteId,
20 | device,
21 | dev,
22 | initialUrl,
23 | perPage: parseInt(perPage, 10),
24 | });
25 |
26 | export const serverStarted = () => ({
27 | type: actionTypes.SERVER_STARTED,
28 | });
29 |
30 | export const serverFinished = ({ timeToRunSagas }) => ({
31 | type: actionTypes.SERVER_FINISHED,
32 | timeToRunSagas,
33 | });
34 |
35 | export const serverSagasInitialized = () => ({
36 | type: actionTypes.SERVER_SAGAS_INITIALIZED,
37 | });
38 |
39 | export const clientStarted = () => ({
40 | type: actionTypes.CLIENT_STARTED,
41 | });
42 |
43 | export const clientSagasInitialized = () => ({
44 | type: actionTypes.CLIENT_SAGAS_INITIALIZED,
45 | });
46 |
47 | export const clientRendered = () => ({
48 | type: actionTypes.CLIENT_RENDERED,
49 | });
50 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Sessions/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Slider from '../Slider';
4 | import Session from './Session';
5 | import TopBar from '../TopBar';
6 | import Nav from '../Nav';
7 |
8 | const Sessions = ({ columns, selectedColumnIndex, handleOnTransitionEnd }) => (
9 |
10 |
11 | {columns.map(({ index, selectedItem }) => {
12 | if (index > selectedColumnIndex + 1) return ;
13 | if (index < selectedColumnIndex - 1) return ;
14 |
15 | return ;
16 | })}
17 |
18 |
19 |
20 |
21 | );
22 |
23 | Sessions.propTypes = {
24 | columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
25 | selectedColumnIndex: PropTypes.number.isRequired,
26 | handleOnTransitionEnd: PropTypes.func.isRequired,
27 | };
28 |
29 | export default Sessions;
30 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Speakers/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Slider from '../Slider';
4 | import Speaker from './Speaker';
5 | import TopBar from '../TopBar';
6 | import Nav from '../Nav';
7 |
8 | const Speakers = ({ columns, selectedColumnIndex, handleOnTransitionEnd }) => (
9 |
10 |
11 | {columns.map(({ index, selectedItem }) => {
12 | if (index > selectedColumnIndex + 1) return ;
13 | if (index < selectedColumnIndex - 1) return ;
14 |
15 | return ;
16 | })}
17 |
18 |
19 |
20 |
21 | );
22 |
23 | Speakers.propTypes = {
24 | columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
25 | selectedColumnIndex: PropTypes.number.isRequired,
26 | handleOnTransitionEnd: PropTypes.func.isRequired,
27 | };
28 |
29 | export default Speakers;
30 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Menu/MenuLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Icons from '../Icons/Menu';
6 |
7 | const MenuLink = ({ name, url, closeMenu }) => {
8 | const Icon = Icons[name];
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | MenuLink.propTypes = {
20 | name: PropTypes.string.isRequired,
21 | url: PropTypes.string.isRequired,
22 | closeMenu: PropTypes.func.isRequired,
23 | };
24 |
25 | export default inject(({ theme }) => ({
26 | closeMenu: theme.menu.close,
27 | }))(MenuLink);
28 |
29 | const Container = styled.div`
30 | box-sizing: border-box;
31 | `;
32 |
33 | const Link = styled.a`
34 | box-sizing: border-box;
35 | height: ${({ theme }) => theme.size.button};
36 | width: ${({ theme }) => theme.size.button};
37 | display: flex;
38 | justify-content: center;
39 | align-items: center;
40 | `;
41 |
--------------------------------------------------------------------------------
/packages/src/pwa/stores/speaker.js:
--------------------------------------------------------------------------------
1 | import { types, getParent } from 'mobx-state-tree';
2 | import Session from './session';
3 |
4 | const Id = types.union(types.number, types.string);
5 |
6 | const Speaker = types
7 | .model('Speaker')
8 | .props({
9 | id: types.identifier(Id),
10 | type: types.optional(types.string, 'wcb_speaker'),
11 | gravatar: types.maybe(types.string),
12 | sessions: types.optional(types.array(types.reference(types.late(() => Session))), []),
13 | })
14 | .views(self => {
15 | const getConnection = () => getParent(self, 3).connection;
16 | return {
17 | get entity() {
18 | return getConnection().entity(self.type, self.id);
19 | },
20 | get name() {
21 | return self.entity.title;
22 | },
23 | get link() {
24 | return self.entity.link;
25 | },
26 | };
27 | })
28 | .actions(self => ({
29 | addSession(session) {
30 | if (!self.sessions.includes(session)) self.sessions.push(session);
31 | },
32 | setGravatar(gravatar) {
33 | self.gravatar = gravatar;
34 | },
35 | }));
36 |
37 | export default Speaker;
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Worona Labs SL
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/core/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Helmet } from 'react-helmet';
4 | import { Provider as ReduxProvider } from 'react-redux';
5 | import { Provider as MobxProvider } from 'mobx-react';
6 | import { Provider as SlotFillProvider } from 'react-slot-fill';
7 | import Universal from './Universal';
8 |
9 | const App = ({ core, packages, store, stores }) => (
10 |
11 |
12 |
13 |
14 |
15 | WP PWA
16 |
17 | {core.map(({ name, Component }) => )}
18 | {packages.map(name => )}
19 |
20 |
21 |
22 |
23 | );
24 |
25 | App.propTypes = {
26 | core: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
27 | packages: PropTypes.arrayOf(PropTypes.string).isRequired,
28 | store: PropTypes.shape().isRequired,
29 | stores: PropTypes.shape().isRequired,
30 | };
31 |
32 | export default App;
33 |
--------------------------------------------------------------------------------
/packages/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Worona Labs SL
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/core/server/requires.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require, import/no-dynamic-require, import/prefer-default-export,
2 | no-restricted-syntax, no-restricted-globals, no-await-in-loop */
3 | import { pathExists } from 'fs-extra';
4 |
5 | const pathExistsPromise = name =>
6 | new Promise((resolve, reject) => {
7 | pathExists(`./packages/${name}/src/${process.env.MODE}/index.js`).then(path => {
8 | if (path) resolve(true);
9 | else reject(new Error(`Module ${name} is not installed.`));
10 | });
11 | });
12 |
13 | export const requireModules = async pkgs => {
14 | const pathPromises = pkgs.map(([, name]) => pathExistsPromise(name));
15 | await Promise.all(pathPromises);
16 | return pkgs.map(([namespace, name]) => {
17 | const module = require(`../../packages/${name}/src/${process.env.MODE}/index`);
18 |
19 | try {
20 | // Only return serverSaga if it exists.
21 | const serverSagas = require(`../../packages/${name}/src/${process.env.MODE}/sagas/server`)
22 | .default;
23 | return { name, namespace, module: { ...module, serverSagas } };
24 | } catch (e) {
25 | return { name, namespace, module };
26 | }
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/facebookIframe.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LazyFacebookIframe from '../components/LazyFacebookIframe';
3 | import { filter } from '../components/HtmlToReactConverter/filter';
4 |
5 | const facebookVideo = /video.php\?href=([^&]+)/;
6 |
7 | export default {
8 | test: ({ tagName, attributes, ignore }) =>
9 | tagName === 'iframe' && attributes.src.startsWith('https://www.facebook.com/') && !ignore,
10 | converter: element => {
11 | const { attributes } = element;
12 |
13 |
14 | const videoData = facebookVideo.exec(attributes.src);
15 | const isVideo = !!videoData;
16 |
17 | const href = isVideo ? decodeURIComponent(videoData[1]) : null;
18 |
19 | let height;
20 |
21 | if (attributes.height && attributes.width) {
22 | height = `${(attributes.height * 100) / attributes.width}vw`; // prettier-ignore
23 | } else {
24 | height = '120px';
25 | }
26 |
27 | return (
28 |
35 | );
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/CardSpeakers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 | import Link from '../Link';
5 | import { speakersContext } from '../../contexts';
6 |
7 | const ScheduleItemSpeakers = ({ speakers }) => (
8 |
9 | {speakers.map(speaker => (
10 | [{ type, id, page }]))}
15 | >
16 | {`${speaker.name}`}
17 |
18 | ))}
19 |
20 | );
21 |
22 | ScheduleItemSpeakers.propTypes = {
23 | speakers: PropTypes.shape({}).isRequired,
24 | };
25 |
26 | export default ScheduleItemSpeakers;
27 |
28 | const Speakers = styled.div`
29 | margin: 0;
30 | font-size: 16px;
31 | padding-bottom: 4px;
32 | color: ${({ theme }) => theme.color.greyText};
33 | `;
34 |
35 | const Speaker = styled.a`
36 | display: inline-block;
37 | line-height: 20px;
38 | color: inherit;
39 |
40 | &:not(:last-child):after {
41 | content: ',';
42 | margin-right: 4px;
43 | }
44 | `;
45 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/ScheduleItemSpeakers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 | import Link from '../Link';
5 | import { speakersContext } from '../../contexts';
6 |
7 | const ScheduleItemSpeakers = ({ speakers }) => (
8 |
9 | {speakers.map(speaker => (
10 | [{ type, id, page }]))}
15 | >
16 | {`${speaker.name}`}
17 |
18 | ))}
19 |
20 | );
21 |
22 | ScheduleItemSpeakers.propTypes = {
23 | speakers: PropTypes.shape({}).isRequired,
24 | };
25 |
26 | export default ScheduleItemSpeakers;
27 |
28 | const Speakers = styled.div`
29 | font-size: 16px;
30 | color: ${({ theme }) => theme.color.lightGreyText};
31 | `;
32 |
33 | const Speaker = styled.a`
34 | display: inline-block;
35 | line-height: 20px;
36 | color: ${({ theme }) => theme.color.lightGreyText};
37 |
38 | &:not(:last-child):after {
39 | content: ',';
40 | margin-right: 4px;
41 | }
42 | `;
43 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/TopBar/Logo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 |
6 | const Logo = ({ src, srcSet }) => (
7 |
8 |
9 |
10 | );
11 |
12 | Logo.propTypes = {
13 | src: PropTypes.string.isRequired,
14 | srcSet: PropTypes.string.isRequired,
15 | };
16 |
17 | export default inject(({ settings }) => {
18 | const sizes = [
19 | { px: 32, ratio: 1 },
20 | { px: 48, ratio: 1.5 },
21 | { px: 64, ratio: 2 },
22 | { px: 80, ratio: 2.5 },
23 | { px: 96, ratio: 3 },
24 | ];
25 |
26 | const { logoUrl } = settings.theme;
27 |
28 | return {
29 | src: logoUrl,
30 | srcSet: sizes.map(size => `${logoUrl}?w=${size.px} ${size.ratio}x`).join(', '),
31 | };
32 | })(Logo);
33 |
34 | const Container = styled.div`
35 | box-sizing: border-box;
36 | width: ${({ theme }) => theme.size.button};
37 | height: ${({ theme }) => theme.size.button};
38 | padding: 12px 0 12px 16px;
39 | `;
40 |
41 | const Img = styled.img`
42 | object-fit: contain;
43 | height: 100%;
44 | width: 100%;
45 | `;
46 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Venues/Venue.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Venue = ({ venue }) => (
6 |
7 | {venue.entity.title}
8 |
9 | `${url} ${width}w`)
14 | .join(', ')}
15 | />
16 |
17 |
18 | );
19 |
20 | Venue.propTypes = {
21 | venue: PropTypes.shape({}).isRequired,
22 | };
23 |
24 | export default Venue;
25 |
26 | const Container = styled.div`
27 | box-sizing: border-box;
28 | width: 100vw;
29 | height: 100vh;
30 | padding: ${({ theme }) => theme.padding.venues};
31 | display: flex;
32 | flex-direction: column;
33 | align-items: center;
34 | `;
35 |
36 | const Title = styled.h1`
37 | font-size: 22px;
38 | margin: 0;
39 | color: ${({ theme }) => theme.color.text};
40 | margin-bottom: 16px;
41 | `;
42 |
43 | const Image = styled.img`
44 | width: 100%;
45 | `;
46 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Sessions/FavoriteButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import FullStarIcon from 'react-icons/lib/fa/star';
6 | import EmptyStarIcon from 'react-icons/lib/fa/star-o';
7 |
8 | const FavoriteButton = ({ isFavorite, toggleFavorite }) => (
9 |
10 | {isFavorite ? : }
11 |
12 | );
13 |
14 | FavoriteButton.propTypes = {
15 | isFavorite: PropTypes.bool.isRequired,
16 | toggleFavorite: PropTypes.func.isRequired,
17 | };
18 |
19 | export default inject((_, { session }) => ({
20 | isFavorite: !!session.isFavorite,
21 | toggleFavorite: session.toggleFavorite,
22 | }))(FavoriteButton);
23 |
24 | const Container = styled.div`
25 | width: 28px;
26 | height: 28px;
27 | margin: 0 0 8px 8px;
28 | float: right;
29 | display: flex;
30 | justify-content: center;
31 | align-items: center;
32 | flex-shrink: 0;
33 |
34 | & > svg {
35 | color: ${({ theme, isFavorite }) => (isFavorite ? theme.color.yellow : theme.color.darkGrey)};
36 | }
37 | `;
38 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Nav/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 | import Link from '../Link';
5 |
6 | const Button = ({ item, icon: Icon, text }) => (
7 |
8 | {item ? (
9 |
10 |
11 |
12 | {text}
13 |
14 |
15 | ) : null}
16 |
17 | );
18 |
19 | Button.propTypes = {
20 | item: PropTypes.shape({}),
21 | icon: PropTypes.func.isRequired,
22 | text: PropTypes.string.isRequired,
23 | };
24 |
25 | Button.defaultProps = {
26 | item: null,
27 | };
28 |
29 | export default Button;
30 |
31 | const Container = styled.div`
32 | width: ${({ theme }) => theme.size.button};
33 | height: ${({ theme }) => theme.size.button};
34 | color: ${({ theme }) => theme.color.black};
35 | `;
36 |
37 | const Content = styled.div`
38 | box-sizing: padding-box;
39 | width: 100%;
40 | height: 100%;
41 | display: flex;
42 | flex-direction: column;
43 | justify-content: center;
44 | align-items: center;
45 | `;
46 |
47 | const Text = styled.div`
48 | line-height: 10px;
49 | text-transform: uppercase;
50 | font-size: 10px;
51 | `;
52 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/VenueLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import Link from '../Link';
5 | import { venueContext } from '../../contexts';
6 | import * as consts from '../../consts';
7 |
8 | const venueFromTrack = id => {
9 | if (id === consts.TRACK_MILKY_WAY) return consts.PAGE_VENUE_MILKY_WAY;
10 | if (id === consts.TRACK_ANDROMEDA) return consts.PAGE_VENUE_ANDROMEDA;
11 | if (id === consts.TRACK_HAYABUSA) return consts.PAGE_VENUE_HAYABUSA;
12 | if (id === consts.TRACK_CASSINI) return consts.PAGE_VENUE_CASSINI;
13 | if (id === consts.TRACK_ROSETTA) return consts.PAGE_VENUE_ROSETTA;
14 | return consts.PAGE_VENUE_ALL;
15 | };
16 |
17 | const VenueLink = ({ trackId, name }) => (
18 |
19 | {name}
20 |
21 | );
22 |
23 | VenueLink.propTypes = {
24 | trackId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
25 | name: PropTypes.string.isRequired,
26 | };
27 |
28 | VenueLink.defaultProps = {
29 | trackId: null,
30 | };
31 |
32 | export default inject(({ theme }, { trackId, name }) => ({
33 | name: trackId && theme.track(trackId) ? theme.track(trackId).name : name,
34 | }))(VenueLink);
35 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/CardTitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 | import Link from '../Link';
5 | import { sessionsContext } from '../../contexts';
6 |
7 | const CardTitle = ({ type, id, title, columns, isSpecial }) =>
8 | isSpecial ? (
9 |
10 | ) : (
11 |
12 |
13 |
14 | );
15 |
16 | CardTitle.propTypes = {
17 | type: PropTypes.string.isRequired,
18 | id: PropTypes.number.isRequired,
19 | title: PropTypes.string.isRequired,
20 | columns: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.shape({}))).isRequired,
21 | isSpecial: PropTypes.bool.isRequired,
22 | };
23 |
24 | export default CardTitle;
25 |
26 | const Title = styled.h2`
27 | display: block;
28 | font-size: 20px;
29 | font-weight: normal;
30 | padding-bottom: 4px;
31 | color: ${({ theme }) => theme.color.text};
32 | text-transform: uppercase;
33 | margin: 0;
34 | `;
35 |
36 | const AnchorTitle = styled(Title)`
37 | font-size: 16px;
38 | text-transform: none;
39 | `.withComponent('a');
40 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Menu/MenuIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'react-emotion';
3 |
4 | const MenuIcon = () => (
5 |
30 | );
31 |
32 | export default MenuIcon;
33 |
34 | const Svg = styled.svg`
35 | width: 24px;
36 | height: 16px;
37 | `;
38 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/instagram.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LazyInstagram from '../components/LazyInstagram';
3 | import { getInstagramId } from '../utils';
4 |
5 | export default {
6 | test: ({ tagName, attributes, ignore }) =>
7 | tagName === 'blockquote' &&
8 | attributes.className &&
9 | attributes.className.includes('instagram-media') &&
10 | !ignore,
11 | converter: element => {
12 | const { attributes, ...rest } = element;
13 | const height = 'auto';
14 | const width = '100%';
15 |
16 | // Overrrides style attributes
17 | const style = {
18 | ...attributes.style,
19 | width: '500px',
20 | maxWidth: '100%',
21 | margin: '0 auto',
22 | boxSizing: 'border-box',
23 | };
24 |
25 | const newAttributes = Object.assign(attributes, { style });
26 | element.children = [{ ...rest, attributes: newAttributes, ignore: true }];
27 |
28 | const instagramId = getInstagramId(element.children);
29 |
30 | return children => (
31 |
38 | {children}
39 |
40 | );
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import Slider from '../Slider';
5 | import NowNext from './NowNext';
6 | import Schedule from './Schedule';
7 | import TopBar from '../TopBar';
8 | import Menu from '../Menu';
9 | import Nav from './Nav';
10 |
11 | const OnNow = inject(({ theme }) => ({ sessions: theme.sessionsOnNow }))(NowNext);
12 | const UpNext = inject(({ theme }) => ({ sessions: theme.sessionsUpNext }))(NowNext);
13 |
14 | const Home = ({ columns, selectedColumnIndex, handleOnTransitionEnd }) => (
15 |
16 |
17 | {columns.map(({ selectedItem: { id, mstId } }) => {
18 | if (id === 13) return ;
19 | if (id === 15) return ;
20 | return ;
21 | })}
22 |
23 |
24 |
25 |
26 |
27 | );
28 |
29 | Home.propTypes = {
30 | columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
31 | selectedColumnIndex: PropTypes.number.isRequired,
32 | handleOnTransitionEnd: PropTypes.func.isRequired,
33 | };
34 |
35 | export default Home;
36 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Media/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 | import IconImage from 'react-icons/lib/fa/image';
5 | import Image from './Image';
6 |
7 | const Media = ({ entity, isRounded }) => (
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
16 | Media.propTypes = {
17 | entity: PropTypes.shape({}).isRequired,
18 | isRounded: PropTypes.bool,
19 | };
20 |
21 | Media.defaultProps = {
22 | isRounded: false,
23 | };
24 |
25 | export default Media;
26 |
27 | const Container = styled.div`
28 | position: relative;
29 | z-index: 0;
30 | box-sizing: border-box;
31 | width: 100%;
32 | height: 160px;
33 | background: ${({ theme }) => theme.color.grey};
34 | border-radius: 3px;
35 |
36 | img {
37 | border-radius: ${({ isRounded }) => (isRounded ? '3px' : 0)};
38 | width: 100%;
39 | height: 100%;
40 | object-fit: cover;
41 | object-position: center;
42 | }
43 | `;
44 |
45 | const Icon = styled.span`
46 | position: absolute;
47 | z-index: -1;
48 | top: 0;
49 | color: white;
50 | width: 100%;
51 | height: 100%;
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | `;
56 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Speakers/SessionCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Link from '../Link';
6 |
7 | const SessionCard = ({ type, id, title, context }) => (
8 |
9 |
10 | SESSION
11 |
12 |
13 |
14 | );
15 |
16 | SessionCard.propTypes = {
17 | type: PropTypes.string.isRequired,
18 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
19 | title: PropTypes.string.isRequired,
20 | context: PropTypes.shape({}).isRequired,
21 | };
22 |
23 | export default inject((_, { session }) => ({
24 | type: session.type,
25 | id: session.id,
26 | title: session.title,
27 | }))(SessionCard);
28 |
29 | const Card = styled.div`
30 | margin: 16px 24px 0 24px;
31 | padding: 16px;
32 | border-radius: 3px;
33 | background-color: ${({ theme }) => theme.color.lightGrey};
34 | `;
35 |
36 | const Session = styled.div`
37 | font-size: 12px;
38 | line-height: 16px;
39 | `;
40 |
41 | const Title = styled.h3`
42 | margin: 0;
43 | font-size: 16px;
44 | line-height: 20px;
45 | color: ${({ theme }) => theme.color.blue};
46 | `;
47 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Theme/AppleMeta.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import { Helmet } from 'react-helmet';
5 |
6 | const AppleMeta = ({ icon120, icon180, startupImage, title, color }) => (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | AppleMeta.propTypes = {
18 | icon120: PropTypes.string.isRequired,
19 | icon180: PropTypes.string.isRequired,
20 | startupImage: PropTypes.string.isRequired,
21 | title: PropTypes.string.isRequired,
22 | color: PropTypes.string.isRequired,
23 | };
24 |
25 | export default inject(({ settings }) => ({
26 | icon120: `${settings.theme.manifest.iosIconSrc}?profile=ios-icon-120`,
27 | icon180: `${settings.theme.manifest.iosIconSrc}?profile=ios-icon-180`,
28 | startupImage: settings.theme.manifest.iosStartupImage,
29 | title: settings.theme.manifest.shortName,
30 | color: settings.theme.manifest.themeColor,
31 | }))(AppleMeta);
32 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/ScheduleItemTitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 | import Link from '../Link';
5 | import { sessionsContext } from '../../contexts';
6 |
7 | const ScheduleItemTitle = ({ type, id, title, columns, isSpecial }) =>
8 | isSpecial ? (
9 |
10 | ) : (
11 |
12 |
13 |
14 | );
15 |
16 | ScheduleItemTitle.propTypes = {
17 | type: PropTypes.string.isRequired,
18 | id: PropTypes.number.isRequired,
19 | title: PropTypes.string.isRequired,
20 | columns: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.shape({}))).isRequired,
21 | isSpecial: PropTypes.bool.isRequired,
22 | };
23 |
24 | export default ScheduleItemTitle;
25 |
26 | const Title = styled.h2`
27 | margin: 0;
28 | font-size: 18px;
29 | color: ${({ theme }) => theme.color.black} !important;
30 | line-height: 24px;
31 | text-transform: uppercase;
32 | margin-left: 8px;
33 | `;
34 |
35 | const AnchorTitle = styled(Title)`
36 | color: ${({ theme }) => theme.color.blue} !important;
37 | text-transform: none;
38 | margin-left: 0;
39 | `.withComponent('a');
40 |
--------------------------------------------------------------------------------
/core/server/utils.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 |
3 | const parse = value =>
4 | Number.isFinite(parseInt(value, 10)) ? parseInt(value, 10) : value;
5 |
6 | export const parseQuery = query => {
7 | const { siteId, perPage, initialUrl } = query;
8 | const env = query.env === 'prod' ? 'prod' : 'pre';
9 | const device = query.device || 'mobile';
10 |
11 | let { type, id, page } = query;
12 |
13 | if (typeof type === 'undefined') {
14 | const { listType, singleType } = query;
15 |
16 | if (typeof listType !== 'undefined') {
17 | type = listType;
18 | page = parse(query.page) || 1;
19 | } else if (typeof singleType !== 'undefined') {
20 | type = singleType;
21 | } else {
22 | type = 'latest';
23 | page = parse(query.page) || 1;
24 | }
25 | } else {
26 | page = parse(query.page);
27 | }
28 |
29 | if (typeof id === 'undefined') {
30 | const { listId, singleId } = query;
31 |
32 | if (typeof listId !== 'undefined') {
33 | id = parse(listId);
34 | } else if (typeof singleId !== 'undefined') {
35 | id = parse(singleId);
36 | } else {
37 | id = 'post';
38 | }
39 | } else {
40 | id = parse(id);
41 | }
42 |
43 | return {
44 | siteId,
45 | perPage,
46 | initialUrl,
47 | env,
48 | device,
49 | type,
50 | id,
51 | page,
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Menu/MenuHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import CloseIcon from '../CloseIcon';
6 | import Logo from '../TopBar/Logo';
7 |
8 | const MenuHeader = ({ close }) => (
9 |
10 |
11 |
12 | Menu
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | MenuHeader.propTypes = {
21 | close: PropTypes.func.isRequired,
22 | };
23 |
24 | export default inject(({ theme }) => ({
25 | close: theme.menu.close,
26 | }))(MenuHeader);
27 |
28 | const Container = styled.div`
29 | height: ${({ theme }) => theme.size.button};
30 | width: 100%;
31 | display: flex;
32 | justify-content: space-between;
33 | background: #e9e9e6;
34 | `;
35 |
36 | const InnerContainer = styled.div`
37 | display: flex;
38 | align-items: center;
39 | height: ${({ theme }) => theme.size.button};
40 | `;
41 |
42 | const Title = styled.div`
43 | font-size: 16px;
44 | text-transform: uppercase;
45 | padding-left: 8px;
46 | `;
47 |
48 | const CloseButton = styled.div`
49 | width: ${({ theme }) => theme.size.button};
50 | display: flex;
51 | justify-content: center;
52 | align-items: center;
53 | `;
54 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/TopBar/CloseButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import { homeContext } from '../../contexts';
6 | import Link from '../Link';
7 | import CloseIcon from '../CloseIcon';
8 |
9 | const CloseButton = ({ previousContextRequested, contextIndex }) =>
10 | contextIndex ? (
11 |
12 |
13 |
14 | ) : (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
24 | CloseButton.propTypes = {
25 | previousContextRequested: PropTypes.func.isRequired,
26 | contextIndex: PropTypes.number.isRequired,
27 | };
28 |
29 | export default inject(({ connection }) => ({
30 | previousContextRequested: connection.previousContextRequested,
31 | contextIndex: connection.selectedContext.index,
32 | }))(CloseButton);
33 |
34 | const Container = styled.div`
35 | width: ${({ theme }) => theme.size.button};
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | `;
40 |
41 | const A = styled.a`
42 | width: 100%;
43 | height: 100%;
44 | display: flex;
45 | align-items: center;
46 | justify-content: center;
47 | `;
48 |
--------------------------------------------------------------------------------
/core/certs/localhost.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDqzCCApOgAwIBAgIJAPlNzpezF+gxMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNV
3 | BAYTAkVTMQ8wDQYDVQQIDAZNYWRyaWQxDzANBgNVBAcMBk1hZHJpZDEXMBUGA1UE
4 | CgwOV29yb25hIExhYnMgU0wxIjAgBgkqhkiG9w0BCQEWE2FjY291bnRzQHdvcm9u
5 | YS5vcmcwHhcNMTcxMDI0MTAzNTE1WhcNNDUwMzEwMTAzNTE1WjBsMQswCQYDVQQG
6 | EwJFUzEPMA0GA1UECAwGTWFkcmlkMQ8wDQYDVQQHDAZNYWRyaWQxFzAVBgNVBAoM
7 | Dldvcm9uYSBMYWJzIFNMMSIwIAYJKoZIhvcNAQkBFhNhY2NvdW50c0B3b3JvbmEu
8 | b3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0MPe8RdLS/oySKD8
9 | k8t3EKaToDkOgelZiOx/mIXVvOzNvf0xV5JqQ95dHq+ykw3E7Pj0/HxD3AlYCJ5R
10 | DAoPdn6byz3Yv/xc5d5pfmKkUBY4b7NYgs0RbnLdm4gI1onPsrmk1ILdUpbfCAMk
11 | 93hDOPhEyaU5NvL/cIhavAnIjLwyBDigX9tMPX1sEkKPUG+ES73QNz55B+v2OoVR
12 | w4Rd0M6wMeGB4eUQKM/x3hvmrv54yzyLIH8J8/NTCCCkCSb50Zwuo52p5gJylD8M
13 | /0BvoyBzHSPH6pC/b+lOjErctlZ7LaX/KUZID9d/L2FR2dVEpVAkvEJsKF/+ltL7
14 | i8+DoQIDAQABo1AwTjAdBgNVHQ4EFgQU8+X9gQVaZZc8S6WXX6mqsQ/74Y8wHwYD
15 | VR0jBBgwFoAU8+X9gQVaZZc8S6WXX6mqsQ/74Y8wDAYDVR0TBAUwAwEB/zANBgkq
16 | hkiG9w0BAQsFAAOCAQEAwueiZndA6ws5dX0yYyUfO5LlcIJecYqj45DhsQOYXLPd
17 | jwVWkELLTNDPD5badvnR/EHb3NdnyvNVi+l5i0Ey0BasPR/kAHEnumX6g4rFgZL6
18 | QEltj04T57sCtowsl6+ayHJ3eJQ1zhBbLPOc/Bxr/O5uKla7QJxacTei3l/Hea53
19 | kQwgcL2ggR6QWbHs28F3LlT8kKJCYX3Uo4BaEel2qeISK4ZwoQ8J9pfEUkx0BGh6
20 | QvyD6btLm2VJMsJ6Hm+xodU2g8iTDNKASEvL7GwZr127kZQ5CzKlw7oOnStIkRjH
21 | cFYKqV4I82ZNLF/xSzT3aYMXXjzdmcLhnv45ynNexg==
22 | -----END CERTIFICATE-----
23 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Twitter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Twitter = ({ size }) => (
6 |
12 | );
13 |
14 | Twitter.propTypes = {
15 | size: PropTypes.number.isRequired,
16 | };
17 |
18 | export default Twitter;
19 |
20 | const Svg = styled.svg`
21 | width: ${({ size }) => `${size}px`};
22 | height: ${({ size }) => `${size}px`};
23 | color: ${({ theme }) => theme.color.darkGrey};
24 | `;
25 |
--------------------------------------------------------------------------------
/core/server/amp-template.js:
--------------------------------------------------------------------------------
1 | export default ({ helmet, css, html }) => `
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ${helmet.title.toString()}
10 | ${helmet.meta.toString()}
11 | ${helmet.link.toString()}
12 | ${helmet.script.toString()}
13 |
14 |
15 |
16 | ${html}
17 |
18 | `;
19 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Media/image.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import { parse } from 'url';
5 |
6 | const Image = ({ alt, src, srcSet }) =>
7 | src || srcSet ?
: null;
8 |
9 | Image.propTypes = {
10 | alt: PropTypes.string.isRequired,
11 | src: PropTypes.string.isRequired,
12 | srcSet: PropTypes.string.isRequired,
13 | };
14 |
15 | // Returns true if width/height ratio of both objects are very, very close.
16 | // Used when computing the srcSet prop value.
17 | const sameRatio = ({ width: w1, height: h1 }, { width: w2, height: h2 }) =>
18 | Math.abs(w1 / h1 - w2 / h2) < 0.01;
19 |
20 | export default inject(({ settings }, { entity }) => {
21 | const cdn = (settings.theme.cdn || {}).images;
22 | const originalPath = parse(entity.original.url).path;
23 | const src = cdn && originalPath ? `${cdn}${originalPath}` : entity.original.url;
24 |
25 | const sizes = entity.sizes
26 | .filter(item => sameRatio(item, entity.original))
27 | .map(item => {
28 | const { path } = parse(item.url);
29 | const url = cdn && path ? `${cdn}${path}` : item.url;
30 | return `${url} ${item.width}w`;
31 | })
32 | .join(', ');
33 |
34 | return {
35 | alt: entity.alt,
36 | src: entity.original.url,
37 | srcSet: sizes || (src ? `${src} 100w` : ''),
38 | };
39 | })(Image);
40 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Spinner/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | eslint no-mixed-operators: 0,
3 | react/no-array-index-key: 0
4 | */
5 |
6 | import React from 'react';
7 | import styled, { keyframes } from 'react-emotion';
8 |
9 | const Spinner = () => {
10 | const circles = Array(12)
11 | .fill()
12 | .map((item, index) => );
13 |
14 | return (
15 |
16 | {circles}
17 |
18 | );
19 | };
20 |
21 | export default Spinner;
22 |
23 | const skCircleFadeDelay = keyframes`
24 | 0%, 39%, 100% { opacity: 0; }
25 | 40% { opacity: 1; }
26 | `;
27 |
28 | const Container = styled.div`
29 | height: 100%;
30 | width: 100%;
31 | display: flex;
32 | justify-content: center;
33 | align-items: center;
34 | `;
35 |
36 | const Wrapper = styled.div`
37 | width: 40px;
38 | height: 40px;
39 | position: relative;
40 | `;
41 |
42 | const Circle = styled.div`
43 | width: 100%;
44 | height: 100%;
45 | position: absolute;
46 | left: 0;
47 | top: 0;
48 | transform: ${({ circle }) => `rotate(${30 * circle}deg)`};
49 |
50 | &:before {
51 | content: '';
52 | display: block;
53 | margin: 0 auto;
54 | width: 15%;
55 | height: 15%;
56 | background-color: #333;
57 | border-radius: 100%;
58 | animation: ${skCircleFadeDelay} 1.2s infinite ease-in-out both;
59 | animation-delay: ${({ circle }) => -1.1 + circle / 10}s;
60 | }
61 | `;
62 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Announcements/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Card from './Card';
6 | import Refresh from './Refresh';
7 | import NextPage from './NextPage';
8 | import TopBar from '../TopBar';
9 | import Menu from '../Menu';
10 | import { postsContext } from '../../contexts';
11 |
12 | const Announcements = ({ entities, list }) => {
13 | const context = postsContext(entities.map(({ type, id }) => [{ type, id }]));
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | {entities.map(entity => )}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | Announcements.propTypes = {
28 | entities: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
29 | list: PropTypes.shape({}).isRequired,
30 | };
31 |
32 | export default inject(({ connection }) => ({
33 | entities: connection.list('latest', 'post').entities.peek(),
34 | list: connection.list('latest', 'post'),
35 | }))(Announcements);
36 |
37 | const Container = styled.div`
38 | box-sizing: border-box;
39 | width: 100vw;
40 | min-height: 100vh;
41 | padding: 56px 0 0 0;
42 | display: flex;
43 | flex-direction: column;
44 | align-items: stretch;
45 | `;
46 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Menu/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import MenuHeader from './MenuHeader';
6 | import MenuList from './MenuList';
7 | import MenuNotifications from './MenuNotifications';
8 | import MenuLinks from './MenuLinks';
9 |
10 | const Menu = ({ isOpen, close }) =>
11 | isOpen ? (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ) : null;
22 |
23 | Menu.propTypes = {
24 | isOpen: PropTypes.bool.isRequired,
25 | close: PropTypes.func.isRequired,
26 | };
27 |
28 | export default inject(({ theme }) => ({
29 | isOpen: theme.menu.isOpen,
30 | close: theme.menu.close,
31 | }))(Menu);
32 |
33 | const Container = styled.div`
34 | height: 100vh;
35 | width: 100vw;
36 | position: fixed;
37 | top: 0;
38 | left: 0;
39 | color: ${({ theme }) => theme.color.text};
40 | z-index: 100;
41 | `;
42 |
43 | const Overlay = styled.div`
44 | width: 100%;
45 | height: 100%;
46 | background-color: ${({ theme }) => theme.color.black};
47 | opacity: 0.5;
48 | `;
49 |
50 | const InnerContainer = styled.div`
51 | width: 100%;
52 | position: absolute;
53 | top: 0;
54 | left: 0;
55 | background-color: #fff;
56 | `;
57 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/HtmlToReactConverter/filter.js:
--------------------------------------------------------------------------------
1 | import htmlMap from './htmlMap';
2 | import svgMap from './svgMap';
3 |
4 | const allMap = { ...htmlMap, ...svgMap };
5 |
6 | const camelCaseToDash = str => str.replace(/([A-Z])/g, g => `-${g[0].toLowerCase()}`);
7 |
8 | export const replaceDataAttrs = dataset => {
9 | const toReturn = {};
10 | if (dataset) {
11 | Object.entries(dataset).forEach(([key, value]) => {
12 | toReturn[`data-${camelCaseToDash(key)}`] = value;
13 | });
14 | }
15 | return toReturn;
16 | };
17 |
18 | export const replaceAttrs = attributes => {
19 | const toReturn = {};
20 | if (attributes) {
21 | Object.entries(attributes).forEach(([key, value]) => {
22 | if (!(/^on/.test(key) && typeof value === 'string')) {
23 | // ignores 'onEvent' attributes
24 | const newKey = allMap[key.toLowerCase()];
25 | toReturn[newKey && newKey !== key ? newKey : key] =
26 | value instanceof Array ? value.join(' ') : value;
27 | }
28 | });
29 | }
30 | return toReturn;
31 | };
32 |
33 | export const filterAllow = allow => {
34 | if (allow) {
35 | return allow
36 | .split(';')
37 | .map(i => i.trim())
38 | .filter(i => i !== 'autoplay')
39 | .join('; ');
40 | }
41 |
42 | return allow;
43 | };
44 |
45 | export const filter = (attributes = {}) => {
46 | const { dataset, allow, controls, ...others } = attributes;
47 | return { ...replaceDataAttrs(dataset), ...replaceAttrs(others) };
48 | };
49 |
--------------------------------------------------------------------------------
/core/packages/analytics/pwa/components/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { dep } from 'worona-deps';
5 | import GoogleTagManager from './GoogleTagManager';
6 | import ComScore from './ComScore';
7 |
8 | const Analytics = ({ isAmp, gtmContainers, comScoreIds }) => {
9 | if (isAmp) return null;
10 |
11 | return (
12 |
13 |
14 | {gtmContainers.map(id => )}
15 | {comScoreIds.map(id => )}
16 |
17 | );
18 | };
19 |
20 | Analytics.propTypes = {
21 | gtmContainers: PropTypes.arrayOf(PropTypes.string),
22 | comScoreIds: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
23 | isAmp: PropTypes.bool.isRequired,
24 | };
25 |
26 | Analytics.defaultProps = {
27 | gtmContainers: [],
28 | comScoreIds: [],
29 | };
30 |
31 | const emptyArray = [];
32 |
33 | const mapStateToProps = state => {
34 | const analytics = dep('settings', 'selectorCreators', 'getSetting')('theme', 'analytics')(state);
35 | const gtmContainers = (analytics && analytics.pwa && analytics.pwa.gtmContainers) || emptyArray;
36 | const comScoreIds = (analytics && analytics.pwa && analytics.pwa.comScoreIds) || emptyArray;
37 | return {
38 | gtmContainers,
39 | comScoreIds,
40 | isAmp: state.build.amp,
41 | };
42 | };
43 |
44 | export default connect(mapStateToProps)(Analytics);
45 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Instagram.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Instagram = ({ size }) => (
6 |
12 | );
13 |
14 | Instagram.propTypes = {
15 | size: PropTypes.number.isRequired,
16 | };
17 |
18 | export default Instagram;
19 |
20 | const Svg = styled.svg`
21 | width: ${({ size }) => `${size}px`};
22 | height: ${({ size }) => `${size}px`};
23 | color: ${({ theme }) => theme.color.darkGrey};
24 | `;
25 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/FavoriteButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import EmptyStarIcon from 'react-icons/lib/fa/star-o';
6 | import FullStarIcon from 'react-icons/lib/fa/star';
7 |
8 | const FavoriteButton = ({ isFavorite, toggleFavorite, inSchedule }) => (
9 |
10 | {isFavorite ? : }
11 |
12 | );
13 |
14 | FavoriteButton.propTypes = {
15 | isFavorite: PropTypes.bool.isRequired,
16 | toggleFavorite: PropTypes.func.isRequired,
17 | inSchedule: PropTypes.bool.isRequired,
18 | };
19 |
20 | export default inject((_, { session, inSchedule }) => ({
21 | isFavorite: !!session.isFavorite,
22 | toggleFavorite: session.toggleFavorite,
23 | inSchedule: !!inSchedule,
24 | }))(FavoriteButton);
25 |
26 | const Container = styled.div`
27 | width: ${({ theme, inSchedule }) => (inSchedule ? '' : theme.size.cardHeader)};
28 | height: ${({ theme, inSchedule }) => (inSchedule ? '' : theme.size.cardHeader)};
29 | display: flex;
30 | justify-content: center;
31 | align-items: center;
32 | flex-shrink: 0;
33 |
34 | & > svg {
35 | padding: ${({ inSchedule }) => (inSchedule ? '3px 0' : '11px')};
36 | color: ${({ theme, inSchedule, isFavorite }) => {
37 | if (inSchedule) return isFavorite ? theme.color.red : theme.color.darkGrey;
38 | return theme.color.white;
39 | }};
40 | }
41 | `;
42 |
--------------------------------------------------------------------------------
/core/store/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | import { createStore, applyMiddleware, compose } from 'redux';
3 | import createSagaMiddleware from 'redux-saga';
4 | import worona from 'worona-deps';
5 |
6 | const dev = process.env.NODE_ENV !== 'production';
7 |
8 | // Init compose, saga and create middlewares.
9 | let composeEnhancers = compose;
10 | const sagaMiddleware = createSagaMiddleware();
11 | const clientMiddleware = [sagaMiddleware];
12 | const serverMiddleware = [sagaMiddleware];
13 |
14 | if (dev) {
15 | const { composeWithDevTools } = require('redux-devtools-extension');
16 | const { createLogger } = require('redux-logger');
17 | // Add Redux Dev Tools.
18 | composeEnhancers = composeWithDevTools({ serialize: false });
19 | // Add logger in dev mode.
20 | clientMiddleware.push(createLogger({ diff: true, collapsed: true }));
21 | serverMiddleware.push(createLogger({ diff: true, collapsed: true }));
22 | }
23 |
24 | export default ({ reducer, initialState = {} }) => {
25 | // Create store for the server.
26 | if (typeof window === 'undefined') {
27 | const store = {
28 | ...createStore(reducer, initialState, compose(applyMiddleware(...serverMiddleware))),
29 | runSaga: sagaMiddleware.run,
30 | };
31 | // Add it to worona.
32 | worona.store = store;
33 | return store;
34 | }
35 | // Create store for the client.
36 | const store = {
37 | ...createStore(reducer, initialState, composeEnhancers(applyMiddleware(...clientMiddleware))),
38 | runSaga: sagaMiddleware.run,
39 | };
40 | window.worona.store = store;
41 | return store;
42 | };
43 |
--------------------------------------------------------------------------------
/core/packages/analytics/shared/helpers/index.js:
--------------------------------------------------------------------------------
1 | import sha256 from 'crypto-js/sha256';
2 | import base64 from 'crypto-js/enc-base64';
3 |
4 | export const getRoute = ({ page }) => (typeof page === 'number' ? 'list' : 'single');
5 |
6 | export const getHash = (site, selectedItem) => {
7 | const { type, id, page } = selectedItem;
8 | const data = JSON.stringify([site, type, id, page]);
9 | return base64.stringify(sha256(JSON.stringify(data))).slice(0, 19);
10 | };
11 |
12 | export const getAnonymousTitle = ({ site, selectedItem, format }) => {
13 | const { type } = selectedItem;
14 | const route = getRoute(selectedItem);
15 | const hash = getHash(site, selectedItem);
16 | return `anonymous - ${format} - ${route} - ${type} - ${hash}`;
17 | };
18 |
19 | export const getAnonymousUrl = ({ site, selectedItem, format }) =>
20 | `anonymous/${format}/${getHash(site, selectedItem)}`;
21 |
22 | export const getTitle = ({ site, selectedItem, format }) => {
23 | const { type, id, page } = selectedItem;
24 | const withPage = page ? ` - page ${page}` : '';
25 | const route = getRoute(selectedItem);
26 | return `${site} - ${format} - ${route} - ${type} - ${id}${withPage}`;
27 | };
28 |
29 | export const getUrl = ({ selectedItem, format }) => {
30 | const { link } = selectedItem.entity;
31 | return format ? `${link}${link.endsWith('/') ? '' : '/'}${format}/` : link;
32 | };
33 |
34 | export const getGaTrackingIds = ({ dev, analyticsSettings = {}, format }) => {
35 | const gaTrackingIds =
36 | (analyticsSettings[format] && analyticsSettings[format].gaTrackingIds) || [];
37 | return dev && gaTrackingIds.length ? ['UA-91312941-7'] : gaTrackingIds;
38 | };
39 |
--------------------------------------------------------------------------------
/core/scripts/build.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require, no-console */
2 | const { emptyDir, writeFile } = require('fs-extra');
3 | const path = require('path');
4 | const webpack = require('webpack');
5 |
6 | const dev = process.env.NODE_ENV !== 'production';
7 |
8 | // Turn webpack into a promise.
9 | const webpackPromise = (clientConfig, serverConfig) =>
10 | new Promise((resolve, reject) => {
11 | webpack([clientConfig, serverConfig]).run((err, stats) => {
12 | if (err) reject(err);
13 | resolve(stats);
14 | });
15 | });
16 |
17 | const clean = async () => {
18 | await emptyDir(`.build/${process.env.MODE}`);
19 | const buildInfo = {
20 | buildPath: path.resolve(__dirname, '../..'),
21 | nodeEnv: dev ? 'development' : 'production',
22 | };
23 | await writeFile(`.build/${process.env.MODE}/buildInfo.json`, JSON.stringify(buildInfo, null, 2));
24 | };
25 |
26 | const build = async () => {
27 | // Import proper configuration files.
28 | const clientConfig = dev
29 | ? require('../webpack/client.dev')
30 | : require('../webpack/client.prod');
31 | const serverConfig = dev
32 | ? require('../webpack/server.dev')
33 | : require('../webpack/server.prod');
34 |
35 | // Run webpack and wait until it finishes. Then save clientStats to a file.
36 | const stats = await webpackPromise(clientConfig, serverConfig);
37 | const clientStats = stats.toJson().children[0];
38 | await writeFile(
39 | `.build/${process.env.MODE}/clientStats.json`,
40 | JSON.stringify(clientStats, null, 2),
41 | );
42 |
43 | // Return webpack stats.
44 | return stats;
45 | };
46 |
47 | module.exports = {
48 | build,
49 | clean,
50 | };
51 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/LazyIframe/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 | import LazyLoad from '@frontity/lazyload';
5 |
6 | const LazyIframe = ({ width, height, attributes }) => {
7 | const {
8 | title,
9 | allowFullScreen,
10 | allowPaymentRequest,
11 | allowTransparency,
12 | width: attributesWidth,
13 | height: attributesHeight,
14 | ...rest
15 | } = attributes;
16 |
17 | return (
18 |
19 |
20 |
29 |
30 |
31 | );
32 | };
33 |
34 | LazyIframe.propTypes = {
35 | width: PropTypes.string.isRequired,
36 | height: PropTypes.string.isRequired,
37 | attributes: PropTypes.shape({}).isRequired,
38 | };
39 |
40 | export default LazyIframe;
41 |
42 | const Container = styled.span`
43 | display: block;
44 | position: relative;
45 | left: -15px;
46 | height: ${({ styles }) => styles.height};
47 | width: ${({ styles }) => styles.width};
48 |
49 | amp-iframe {
50 | max-width: 100%;
51 | }
52 |
53 | & > .LazyLoad {
54 | display: block;
55 | width: 100%;
56 | height: 100%;
57 |
58 | iframe {
59 | width: 100%;
60 | height: 100%;
61 | }
62 | }
63 | `;
64 |
--------------------------------------------------------------------------------
/packages/src/pwa/consts/index.js:
--------------------------------------------------------------------------------
1 | export const PAGE_HOME_ON_NOW = 13;
2 | export const PAGE_HOME_UP_NEXT = 15;
3 | export const PAGE_HOME_SCHEDULE = 17;
4 |
5 | export const PAGE_ANNOUNCEMENTS = 19;
6 |
7 | export const PAGE_VENUE_ALL = 23;
8 | export const PAGE_VENUE_MILKY_WAY = 26;
9 | export const PAGE_VENUE_ANDROMEDA = 28;
10 | export const PAGE_VENUE_HAYABUSA = 30;
11 | export const PAGE_VENUE_CASSINI = 32;
12 | export const PAGE_VENUE_ROSETTA = 34;
13 |
14 | export const PAGE_CREDITS = 36;
15 |
16 | export const PAGE_SESSION_BEFORE = 50;
17 | export const PAGE_SESSION_CONTRIBUTORS = 52;
18 | export const PAGE_SESSION_THURSDAY_NIGHT = 56;
19 | export const PAGE_SESSION_FRIDAY_NIGHT = 58;
20 | export const PAGE_SESSION_AFTER_PARTY = 60;
21 | export const PAGE_SESSION_AFTER = 62;
22 |
23 | export const PAGE_MENU_COC = 76;
24 | export const PAGE_MENU_MENUS = 78;
25 |
26 | export const TRACK_NETWORKING = 5551;
27 | export const TRACK_MILKY_WAY = 13411;
28 | export const TRACK_ANDROMEDA = 13412;
29 | export const TRACK_HAYABUSA = 919798;
30 | export const TRACK_CASSINI = 919799;
31 | export const TRACK_ROSETTA = 1242640;
32 |
33 | export const venues = [
34 | PAGE_VENUE_ALL,
35 | PAGE_VENUE_MILKY_WAY,
36 | PAGE_VENUE_ANDROMEDA,
37 | PAGE_VENUE_HAYABUSA,
38 | PAGE_VENUE_CASSINI,
39 | PAGE_VENUE_ROSETTA,
40 | ];
41 |
42 | export const customSessions = [
43 | PAGE_SESSION_BEFORE,
44 | PAGE_SESSION_CONTRIBUTORS,
45 | PAGE_SESSION_THURSDAY_NIGHT,
46 | PAGE_SESSION_FRIDAY_NIGHT,
47 | PAGE_SESSION_AFTER_PARTY,
48 | PAGE_SESSION_AFTER,
49 | ];
50 |
51 | export const menu = [PAGE_MENU_COC, PAGE_MENU_MENUS];
52 |
53 | export const allPages = [PAGE_ANNOUNCEMENTS]
54 | .concat(venues)
55 | .concat(customSessions)
56 | .concat(menu);
57 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/TopBar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Logo from './Logo';
6 | import MenuButton from '../Menu/MenuButton';
7 | import CloseButton from './CloseButton';
8 |
9 | const TopBar = ({ contextTitle, contextName, contextColor }) => (
10 |
11 |
12 |
13 | {contextTitle}
14 |
15 | {['home', 'announcements'].includes(contextName) ? : }
16 |
17 | );
18 |
19 | TopBar.propTypes = {
20 | contextName: PropTypes.string.isRequired,
21 | contextTitle: PropTypes.string.isRequired,
22 | contextColor: PropTypes.string.isRequired,
23 | };
24 |
25 | export default inject(({ connection }) => ({
26 | contextName: connection.selectedContext.options.name,
27 | contextTitle: connection.selectedContext.options.title,
28 | contextColor: connection.selectedContext.options.color,
29 | }))(TopBar);
30 |
31 | const Container = styled.div`
32 | position: fixed;
33 | top: 0;
34 | left: 0;
35 | height: ${({ theme }) => theme.size.button};
36 | width: 100vw;
37 | display: flex;
38 | justify-content: space-between;
39 | background: ${({ theme, color }) => theme.color[color] || theme.color.grey};
40 | z-index: 10;
41 | `;
42 |
43 | const InnerContainer = styled.div`
44 | display: flex;
45 | align-items: center;
46 | height: ${({ theme }) => theme.size.button};
47 | `;
48 |
49 | const Title = styled.div`
50 | font-size: 16px;
51 | text-transform: uppercase;
52 | color: ${({ theme }) => theme.color.text};
53 | padding-left: 8px;
54 | `;
55 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/NavItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Link from '../Link';
6 |
7 | const routes = {
8 | 'on-now': {
9 | id: 13,
10 | text: 'On Now',
11 | },
12 | 'up-next': {
13 | id: 15,
14 | text: 'Up Next',
15 | },
16 | schedule: {
17 | id: 17,
18 | text: 'Full',
19 | },
20 | };
21 |
22 | const NavItem = ({ label, isSelected }) => (
23 |
24 |
25 | {routes[label].text}
26 |
27 |
28 | );
29 |
30 | NavItem.propTypes = {
31 | label: PropTypes.string.isRequired,
32 | isSelected: PropTypes.bool.isRequired,
33 | };
34 |
35 | export default inject(({ connection }, { label }) => ({
36 | isSelected: connection.selectedContext.getItem({ item: { type: 'page', id: routes[label].id } })
37 | .isSelected,
38 | }))(NavItem);
39 |
40 | const Container = styled.div`
41 | box-sizing: border-box;
42 | flex-grow: 1;
43 | width: 33%;
44 | color: ${({ theme, isSelected }) => (isSelected ? theme.color.blue : '#7C7A69')};
45 | background-color: ${({ theme, isSelected }) => (isSelected ? theme.color.white : null)}
46 | font-size: 16px;
47 | ${({ isSelected, theme }) =>
48 | isSelected ? null : `box-shadow: inset 0 0 0 1px ${theme.color.grey}`};
49 | `;
50 |
51 | const A = styled.a`
52 | width: 100%;
53 | height: ${({ theme }) => theme.size.button};
54 | color: ${({ theme, isSelected }) => (isSelected ? theme.color.blue : '#7C7A69')};
55 | display: flex;
56 | justify-content: center;
57 | align-items: center;
58 | `;
59 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Bullhorn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Bullhorn = ({ size, inCard }) => (
6 |
23 | );
24 |
25 | Bullhorn.propTypes = {
26 | size: PropTypes.number.isRequired,
27 | inCard: PropTypes.bool.isRequired,
28 | };
29 |
30 | export default Bullhorn;
31 |
32 | const Svg = styled.svg`
33 | width: ${({ size }) => `${size}px`};
34 | height: ${({ size }) => `${size}px`};
35 | color: ${({ theme, inCard }) => (inCard ? theme.color.blue : theme.color.black)};
36 | ${({ inCard }) => (inCard ? 'margin-right: 12px' : null)};
37 | `;
38 |
--------------------------------------------------------------------------------
/packages/src/pwa/utils/index.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment-timezone';
2 |
3 | export const TIMEZONE = 'Europe/Belgrade';
4 |
5 | export const formatDate = (date, formatString) =>
6 | moment(date.toISOString())
7 | .tz(TIMEZONE)
8 | .format(formatString);
9 |
10 | // This function iterates the element object recursively until it finds an 'Element'
11 | // with tagName 'a' and its 'href' attribute matches a RegExp that captures a tweet ID.
12 | export const getTweetId = children => {
13 | if (!children) return '';
14 |
15 | const results = [];
16 |
17 | for (let i = 0; i < children.length; i += 1) {
18 | const child = children[i];
19 |
20 | if (child.type === 'Element' && child.tagName === 'a') {
21 | const match = child.attributes.href.match(/\/status\/(\d+)/);
22 |
23 | if (match) return match[1];
24 | }
25 |
26 | if (child.children) results.push(getTweetId(child.children));
27 | }
28 |
29 | return results.reduce((result, current) => current || result, '');
30 | };
31 |
32 | // This function iterates the element object recursively until it finds an 'Element'
33 | // with tagName 'a' and its 'href' attribute matches a RegExp that captures an instagram ID.
34 | export const getInstagramId = children => {
35 | if (!children) return '';
36 |
37 | const results = [];
38 |
39 | for (let i = 0; i < children.length; i += 1) {
40 | const child = children[i];
41 |
42 | if (child.type === 'Element' && child.tagName === 'a') {
43 | const match = child.attributes.href.match(/https:\/\/www\.instagram\.com\/p\/([\w\d]+)/);
44 |
45 | if (match) return match[1];
46 | }
47 |
48 | if (child.children) results.push(getInstagramId(child.children));
49 | }
50 |
51 | return results.reduce((result, current) => current || result, '');
52 | };
53 |
--------------------------------------------------------------------------------
/core/certs/localhost.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDQw97xF0tL+jJI
3 | oPyTy3cQppOgOQ6B6VmI7H+YhdW87M29/TFXkmpD3l0er7KTDcTs+PT8fEPcCVgI
4 | nlEMCg92fpvLPdi//Fzl3ml+YqRQFjhvs1iCzRFuct2biAjWic+yuaTUgt1Slt8I
5 | AyT3eEM4+ETJpTk28v9wiFq8CciMvDIEOKBf20w9fWwSQo9Qb4RLvdA3PnkH6/Y6
6 | hVHDhF3QzrAx4YHh5RAoz/HeG+au/njLPIsgfwnz81MIIKQJJvnRnC6jnanmAnKU
7 | Pwz/QG+jIHMdI8fqkL9v6U6MSty2Vnstpf8pRkgP138vYVHZ1USlUCS8QmwoX/6W
8 | 0vuLz4OhAgMBAAECggEBAIOalpZhWuRAiyZh5he9cgebGwEsNssKsZixjhY2eHEq
9 | zuBwFoTgFgFG0mkf1XECgxvoLXhlVdoGQerc8ramOO04DzpmeJc+ncmDFfnIcVDT
10 | USyJYMjEQbLg0nbToM6t/bLreM9G2ALSfTLt2IXw2F+3kpWThKfxM7pbGufjOvwM
11 | G+u9rUzFioNYsP9MpLr8o4OdjM2csIa4kBKTRn0v5WNbEqkofJrQK6n5Uk6W5L0t
12 | uWI6TDyX0oGUm8phhEb+kIVLFBs0oa4JHLIeQYAbEK20MI121W37Q8MwC68rQTQM
13 | fiLQZQMnO+ZNXBqqrVKi8LGY2tRgKj0zOokdtNl5vAkCgYEA6+VWVoQhob4Qu3eK
14 | dSrI6YfNkHcPqkHdeDKtIZki6XCZE/2ju/VjdKpWLB5gE6M1RXbSLLWvLw8Fy7Ac
15 | EifU0WYAiMqOKor4gjwWWtI/Ecsuel+h6jlIZWAm+Z6r4dkBDelrcTNWZZbcil4E
16 | tCucGqKqTv6BhYhLr7psKvaInT8CgYEA4o6bveC24c0+lg8Q3B94fMw8MKlPB0Od
17 | WkhQ/MtMzfGjHRnjC9YlCrtUwM4D59dDEZi6I5UoeEZ6UNvIjAcNVK0ei5kVdyKP
18 | O3ZkSIyGuL2a3EN3Q7fYt9xOFLzmE16hcZVi8a09IquvKKtlww/Q0fFv1iFTAmB+
19 | pAXUXkBYRx8CgYAjW3GQcCeWVaWuY6GGJE6O6kL+d0oKy+qFHJtkoM5hzPW00Fyd
20 | TI+Gqg1WeYsPBnRyn/eqqeTDGe5VdGUQt5WneTF1T171s3TCGH2FSWQYp6qkvvbG
21 | 46UdRMcyvPyoUl1dF3J6Oial/XG675Km6q5wM5dxVaZWPHKTuU6DNgaOAwKBgQCl
22 | 9Bx1HBQalwOPW/3XoF3b3KcCZiLziT3MEkjww+LAwI3hFUNROk3r7IkKql4GB5Kj
23 | YyKGSg5Y/BdA/52JpOjI6/8B9uLYX8JXpkOTOQy+anTzf5/vRM5E7+VqH2zRJ3JP
24 | B0RxD5tGuEgYHGTwW7d08cwgKEzr4s8hSvvjBnMjtQKBgQDABfs/MfhE7e92q3N2
25 | HhecfpcsqAPoEcAvk704B0VMiW+es85FovrVtwJr8BzMJMfEyY5EOp93pU0n9B3u
26 | InRlMdkXVvGMRnk3ROp0yezUK5/wwDMXvh9GBJd9C1IvhfZp0PXs2N1xXG4GpowI
27 | g/dYRq/B6a5VteY8ooYJvYIW2w==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Link/index.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | import React, { Component } from 'react';
3 | import PropTypes from 'prop-types';
4 | import { inject } from 'mobx-react';
5 |
6 | class Link extends Component {
7 | static propTypes = {
8 | type: PropTypes.string.isRequired,
9 | id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
10 | page: PropTypes.number,
11 | context: PropTypes.shape({}),
12 | method: PropTypes.string,
13 | children: PropTypes.node.isRequired,
14 | href: PropTypes.string.isRequired,
15 | routeChangeRequested: PropTypes.func.isRequired,
16 | };
17 |
18 | static defaultProps = {
19 | page: null,
20 | method: 'push',
21 | context: null,
22 | };
23 |
24 | constructor() {
25 | super();
26 |
27 | this.linkClicked = this.linkClicked.bind(this);
28 | }
29 |
30 | linkClicked(e) {
31 | // ignore click for new tab / new window behavior
32 | if (
33 | e.currentTarget.nodeName === 'A' &&
34 | (e.metaKey || e.ctrlKey || e.shiftKey || (e.nativeEvent && e.nativeEvent.which === 2))
35 | )
36 | return;
37 |
38 | e.preventDefault();
39 |
40 | const { routeChangeRequested, type, id, page, context, method } = this.props;
41 |
42 | routeChangeRequested({
43 | selectedItem: { type, id, page },
44 | context,
45 | method,
46 | });
47 | }
48 |
49 | render() {
50 | const { children, href } = this.props;
51 | return React.cloneElement(children, { onClick: this.linkClicked, href });
52 | }
53 | }
54 |
55 | export default inject(({ connection }, { type, id, page }) => ({
56 | routeChangeRequested: connection.routeChangeRequested,
57 | href: page ? connection.entity(type, id).pagedLink(page) : connection.entity(type, id).link,
58 | }))(Link);
59 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Nav/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import PrevIcon from 'react-icons/lib/fa/angle-left';
6 | import NextIcon from 'react-icons/lib/fa/angle-right';
7 | import Share from './Share';
8 | import Button from './Button';
9 |
10 | const Nav = ({ previousItem, nextItem }) => (
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
18 | Nav.propTypes = {
19 | previousItem: PropTypes.shape({}),
20 | nextItem: PropTypes.shape({}),
21 | };
22 |
23 | Nav.defaultProps = {
24 | previousItem: null,
25 | nextItem: null,
26 | };
27 |
28 | export default inject(({ connection }) => {
29 | const {
30 | hasPreviousColumn,
31 | previousColumn,
32 | hasNextColumn,
33 | nextColumn,
34 | } = connection.selectedColumn;
35 | return {
36 | previousItem: hasPreviousColumn ? previousColumn.selectedItem : null,
37 | nextItem: hasNextColumn ? nextColumn.selectedItem : null,
38 | };
39 | })(Nav);
40 |
41 | const Container = styled.div`
42 | position: fixed;
43 | bottom: 0;
44 | left: 0;
45 | width: 100vw;
46 | box-sizing: padding-box;
47 | height: ${({ theme }) => theme.size.button};
48 | background-color: ${({ theme }) => theme.color.lightGrey};
49 | border-top: 1px solid ${({ theme }) => theme.color.grey};
50 | display: flex;
51 | justify-content: space-between;
52 | `;
53 |
54 | const PrevButton = styled(Button)`
55 | position: absolute;
56 | top: 0;
57 | left: 0;
58 | `;
59 |
60 | const NextButton = styled(Button)`
61 | position: absolute;
62 | top: 0;
63 | right: 0;
64 | `;
65 |
--------------------------------------------------------------------------------
/core/packages/analytics/amp/components/GoogleAnalytics.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-no-target-blank, react/no-danger */
2 | import React, { Fragment } from 'react';
3 | import PropTypes from 'prop-types';
4 | import { Helmet } from 'react-helmet';
5 |
6 | const GoogleAnalytics = ({
7 | trackingId,
8 | title,
9 | documentLocation,
10 | extraUrlParams,
11 | vars,
12 | triggers,
13 | }) => (
14 |
15 |
16 |
21 |
22 |
23 |
46 |
47 |
48 | );
49 |
50 | GoogleAnalytics.propTypes = {
51 | trackingId: PropTypes.string.isRequired,
52 | title: PropTypes.string.isRequired,
53 | documentLocation: PropTypes.string.isRequired,
54 | extraUrlParams: PropTypes.shape({}),
55 | vars: PropTypes.shape({}),
56 | triggers: PropTypes.shape({}),
57 | };
58 |
59 | GoogleAnalytics.defaultProps = {
60 | extraUrlParams: {},
61 | vars: {},
62 | triggers: {},
63 | };
64 |
65 | export default GoogleAnalytics;
66 |
--------------------------------------------------------------------------------
/packages/src/pwa/converters/image.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import he from 'he';
3 | import Image from '../components/Image';
4 |
5 | export default {
6 | test: element => {
7 | const { tagName, ignore } = element;
8 | // Returns false if it's already a lazy component.
9 | if (ignore) return false;
10 |
11 | // Returns true if element is an
.
12 | if (tagName === 'img') return true;
13 |
14 | // Filters comments out of children.
15 | return false;
16 | },
17 | converter: element => {
18 | const { attributes } = element;
19 | const { alt, srcset, width, height } = attributes;
20 |
21 | // Return an Image component with id if image has attachedId.
22 | if (attributes.dataset && attributes.dataset.attachmentId) {
23 | const attachmentId = parseInt(attributes.dataset.attachmentId, 10);
24 |
25 | return (
26 |
32 | );
33 | }
34 |
35 | let src;
36 |
37 | // Get src attribute from different cases or assign an empty string.
38 | if (attributes.src && typeof attributes.src === 'string') {
39 | ({ src } = attributes);
40 | } else if (
41 | attributes.dataset &&
42 | attributes.dataset.original &&
43 | typeof attributes.dataset.original === 'string'
44 | ) {
45 | src = attributes.dataset.original;
46 | } else {
47 | src = '';
48 | }
49 |
50 | return (
51 |
59 | );
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Hyperlink.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Hyperlink = ({ size }) => (
6 |
12 | );
13 |
14 | Hyperlink.propTypes = {
15 | size: PropTypes.number.isRequired,
16 | };
17 |
18 | export default Hyperlink;
19 |
20 | const Svg = styled.svg`
21 | width: ${({ size }) => `${size}px`};
22 | height: ${({ size }) => `${size}px`};
23 | color: ${({ theme }) => theme.color.darkGrey};
24 | `;
25 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Comments.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Comments = ({ size, inCard }) => (
6 |
23 | );
24 |
25 | Comments.propTypes = {
26 | size: PropTypes.number.isRequired,
27 | inCard: PropTypes.bool.isRequired,
28 | };
29 |
30 | export default Comments;
31 |
32 | const Svg = styled.svg`
33 | width: ${({ size }) => `${size}px`};
34 | height: ${({ size }) => `${size}px`};
35 | color: ${({ theme, inCard }) => (inCard ? theme.color.blue : theme.color.black)};
36 | ${({ inCard }) => (inCard ? 'margin-right: 12px' : null)};
37 | `;
38 |
--------------------------------------------------------------------------------
/core/scripts/start.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, global-require */
2 | const argv = require('minimist')(process.argv.slice(2));
3 | const { clean, build } = require('./build');
4 | const { serve, createApp } = require('./serve');
5 |
6 | const dev = process.env.NODE_ENV !== 'production';
7 |
8 | const start = async () => {
9 | // If it's not serve (so it's build or start) clean the .build folder.
10 | if (!argv.serve) await clean();
11 |
12 | if (argv.build) {
13 | // Only build.
14 | console.log(`> Building ${process.env.MODE} for ${dev ? 'development' : 'production'}...\n`);
15 | await build();
16 | console.log('> Finished.\n');
17 | } else if (argv.serve) {
18 | // Only serve.
19 | await serve();
20 | } else if (dev) {
21 | const webpack = require('webpack');
22 | const webpackDevMiddleware = require('webpack-dev-middleware');
23 | const webpackHotMiddleware = require('webpack-hot-middleware');
24 | const webpackHotServerMiddleware = require('webpack-hot-server-middleware');
25 | const clientConfig = require('../webpack/client.dev');
26 | const serverConfig = require('../webpack/server.dev');
27 | // Start in DEV mode using webpack dev server with express.
28 | const { app, done } = await createApp();
29 | const compiler = webpack([clientConfig, serverConfig]);
30 | const clientCompiler = compiler.compilers[0];
31 | const options = { stats: { colors: true, progress: true } };
32 | app.use(webpackDevMiddleware(compiler, options));
33 | app.use(webpackHotMiddleware(clientCompiler));
34 | app.use(webpackHotServerMiddleware(compiler));
35 | compiler.plugin('done', done);
36 | } else {
37 | // Start in PROD mode.
38 | console.log(`> Building ${process.env.MODE} for production...`);
39 | await build();
40 | console.log('> Finished. Now serving...\n');
41 | await serve();
42 | }
43 | };
44 |
45 | process.on('unhandledRejection', err => {
46 | console.log(err.stack);
47 | process.exit(1);
48 | });
49 |
50 | start();
51 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Announcements/NextPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Waypoint from 'react-waypoint';
6 | import FetchingIcon from 'react-icons/lib/fa/refresh';
7 |
8 | class NextPage extends Component {
9 | constructor() {
10 | super();
11 | this.getNextPage = this.getNextPage.bind(this);
12 | }
13 | getNextPage() {
14 | const { type, id, fetchedPages, totalPages, fetchListPage, isFetching } = this.props;
15 | if (!isFetching && totalPages && fetchedPages < totalPages)
16 | fetchListPage({ type, id, page: totalPages + 1 });
17 | }
18 | render() {
19 | const { fetchedPages, totalPages, isFetching } = this.props;
20 | return fetchedPages < totalPages ? (
21 |
22 | {isFetching ? : ''}
23 |
24 | ) : null;
25 | }
26 | }
27 |
28 | NextPage.propTypes = {
29 | type: PropTypes.string.isRequired,
30 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
31 | isFetching: PropTypes.bool.isRequired,
32 | fetchedPages: PropTypes.number.isRequired,
33 | totalPages: PropTypes.number.isRequired,
34 | fetchListPage: PropTypes.func.isRequired,
35 | };
36 |
37 | export default inject(({ connection }, { list }) => ({
38 | type: list.type,
39 | id: list.id,
40 | isFetching: list.isFetching,
41 | fetchedPages: list.total.fetched.pages || 0,
42 | totalPages: list.total.pages || 0,
43 | fetchListPage: connection.fetchListPage,
44 | }))(NextPage);
45 |
46 | const Container = styled.div`
47 | box-sizing: border-box;
48 | width: 100vw;
49 | height: 56px;
50 | display: flex;
51 | justify-content: center;
52 | align-items: center;
53 | `;
54 | const Fetching = styled(FetchingIcon)`
55 | width: 28px;
56 | height: 28px;
57 | color: #5566c3;
58 | `;
59 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Utensils.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Utensils = ({ size, inCard }) => (
6 |
23 | );
24 |
25 | Utensils.propTypes = {
26 | size: PropTypes.number.isRequired,
27 | inCard: PropTypes.bool.isRequired,
28 | };
29 |
30 | export default Utensils;
31 |
32 | const Svg = styled.svg`
33 | width: ${({ size }) => `${size}px`};
34 | height: ${({ size }) => `${size}px`};
35 | color: ${({ theme, inCard }) => (inCard ? theme.color.blue : theme.color.black)};
36 | ${({ inCard }) => (inCard ? 'margin-right: 12px' : null)};
37 | `;
38 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/LazyYoutube/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import IconVideo from 'react-icons/lib/md/ondemand-video';
4 | import styled from 'react-emotion';
5 | import LazyLoad from '@frontity/lazyload';
6 |
7 | const LazyYoutube = ({ width, height, youtubeId, attributes }) => (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
18 | LazyYoutube.propTypes = {
19 | width: PropTypes.string.isRequired,
20 | height: PropTypes.string.isRequired,
21 | youtubeId: PropTypes.string,
22 | attributes: PropTypes.shape({}).isRequired,
23 | };
24 |
25 | LazyYoutube.defaultProps = {
26 | youtubeId: null,
27 | };
28 |
29 | export default LazyYoutube;
30 |
31 | const Container = styled.span`
32 | position: relative;
33 | box-sizing: border-box;
34 | width: ${({ styles }) => styles.width};
35 | height: ${({ styles }) => styles.height};
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | margin: 15px 0;
40 | left: -15px;
41 |
42 | & > .LazyLoad {
43 | box-sizing: border-box;
44 | position: absolute;
45 | top: 0;
46 | left: 0;
47 | width: 100%;
48 | height: 100%;
49 | object-fit: cover;
50 | object-position: center;
51 | background-color: transparent;
52 | color: transparent;
53 | border: none;
54 | }
55 |
56 | amp-youtube,
57 | iframe {
58 | width: ${({ styles }) => styles.width};
59 | height: ${({ styles }) => styles.height};
60 | }
61 | `;
62 |
63 | const Icon = styled.span`
64 | position: absolute;
65 | top: 0;
66 | left: 0;
67 | box-sizing: border-box;
68 | color: #bdbdbd;
69 | width: 100%;
70 | height: 100%;
71 | display: flex;
72 | justify-content: center;
73 | align-items: center;
74 | `;
75 |
--------------------------------------------------------------------------------
/core/packages/build/shared/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import * as actionTypes from '../actionTypes';
3 |
4 | export const packages = (state = {}, action) => {
5 | if (action.type === actionTypes.BUILD_UPDATED && action.packages) return action.packages;
6 | return state;
7 | };
8 |
9 | export const siteId = (state = null, action) => {
10 | if (action.type === actionTypes.BUILD_UPDATED && action.siteId) return action.siteId;
11 | return state;
12 | };
13 |
14 | export const env = (state = 'pre', action) => {
15 | if (action.type === actionTypes.BUILD_UPDATED && action.env) return action.env;
16 | return state;
17 | };
18 |
19 | export const amp = (state = false, action) => {
20 | if (action.type === actionTypes.BUILD_UPDATED && action.amp) return action.amp;
21 | return state;
22 | };
23 |
24 | export const ssr = (state = true, action) => {
25 | if (action.type === actionTypes.CLIENT_RENDERED) return false;
26 | return state;
27 | };
28 |
29 | export const server = (state = true, action) => {
30 | if (action.type === actionTypes.CLIENT_STARTED) return false;
31 | return state;
32 | };
33 |
34 | export const perPage = (state = 10, action) => {
35 | if (action.type === actionTypes.BUILD_UPDATED && action.perPage) return action.perPage;
36 | return state;
37 | };
38 |
39 | export const device = (state = 'mobile', action) => {
40 | if (action.type === actionTypes.BUILD_UPDATED && action.device) return action.device;
41 | return state;
42 | };
43 |
44 | export const dev = (state = true, action) => {
45 | if (action.type === actionTypes.BUILD_UPDATED && action.dev) return action.dev === 'true';
46 | return state;
47 | };
48 |
49 | export const initialUrl = (state = null, action) => {
50 | if (action.type === actionTypes.BUILD_UPDATED && action.initialUrl) return action.initialUrl;
51 | return state;
52 | };
53 |
54 | export default () =>
55 | combineReducers({
56 | ssr,
57 | server,
58 | siteId,
59 | env,
60 | amp,
61 | packages,
62 | perPage,
63 | device,
64 | dev,
65 | initialUrl,
66 | });
67 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "presets": [["env", { "targets": { "node": "current" } }], "react"],
5 | "plugins": [
6 | "emotion",
7 | "syntax-dynamic-import",
8 | "transform-object-rest-spread",
9 | "transform-class-properties",
10 | ["universal-import", { "disableWarnings": true }]
11 | ]
12 | },
13 | "devClient": {
14 | "presets": [
15 | [
16 | "env",
17 | {
18 | "targets": { "browsers": ["android >= 5", "ios_saf > 9", "and_chr >= 40"] },
19 | "useBuiltIns": "usage"
20 | }
21 | ],
22 | "react"
23 | ],
24 | "plugins": [
25 | "emotion",
26 | "transform-inline-environment-variables",
27 | ["lodash", { "id": ["lodash", "recompose"] }],
28 | "syntax-dynamic-import",
29 | "transform-object-rest-spread",
30 | "transform-class-properties",
31 | ["universal-import", { "disableWarnings": true }],
32 | "react-hot-loader/babel"
33 | ]
34 | },
35 | "prodClient": {
36 | "presets": [
37 | [
38 | "env",
39 | {
40 | "targets": { "browsers": ["android >= 5", "ios_saf > 9", "and_chr >= 40"] },
41 | "useBuiltIns": "usage"
42 | }
43 | ],
44 | "react"
45 | ],
46 | "plugins": [
47 | "emotion",
48 | "transform-inline-environment-variables",
49 | ["lodash", { "id": ["lodash", "recompose"] }],
50 | "syntax-dynamic-import",
51 | "transform-object-rest-spread",
52 | "transform-class-properties",
53 | ["universal-import", { "disableWarnings": true }]
54 | ]
55 | },
56 | "server": {
57 | "presets": [["env", { "targets": { "node": "current" } }], "react"],
58 | "plugins": [
59 | "emotion",
60 | "syntax-dynamic-import",
61 | "transform-object-rest-spread",
62 | "transform-class-properties",
63 | ["universal-import", { "disableWarnings": true }]
64 | ]
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/LazyAudio/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/media-has-caption */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import LazyLoad from '@frontity/lazyload';
5 | import IconAudio from 'react-icons/lib/md/audiotrack';
6 | import styled from 'react-emotion';
7 |
8 | const LazyAudio = ({ width, height, attributes, children }) => (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
19 | LazyAudio.propTypes = {
20 | width: PropTypes.string.isRequired,
21 | height: PropTypes.string.isRequired,
22 | attributes: PropTypes.shape({}).isRequired,
23 | children: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.arrayOf(PropTypes.shape({}))]),
24 | };
25 |
26 | LazyAudio.defaultProps = {
27 | children: null,
28 | };
29 |
30 | export default LazyAudio;
31 |
32 | const Container = styled.span`
33 | position: relative;
34 | box-sizing: border-box;
35 | width: ${({ styles }) => styles.width};
36 | height: ${({ styles }) => styles.height};
37 | display: flex;
38 | justify-content: center;
39 | align-items: center;
40 | margin: 15px 0;
41 |
42 | & > .LazyLoad {
43 | box-sizing: border-box;
44 | position: absolute;
45 | top: 0;
46 | left: 0;
47 | width: 100%;
48 | height: 100%;
49 | object-fit: cover;
50 | object-position: center;
51 | background-color: transparent;
52 | color: transparent;
53 | border: none;
54 | }
55 |
56 | amp-audio,
57 | audio {
58 | width: ${({ styles }) => styles.width};
59 | height: ${({ styles }) => styles.height};
60 | }
61 | `;
62 |
63 | const Icon = styled.span`
64 | position: absolute;
65 | top: 0;
66 | left: 0;
67 | box-sizing: border-box;
68 | color: #bdbdbd;
69 | width: 100%;
70 | height: 100%;
71 | display: flex;
72 | justify-content: center;
73 | align-items: center;
74 | `;
75 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/LazyVideo/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/media-has-caption */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import IconVideo from 'react-icons/lib/md/ondemand-video';
5 | import styled from 'react-emotion';
6 | import LazyLoad from '@frontity/lazyload';
7 |
8 | const LazyVideo = ({ children, width, height, attributes }) => {
9 | const { autoPlay, loop, className, ...filteredAttributes } = attributes;
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 | );
23 | };
24 |
25 | LazyVideo.propTypes = {
26 | children: PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.shape({}))])
27 | .isRequired,
28 | width: PropTypes.string.isRequired,
29 | height: PropTypes.string.isRequired,
30 | attributes: PropTypes.shape({}).isRequired,
31 | };
32 |
33 | export default LazyVideo;
34 |
35 | const Container = styled.span`
36 | position: relative;
37 | box-sizing: border-box;
38 | width: ${({ styles }) => styles.width};
39 | height: ${({ styles }) => styles.height};
40 | display: flex;
41 | justify-content: center;
42 | align-items: center;
43 | margin: 15px 0;
44 |
45 | video {
46 | width: 100%;
47 | height: 100%;
48 | }
49 |
50 | .LazyLoad {
51 | box-sizing: border-box;
52 | position: absolute;
53 | top: 0;
54 | left: 0;
55 | width: 100%;
56 | height: 100%;
57 | object-fit: cover;
58 | object-position: center;
59 | background-color: transparent;
60 | color: transparent;
61 | border: none;
62 | }
63 | `;
64 |
65 | const Icon = styled.span`
66 | position: absolute;
67 | top: 0;
68 | left: 0;
69 | box-sizing: border-box;
70 | color: #bdbdbd;
71 | width: 100%;
72 | height: 100%;
73 | display: flex;
74 | justify-content: center;
75 | align-items: center;
76 | `;
77 |
--------------------------------------------------------------------------------
/core/packages/analytics/shared/stores/index.js:
--------------------------------------------------------------------------------
1 | import { types, getEnv } from 'mobx-state-tree';
2 | import { dep } from 'worona-deps';
3 |
4 | const mapCustomDimensions = (self, action) => {
5 | const { customDimensions } = self;
6 | const { entities } = action;
7 |
8 | const entityTypes = Object.keys(entities);
9 |
10 | entityTypes.forEach(type => {
11 | const entityIds = Object.keys(entities[type]);
12 |
13 | entityIds.forEach(id => {
14 | if (entities[type][id].custom_analytics) {
15 | if (!customDimensions.get(entities[type][id].type)) {
16 | customDimensions.set(entities[type][id].type, {});
17 | }
18 |
19 | if (!customDimensions.get(entities[type][id].type).get(id)) {
20 | customDimensions
21 | .get(entities[type][id].type, {})
22 | .set(id, entities[type][id].custom_analytics);
23 | }
24 | }
25 | });
26 | });
27 | };
28 |
29 | const Analytics = types
30 | .model('Analytics')
31 | .props({
32 | customDimensions: types.optional(types.map(types.map(types.frozen)), {}),
33 | })
34 | .actions(self => ({
35 | [dep('connection', 'actionTypes', 'ENTITY_SUCCEED')](action) {
36 | mapCustomDimensions(self, action);
37 | },
38 | [dep('connection', 'actionTypes', 'LIST_SUCCEED')](action) {
39 | mapCustomDimensions(self, action);
40 | },
41 | }))
42 | .views(self => ({
43 | getCustomDimensions({ type, id }) {
44 | if (type && id) {
45 | const typeList = self.customDimensions.get(type);
46 |
47 | if (typeList) {
48 | const dimensions = typeList.get(id.toString());
49 |
50 | if (dimensions) {
51 | return dimensions;
52 | }
53 | }
54 | }
55 |
56 | return null;
57 | },
58 | }))
59 | .actions(self => ({
60 | afterCreate: () => {
61 | const { store } = getEnv(self);
62 | if (store)
63 | store.subscribe(() => {
64 | const action = store.getState().lastAction;
65 | if (self[action.type]) {
66 | self[action.type](action);
67 | }
68 | });
69 | },
70 | }));
71 |
72 | export default Analytics;
73 |
--------------------------------------------------------------------------------
/core/server/pwa-template.js:
--------------------------------------------------------------------------------
1 | import { getSnapshot } from 'mobx-state-tree';
2 | import htmlescape from 'htmlescape';
3 |
4 | export default ({
5 | dev,
6 | helmet,
7 | css,
8 | styles,
9 | preloadScripts,
10 | html,
11 | cssHash,
12 | publicPath,
13 | ids,
14 | store,
15 | stores,
16 | chunksForArray,
17 | bootstrapString,
18 | }) => `
19 |
20 |
21 | ${dev ? '' : ''}
22 |
23 |
24 |
25 | ${styles}
26 | ${preloadScripts}
27 | ${helmet.title.toString()}
28 | ${helmet.meta.toString()}
29 | ${helmet.link.toString()}
30 | ${helmet.script.toString()}
31 |
32 |
33 |
34 | ${html}
35 |
57 |
58 | `;
59 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/Schedule.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import ScheduleList from './ScheduleList';
6 |
7 | const Schedule = ({ options, selected, selectTrack }) => (
8 |
9 |
16 |
17 |
18 | );
19 |
20 | Schedule.propTypes = {
21 | options: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
22 | selected: PropTypes.shape({}).isRequired,
23 | selectTrack: PropTypes.func.isRequired,
24 | };
25 |
26 | export default inject(({ theme }) => ({
27 | options: theme.schedule.tracks,
28 | selected: theme.schedule.selected,
29 | selectTrack: event => {
30 | theme.schedule.selectTrack(event.target.value);
31 | },
32 | }))(Schedule);
33 |
34 | const Container = styled.div`
35 | box-sizing: border-box;
36 | min-height: 100vh;
37 | width: 100vw;
38 | display: flex;
39 | flex-direction: column;
40 | align-items: center;
41 | padding: ${({ theme }) => theme.padding.schedule};
42 | `;
43 |
44 | const Select = styled.select`
45 | height: 48px;
46 | width: calc(100vw - 48px);
47 | padding: 8px 16px;
48 | border: 1px solid ${({ theme }) => theme.color.grey};
49 | border-radius: 3px;
50 | font-size: 16px;
51 | color: ${({ theme }) => theme.color.text};
52 | background-color: ${({ theme }) => theme.color.white};
53 | appearance: none;
54 | text-transform: uppercase;
55 |
56 | background-image: linear-gradient(45deg, transparent 50%, ${({ theme }) => theme.color.text} 50%),
57 | linear-gradient(135deg, ${({ theme }) => theme.color.text} 50%, transparent 50%);
58 | background-position: calc(100% - 20px) calc(1.2em + 2px), calc(100% - 15px) calc(1.2em + 2px),
59 | calc(100% - 2.5em) 0.75em;
60 | background-size: 5px 5px, 5px 5px, 1px 1.5em;
61 | background-repeat: no-repeat;
62 |
63 | &:focus {
64 | outline: none;
65 | }
66 | `;
67 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Announcements/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 | import { inject } from 'mobx-react';
5 | import Media from '../Media';
6 | import Link from '../Link';
7 | import { formatDate } from '../../utils';
8 |
9 | const Card = ({ type, id, title, creationDate, authorName, featured, context }) => (
10 |
11 |
12 |
13 | {title}
14 |
15 | {creationDate}
16 | ―
17 | {authorName}
18 |
19 |
20 |
21 | );
22 |
23 | Card.propTypes = {
24 | type: PropTypes.string.isRequired,
25 | id: PropTypes.number.isRequired,
26 | title: PropTypes.string.isRequired,
27 | creationDate: PropTypes.string.isRequired,
28 | authorName: PropTypes.string.isRequired,
29 | featured: PropTypes.shape({}).isRequired,
30 | context: PropTypes.shape({}).isRequired,
31 | };
32 |
33 | export default inject((_, { entity }) => ({
34 | type: entity.type,
35 | id: entity.id,
36 | title: entity.title,
37 | creationDate: formatDate(new Date(entity.creationDate), 'MMMM Do'),
38 | authorName: entity.author.name,
39 | featured: entity.media.featured,
40 | }))(Card);
41 |
42 | const Container = styled.div`
43 | box-sizing: border-box;
44 | padding: 0 24px 16px 24px;
45 | `;
46 |
47 | const Title = styled.h2`
48 | width: 100%;
49 | margin: 0;
50 | padding-top: 8px;
51 | line-height: 24px;
52 | font-size: 20px;
53 | font-weight: normal;
54 | color: ${({ theme }) => theme.color.blue};
55 | `;
56 |
57 | const Info = styled.div`
58 | box-sizing: border-box;
59 | width: 100%;
60 | display: flex;
61 | flex-wrap: wrap;
62 | justify-content: flex-start;
63 | line-height: 20px;
64 | font-size: 16px;
65 | color: ${({ theme }) => theme.color.darkGrey};
66 | `;
67 |
68 | const Fecha = styled.p`
69 | margin: 0;
70 | `;
71 |
72 | const Dash = styled.p`
73 | margin: 0;
74 | padding: 0 8px;
75 | `;
76 |
77 | const Author = styled.p`
78 | margin: 0;
79 | `;
80 |
--------------------------------------------------------------------------------
/core/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const argv = require('minimist')(process.argv.slice(2));
3 | const { emptyDirSync } = require('fs-extra');
4 | const { spawn } = require('child_process');
5 |
6 | // Ignores invalid self-signed ssl certificates
7 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
8 |
9 | if (process.env.MODE !== 'pwa' && process.env.MODE !== 'amp' && !argv.pwa && !argv.amp) {
10 | throw new Error('MODE not selected. Please use --pwa, --amp or an environment variable.');
11 | } else {
12 | process.env.MODE = process.env.MODE || (argv.pwa && 'pwa') || (argv.amp && 'amp');
13 | console.log(`> Using MODE=${process.env.MODE}`);
14 | }
15 |
16 | process.env.NODE_ENV = argv.p || argv.prod ? 'production' : 'development';
17 | console.log(`> Using NODE_ENV=${process.env.NODE_ENV}`);
18 |
19 | if (!argv.build && !!(argv.s || argv.https)) {
20 | process.env.HTTPS_SERVER = true;
21 | console.log(`> Using HTTPS_SERVER=${process.env.HTTPS_SERVER}`);
22 | }
23 |
24 | if (!argv.build) {
25 | process.env.PORT = argv.port || 3000;
26 | if (argv.port) console.log(`> Using PORT=${process.env.PORT}`);
27 | }
28 |
29 | if (argv.hmr && process.env.NODE_ENV === 'development') {
30 | process.env.HMR_PATH = `${argv.hmr.replace(/\/$/g, '')}/`;
31 | console.log(`> Using HMR_PATH=${process.env.HMR_PATH}`);
32 | } else if ((argv.w || argv.wp) && process.env.NODE_ENV === 'development') {
33 | const protocol = argv.s || argv.https ? 'https://' : 'http://';
34 | process.env.HMR_PATH = `${protocol}localhost:${process.env.PORT}/`;
35 | console.log(`> Using HMR_PATH=${protocol}localhost:${process.env.PORT}/`);
36 | }
37 |
38 | if (argv.a || argv.analyze) {
39 | emptyDirSync('.build/analyize/pwa');
40 | process.env.ANALYZE = true;
41 | console.log('> Using ANALYZE=true');
42 | console.log('> You can load the stats in https://webpack.github.io/analyse/');
43 | }
44 |
45 | console.log();
46 |
47 | const args = [`core/scripts/start.js`];
48 |
49 | if (argv.build) args.push('--build');
50 | else if (argv.serve) args.push('--serve');
51 | else args.push('--start');
52 |
53 | if (argv.d || argv.debug) args.unshift('--inspect');
54 |
55 | spawn('node', args, { stdio: 'inherit', env: process.env });
56 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Icons/Users.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'react-emotion';
4 |
5 | const Users = ({ size, inCard }) => (
6 |
23 | );
24 |
25 | Users.propTypes = {
26 | size: PropTypes.number.isRequired,
27 | inCard: PropTypes.bool.isRequired,
28 | };
29 |
30 | export default Users;
31 |
32 | const Svg = styled.svg`
33 | width: ${({ size }) => `${size}px`};
34 | height: ${({ size }) => `${size}px`};
35 | color: ${({ theme, inCard }) => (inCard ? theme.color.blue : theme.color.black)};
36 | ${({ inCard }) => (inCard ? 'margin-right: 12px' : null)};
37 | `;
38 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Anchor/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { dep } from 'worona-deps';
5 | import fastdom from 'fastdom';
6 | import fdPromised from 'fastdom/extensions/fastdom-promised';
7 | import { getScrollingElement } from '../../utils';
8 |
9 | const fastdomPromised = fastdom.extend(fdPromised);
10 |
11 | class Anchor extends Component {
12 | constructor(props) {
13 | super(props);
14 | this.onClick = this.onClick.bind(this);
15 | }
16 |
17 | onClick(e) {
18 | e.preventDefault();
19 | const { scrollAndChangeRoute, hash } = this.props;
20 | scrollAndChangeRoute({ hash });
21 | }
22 |
23 | render() {
24 | const { hash, children, className } = this.props;
25 | return (
26 |
27 | {children}
28 |
29 | );
30 | }
31 | }
32 |
33 | Anchor.propTypes = {
34 | hash: PropTypes.string.isRequired,
35 | scrollAndChangeRoute: PropTypes.func.isRequired,
36 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
37 | className: PropTypes.string,
38 | };
39 |
40 | Anchor.defaultProps = {
41 | children: null,
42 | className: null,
43 | };
44 |
45 | const mapDispatchToProps = (dispatch, { item }) => {
46 | const routeChangeRequested = dep('connection', 'actions', 'routeChangeRequested');
47 | return {
48 | async scrollAndChangeRoute({ hash }) {
49 | const scrollingElement = await getScrollingElement();
50 | const element = window.document.querySelector(hash);
51 | let top;
52 | let scrollTop;
53 | await fastdomPromised.measure(() => {
54 | top = Math.floor(element.getBoundingClientRect().top);
55 | ({ scrollTop } = scrollingElement);
56 | });
57 | dispatch(routeChangeRequested({ selectedItem: item, method: 'push' }));
58 | await fastdomPromised.mutate(() => {
59 | if (scrollingElement.scrollBy) scrollingElement.scrollBy({ top, behavior: 'smooth' });
60 | else scrollingElement.scrollTop = scrollTop + top;
61 | });
62 | },
63 | };
64 | };
65 |
66 | export default connect(null, mapDispatchToProps)(Anchor);
67 |
--------------------------------------------------------------------------------
/core/packages/ads/shared/components/LazyUnload/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Lazy from '@frontity/lazyload';
4 |
5 | class LazyUnload extends Component {
6 | static propTypes = {
7 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
8 | height: PropTypes.number.isRequired,
9 | minTime: PropTypes.number.isRequired,
10 | maxTime: PropTypes.number.isRequired,
11 | offset: PropTypes.number.isRequired,
12 | debounce: PropTypes.bool,
13 | active: PropTypes.bool.isRequired,
14 | children: PropTypes.node.isRequired,
15 | className: PropTypes.string,
16 | isLazy: PropTypes.bool,
17 | };
18 |
19 | static defaultProps = {
20 | className: '',
21 | debounce: false,
22 | isLazy: true,
23 | };
24 |
25 | static randomBetween(min, max) {
26 | return Math.random() * (max - min) + min;
27 | }
28 |
29 | constructor(props) {
30 | super(props);
31 | this.state = { dying: false };
32 | }
33 |
34 | componentWillReceiveProps(nextProps) {
35 | const { active, minTime, maxTime } = this.props;
36 | const { dying } = this.state;
37 | if (active && !nextProps.active) {
38 | this.setState({ dying: true });
39 | this.countdown = setTimeout(() => {
40 | this.setState({ dying: false });
41 | }, LazyUnload.randomBetween(minTime, maxTime));
42 | }
43 |
44 | if (!active && nextProps.active && dying) {
45 | clearTimeout(this.countdown);
46 | }
47 | }
48 |
49 | componentWillUnmount() {
50 | clearTimeout(this.countdown);
51 | }
52 |
53 | render() {
54 | const { width, height, active, offset, debounce, children, className, isLazy } = this.props;
55 | const { dying } = this.state;
56 |
57 | const LazyComponent = isLazy ? Lazy : 'div';
58 | const lazyProps = { offsetVertical: offset, offsetHorizontal: 40, debounce };
59 |
60 | return (
61 | (active || dying) && (
62 |
68 | {children}
69 |
70 | )
71 | );
72 | }
73 | }
74 |
75 | export default LazyUnload;
76 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/FilterFavorites.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Switch from 'rc-switch';
6 |
7 | const FilterFavorites = ({ isFiltered, toggleFilter }) => (
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
16 | FilterFavorites.propTypes = {
17 | isFiltered: PropTypes.bool.isRequired,
18 | toggleFilter: PropTypes.func.isRequired,
19 | };
20 |
21 | export default inject(({ theme }) => ({
22 | isFiltered: theme.schedule.isFiltered,
23 | toggleFilter: theme.schedule.toggleFilter,
24 | }))(FilterFavorites);
25 |
26 | const Container = styled.div`
27 | display: flex;
28 | align-items: center;
29 | `;
30 |
31 | const Label = styled.span`
32 | font-size: 14px;
33 | color: ${({ theme }) => theme.color.text};
34 | margin-right: 8px;
35 | line-height: 20px;
36 | `;
37 |
38 | const SwitchWrapper = styled.span`
39 | display: flex;
40 | flex-shrink: 0;
41 | justify-content: center;
42 | align-items: center;
43 |
44 | .rc-switch {
45 | position: relative;
46 | display: inline-block;
47 | box-sizing: border-box;
48 | width: 32px;
49 | height: 20px;
50 | line-height: 20px;
51 | vertical-align: middle;
52 | border-radius: 20px 20px;
53 | background-color: #e9e9e6;
54 | cursor: pointer;
55 | transition: all 0.3s cubic-bezier(0.35, 0, 0.25, 1);
56 |
57 | &:after {
58 | position: absolute;
59 | width: 12px;
60 | height: 12px;
61 | left: 4px;
62 | top: 4px;
63 | border-radius: 50% 50%;
64 | content: ' ';
65 | cursor: pointer;
66 | transition: left 0.3s cubic-bezier(0.35, 0, 0.25, 1);
67 | background: ${({ theme }) => theme.color.switch};
68 | }
69 | &:focus {
70 | outline: none;
71 | }
72 | &.rc-switch-checked span {
73 | left: 6px;
74 | }
75 | &.rc-switch-checked:after {
76 | left: 16px;
77 | background: ${({ theme }) => theme.color.red};
78 | }
79 | &.rc-switch-disabled {
80 | cursor: no-drop;
81 | background: #ccc;
82 | border-color: #ccc;
83 | }
84 | &.rc-switch-disabled:after {
85 | background: #9e9e9e;
86 | cursor: no-drop;
87 | }
88 | }
89 | `;
90 |
--------------------------------------------------------------------------------
/packages/src/pwa/stores/track.js:
--------------------------------------------------------------------------------
1 | import { types, getParent } from 'mobx-state-tree';
2 | import Session from './session';
3 |
4 | const Id = types.union(types.number, types.string);
5 |
6 | const Track = types
7 | .model('Track')
8 | .props({
9 | id: types.identifier(Id),
10 | sessions: types.optional(types.array(types.reference(types.late(() => Session))), []),
11 | })
12 | .views(self => {
13 | const getConnection = () => getParent(self, 3).connection;
14 | return {
15 | get entity() {
16 | return getConnection().entity('wcb_track', self.id);
17 | },
18 | get name() {
19 | return self.entity.name;
20 | },
21 | get firstDaySessions() {
22 | return self.sessionsByWithFilter(new Date('2018-06-15T08:00:00+02:00'));
23 | },
24 | get secondDaySessions() {
25 | return self.sessionsByWithFilter(new Date('2018-06-16T08:00:00+02:00'));
26 | },
27 | sessionsByWithFilter(date) {
28 | const onlyFavorites = getParent(self, 2).schedule.isFiltered;
29 |
30 | return this.sessionsBy(date, onlyFavorites).filter(
31 | session => session.type === 'wcb_session',
32 | );
33 | },
34 | sessionsBy(date, onlyFavorites = false) {
35 | const day = new Date(date); // Copy date passed as argument
36 | day.setHours(0);
37 | day.setMinutes(0);
38 | day.setSeconds(0);
39 | day.setMilliseconds(0);
40 |
41 | const nextDay = new Date(day);
42 | nextDay.setHours(24);
43 |
44 | return self.sessions.filter(
45 | session =>
46 | (!onlyFavorites || session.isFavorite) && session.date >= day && session.date < nextDay,
47 | );
48 | },
49 | sessionOnNow(date) {
50 | const currentTime = date || new Date();
51 | return self.sessions.reverse().find(({ date: d }) => d <= currentTime);
52 | },
53 | sessionUpNext(date) {
54 | const currentTime = date || new Date();
55 | return self.sessions.find(({ date: d }) => d > currentTime);
56 | },
57 | };
58 | })
59 | .actions(self => ({
60 | addSession(session) {
61 | if (!self.sessions.includes(session)) {
62 | self.sessions.push(session);
63 | // Ensures that sessions are always sorted
64 | self.sessions = self.sessions.sort((a, b) => a.timestamp - b.timestamp);
65 | }
66 | },
67 | }));
68 |
69 | export default Track;
70 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/HtmlToReactConverter/htmlMap.js:
--------------------------------------------------------------------------------
1 | const attributes = [
2 | 'accept',
3 | 'acceptCharset',
4 | 'accessKey',
5 | 'action',
6 | 'allowFullScreen',
7 | 'allowTransparency',
8 | 'alt',
9 | 'async',
10 | 'autoComplete',
11 | 'autoFocus',
12 | 'autoPlay',
13 | 'capture',
14 | 'cellPadding',
15 | 'cellSpacing',
16 | 'challenge',
17 | 'charSet',
18 | 'checked',
19 | 'cite',
20 | 'classID',
21 | 'className',
22 | 'colSpan',
23 | 'cols',
24 | 'content',
25 | 'contentEditable',
26 | 'contextMenu',
27 | 'controls',
28 | 'coords',
29 | 'crossOrigin',
30 | 'data',
31 | 'dateTime',
32 | 'default',
33 | 'defer',
34 | 'dir',
35 | 'disabled',
36 | 'download',
37 | 'draggable',
38 | 'encType',
39 | 'form',
40 | 'formAction',
41 | 'formEncType',
42 | 'formMethod',
43 | 'formNoValidate',
44 | 'formTarget',
45 | 'frameBorder',
46 | 'headers',
47 | 'height',
48 | 'hidden',
49 | 'high',
50 | 'href',
51 | 'hrefLang',
52 | 'htmlFor',
53 | 'httpEquiv',
54 | 'icon',
55 | 'id',
56 | 'inputMode',
57 | 'integrity',
58 | 'is',
59 | 'keyParams',
60 | 'keyType',
61 | 'kind',
62 | 'label',
63 | 'lang',
64 | 'list',
65 | 'loop',
66 | 'low',
67 | 'manifest',
68 | 'marginHeight',
69 | 'marginWidth',
70 | 'max',
71 | 'maxLength',
72 | 'media',
73 | 'mediaGroup',
74 | 'method',
75 | 'min',
76 | 'minLength',
77 | 'multiple',
78 | 'muted',
79 | 'name',
80 | 'noValidate',
81 | 'nonce',
82 | 'open',
83 | 'optimum',
84 | 'pattern',
85 | 'placeholder',
86 | 'poster',
87 | 'preload',
88 | 'profile',
89 | 'radioGroup',
90 | 'readOnly',
91 | 'rel',
92 | 'required',
93 | 'reversed',
94 | 'role',
95 | 'rowSpan',
96 | 'rows',
97 | 'sandbox',
98 | 'scope',
99 | 'scoped',
100 | 'scrolling',
101 | 'seamless',
102 | 'selected',
103 | 'shape',
104 | 'size',
105 | 'sizes',
106 | 'span',
107 | 'spellCheck',
108 | 'src',
109 | 'srcDoc',
110 | 'srcLang',
111 | 'srcSet',
112 | 'start',
113 | 'step',
114 | 'style',
115 | 'summary',
116 | 'tabIndex',
117 | 'target',
118 | 'title',
119 | 'type',
120 | 'useMap',
121 | 'value',
122 | 'width',
123 | 'wmode',
124 | 'wrap',
125 | ];
126 |
127 | export default attributes.reduce((map, value) => ({ ...map, [value.toLowerCase()]: value }), {});
128 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/ScheduleItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import FavoriteButton from './FavoriteButton';
6 | import Title from './ScheduleItemTitle';
7 | import Speakers from './ScheduleItemSpeakers';
8 | import Icon from '../Icons/Schedule';
9 |
10 | const ScheduleItem = ({ session, position, columns, isFavorite, isSpecial }) => (
11 |
12 |
13 |
14 | {isSpecial && }
15 |
22 | {!isSpecial && }
23 |
24 | {!isSpecial && }
25 |
26 | );
27 |
28 | ScheduleItem.propTypes = {
29 | session: PropTypes.shape({}).isRequired,
30 | position: PropTypes.number.isRequired,
31 | columns: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.shape({}))).isRequired,
32 | isFavorite: PropTypes.bool.isRequired,
33 | isSpecial: PropTypes.bool.isRequired,
34 | };
35 |
36 | export default inject((_, { session }) => ({
37 | isFavorite: !!session.isFavorite,
38 | }))(ScheduleItem);
39 |
40 | const Container = styled.div`
41 | box-sizing: border-box;
42 | display: flex;
43 | align-items: flex-start;
44 | width: 100%;
45 | padding: ${({ theme }) => theme.padding.scheduleItem};
46 | border-bottom: 1px solid ${({ theme }) => theme.color.grey};
47 | background-color: ${({ theme, isFavorite, isSpecial }) => {
48 | if (isFavorite) return theme.color.yellow;
49 | if (isSpecial) return theme.color.grey;
50 | return null;
51 | }};
52 |
53 | &:first-child {
54 | border-top: 1px solid ${({ theme }) => theme.color.grey};
55 | }
56 | `;
57 |
58 | const Time = styled.div`
59 | font-size: 14px;
60 | flex-shrink: 0;
61 | line-height: 24px;
62 | color: ${({ theme }) => theme.color.darkGrey};
63 | `;
64 |
65 | const InnerContainer = styled.div`
66 | display: flex;
67 | flex-direction: ${({ isSpecial }) => (isSpecial ? 'row' : 'column')};
68 | align-items: flex-start;
69 | flex-grow: 1;
70 | padding: 0 8px;
71 | `;
72 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Posts/Post.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Media from '../Media';
6 | import Content from '../Content';
7 | import { formatDate } from '../../utils';
8 |
9 | const Post = ({ title, creationDate, authorName, hasFeaturedMedia, featured, content }) => (
10 |
11 |
12 | {title}
13 |
14 | {creationDate}
15 | ―
16 | {authorName}
17 |
18 |
19 | {hasFeaturedMedia && }
20 |
21 |
22 |
23 |
24 | );
25 |
26 | Post.propTypes = {
27 | title: PropTypes.string.isRequired,
28 | creationDate: PropTypes.string.isRequired,
29 | authorName: PropTypes.string.isRequired,
30 | hasFeaturedMedia: PropTypes.bool.isRequired,
31 | featured: PropTypes.shape({}).isRequired,
32 | content: PropTypes.string.isRequired,
33 | };
34 |
35 | export default inject((_, { post }) => ({
36 | title: post.entity.title,
37 | creationDate: formatDate(new Date(post.entity.creationDate), 'MMMM Do'),
38 | authorName: post.entity.author.name,
39 | featured: post.entity.media.featured,
40 | hasFeaturedMedia: post.entity.hasFeaturedMedia,
41 | content: post.entity.content,
42 | }))(Post);
43 |
44 | const Container = styled.div`
45 | box-sizing: border-box;
46 | padding: 56px 0;
47 | width: 100vw;
48 | min-height: 100vh;
49 | display: flex;
50 | flex-direction: column;
51 | align-items: stretch;
52 | `;
53 |
54 | const Header = styled.div`
55 | padding: 24px 24px 16px 24px;
56 | `;
57 |
58 | const Title = styled.h3`
59 | margin: 0;
60 | font-size: 22px;
61 | line-height: 28px;
62 | color: ${({ theme }) => theme.color.black};
63 | `;
64 |
65 | const Info = styled.div`
66 | box-sizing: border-box;
67 | display: flex;
68 | flex-wrap: wrap;
69 | justify-content: flex-start;
70 | color: ${({ theme }) => theme.color.darkGrey};
71 | font-size: 16px;
72 | line-height: 24px;
73 | `;
74 |
75 | const Fecha = styled.p`
76 | margin: 0;
77 | `;
78 |
79 | const Dash = styled.p`
80 | margin: 0;
81 | padding: 0 8px;
82 | `;
83 |
84 | const Author = styled.p`
85 | margin: 0;
86 | `;
87 |
88 | const ContentContainer = styled.div`
89 | margin: 24px 0 8px 0;
90 | `;
91 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Nav/Share.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import { inject } from 'mobx-react';
5 | import styled from 'react-emotion';
6 | import he from 'he';
7 | import {
8 | FacebookShareButton,
9 | WhatsappShareButton,
10 | TwitterShareButton,
11 | LinkedinShareButton,
12 | // EmailShareButton,
13 | } from 'react-share';
14 | // import EmailIcon from 'react-icons/lib/fa/envelope-o';
15 | import FacebookIcon from 'react-icons/lib/fa/facebook-official';
16 | import TwitterIcon from 'react-icons/lib/fa/twitter';
17 | import WhatsappIcon from 'react-icons/lib/fa/whatsapp';
18 | import LinkedinIcon from 'react-icons/lib/fa/linkedin-square';
19 |
20 | const Share = ({ link, title }) => (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {/*
43 |
46 | */}
47 |
48 | );
49 |
50 | Share.propTypes = {
51 | title: PropTypes.string.isRequired,
52 | link: PropTypes.string.isRequired,
53 | };
54 |
55 | export default inject(({ connection }) => ({
56 | title: he.unescape(connection.selectedItem.entity.title),
57 | link: connection.selectedItem.entity.link,
58 | }))(Share);
59 |
60 | const Container = styled.div`
61 | box-sizing: border-box;
62 | width: auto;
63 | display: flex;
64 | align-items: center;
65 | height: ${({ theme }) => theme.size.button};
66 | `;
67 |
68 | const ShareButton = styled.div`
69 | width: 40px;
70 | height: 40px;
71 | margin: 0 4px;
72 | border-radius: 20px;
73 | border: 1px solid ${({ theme }) => theme.color.grey};
74 | color: ${({ theme }) => theme.color.darkGrey};
75 | display: flex;
76 | justify-content: center;
77 | align-items: center;
78 | `;
79 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Menu/MenuRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import { homeContext, venueContext, announcementsContext, pageContext } from '../../contexts';
6 | import Link from '../Link';
7 | import {
8 | PAGE_HOME_ON_NOW,
9 | PAGE_VENUE_ALL,
10 | PAGE_MENU_COC,
11 | PAGE_MENU_MENUS,
12 | } from '../../consts'
13 |
14 | const routes = {
15 | 'schedule': {
16 | type: 'page',
17 | id: PAGE_HOME_ON_NOW,
18 | context: homeContext,
19 | text: 'Schedule',
20 | },
21 | 'venue-map': {
22 | type: 'page',
23 | id: PAGE_VENUE_ALL,
24 | context: venueContext,
25 | },
26 | announcements: {
27 | type: 'latest',
28 | id: 'post',
29 | page: 1,
30 | context: announcementsContext,
31 | text: 'Announcements',
32 | },
33 | menus: {
34 | type: 'page',
35 | id: PAGE_MENU_MENUS,
36 | context: pageContext({ id: PAGE_MENU_MENUS, title: 'Menus' }),
37 | },
38 | 'code-of-conduct': {
39 | type: 'page',
40 | id: PAGE_MENU_COC,
41 | context: pageContext({ id: PAGE_MENU_COC, title: 'Code of Conduct' }),
42 | },
43 | };
44 |
45 | const MenuRoute = ({ type, id, page, context, text, closeMenu }) => (
46 |
47 |
48 | {text}
49 |
50 |
51 | );
52 |
53 | MenuRoute.propTypes = {
54 | type: PropTypes.string.isRequired,
55 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
56 | page: PropTypes.number,
57 | context: PropTypes.shape({}).isRequired,
58 | text: PropTypes.string.isRequired,
59 | closeMenu: PropTypes.func.isRequired,
60 | };
61 |
62 | MenuRoute.defaultProps = {
63 | page: null,
64 | };
65 |
66 | export default inject(({ connection, theme }, { name }) => {
67 | const { type, id, page, context, text } = routes[name];
68 |
69 | return {
70 | type,
71 | id,
72 | page,
73 | context,
74 | text: text || connection.entity(type, id).title,
75 | closeMenu: theme.menu.close,
76 | };
77 | })(MenuRoute);
78 |
79 | const Container = styled.div`
80 | box-sizing: border-box;
81 | height: ${({ theme }) => theme.size.button};
82 | box-shadow: inset 0 -1px 0 0 rgba(40, 36, 9, 0.1);
83 | font-size: 20px;
84 | line-height: 20px;
85 | `;
86 |
87 | const A = styled.a`
88 | box-sizing: border-box;
89 | height: ${({ theme }) => theme.size.button};
90 | padding: 16px 24px;
91 | display: flex;
92 | justify-content: flex-start;
93 | align-items: center;
94 | color: ${({ theme }) => theme.color.text};
95 | `;
96 |
--------------------------------------------------------------------------------
/packages/src/pwa/contexts/index.js:
--------------------------------------------------------------------------------
1 | import memoize from 'lodash/memoize';
2 | import * as consts from '../consts';
3 |
4 | export const homeContext = {
5 | columns: [
6 | [
7 | {
8 | type: 'page',
9 | id: consts.PAGE_HOME_ON_NOW,
10 | },
11 | ],
12 | [
13 | {
14 | type: 'page',
15 | id: consts.PAGE_HOME_UP_NEXT,
16 | },
17 | ],
18 | [
19 | {
20 | type: 'page',
21 | id: consts.PAGE_HOME_SCHEDULE,
22 | },
23 | ],
24 | ],
25 | options: {
26 | name: 'home',
27 | title: 'Schedule',
28 | color: 'grey',
29 | },
30 | };
31 |
32 | export const venueContext = {
33 | columns: [
34 | [
35 | {
36 | type: 'page',
37 | id: consts.PAGE_VENUE_ALL,
38 | },
39 | ],
40 | [
41 | {
42 | type: 'page',
43 | id: consts.PAGE_VENUE_MILKY_WAY,
44 | },
45 | ],
46 | [
47 | {
48 | type: 'page',
49 | id: consts.PAGE_VENUE_ANDROMEDA,
50 | },
51 | ],
52 | [
53 | {
54 | type: 'page',
55 | id: consts.PAGE_VENUE_HAYABUSA,
56 | },
57 | ],
58 | [
59 | {
60 | type: 'page',
61 | id: consts.PAGE_VENUE_CASSINI,
62 | },
63 | ],
64 | [
65 | {
66 | type: 'page',
67 | id: consts.PAGE_VENUE_ROSETTA,
68 | },
69 | ],
70 | ],
71 | options: {
72 | name: 'venues',
73 | title: 'Venue Map',
74 | color: 'grey',
75 | },
76 | };
77 |
78 | export const announcementsContext = {
79 | columns: [
80 | [
81 | {
82 | type: 'latest',
83 | id: 'post',
84 | page: 1,
85 | },
86 | ],
87 | ],
88 | options: {
89 | name: 'announcements',
90 | title: 'Announcements',
91 | color: 'grey',
92 | },
93 | };
94 |
95 | export const postsContext = memoize((columns = []) => ({
96 | columns,
97 | options: {
98 | name: 'posts',
99 | title: 'Announcement',
100 | color: 'grey',
101 | },
102 | }));
103 |
104 | export const sessionsContext = memoize((columns = []) => ({
105 | columns,
106 | options: {
107 | name: 'sessions',
108 | title: 'Session',
109 | color: 'lightGrey',
110 | },
111 | }));
112 |
113 | export const speakersContext = memoize((columns = []) => ({
114 | columns,
115 | options: {
116 | name: 'speakers',
117 | title: 'Speaker',
118 | color: 'lightGrey',
119 | },
120 | }));
121 |
122 | export const pageContext = memoize(({ id, title, color = 'grey' }) => ({
123 | columns: [[{ type: 'page', id }]],
124 | options: { name: 'page', title, color },
125 | }));
126 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Menu/MenuNotifications.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import Switch from 'rc-switch';
6 |
7 | const MenuNotifications = ({ areSupported, areEnabled, toggleEnabled }) =>
8 | areSupported && (
9 |
10 | Push Notifications
11 |
12 |
13 |
14 |
15 | );
16 |
17 | MenuNotifications.propTypes = {
18 | areSupported: PropTypes.bool.isRequired,
19 | areEnabled: PropTypes.bool.isRequired,
20 | toggleEnabled: PropTypes.func.isRequired,
21 | };
22 |
23 | export default inject(({ notifications }) => ({
24 | areSupported: notifications.areSupported,
25 | areEnabled: notifications.areEnabled,
26 | toggleEnabled: notifications.toggleEnabled,
27 | }))(MenuNotifications);
28 |
29 | const Container = styled.div`
30 | box-sizing: border-box;
31 | height: ${({ theme }) => theme.size.button};
32 | width: 100%;
33 | display: flex;
34 | box-shadow: inset 0 -1px 0 0 rgba(40, 36, 9, 0.1);
35 | font-size: 20px;
36 | padding: ${({ theme }) => theme.padding.menuItem};
37 | `;
38 |
39 | const Text = styled.div`
40 | display: flex;
41 | justify-content: flex-start;
42 | align-items: center;
43 | flex-grow: 1;
44 | `;
45 |
46 | const SwitchWrapper = styled.div`
47 | display: flex;
48 | flex-shrink: 0;
49 | justify-content: center;
50 | align-items: center;
51 |
52 | .rc-switch {
53 | position: relative;
54 | display: inline-block;
55 | box-sizing: border-box;
56 | width: 32px;
57 | height: 20px;
58 | line-height: 20px;
59 | vertical-align: middle;
60 | border-radius: 20px 20px;
61 | background-color: #e9e9e6;
62 | cursor: pointer;
63 | transition: all 0.3s cubic-bezier(0.35, 0, 0.25, 1);
64 |
65 | &:after {
66 | position: absolute;
67 | width: 12px;
68 | height: 12px;
69 | left: 4px;
70 | top: 4px;
71 | border-radius: 50% 50%;
72 | content: ' ';
73 | cursor: pointer;
74 | transition: left 0.3s cubic-bezier(0.35, 0, 0.25, 1);
75 | background: ${({ theme }) => theme.color.switch};
76 | }
77 | &:focus {
78 | outline: none;
79 | }
80 | &.rc-switch-checked span {
81 | left: 6px;
82 | }
83 | &.rc-switch-checked:after {
84 | left: 16px;
85 | background: ${({ theme }) => theme.color.red};
86 | }
87 | &.rc-switch-disabled {
88 | cursor: no-drop;
89 | background: #ccc;
90 | border-color: #ccc;
91 | }
92 | &.rc-switch-disabled:after {
93 | background: #9e9e9e;
94 | cursor: no-drop;
95 | }
96 | }
97 | `;
98 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Announcements/Refresh.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled, { keyframes } from 'react-emotion';
5 | import FetchingIcon from 'react-icons/lib/fa/refresh';
6 |
7 | class Refresh extends Component {
8 | constructor() {
9 | super();
10 | this.state = { isClicked: false };
11 | this.getFirstPage = this.getFirstPage.bind(this);
12 | this.unclick = this.unclick.bind(this);
13 | }
14 |
15 | componentDidMount() {
16 | this.getFirstPage();
17 | }
18 |
19 | componentWillUnmount() {
20 | clearTimeout(this.timeout);
21 | }
22 |
23 | getFirstPage() {
24 | const { type, id, fetchListPage, isFetching } = this.props;
25 | if (!isFetching) fetchListPage({ type, id, page: 1, force: true });
26 | this.setState({ isClicked: true });
27 |
28 | clearTimeout(this.timeout);
29 | this.timeout = setTimeout(this.unclick, 500);
30 | }
31 |
32 | unclick() {
33 | this.setState({ isClicked: false });
34 | }
35 |
36 | render() {
37 | const isFetching = this.props.isFetching || this.state.isClicked;
38 | return (
39 |
43 | );
44 | }
45 | }
46 |
47 | Refresh.propTypes = {
48 | type: PropTypes.string.isRequired,
49 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
50 | isFetching: PropTypes.bool.isRequired,
51 | fetchListPage: PropTypes.func.isRequired,
52 | };
53 |
54 | export default inject(({ connection }, { list }) => ({
55 | type: list.type,
56 | id: list.id,
57 | isFetching: list.isFetching,
58 | fetchListPage: connection.fetchListPage,
59 | }))(Refresh);
60 |
61 | const spinner = keyframes`
62 | 0% { transform: rotate(0deg); }
63 | 100% { transform: rotate(360deg); }
64 | `;
65 |
66 | const Button = styled.div`
67 | box-sizing: border-box;
68 | width: 100%;
69 | height: 40px;
70 | color: ${({ isFetching }) => (isFetching ? '#282409' : '#5566C3')};
71 | background: ${({ isFetching }) => (isFetching ? '#FCF8D7' : 'white')};
72 | display: flex;
73 | justify-content: center;
74 | align-items: center;
75 | margin-bottom: 24px;
76 | box-shadow: ${({ isFetching }) => (isFetching ? 'none' : 'inset 0 -1px 0 0 #E9E9E6')};
77 |
78 | svg {
79 | animation: ${({ isFetching }) => (isFetching ? `${spinner} 1s ease infinite` : 'none')};
80 | }
81 | `;
82 |
83 | const Text = styled.div`
84 | font-size: 14px;
85 | font-weight: bold;
86 | letter-spacing: 0;
87 | line-height: 24px;
88 | padding-right: 8px;
89 | `;
90 |
91 | const Icon = styled(FetchingIcon)`
92 | width: 20px;
93 | height: 20px;
94 | color: #5566c3;
95 | `;
96 |
--------------------------------------------------------------------------------
/core/webpack/server.prod.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | const fs = require('fs');
3 | const path = require('path');
4 | const webpack = require('webpack');
5 |
6 | // if you're specifying externals to leave unbundled, you need to tell Webpack
7 | // to still bundle `react-universal-component`, `webpack-flush-chunks` and
8 | // `require-universal-module` so that they know they are running
9 | // within Webpack and can properly make connections to client modules:
10 | const externals = fs
11 | .readdirSync(path.resolve(__dirname, '../../node_modules'))
12 | .filter(x => !/\.bin|react-universal-component|webpack-flush-chunks/.test(x))
13 | .reduce((external, mod) => {
14 | external[mod] = `commonjs ${mod}`;
15 | return external;
16 | }, {});
17 |
18 | externals['react-dom/server'] = 'commonjs react-dom/server';
19 |
20 | const config = {
21 | name: 'server',
22 | target: 'node',
23 | entry: [path.resolve(__dirname, `../server`)],
24 | output: {
25 | path: path.resolve(__dirname, `../../.build/${process.env.MODE}/server`),
26 | filename: '[name].js',
27 | libraryTarget: 'commonjs2',
28 | },
29 | externals,
30 | module: {
31 | rules: [
32 | {
33 | test: /\.js$/,
34 | exclude: /node_modules/,
35 | use: {
36 | loader: 'babel-loader',
37 | options: {
38 | forceEnv: 'server',
39 | },
40 | },
41 | },
42 | {
43 | test: /\.css$/,
44 | exclude: /node_modules/,
45 | use: [
46 | {
47 | loader: 'css-loader/locals',
48 | options: {
49 | modules: true,
50 | localIdentName: '[name]__[local]--[hash:base64:5]',
51 | },
52 | },
53 | ],
54 | },
55 | ],
56 | },
57 | plugins: [
58 | new webpack.optimize.LimitChunkCountPlugin({
59 | maxChunks: 1,
60 | }),
61 |
62 | new webpack.DefinePlugin({
63 | 'process.env': {
64 | NODE_ENV: JSON.stringify('production'),
65 | MODE: JSON.stringify(process.env.MODE),
66 | },
67 | }),
68 | new webpack.WatchIgnorePlugin([/\.build/]),
69 | new webpack.IgnorePlugin(/vertx/),
70 | ],
71 | };
72 |
73 | if (process.env.ANALYZE) {
74 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
75 | const Visualizer = require('webpack-visualizer-plugin');
76 | config.plugins.push(new BundleAnalyzerPlugin({
77 | analyzerMode: 'static',
78 | reportFilename: `../../analyize/${process.env.MODE}/server-prod-analyzer.html`,
79 | openAnalyzer: false,
80 | generateStatsFile: true,
81 | statsFilename: `../../analyize/${process.env.MODE}/server-prod-stats.json`,
82 | }));
83 | config.plugins.push(new Visualizer({
84 | filename: `../../analyize/${process.env.MODE}/server-prod-visualizer.html`,
85 | }));
86 | }
87 |
88 | module.exports = config;
89 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/Home/ScheduleList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import styled from 'react-emotion';
5 | import ScheduleItem from './ScheduleItem';
6 | import FilterFavorites from './FilterFavorites';
7 |
8 | const ScheduleList = ({
9 | firstDaySessions,
10 | secondDaySessions,
11 | firstDayColumns,
12 | secondDayColumns,
13 | }) => (
14 |
15 |
16 | Friday, June 15th
17 |
18 |
19 |
20 | {firstDaySessions.map((session, index) => (
21 |
28 | ))}
29 |
30 |
31 | Saturday, June 16th
32 |
33 |
34 | {secondDaySessions.map((session, index) => (
35 |
42 | ))}
43 |
44 |
45 | );
46 |
47 | ScheduleList.propTypes = {
48 | firstDaySessions: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
49 | secondDaySessions: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
50 | firstDayColumns: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.shape({}))).isRequired,
51 | secondDayColumns: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.shape({}))).isRequired,
52 | };
53 |
54 | export default inject(({ theme }, { track }) => ({
55 | firstDaySessions: theme.track(track).firstDaySessions,
56 | secondDaySessions: theme.track(track).secondDaySessions,
57 | firstDayColumns: theme
58 | .track(track)
59 | .firstDaySessions.filter(item => item.speakers.length !== 0)
60 | .map(({ type, id, page }) => [{ type, id, page }]),
61 | secondDayColumns: theme
62 | .track(track)
63 | .secondDaySessions.filter(item => item.speakers.length !== 0)
64 | .map(({ type, id, page }) => [{ type, id, page }]),
65 | }))(ScheduleList);
66 |
67 | const Container = styled.div`
68 | width: 100%;
69 | display: flex;
70 | flex-direction: column;
71 | align-items: center;
72 | `;
73 |
74 | const InnerContainer = styled.div`
75 | width: calc(100vw - 48px);
76 | display: flex;
77 | justify-content: space-between;
78 | align-items: center;
79 | margin-top: 24px;
80 | margin-bottom: 16px;
81 | `;
82 |
83 | const ScheduleWrapper = styled.div`
84 | width: 100%;
85 | `;
86 |
87 | const SessionDay = styled.h3`
88 | margin: 0;
89 | font-size: 14px;
90 | color: ${({ theme }) => theme.color.lightGreyText};
91 | align-self: flex-start;
92 | line-height: 20px;
93 | `;
94 |
--------------------------------------------------------------------------------
/core/webpack/server.dev.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | const fs = require('fs');
3 | const path = require('path');
4 | const webpack = require('webpack');
5 | const WriteFilePlugin = require('write-file-webpack-plugin');
6 |
7 | // if you're specifying externals to leave unbundled, you need to tell Webpack
8 | // to still bundle `react-universal-component`, `webpack-flush-chunks` and
9 | // `require-universal-module` so that they know they are running
10 | // within Webpack and can properly make connections to client modules:
11 | const externals = fs
12 | .readdirSync(path.resolve(__dirname, '../../node_modules'))
13 | .filter(x => !/\.bin|react-universal-component|webpack-flush-chunks/.test(x))
14 | .reduce((external, mod) => {
15 | external[mod] = `commonjs ${mod}`;
16 | return external;
17 | }, {});
18 |
19 | externals['react-dom/server'] = 'commonjs react-dom/server';
20 |
21 | const config = {
22 | name: 'server',
23 | target: 'node',
24 | // devtool: 'source-map',
25 | devtool: 'eval',
26 | entry: [path.resolve(__dirname, `../server`)],
27 | externals,
28 | output: {
29 | path: path.resolve(__dirname, `../../.build/${process.env.MODE}/server`),
30 | filename: '[name].js',
31 | libraryTarget: 'commonjs2',
32 | },
33 | module: {
34 | rules: [
35 | {
36 | test: /\.js$/,
37 | exclude: /node_modules/,
38 | use: {
39 | loader: 'babel-loader',
40 | options: {
41 | forceEnv: 'server',
42 | },
43 | },
44 | },
45 | {
46 | test: /\.css$/,
47 | exclude: /node_modules/,
48 | use: [
49 | {
50 | loader: 'css-loader/locals',
51 | options: {
52 | modules: true,
53 | localIdentName: '[name]__[local]--[hash:base64:5]',
54 | },
55 | },
56 | ],
57 | },
58 | ],
59 | },
60 | plugins: [
61 | new WriteFilePlugin(),
62 | new webpack.optimize.LimitChunkCountPlugin({
63 | maxChunks: 1,
64 | }),
65 | new webpack.DefinePlugin({
66 | 'process.env': {
67 | NODE_ENV: JSON.stringify('development'),
68 | MODE: JSON.stringify(process.env.MODE),
69 | },
70 | }),
71 | new webpack.WatchIgnorePlugin([/\.build/, /packages$/]),
72 | new webpack.IgnorePlugin(/vertx/),
73 | ],
74 | };
75 |
76 | if (process.env.ANALYZE) {
77 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
78 | const Visualizer = require('webpack-visualizer-plugin');
79 | config.plugins.push(new BundleAnalyzerPlugin({
80 | analyzerMode: 'static',
81 | reportFilename: `../../analyize/${process.env.MODE}/server-dev-analyzer.html`,
82 | openAnalyzer: false,
83 | generateStatsFile: true,
84 | statsFilename: `../../analyize/${process.env.MODE}/server-dev-stats.json`,
85 | }));
86 | config.plugins.push(new Visualizer({
87 | filename: `../../analyize/${process.env.MODE}/server-dev-visualizer.html`,
88 | }));
89 | }
90 |
91 | module.exports = config;
92 |
--------------------------------------------------------------------------------
/packages/src/pwa/components/LazyTweet/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import LazyLoad from '@frontity/lazyload';
4 | import IconTwitter from 'react-icons/lib/fa/twitter';
5 | import styled from 'react-emotion';
6 |
7 | class LazyTweet extends Component {
8 | static propTypes = {
9 | children: PropTypes.shape({}).isRequired,
10 | width: PropTypes.string.isRequired,
11 | height: PropTypes.string.isRequired,
12 | };
13 |
14 | constructor() {
15 | super();
16 |
17 | this.ref = null;
18 | this.state = {
19 | loaded: false,
20 | };
21 |
22 | this.handleContentVisible = this.handleContentVisible.bind(this);
23 | }
24 |
25 | shouldComponentUpdate(nextProps, nextState) {
26 | return this.state.loaded !== nextState.loaded;
27 | }
28 |
29 | componentWillUpdate() {
30 | if (window.document.getElementById('lazy-twitter') && window.twttr) {
31 | window.twttr.widgets.load(this.ref);
32 | } else {
33 | const script = window.document.createElement('script');
34 | script.id = 'lazy-twitter';
35 | script.src = '//platform.twitter.com/widgets.js';
36 | script.async = true;
37 | script.chartset = 'utf-8';
38 |
39 | window.document.body.appendChild(script);
40 | }
41 | }
42 |
43 | handleContentVisible() {
44 | this.setState({
45 | loaded: true,
46 | });
47 | }
48 |
49 | render() {
50 | const { children, width, height } = this.props;
51 | const { loaded } = this.state;
52 |
53 | return (
54 | {
57 | this.ref = node;
58 | }}
59 | >
60 | {!loaded && (
61 |
62 |
63 |
64 | )}
65 |
71 | {children}
72 |
73 |
74 | );
75 | }
76 | }
77 |
78 | export default LazyTweet;
79 |
80 | const Container = styled.div`
81 | position: relative;
82 | box-sizing: border-box;
83 | width: ${({ styles }) => styles.width};
84 | height: ${({ styles }) => styles.height};
85 | min-height: 170px;
86 | margin: 15px 0;
87 |
88 | blockquote {
89 | margin: 0;
90 | }
91 | `;
92 |
93 | const Icon = styled.div`
94 | position: absolute;
95 | top: 65px;
96 | left: 0;
97 | color: #bdbdbd;
98 | width: 100%;
99 | display: flex;
100 | justify-content: center;
101 | align-items: center;
102 | `;
103 |
104 | const StyledLazyLoad = styled(LazyLoad)`
105 | width: 100%;
106 | height: 100%;
107 | object-fit: cover;
108 | object-position: center;
109 | background-color: transparent;
110 | border: none;
111 | z-index: 10;
112 | `;
113 |
--------------------------------------------------------------------------------
/packages/src/pwa/stores/session.js:
--------------------------------------------------------------------------------
1 | import { types, getParent, resolveIdentifier } from 'mobx-state-tree';
2 | import Speaker from './speaker';
3 | import Track from './track';
4 | import { formatDate } from '../utils';
5 |
6 | // Hours -- correction - move this to database
7 | const hoursOffset = -2;
8 |
9 | const SpeakerReference = types.reference(types.late(() => Speaker), {
10 | get: (id, parent) => resolveIdentifier(Speaker, parent, id) || null,
11 | set: speaker => speaker.id || speaker,
12 | });
13 |
14 | const TrackReference = types.reference(types.late(() => Track), {
15 | get: (id, parent) => resolveIdentifier(Track, parent, id) || null,
16 | set: track => track.id || track,
17 | });
18 |
19 | const Session = types
20 | .model('Session')
21 | .props({
22 | mstId: types.identifier(types.string),
23 | id: types.union(types.number, types.string),
24 | type: types.optional(types.string, 'wcb_session'),
25 | sessionTitle: types.maybe(types.string),
26 | sessionTimestamp: types.maybe(types.number),
27 | speakers: types.optional(types.array(SpeakerReference), []),
28 | tracks: types.optional(types.array(TrackReference), []),
29 | })
30 | .views(self => {
31 | const getConnection = () => getParent(self, 3).connection;
32 | const date = new Date();
33 |
34 | return {
35 | get isFavorite() {
36 | return getParent(self, 2).favoritesMap.get(`${self.id}`)
37 | ? getParent(self, 2).favoritesMap.get(`${self.id}`).val
38 | : false;
39 | },
40 | get hasSpeakers() {
41 | return !!self.speakers.length;
42 | },
43 | get entity() {
44 | return getConnection().entity(self.type, self.id);
45 | },
46 | get title() {
47 | return self.sessionTitle || self.entity.title;
48 | },
49 | get link() {
50 | return self.entity.link;
51 | },
52 | get timestamp() {
53 | const { _wcpt_session_time: time } = self.entity.meta;
54 | // seconds to milliseconds
55 | return self.sessionTimestamp || (time + hoursOffset * 3600) * 1000;
56 | },
57 | get date() {
58 | date.setTime(self.timestamp);
59 | return date;
60 | },
61 | get startTime() {
62 | return formatDate(self.date, 'HH:mm');
63 | },
64 | get endTime() {
65 | const { nextSessions } = self;
66 | return nextSessions.length
67 | ? nextSessions.sort((a, b) => a.timestamp - b.timestamp)[0].startTime
68 | : null;
69 | },
70 | get nextSessions() {
71 | return self.tracks
72 | .map(t => {
73 | const index = t.sessions.indexOf(self) + 1;
74 | return index < t.sessions.length ? t.sessions[index] : null;
75 | })
76 | .filter(s => !!s);
77 | },
78 | };
79 | })
80 | .actions(self => ({
81 | toggleFavorite() {
82 | getParent(self, 2).toggleFavorite(`${self.id}`);
83 | },
84 | afterCreate() {
85 | self.speakers.forEach(speaker => speaker && speaker.addSession(self));
86 | self.tracks.forEach(track => track && track.addSession(self));
87 | },
88 | }));
89 |
90 | export default Session;
91 |
--------------------------------------------------------------------------------
/packages/src/server/manifest.js:
--------------------------------------------------------------------------------
1 | const request = require('superagent');
2 | const { normalize, schema } = require('normalizr');
3 |
4 | const settingSchema = new schema.Entity(
5 | 'settings',
6 | {},
7 | { idAttribute: setting => setting.woronaInfo.namespace },
8 | );
9 | const settingsSchema = [settingSchema];
10 |
11 | // Fetch settings from the backend.
12 | const getSettings = async ({ siteId, env }) => {
13 | const cdn = env === 'prod' ? 'cdn' : 'precdn';
14 | try {
15 | const { body } = await request(
16 | `https://${cdn}.worona.io/api/v1/settings/site/${siteId}/app/prod/live`,
17 | );
18 | const { entities: { settings } } = normalize(body, settingsSchema);
19 | return settings;
20 | } catch (error) {
21 | return null;
22 | }
23 | };
24 |
25 | module.exports = async (req, res) => {
26 | const { siteId } = req.params;
27 |
28 | const settings = (await getSettings({ siteId, env: 'prod' })).theme.manifest;
29 |
30 | // This is the manifest to be sent.
31 | const manifest = {
32 | name: settings.name,
33 | short_name: settings.shortName,
34 | description: settings.description,
35 | start_url: settings.startUrl,
36 | theme_color: settings.themeColor,
37 | background_color: '#FFF',
38 | dir: 'auto',
39 | display: 'standalone',
40 | orientation: 'portrait',
41 | lang: 'es',
42 | icons: [
43 | {
44 | src: `${settings.iconSrc}?profile=android-icon-1024`,
45 | type: 'image/png',
46 | sizes: '1024x1024',
47 | },
48 | {
49 | src: `${settings.iconSrc}?profile=android-icon-512`,
50 | type: 'image/png',
51 | sizes: '512x512',
52 | },
53 | {
54 | src: `${settings.iconSrc}?profile=android-icon-256`,
55 | type: 'image/png',
56 | sizes: '256x256',
57 | },
58 | {
59 | src: `${settings.iconSrc}?profile=android-icon-192`,
60 | type: 'image/png',
61 | sizes: '192x192',
62 | },
63 | {
64 | src: `${settings.iconSrc}?profile=android-icon-152`,
65 | type: 'image/png',
66 | sizes: '152x152',
67 | },
68 | {
69 | src: `${settings.iconSrc}?profile=android-icon-144`,
70 | type: 'image/png',
71 | sizes: '144x144',
72 | },
73 | {
74 | src: `${settings.iconSrc}?profile=android-icon-128`,
75 | type: 'image/png',
76 | sizes: '128x128',
77 | },
78 | {
79 | src: `${settings.iconSrc}?profile=android-icon-96`,
80 | type: 'image/png',
81 | sizes: '96x96',
82 | },
83 | {
84 | src: `${settings.iconSrc}?profile=android-icon-72`,
85 | type: 'image/png',
86 | sizes: '72x72',
87 | },
88 | {
89 | src: `${settings.iconSrc}?profile=android-icon-48`,
90 | type: 'image/png',
91 | sizes: '48x48',
92 | },
93 | {
94 | src: `${settings.iconSrc}?profile=android-icon-36`,
95 | type: 'image/png',
96 | sizes: '36x36',
97 | },
98 | ],
99 | };
100 | res.type('application/manifest+json');
101 | res.json(manifest);
102 | };
103 |
--------------------------------------------------------------------------------