Skip to content

Commit

Permalink
Added support for interactive articles (#179)
Browse files Browse the repository at this point in the history
* Add support for differentiated interactive articles

* Added ILT to InteractiveArticle
  • Loading branch information
emilgoldsmith authored Sep 16, 2017
1 parent c064b4b commit 17c8d7a
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 99 deletions.
6 changes: 5 additions & 1 deletion src/components/ArticlePreview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ export default class ArticlePreview extends BaseComponent {
render() {
const article = this.props.article;
let url = `/issue/${article.issueNumber.toString()}/${article.category}/${article.slug}`;
if (article.is_interactive) {
// We don't use standard url for interactive articles
url = `/interactive/${article.slug}`;
}
if (!article.image) { // Article image default
article.image = 'https://thegazelle.s3.amazonaws.com/gazelle/2016/02/saadiyat-reflection.jpg';
}
return (
<div className="article-preview">
<Link to={`/issue/${article.issueNumber.toString()}/${article.category}/${article.slug}`}>
<Link to={url}>
<img
className="article-preview__featured-image"
src={article.image}
Expand Down
66 changes: 66 additions & 0 deletions src/components/InteractiveArticle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import FalcorController from 'lib/falcor/FalcorController';
import Helmet from 'react-helmet';
import NotFound from 'components/NotFound';
import InteractiveArticleLoad from 'transitions/InteractiveArticleLoad';

export default class InteractiveArticle extends FalcorController {
static getFalcorPathSets(params) {
return [
// Fetch article metadata
['articles', 'bySlug', params.articleSlug,
['title', 'teaser', 'slug', 'image', 'published_at']],
// Fetch interactive article html/js/css
// For now we only use the html part, the js and css parts are for further improvements
['articles', 'bySlug', params.articleSlug, 'interactiveData', ['html', 'js', 'css']],
];
}

render() {
if (this.state.ready) {
if (!this.state.data) {
return <NotFound />;
}
const articleSlug = this.props.params.articleSlug;
const publishDate = this.state.data.articles.bySlug[articleSlug].published_at;
if (!publishDate) {
return <NotFound />;
}
// Access data fetched via Falcor
const articleData = this.state.data.articles.bySlug[articleSlug];
const interactiveCode = articleData.interactiveData;
// make sure article meta image has default
const articleMetaImage = articleData.image || 'https://thegazelle.s3.amazonaws.com/gazelle/2016/02/saadiyat-reflection.jpg';
const meta = [
// Search results
{ name: 'description', content: articleData.teaser },

// Social media sharing
{ property: 'og:title', content: `${articleData.title} | The Gazelle` },
{ property: 'og:type', content: 'article' },
{
property: 'og:url',
content: `www.thegazelle.org/interactive/${articleData.slug}/`,
},
{ property: 'og:image', content: articleMetaImage },
{ property: 'og:image:width', content: '540' }, // 1.8:1 ratio
{ property: 'og:image:height', content: '300' },
{ property: 'og:description', content: articleData.teaser },
{ property: 'og:site_name', content: 'The Gazelle' },
];
const reactHtml = {
__html: interactiveCode.html,
};
return (
<div>
<Helmet
meta={meta}
title={`${articleData.title} | The Gazelle`}
/>
<div dangerouslySetInnerHTML={reactHtml} />
</div>
);
}
return <InteractiveArticleLoad />;
}
}
121 changes: 31 additions & 90 deletions src/components/IssueController.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,125 +24,66 @@ export default class IssueController extends FalcorController {
// Conditional return allows The Gazelle to return the correct issue
// User is either requesting the 'issues', 'latest' on the home page or an
// old issue.
const articleFields = ['title', 'teaser', 'issueNumber', 'category',
'slug', 'image', 'is_interactive'];
const authorFields = ['name', 'slug'];
const issueFields = ['issueNumber', 'published_at'];
const categoryFields = ['name', 'slug'];
if (params.issueNumber) { // If not on home page grab specificed issue
const issueNumber = mapLegacyIssueSlugsToIssueNumber(params.issueNumber);
return [
['issues', 'byNumber', issueNumber, ['issueNumber', 'published_at']],
['issues', 'byNumber', issueNumber, issueFields],

// Request the featured article
[
'issues', 'byNumber',
issueNumber,
'featured',
['title', 'teaser', 'issueNumber', 'category', 'slug', 'image'],
],
[
'issues',
'byNumber',
issueNumber,
'featured',
'authors',
{ length: 10 },
['name', 'slug'],
],
['issues', 'byNumber', issueNumber, 'featured', articleFields],
['issues', 'byNumber', issueNumber, 'featured', 'authors', { length: 10 }, authorFields],

// Request first two Editor's Picks
[
'issues', 'byNumber',
issueNumber,
'picks',
{ length: 2 },
['title', 'teaser', 'issueNumber', 'category', 'slug', 'image'],
],
[
'issues', 'byNumber',
issueNumber,
'picks',
{ length: 2 },
'authors',
{ length: 10 },
['name', 'slug'],
],
['issues', 'byNumber', issueNumber, 'picks', { length: 2 }, articleFields],
['issues', 'byNumber', issueNumber, 'picks', { length: 2 },
'authors', { length: 10 }, authorFields],

// Request first five Trending articles
['trending', { length: 6 }, ['title', 'issueNumber', 'category', 'slug', 'image']],
['trending', { length: 6 }, 'authors', { length: 10 }, ['name', 'slug']],
['trending', { length: 6 }, articleFields],
['trending', { length: 6 }, 'authors', { length: 10 }, authorFields],

// Request all category names and slugs (max 10 categories)
['issues', 'byNumber', issueNumber, 'categories', { length: 10 }, ['name', 'slug']],
['issues', 'byNumber', issueNumber, 'categories', { length: 10 }, categoryFields],

// Request necessary data from all articles from each category (max 30 articles)
[
'issues', 'byNumber',
issueNumber,
'categories',
{ length: 10 },
'articles',
{ length: 30 },
['title', 'teaser', 'issueNumber', 'category', 'slug', 'image'],
],
['issues', 'byNumber', issueNumber, 'categories', { length: 10 },
'articles', { length: 30 }, articleFields],

// Request author name and slug for each article (max 10 authors)
[
'issues', 'byNumber',
issueNumber,
'categories',
{ length: 10 },
'articles',
{ length: 30 },
'authors',
{ length: 10 },
['name', 'slug'],
],
['issues', 'byNumber', issueNumber, 'categories', { length: 10 },
'articles', { length: 30 }, 'authors', { length: 10 }, authorFields],
];
} // User is on home page
}
// User is on home page
return [
['issues', 'latest', ['issueNumber', 'published_at']],
['issues', 'latest', issueFields],

// Request the featured article
[
'issues', 'latest',
'featured',
['title', 'teaser', 'issueNumber', 'category', 'slug', 'image'],
],
['issues', 'latest', 'featured', 'authors', { length: 10 }, ['name', 'slug']],
['issues', 'latest', 'featured', articleFields],
['issues', 'latest', 'featured', 'authors', { length: 10 }, authorFields],

// Request first two Editor's Picks
[
'issues', 'latest',
'picks',
{ length: 2 },
['title', 'teaser', 'issueNumber', 'category', 'slug', 'image'],
],
['issues', 'latest', 'picks', { length: 2 }, 'authors', { length: 10 }, ['name', 'slug']],
['issues', 'latest', 'picks', { length: 2 }, articleFields],
['issues', 'latest', 'picks', { length: 2 }, 'authors', { length: 10 }, authorFields],

// Request first five Trending articles
['trending', { length: 6 }, ['title', 'issueNumber', 'category', 'slug', 'image']],
['trending', { length: 6 }, 'authors', { length: 10 }, ['name', 'slug']],
['trending', { length: 6 }, articleFields],
['trending', { length: 6 }, 'authors', { length: 10 }, authorFields],

// Request all category names and slugs (max 10 categories)
['issues', 'latest', 'categories', { length: 10 }, ['name', 'slug']],
['issues', 'latest', 'categories', { length: 10 }, categoryFields],

// Request necessary data from all articles from each category (max 30 articles)
['issues', 'latest',
'categories',
{ length: 10 },
'articles',
{ length: 30 },
['title', 'teaser', 'issueNumber', 'category', 'slug', 'image'],
],
['issues', 'latest', 'categories', { length: 10 }, 'articles', { length: 30 }, articleFields],

// Request author name and slug for each article (max 10 authors)
[
'issues', 'latest',
'categories',
{ length: 10 },
'articles',
{ length: 30 },
'authors',
{ length: 10 },
['name', 'slug'],
],
['issues', 'latest', 'categories', { length: 10 }, 'articles',
{ length: 30 }, 'authors', { length: 10 }, authorFields],
];
}

Expand Down
10 changes: 8 additions & 2 deletions src/components/editor/EditorIssueArticleController.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { formatDate } from 'lib/utilities';
// material-ui
import CircularProgress from 'material-ui/CircularProgress';

const ARTICLE_FIELDS = ['id', 'title', 'slug', 'category', 'published_at', 'html'];
const ARTICLE_FIELDS = ['id', 'title', 'slug', 'category',
'published_at', 'html', 'is_interactive'];
const ARTICLE_LIST_LENGTH = 100;

const styles = {
Expand Down Expand Up @@ -342,7 +343,8 @@ export default class EditorIssueArticleController extends FalcorController {
}
if (isPublished) {
// Check that all articles are valid since issue is already published
const fields = ARTICLE_FIELDS.filter((field) => field !== 'published_at');
const fields = ARTICLE_FIELDS.filter((field) => (
field !== 'published_at' && field !== 'is_interactive'));
const articlesValid = allArticles.every((article) => {
const slug = article.slug;
const isOldArticle = (
Expand All @@ -358,6 +360,10 @@ export default class EditorIssueArticleController extends FalcorController {
return true;
}
const fieldsValid = fields.every((field) => {
// Special case for interactive articles, don't need html key
if (article.is_interactive && field === 'html') {
return true;
}
if (!article[field]) {
window.alert(
`${article.title} has no ${field}. Please correct this ` +
Expand Down
14 changes: 14 additions & 0 deletions src/lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,20 @@ export default class db {
});
}

interactiveArticleQuery(slugs, columns) {
// Fetch information from interactive_meta
return new Promise((resolve) => {
const processedColumns = columns.map(col => `interactive_meta.${col}`);
database.select('slug', ...processedColumns)
.from('posts')
.innerJoin('posts_meta', 'posts.id', '=', 'posts_meta.id')
.leftJoin('interactive_meta', 'interactive_meta.id', '=', 'posts.id')
.whereIn('slug', slugs)
.then(rows => resolve(rows))
.catch((e) => { throw new Error(e); });
});
}

categoryQuery(slugs, columns) {
// slugs parameter is an array of category slugs
// to fetch the name of
Expand Down
23 changes: 22 additions & 1 deletion src/lib/falcor/routes/articles/by-slug.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default [
},
{
// Get custom article data from MariaDB
route: "articles['bySlug'][{keys:slugs}]['category', 'published_at', 'views']",
route: "articles['bySlug'][{keys:slugs}]['category', 'published_at', 'views', 'is_interactive']", // eslint-disable-line max-len
get: (pathSet) => (
new Promise((resolve) => {
const requestedFields = pathSet[3];
Expand Down Expand Up @@ -179,6 +179,27 @@ export default [
})
),
},
{
route: "articles['bySlug'][{keys:slugs}]['interactiveData']['html', 'js', 'css']",
// Get interactive article meta data
get: (pathSet) => (
new Promise((resolve) => {
const fields = pathSet[4];
db.interactiveArticleQuery(pathSet.slugs, fields).then((data) => {
const results = [];
data.forEach((article) => {
fields.forEach((field) => {
results.push({
path: ['articles', 'bySlug', article.slug, 'interactiveData', field],
value: article[field],
});
});
});
resolve(results);
});
})
),
},
{
// Add authors to an article
route: "articles['bySlug'][{keys:slugs}]['authors']['updateAuthors']",
Expand Down
9 changes: 6 additions & 3 deletions src/lib/routes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Route, IndexRoute } from 'react-router';
import React from 'react';

import InteractiveArticle from 'components/InteractiveArticle';
import AppController from 'components/AppController';
import ArticleController from 'components/ArticleController';
import ArchivesController from 'components/ArchivesController';
Expand All @@ -12,7 +14,8 @@ import NotFoundController from 'components/NotFoundController';
import SearchController from 'components/SearchController';
import Login from 'components/Login';

export default (
export default [
<Route path="/interactive/:articleSlug" component={InteractiveArticle} />,
<Route path="/" component={AppController}>
<Route path="login" component={Login} />
<Route path="issue/:issueNumber/:articleCategory/:articleSlug" component={ArticleController} />
Expand All @@ -25,5 +28,5 @@ export default (
<Route path=":slug" component={TextPageController} />
<Route path="*" component={NotFoundController} />
<IndexRoute component={IssueController} />
</Route>
);
</Route>,
];
5 changes: 3 additions & 2 deletions src/styles/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
'pages/issue',
'pages/category';

// 7. Transitions
// 7. Skeleton Code Transitions
@import
'transitions/text-page-load';
'transitions/text-page-load',
'transitions/interactive-article-load';
Loading

0 comments on commit 17c8d7a

Please sign in to comment.