Finished building blog overview page.
This commit is contained in:
57
public/api/api.js
Normal file
57
public/api/api.js
Normal file
@ -0,0 +1,57 @@
|
||||
// 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.
|
||||
|
||||
import queryString from 'query-string';
|
||||
|
||||
export const getUrl = request => {
|
||||
request = request || "";
|
||||
request = request.split('#');
|
||||
|
||||
let r = "";
|
||||
if(request.length) r = request[0].toLowerCase();
|
||||
|
||||
let slash = '/';
|
||||
if(r.startsWith('/')) slash = '';
|
||||
|
||||
return `/api${slash}${r}`;
|
||||
}
|
||||
|
||||
export const get = async (url, params) => {
|
||||
url = url || "";
|
||||
|
||||
//Generate URL from query string
|
||||
let paramString = queryString.stringify(params);
|
||||
url = getUrl(url);
|
||||
if(url.indexOf('?') !== -1) {
|
||||
url += `&${paramString}`;
|
||||
} else {
|
||||
url += `?${paramString}`;
|
||||
}
|
||||
|
||||
//Now make our fetch request.
|
||||
let res = await fetch(url, {
|
||||
crossDomain:true
|
||||
});
|
||||
if(res.status >= 400) throw new Error(`Server Responded with ${res.status}`);
|
||||
return await res.json();
|
||||
};
|
64
public/blog/Blog.jsx
Normal file
64
public/blog/Blog.jsx
Normal file
@ -0,0 +1,64 @@
|
||||
// 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.
|
||||
|
||||
import React from 'react';
|
||||
import { get } from '@public/api/api';
|
||||
|
||||
export const withBlogTemplate = WrappedComponent => {
|
||||
return class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
pending: false,
|
||||
error: undefined,
|
||||
pages: undefined,
|
||||
articles: undefined
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let { page, perPage } = this.props.match.params;
|
||||
page = page || 1;
|
||||
perPage = perPage || 7;
|
||||
|
||||
this.setState({ pending: true, page, perPage });
|
||||
get('blog', { page, perPage }).then(blog => {
|
||||
let { articles, pages } = blog;
|
||||
|
||||
articles.forEach(article => {
|
||||
article.url = `/blogs/articles/${article.handle}`
|
||||
article.image = require(`@assets/images/${article.image}`);
|
||||
});
|
||||
|
||||
this.setState({ pending: undefined, error: undefined, articles, pages });
|
||||
}).catch(e => {
|
||||
this.setState({ pending: undefined, error: e });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <WrappedComponent {...this.props} {...this.state} />;
|
||||
}
|
||||
}
|
||||
};
|
@ -39,7 +39,8 @@ const AppRoutes = (props) => {
|
||||
<RouteWrapper exact path="/" page={ () => import('@pages/home/HomePage') } />
|
||||
<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={ () => import('@pages/blog/BlogPage') } />
|
||||
|
||||
<RouteWrapper exact path="/blog/:page?" page={ () => import('@pages/blog/BlogPage') } />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
@ -39,12 +39,12 @@ const PageLoading = (props) => {
|
||||
};
|
||||
|
||||
export const RouteWrapper = (props) => {
|
||||
let render = () => {
|
||||
let render = subProps => {
|
||||
let CustomLoadable = Loadable({
|
||||
loader: props.page,
|
||||
loading: PageLoading
|
||||
});
|
||||
return <CustomLoadable />
|
||||
return <CustomLoadable {...props} {...subProps} />
|
||||
};
|
||||
|
||||
return <Route {...props} render={render} />;
|
||||
@ -53,11 +53,5 @@ export const RouteWrapper = (props) => {
|
||||
export default withRouter((props) => {
|
||||
const { match, location, history, children } = props;
|
||||
|
||||
return (
|
||||
<Route>
|
||||
<Switch location={ location }>
|
||||
{ children }
|
||||
</Switch>
|
||||
</Route>
|
||||
);
|
||||
return <Switch {...props} />;
|
||||
});
|
||||
|
@ -30,6 +30,7 @@ import { createStore, applyMiddleware } from 'redux';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { Provider } from 'react-redux';
|
||||
import RootReducer from './reducers/RootReducer';
|
||||
import promiseMiddleware from 'redux-promise-middleware';
|
||||
import Keyboard from './keyboard/Keyboard';
|
||||
|
||||
|
||||
@ -41,6 +42,7 @@ import App from './components/App';
|
||||
|
||||
//Create our redux middleware
|
||||
const store = createStore(RootReducer, applyMiddleware(
|
||||
promiseMiddleware(),
|
||||
createLogger({ collapsed: true })
|
||||
));
|
||||
|
||||
|
@ -73,7 +73,7 @@ export default withLanguage(props => {
|
||||
{ article.shortDescription }
|
||||
</Paragraph>
|
||||
|
||||
<NavLink to={ article.url } itemProps="sameAs">
|
||||
<NavLink to={ article.url } itemProp="sameAs">
|
||||
{ lang.blog.article.readMore }
|
||||
</NavLink>
|
||||
</div>
|
||||
|
97
public/objects/pagination/Pagination.jsx
Normal file
97
public/objects/pagination/Pagination.jsx
Normal file
@ -0,0 +1,97 @@
|
||||
// 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.
|
||||
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import PageBoundary from '@components/page/boundary/PageBoundary';
|
||||
|
||||
import Styles from './Pagination.scss';
|
||||
|
||||
|
||||
const PaginationLink = props => {
|
||||
let { to, page, children, current } = props;
|
||||
|
||||
let url = to;
|
||||
if(url.indexOf('$page') !== -1) {
|
||||
url = url.replace('$page', page);
|
||||
} else {
|
||||
if(!url.endsWith("/")) url += '/';
|
||||
url += page;
|
||||
}
|
||||
|
||||
let className = `o-pagination__link`;
|
||||
if(current && current == page) className += ` is-active`;
|
||||
|
||||
return (
|
||||
<NavLink to={ url } className={className}>
|
||||
{ children }
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default props => {
|
||||
//Where Page = current page,
|
||||
//pages = total pages and
|
||||
//to = url, with $page
|
||||
let { page, pages, to, className } = props;
|
||||
page = parseInt(page) || 1;
|
||||
pages = parseInt(pages) || 1;
|
||||
|
||||
let inners = [];
|
||||
|
||||
//Internal Numbers
|
||||
let numbers = [1, pages];//Always start with page 1 and pages
|
||||
//Now add numbers page-2, page-1,page(active),page+1, page+2
|
||||
for(let i = page-2; i <= page+2; i++) {
|
||||
if(i < 1) continue;//Don't add -2, -1, 0 etc
|
||||
if(i > pages) continue;//Don't go pages+1 for example 22 pages, 23
|
||||
numbers.push(i);
|
||||
}
|
||||
|
||||
//Uniqify and then sort.
|
||||
numbers = [...new Set(numbers)].sort((a,b) => a-b);
|
||||
|
||||
|
||||
|
||||
//Prev Button
|
||||
if(page > 1) {
|
||||
inners.push(<PaginationLink key="prev" to={to} page={page-1} children="<" />);
|
||||
}
|
||||
|
||||
numbers.forEach(i => {
|
||||
inners.push(<PaginationLink key={i} to={to} current={page} page={i} children={i} />);
|
||||
});
|
||||
|
||||
//Next Button
|
||||
if(page < pages-1) {
|
||||
inners.push(<PaginationLink key="next" to={to} page={page+1} children=">" />);
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={`o-pagination ${className}`}>
|
||||
{ inners }
|
||||
</nav>
|
||||
);
|
||||
};
|
36
public/objects/pagination/Pagination.scss
Normal file
36
public/objects/pagination/Pagination.scss
Normal file
@ -0,0 +1,36 @@
|
||||
@import '~@styles/global';
|
||||
|
||||
$o-pagination--text: black;
|
||||
$o-pagination--background: white;
|
||||
|
||||
.o-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
&__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 0.5em;
|
||||
min-width: 2.5em;
|
||||
min-height: 2.5em;
|
||||
border: 1px solid $o-pagination--text;
|
||||
|
||||
color: $o-pagination--text;
|
||||
background: $o-pagination--background;
|
||||
transition: transform 0.2s $s-animation--ease-out;
|
||||
|
||||
&:hover {
|
||||
color: $o-pagination--background;
|
||||
background: $o-pagination--text;
|
||||
transform: translateY(-0.1em);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: $o-pagination--background;
|
||||
background: $o-pagination--text;
|
||||
}
|
||||
}
|
||||
}
|
@ -24,32 +24,40 @@
|
||||
import React from 'react';
|
||||
|
||||
import { withLanguage } from '@public/language/Language';
|
||||
import { withBlogTemplate} from '@public/blog/Blog';
|
||||
import Page, { PageBoundary } from '@components/page/Page';
|
||||
|
||||
import FeaturedArticleSection from '@sections/blog/article/FeaturedArticleSection';
|
||||
import ArticleGridSection from '@sections/blog/article/ArticleGridSection';
|
||||
import ClearSection from '@sections/layout/ClearSection';
|
||||
|
||||
import Loader from '@objects/loading/Loader';
|
||||
import Pagination from '@objects/pagination/Pagination';
|
||||
|
||||
import Styles from './BlogPage.scss';
|
||||
|
||||
const TestBlogData = {
|
||||
handle: "test-blog",
|
||||
title: "Test Blog Article",
|
||||
url: '/',
|
||||
image: require('@assets/images/photo.jpg'),
|
||||
shortDescription: `Read how the latest lorem ipsum is dolor sit amet for business owners...`,
|
||||
description: `Est magna esse amet admodum est ex noster elit quem probant, id qui minim
|
||||
possumus, ut esse enim esse senserit. Ullamco quae quis incurreret dolore.
|
||||
Laborum est ingeniis, quibusdam fugiat non deserunt adipisicing.Nam quid velit
|
||||
aut litteris, laborum export incididunt admodum et nam fabulas instituendarum,
|
||||
id nam praesentibus. Aliquip anim consequat, est export commodo praetermissum, e
|
||||
ab multos ingeniis ut ipsum ab laborum de tamen. Sed quem proident fidelissimae,
|
||||
quae te singulis o ita sint culpa qui ingeniis, e export officia. Quem vidisse
|
||||
ut quis aliqua.`
|
||||
};
|
||||
export default withBlogTemplate(withLanguage(props => {
|
||||
let { lang, articles, page, pages, pending, error } = props;
|
||||
|
||||
export default withLanguage(props => {
|
||||
let { lang } = props;
|
||||
console.log(props);
|
||||
|
||||
let children;
|
||||
|
||||
if(error) error = "An error occured";
|
||||
if(pending) pending = <Loader />;
|
||||
|
||||
if(articles && articles.length) {
|
||||
children = (
|
||||
<React.Fragment>
|
||||
<FeaturedArticleSection article={ articles.shift() } />
|
||||
<ArticleGridSection articles={ articles } />
|
||||
<Pagination page={ page } pages={ pages } to="/blog/$page" />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
*/
|
||||
|
||||
return (
|
||||
<Page
|
||||
@ -57,9 +65,10 @@ export default withLanguage(props => {
|
||||
background={require('@assets/images/banners/sunset.svg')}
|
||||
>
|
||||
<ClearSection />
|
||||
{/* First (Featured) Blog */}
|
||||
<FeaturedArticleSection article={ TestBlogData } />
|
||||
<ArticleGridSection articles={[ TestBlogData, TestBlogData, TestBlogData, TestBlogData, TestBlogData]} />
|
||||
{ error }
|
||||
{ pending }
|
||||
{ children }
|
||||
<ClearSection />
|
||||
</Page>
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
Reference in New Issue
Block a user