Setup server to handle article page (untested)

This commit is contained in:
2018-11-26 21:48:03 +11:00
parent 829a152a1e
commit 66336a469f
14 changed files with 241 additions and 22 deletions

View File

@ -36,13 +36,13 @@ module.exports = class Articles extends DatabaseInterface {
async getArticleById(id) {
return await this.store.getFromDatabase(
`getArticleById_${id}`, `getArticleById`, {id}, 'one'
`getArticleById_${id}`, `getArticleById`, {id}, 'oneOrNone'
);
}
async getArticleByHandle(handle) {
return await this.store.getFromDatabase(
`getArticleByHandle_${handle}`, `getArticleByHandle`, {handle}, 'one'
`getArticleByHandle_${handle}`, `getArticleByHandle`, {handle}, 'oneOrNone'
);
}

View File

@ -0,0 +1,52 @@
// Copyright (c) 2018 Dominic Masters
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
const
APIHandler = require('./../../APIHandler'),
sanitizeHtml = require('sanitize-html')
;
const ERRORS = {
missingHandle: "Missing article handle.",
notFound: "Cannot find that article."
};
module.exports = class GetBlogArticle extends APIHandler {
constructor(api) {
super(api, ['GET'], '/blog/article');
}
async handle(request) {
if(!request.hasString('article', 128)) return { ok: false, data: ERRORS.missingHandle };
let handle = request.getApp().createHandle(request.getString('article', 128));
let article = await request.getApp().getArticles().getArticleByHandle(handle);
if(!article) return { ok: false, data: ERRORS.notFound };
return {
ok: true,
data: article
};
}
}

View File

@ -42,7 +42,7 @@ export const get = async (url, params, test) => {
if(process.env.NODE_ENV === 'development') {
console.log('testing mode');
return await new Promise((resolve,reject) => {
setTimeout(e => resolve(test), 1000);
//setTimeout(e => resolve(test), 1000);
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

@ -38,6 +38,14 @@ const TestBlogs = {
articles: [...Array(20).keys()].map(TestBlogArticle)
}
//Functions for normalization
const NormalizeArticle = article => {
article.url = `/blog/article/${article.handle}`;
article.image = require(`@assets/images/${article.image}`);
return article;
};
//Template Wrappers
export const withBlogTemplate = WrappedComponent => {
return class extends React.Component {
constructor(props) {
@ -59,12 +67,7 @@ export const withBlogTemplate = WrappedComponent => {
this.setState({ pending: true, page, perPage });
get('blog', { page, perPage }, TestBlogs).then(blog => {
let { articles, pages } = blog;
articles.forEach(article => {
article.url = `/blogs/articles/${article.handle}`
article.image = require(`@assets/images/${article.image}`);
});
articles.forEach(NormalizeArticle);
this.setState({ pending: undefined, error: undefined, articles, pages });
}).catch(e => {
console.error(e);
@ -77,3 +80,34 @@ export const withBlogTemplate = WrappedComponent => {
}
}
};
export const withArticleTemplate = WrappedComponent => {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
pending: true,
error: undefined,
article: undefined
};
}
componentDidMount() {
let { article } = this.props.match.params;
this.setState({ pending: true });
get('blog/article', { article }, TestBlogArticle(1)).then(article => {
NormalizeArticle(article);
this.setState({ pending: undefined, error: undefined, article });
}).catch(error => {
console.error(error);
this.setState({ pending: undefined, error });
});
}
render() {
return <WrappedComponent {...this.props} {...this.state} />;
}
}
}

View File

@ -40,7 +40,8 @@ const AppRoutes = (props) => {
<RouteWrapper exact path="/contact" page={ () => import ('@pages/contact/ContactPage') } />
<RouteWrapper exact path="/legal/privacy" page={ () => import('@pages/legal/privacy/PrivacyPolicyPage') } />
<RouteWrapper exact path="/blog/:page?" page={ () => import('@pages/blog/BlogPage') } />
<RouteWrapper exact path="/blog/:page?" page={ ()=>import('@pages/blog/BlogPage') } />
<RouteWrapper exact path="/blog/article/:article?" page={ ()=>import('@pages/blog/article/ArticlePage') } />
</Routes>
);
};

View File

@ -11,7 +11,7 @@
@import '~@styles/global';
.c-page {
flex-grow: 1;
min-height: 80vh;
&.has-background {
position: relative;

View File

@ -44,7 +44,10 @@ export default withLanguage(props => {
<article role="article" itemScope itemType="http://schema.org/Article" className="c-featured-article">
<ContentBox box className="c-featured-article__content">
<NavLink to={ article.url } className="c-featured-article__box is-image">
<Image src={ article.image } className="c-featured-article__image" maxWidth="800" />
<Image
src={ article.image } className="c-featured-article__image"
maxWidth="800" loadable
/>
</NavLink>
<div className="c-featured-article__box is-content">

View File

@ -25,10 +25,6 @@ export default {
},
"blog": {
"error": {
"title": "Failed to get the blog",
"body": "Failed to get the blogs and articles from the server, please try again later or refresh your browser."
},
"article": {
"readMore": "Read More"
}
@ -231,7 +227,19 @@ export default {
},
"blog": {
"title": "Blog"
"title": "Blog",
"error": {
"title": "Failed to get the blog",
"body": "Failed to get the blogs and articles from the server, please try again later or refresh your browser."
}
},
"article": {
"title": "Article",
"error": {
"title": "Failed to get article",
"body": "Failed to get the article from the server, please try again later or refresh your browser."
}
}
},

View File

@ -39,9 +39,10 @@ import Styles from './BlogPage.scss';
export default withBlogTemplate(withLanguage(props => {
let { lang, articles, page, pages, pending, error } = props;
let l = lang.pages.blog;
let children;
if(error) error = <ErrorSection title={lang.blog.error.title} body={lang.blog.error.body} error={error} />;
if(error) error = <ErrorSection title={l.error.title} body={l.error.body} error={error} />;
if(pending) pending = <Loader />;
if(articles && articles.length) {
@ -49,21 +50,23 @@ export default withBlogTemplate(withLanguage(props => {
<React.Fragment>
<FeaturedArticleSection article={ articles.shift() } />
<ArticleGridSection articles={ articles } />
<Pagination page={ page } pages={ pages } to="/blog/$page" />
<Pagination
className="p-blog-page__pagination" page={ page } pages={ pages }
to="/blog/$page"
/>
</React.Fragment>
);
}
return (
<Page
style="blog-page" className="p-blog-page" title={ lang.pages.blog.title }
style="blog-page" className="p-blog-page" title={ error ? l.error.title : l.title }
background={require('@assets/images/banners/sunset.svg')}
>
<ClearSection />
{ error }
{ pending }
{ children }
<ClearSection />
</Page>
);
}));

View File

@ -6,4 +6,7 @@
* 1.0.0 - 2018/10/30
*/
.p-blog-page {
&__pagination {
padding: 6em 0;
}
}

View File

@ -22,3 +22,75 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React from 'react';
import { withLanguage } from '@public/language/Language';
import { withArticleTemplate } from '@public/blog/Blog';
import Page, { PageBoundary } from '@components/page/Page';
import ErrorSection from '@sections/error/ErrorSection';
import ClearSection from '@sections/layout/ClearSection';
import Loader from '@objects/loading/Loader';
import ContentBox from '@objects/content/box/ContentBox';
import { Title, Paragraph } from '@objects/typography/typography';
import Image from '@objects/image/Image';
import Styles from './ArticlePage.scss';
export default withArticleTemplate(withLanguage(props => {
let { error, pending, article, lang } = props;
let l = lang.pages.article;
let children;
if(error) error = <ErrorSection title={l.error.title} body={l.error.body} error={error} />;
if(pending) pending = <Loader />;
if(article) {
children = (
<PageBoundary>
<article
role="article" itemScope itemType="http://schema.org/Article"
className="p-article-page__article"
>
{/* Title */}
<ContentBox box itemProp="name" className="p-article-page__header">
<Title children={ article.title } />
</ContentBox>
{/* Image */}
<div className="p-article-page__picture">
<ContentBox box>
<Image
src={ article.image } maxWidth="800" loadable
className="p-article-page__picture-image"
/>
</ContentBox>
</div>
{/* Description */}
<ContentBox box itemProp="description" className="p-article-page__description">
<Paragraph>
{ article.description || article.shortDescription }
</Paragraph>
</ContentBox>
</article>
</PageBoundary>
);
}
return (
<Page
style="article-page" className="p-article-page"
title={error ? l.error.title : l.title}
background={require('@assets/images/banners/sunset.svg')}
>
<ClearSection />
{ error }
{ pending }
{ children }
<ClearSection />
</Page>
);
}));

View File

@ -0,0 +1,43 @@
@import '~@styles/global';
$p-article-page--spacing: 1.5em;
.p-article-page {
&__article {
display: flex;
flex-wrap: wrap;
align-content: stretch;
}
&__header,
&__picture,
&__description
{
width: 100%;
}
&__picture {
position: relative;
&-image {width: 100%;}
&-box {
position: sticky;
top:0;
}
}
@include t-media-query($s-small-up) {
position: relative;
&__header {
margin-bottom: $p-article-page--spacing;
}
&__picture {
width: 40%;
padding-right: $p-article-page--spacing;
}
&__description {
width: 60%;
padding-left: $p-article-page--spacing;
}
}
}

View File

@ -111,7 +111,7 @@ class ContactPage extends React.Component {
style="contact-page"
className="p-contact-page"
title={ lang.pages.contact.title }
background={ require('@assets/images/banners/sunset.svg') }
background={ require('@assets/images/banners/hills-night.svg') }
>
<ClearSection />
<PageBoundary small children={inners} />