Expressjs Pagination Redirect - node.js

I have a pagination setup where my feed is displayed with the index route path / and then additional pages are accessed with /feed/:pageNumber. I have no issue with the pagination delivering the next and previous records, but when I want to have the ability on the last previousPage click to redirect to the / path since this has the most recent records. I tried to use res.redirect('/') on the else statement, but I get an error Error: Can't set headers after they are sent.. Is there a better approach to redirect to the home on the last previousPage click?
E.x. User is on '/', clicks Next and is sent to '/feed/2'. When User clicks Previous then they should be brought back to '/'. If they are on /feed/3,4,5,etc/ then it will bring the user to one less than the current parameter value.
Section I'm trying to fix:
if(req.params.pageNumber > 2){
res.locals.previous = true;
res.locals.previousPage = req.params.pageNumber - 1;
} else {
res.locals.previous = true;
res.locals.previous = res.redirect('/');
}
View:
<!DOCTYPE html>
<head>
{{> app/app-head}}
</head>
<body>
{{> app/app-navigation}}
<div class="container">
<h1 class="page-title-header">Activity Feed</h1>
{{> app/card}}
</div>
{{#if previous}}
Previous Page
{{/if}}
{{#if secondPage}}
Next Page
{{/if}}
{{#if next}}
Third Page
{{/if}}
</body>
Routes:
/*==== / ====*/
appRoutes.route('/')
.get(function(req, res){
models.Card.findAll({
order: 'cardDate DESC',
include: [{
model: models.User,
where: { organizationId: req.user.organizationId },
attributes: ['organizationId', 'userId']
}],
limit: 10
}).then(function(card){
function feedLength(count){
if (count >= 10){
return 2;
} else {
return null;
}
};
res.render('pages/app/high-level-activity-feed.hbs',{
card: card,
user: req.user,
secondPage: feedLength(card.length)
});
});
})
.post(function(req, res){
models.Card.create({
card: req.body.cardDate,
userId: req.user.userId
}).then(function() {
res.redirect('/app');
}).catch(function(error){
res.send(error);
})
});
appRoutes.route('/feed/:pageNumber')
.get(function(req, res){
function paginationPage(count){
if(count == 2){
return 10;
} else {
return (count - 1) * 10;
}
};
var skip = parseInt(req.params.pageNumber);
models.Card.findAll({
order: 'cardDate DESC',
include: [{
model: models.User,
where: { organizationId: req.user.organizationId },
attributes: ['organizationId', 'userId']
}],
offset: paginationPage(skip),
limit: 10
}).then(function(annotation){
if(annotation.length == 10){
res.locals.next = true;
res.locals.nextPage = parseInt(req.params.pageNumber) + 1;
console.log('This is the next page pagination: ' + res.locals.nextPage);
}
if(req.params.pageNumber > 2){
res.locals.previous = true;
res.locals.previousPage = req.params.pageNumber - 1;
} else {
res.locals.previous = true;
res.locals.previous = res.redirect('/');
}
res.render('pages/app/high-level-activity-feed.hbs',{
card: card,
user: req.user
});
});
})

The issue is two-fold:
your template always prefixes the previous page link with /feed/:
Previous Page
res.redirect('/') performs an actual redirect from within your Express server (it does not produce a link, or a client-side javascript)
A possible solution would be to add a third state to your template:
{{#if backHome}}
Previous Page
{{/if}}
And in your server code:
if (req.params.pageNumber > 2) {
res.locals.previous = true;
res.locals.previousPage = req.params.pageNumber - 1;
} else {
res.locals.backHome = true;
}

Related

Nodejs Pagination of API Call Using Offset

I apologize if this question is documented already, please point me to those resources.
I have a nodejs app making an api call to Untappd and per most public APIs, I'm restricted to a max number of items returned in the call, in this case 50 is the max. I'd like to set up the pagination using Offset (Skip) so that I could move through the 600+ items instead of just the 50.
What Works
I currently have the pagination working to view the first 50 items through these different pieces...
API call on server.js
const untappdAPI = { method: 'GET',
url: 'https://api.untappd.com/v4/user/beers/username',
qs:
{ access_token: 'abc123'
,limit:'50'
}
};
app.get with pagination on server.js
app.get('/untappd', function (req, res) {
try {
request(untappdAPI, function (error, response, body) {
if (error) throw new Error(error);
const untappdBeers = JSON.parse(body);
const utBeerList = untappdBeers.response.beers.items.map(item => item );
//pagination
const perPage = 5;
let currentPage = 1;
const totalBeerList = utBeerList.length;
const pageCount = Math.ceil(totalBeerList / perPage);
if(req.query.page) {
currentPage = parseInt(req.query.page, 10);
}
const start = (currentPage - 1) * perPage;
const end = currentPage * perPage;
res.render('untappd.ejs', {
utBeerList:utBeerList.slice(start, end),
perPage: perPage,
pageCount: pageCount,
currentPage: currentPage,
});
});
} catch(e) {
console.log("Something went wrong", e)
}
});
And then the items render on a page called untappd.ejs and the working pagination is provided with this code
EJS Client Pagination on untappd.ejs
<div id="pagination">
<% if (pageCount > 1) { %>
<ul class="pagination">
<% if (currentPage > 1) { %>
<li>«</li>
<% } %>
<% var i = 1;
if (currentPage > 5) {
i = +currentPage - 4;
} %>
<% if (i !== 1) { %>
<li>...</li>
<% } %>
<% for (i; i<=pageCount; i++) { %>
<% if (currentPage == i) { %>
<li class="active"><span><%= i %> <span class="sr-only">(current)</span></span></li>
<% } else { %>
<li><%= i %></li>
<% } %>
<% if (i == (+currentPage + 4)) { %>
<li>...</li>
<% break; } %>
<% } %>
<% if (currentPage != pageCount) { %>
<li>»</li>
<% } %>
</ul>
<% } %>
</div>
Again, the above code is all well and good, I get functioning pagination with 5 items per page and 10 pages to paginate through, but I am limited to these 50 items. From what I've read about offset, it seems offset would allow me to cycle through the entire set of 600+ items, but I can't find documentation/help that fits this particular scenario.
How do I incorporate 'offset' into what I'm working with in order to paginate through the full list of 600+ items?
Many thanks for your help!
Red
The documentation is quite straight forward (UNLESS you are trying something else... but I'm assuming /v4/search/beer), all you need to do is use the offset and limit that the API provides, that would work like:
offset (int, optional) - The numeric offset that you what results to start
limit (int, optional) - The number of results to return, max of 50, default is 25
this means that
if you want to search beers from 0 to 50, set: offset: 0, limit: 50
if you want to search beers from 51 to 100, set offset: 50, limit: 50
if you want to search beers from 101 to 150, set offset: 100, limit: 50
I would change your code as:
const untappdAPI = (name, currentPage, perPage) => ({ method: 'GET',
url: 'https://api.untappd.com/v4/search/beer',
qs: {
q: name,
access_token: 'abc123',
limit: perPage, // 50
offset: currentPage * perPage // 0*50 = 0 | 1*50 = 50 | 2*50 = 100
}
})
having that as a function, will allow you to simply pass parameters such as
app.get('/untappd', function(req, res) {
const { search, currentPage, perPage } = req.query
const url = untappdAPI(search, currentPage, perPage) // (currentPage - 1) if you start with 1
try {
request(url, (error, response, body) => {
if (error) throw new Error(error)
const apiRes = JSON.parse(body).response
const beers = apiRes.beers.items
const total = apiRes.found // outputs in the API, first item in the response
res.render('untappd.ejs', {
utBeerList: beers,
perPage: perPage,
pageCount: Math.ceil(total / perPage),
currentPage: currentPage,
})
})
} catch (err) {
console.log("Something went wrong", err.message)
}
});
The only thing left to do, is update the HTML, as you can see from the call, we need 3 parameters (you can make some static and do not give the user the option to change, for example, items per page, and always display 50 at a time...
»
Note
in your code, you are outputting const pageCount = Math.ceil(totalBeerList / perPage); but the API response gives you the total items that can be retrieved in the first item in the documentation response as the found variable
P.S. I've request access to the API to verify if all this works, I will update the answer soon I have a client id and a secret

how can I pass data like the name of my user and put it in the post they created

so I am making an application for events and for some reason when a user creates an event the even info shows but the user info like their name and photo doesn't show up please help I've been having this problem for almost a week now.
THIS IS THE componentDidMount function
async componentDidMount() {
const { data } = await getCategories();
const categories = [{ _id: "", name: "All Categories" }, ...data];
const { data: events } = await getEvents();
this.setState({ events, categories });
console.log(events);
}
THIS IS THE STATE
class Events extends Component {
state = {
events: [],
user: getUser(),
users: getUsers(),
showDetails: false,
shownEventID: 0,
showUserProfile: false,
shownUserID: 0,
searchQuery: ""
};
THIS IS THE EVENTS FILE WHERE THE USER'S NAME AND PHOTO SHOULD BE DISPLAYED
<Link>
<img
className="profilePic mr-2"
src={"/images/" + event.hostPicture}
alt=""
onClick={() => this.handleShowUserProfile(event.userId)}
/>
</Link>
<Link style={{ textDecoration: "none", color: "black" }}>
<h4
onClick={() => this.handleShowUserProfile(event.userId)}
className="host-name"
>
{getUser(event.userId).name}
</h4>
</Link>
This is the userService file where the getUser function is
import http from "./httpService";
const apiEndPoint = "http://localhost:3100/api/users";
export function register(user) {
return http.post(apiEndPoint, {
email: user.email,
password: user.password,
name: user.name
});
}
export function getUsers() {
return http.get(apiEndPoint);
}
export async function getUser(userId) {
const result = await http.get(apiEndPoint + "/" + userId);
return result.data;
}
This is the eventService file where the event is
import http from "./httpService";
const apiEndPoint = "http://localhost:3100/api/events";
export function getEvents() {
return http.get(apiEndPoint);
}
export function getEvent(eventId) {
return http.get(apiEndPoint + "/" + eventId);
}
export function saveEvent(event) {
if(event._id){
const body = {...event}
delete body._id
return http.put(apiEndPoint + '/' + event._id, body)
}
return http.post(apiEndPoint, event);
}
export function deleteEvent(eventId) {
return http.delete(apiEndPoint + "/" + eventId);
}
First, you have some mistakes to use the class in <div> elements.
please use className instead class.
And then second I am not sure what it is.
class Events extends Component {
state = {
... ...
user: getUser(),
... ...
};
As you seen getUser() function requires one parameter userId.
But you did not send this.
So you met internal server error to do it.
Since I did not investigate all projects, I could not provide perfectly solution.
However, it is main reason, I think.
Please check it.

How do I have a variable available to display on my success page, after adding items to a database via a /POST route?

I would like to display the doc.id variable of a successful /POST of data to a route, on the success page that the user will be redirected to afterward. I'm trying to work out how to carry the variable teamId through to the Handlebar template page success.hbs
I've tried making it a variable, and setting up a Handlebar helper to display it, but nothing is working.
/POST route redirecting to success.hbs:
app.post('/create', (req, res) => {
var players = [];
var playerObj = {};
for (let i = 1; i < 21; i++) {
var playerObj = { playerName: req.body[`player${i}Name`], playerNumber: req.body[`player${i}Number`], playerPosition: req.body[`player${i}Position`] };
if (req.body["player" + i + "Name"] === '') {
console.log("Empty player name detected, disregarding");
} else {
players.push(playerObj);
}
}
var newTeam = new Team({
// WEB SETUP BELOW
"team.teamRoster.teamCoach": req.body.coachName,
"team.shortTeamName": req.body.teamShortName,
"team.teamName": req.body.teamName,
"team.teamRoster.players": players
});
newTeam.save().then((doc) => {
var teamId = doc.id;
console.log(teamId);
res.render('success.hbs');
console.log("Team Added");
}, (e) => {
res.status(400).send(e);
});
});
/views/success.hbs
<div class="container-fluid" id="body">
<div class="container" id="page-header">
<h1><span id="headline">Team Added Succesfully</span></h1>
<hr>
<h3><span id="subheadline">Input the following address as a JSON Data Source within vMix.</span></h3>
<span id="content">
<div class="row">
<div class="container col-md-12">
{{{teamId}}}
</div>
</div>
</span>
</div>
<hr>
</div>
I'd like a Handlebar helper to get the doc.id value of the /POST request, and store it as teamId to display on the success page. It's finding nothing at the moment.
Any help is appreciated.
Node.js can pass variables to the handlebars-view like this:
newTeam.save().then((doc) => {
var teamId = doc.id;
console.log(teamId);
res.render('success.hbs', {
teamId
});
console.log("Team Added");
}, (e) => {
res.status(400).send(e);
});

Generating an array in the view and passing it into the controller

It's a little strange but I cannot think of a better way to get it done.
First of all this is my code:
The router to get the view in the first place
router.get('/', function(req, res, next) {
Account.findOne(
{
_id: req.user._id,
},
function(err, acc) {
if (err) {
console.log(err);
}
// console.log(acc.websites);
res.render('reports/index', {
title: 'Reports!',
websites: acc.websites,
user: req.user,
});
}
);
});
The view:
<% include ./../partials/header.ejs %>
<h1 class="text-center">This is your report page</h1>
<form method="post">
<% for(let i=0; i<websites.length; i++){ let website = websites[i]; %>
<fieldset>
<label for="website<%=i%>" class="col-sm-2">Website <%=i+1%></label>
<input name="website<%=i%>" id="website<%=i%>" value="<%=website%>" type="text" />
</fieldset>
<% } %>
Generate report
</form>
<% include ./../partials/footer.ejs %>
The router, that's supposed to fire up after the on click.
router.get('/reports', function(req, res, next) {
if (req.user.isPremium == false) {
// Free user - Single report
var builtWithCall = `https://api.builtwith.com/free1/api.json?KEY=00000000-0000-0000-0000-000000000000&LOOKUP=${website}`;
let website = req.body.website0;
console.log(website);
}
});
How it works: The controller finds the account, grabs an array from inside of it, sends it to the view. The view prints it out and allows to make some changes to the values.
And now is where the problems begin. I need to gather the new values into an array and send it to the next router, which will then use them to call a bunch of APIs and print out the data. How do I gather the array and pass it to the controller? Also, should I use GET or POST?
With your logic, the name attribute will have the following form: name=website[0...n]. With that in mind, we can filter out the keys to gather all the website[n] into an array you seek:
const example = {
website0: 'example',
website1: 'example',
website2: 'example',
website3: 'example',
shouldBeIgnored: 'ignoreMe',
ignore: 'shouldIgnore'
}
const websites = Object.keys(example).filter(key => key.startsWith('website'))
console.log(websites)
So you're controller can be:
router.get('/reports', (req, res, next) => {
if (!req.user.isPremium) {
// Free user - Single report
const builtWithCall = `https://api.builtwith.com/free1/api.json?KEY=00000000-0000-0000-0000-000000000000&LOOKUP=${website}`;
const websites = Object.keys(req.body).filter(key => key.startsWith('website'));
console.log(websites);
}
});

Template actual data context

I have troubles with data context;
Here is my code (unfortunatly, meteorpad is broken)
router.js(I use iron:router)
Router.configure({
layoutTemplate: 'layout'
});
Router.route('home',{
path: '/',
action: function(){
this.redirect('sections', {page: 0});
}
});
Router.route('sections', {
path: '/sections/:page',
data: function(){
var data = {};
data.params = {};
data.params.page = this.params.page?this.params.page:0;
return data;
}
});
template.html
<template name="layout">
{{>yield}}
</template>
<template name="sections">
Page: {{params.page}}
<br>
Page 0
Page 1
Page 2
<br>
<button>what page?</button>
</template>
template.js
Template.sections.onRendered(function(){
let scope = this;
$("button").on("click", function(){
alert("page: " + scope.data.params.page);
});
});
When I click button, button-handler has the scope, which had the template, when rendered, but not actual in this moment;
thanks to #user3374348
method Blaze.getData(scope.view) returns actual data context.
template.js
Template.sections.onRendered(function(){
let scope = this;
$("button").on("click", function(){
alert("page: " + Blaze.getData(scope.view).params.page);
});
});

Resources