Finished building blog overview page.
This commit is contained in:
@ -45,6 +45,7 @@
|
||||
"nodemailer": "^4.6.8",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"pg-promise": "^8.5.1",
|
||||
"query-string": "^6.2.0",
|
||||
"react": "^16.5.2",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-helmet": "^5.2.0",
|
||||
@ -56,11 +57,14 @@
|
||||
"react-transition-group": "^2.5.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-promise-middleware": "^5.1.1",
|
||||
"redux-promise-middleware-actions": "^2.1.0",
|
||||
"responsive-loader": "^1.1.0",
|
||||
"sanitize-html": "^1.19.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sharp": "^0.21.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"uglifyjs-webpack-plugin": "^2.0.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.22.0"
|
||||
|
@ -94,7 +94,6 @@ class App {
|
||||
}
|
||||
|
||||
this.log('App ready');
|
||||
console.log(await this.articles.getArticlesByPage(2, 20));
|
||||
}
|
||||
|
||||
// Common Functions //
|
||||
|
@ -46,6 +46,12 @@ module.exports = class Articles extends DatabaseInterface {
|
||||
);
|
||||
}
|
||||
|
||||
async getArticlesPageCount(perPage) {
|
||||
if(!perPage) perPage = 10;
|
||||
let count = await this.getArticlesCount(perPage);
|
||||
return Math.ceil(count/perPage);
|
||||
}
|
||||
|
||||
async getArticlesByPage(page, perPage) {
|
||||
if(!page) page = 1;
|
||||
if(!perPage) perPage = 10;
|
||||
|
@ -1,5 +1,5 @@
|
||||
INSERT INTO "BlogArticles" (
|
||||
"handle", "image", "shortDescription", "description", "date"
|
||||
"handle", "title", "image", "shortDescription", "description", "date"
|
||||
) VALUES (
|
||||
${handle}, ${image}, ${shortDescription}, ${description}, ${date}
|
||||
${handle}, ${title}, ${image}, ${shortDescription}, ${description}, ${date}
|
||||
) RETURNING *;
|
||||
|
@ -44,7 +44,10 @@ module.exports = class GetBlogArticles extends APIHandler {
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
data: await request.getApp().getArticles().getArticlesByPage(page, perPage)
|
||||
data: {
|
||||
pages: await request.getApp().getArticles().getArticlesPageCount(perPage),
|
||||
articles: await request.getApp().getArticles().getArticlesByPage(page, perPage)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ const
|
||||
SharpLoader = require('responsive-loader/sharp'),
|
||||
HtmlWebpackPlugin = require('html-webpack-plugin'),
|
||||
CompressionPlugin = require("compression-webpack-plugin"),
|
||||
UglifyJsPlugin = require('uglifyjs-webpack-plugin'),
|
||||
TerserPlugin = require('terser-webpack-plugin'),
|
||||
MiniCssExtractPlugin = require("mini-css-extract-plugin"),
|
||||
OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")
|
||||
;
|
||||
@ -44,7 +44,7 @@ module.exports = (isDev) => {
|
||||
let config = {
|
||||
devtool: 'source-map',
|
||||
entry: [ `${base}/public/index.jsx` ],
|
||||
output: { path: `${base}/dist`, filename: "app.js" },
|
||||
output: { path: `${base}/dist`, filename: "app.js", publicPath: '/' },
|
||||
mode: isDev ? 'development' : 'production',
|
||||
resolve: {
|
||||
modules: [`${base}/node_modules`, `${base}/public`],
|
||||
@ -153,7 +153,7 @@ module.exports = (isDev) => {
|
||||
]
|
||||
};
|
||||
} else {
|
||||
let UglifyPluginConfig = new UglifyJsPlugin({
|
||||
let TerserPluginConfig = new TerserPlugin({
|
||||
test: /\.js($|\?)/i
|
||||
});
|
||||
|
||||
@ -172,7 +172,7 @@ module.exports = (isDev) => {
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
UglifyPluginConfig,
|
||||
TerserPluginConfig,
|
||||
MiniCssExtractConfig,
|
||||
new OptimizeCSSAssetsPlugin({}),
|
||||
]
|
||||
|
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