Grouping my data in the view - nodejs - node.js

I grab some data from postgresql DB and want to display it in a view, simple enough as below
routes.js
app.get('/fixtures', async (req, res) => {
const fixtures = await queries.getFixtures();
res.render('fixtures', { fixtures });
});
fixtures returns
[ { id: 27,
home_team: 'Chelsea',
away_team: 'Liverpool',
league_name: 'English Premiership',
},
{ id: 25,
home_team: 'Man Utd',
away_team: 'Everton',
league_name: 'English Premiership',
},
{ id: 30,
home_team: 'Istanbul Basaksehir',
away_team: 'Akhisar Belediye',
league_name: 'Turkish Super Lig',
}
]
getFixtures();
async function getFixtures() {
let response;
try {
response = await pool.query('select * from fixtures ORDER BY league_name ASC');
} catch (e) {
console.error('Error Occurred', e);
}
return response.rows;
}
fixtures.ejs
<% fixtures.forEach((fixture) => { %>
<p><%=fixture.league_name %></p>
<p><%= fixture.home_team %> vs <%= fixture.away_team %> </p>
<% }) %>
So the above will output
English Premiership
Chelsea vs Liverpool
English Premiership
Man Utd v Everton
Turkish Super Lig
Istanbul Basaksehir vs Akhisar Belediye
However I would like to group my fixtures by league and would prefer to have the view output as
English Premiership
Chelsea vs Liverpool
Man Utd v Everton
Turkish Super Lig
Istanbul Basaksehir vs Akhisar Belediye
How do I go about achieving this? Is this something at DB query level or something I do in the view? (though probably not the best place to keep logic I guess)
Thanks

I'm not sure what tools/libs you are using but without depending on any libs/tools you can do it using vanilla JavaScript. For example, check the following code:
app.get('/fixtures', async (req, res) => {
const fixtures = await queries.getFixtures();
const grouped = groupByLeagueName(fixtures);
res.render('fixtures', { fixtures: grouped });
});
Then, implement the groupByLeagueName function like this:
function groupByLeagueName(fixtures) {
return fixtures.reduce((result, item) => {
result[item.league_name] = result[item.league_name] || [];
result[item.league_name].push(item);
return result;
}, {});
}
Then in your view you may loop it something like this:
<% for (let leagueName in fixtures) { %>
<p><%= leagueName %></p>
<% fixtures[leagueName].forEach((match, key) => { %>
<p><%= match.home_team %> vs <%= match.away_team %> </p>
<% }) %>
<% } %>

Related

Issues displaying data in node js , ejs

I am a newbie
i have been trying to fetch data from an API using node js express and ejs.
My problem is that i cannot display the data in my page.
looks like something is wrong with the way that i render the data . when i console.log my response, i have access to it but it does not seem to want to display
can you help me plz.
I tried to fetch the data like this :
const fetch = require('node-fetch');
exports.allCarte = async (req, res) => {
fetch('https://pokeapi-enoki.netlify.app/')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then((response) => {
console.log(response)
res.render("../views/home",{
response
})
});
}
and here is what I did in the home.ejs :
<body>
<main>
<ul>
<% for(var i = 0; i < response.length; i++) {%>
<li><%=response[i].name %></li>
<% } %>
<h1>Hi, there!</h1>
</ul>
</main>
<
If you have a look at results from the https://pokeapi-enoki.netlify.app/ API, it is structured something like this:
{
"pokemons": [
{
"id": 1,
"name": "germignon",
...
},
{
"id": 2,
"name": "kaiminus",
...
...which means the array of interest is response.pokemons not just response.
Your home.ejs should be changed like this:
<% for(var i = 0; i < response.pokemons.length; i++) {%>
<li><%=response.pokemons[i].name %></li>
<% } %>

Updating array of objects in mongoose based off key in objects value

I have two ejs forms that when hit, make an HTTP post request to my /api/users/makePicks/:id route. This route hits my controller which updates the Users model in my mongodb with the NFL picks they submitted in the EJS form.
I need this route to create the picks object for each route if they do not exist for that particular week, and if they do exist it needs to update the picks that are already there. The picks are being stored in my User model in an array, this array contains objects for each weeks picks. Currently the code, with much help from Mohammed, is successfully pushing code to to array. But i cannot seem to figure out how to update the picks if an object with a key of that week exists.
My validation is finally working properly. What I mean is we are running a for loop on the picks array, it will console.log true if there is already a matching picks object with for that weeks picks, if the object with a first key value with the current weeks form doesn't exist, it will console.log false and push the new picks to the array.
The only part that isn't working is the if statement nested within my for loop, it is not updating the object if it already exists in the picks.array. But as I said, the validation is working correctly. I suspect the line of code
result.picks[i] = { [`week-${req.params.week}`]:req.body };
is for some reason not updating object with the updated req.body.
Controller
exports.makePicks = async (req, res, next) => {
const picks = req.body;
try {
let result = await User.findById(req.user._id);
if (result.picks.length > 0) {
for (let i = 0; i < result.picks.length; i++) {
if ((Object.keys(result.picks[i])[0] == [`week-${req.params.week}`])) {
console.log(chalk.green("true"));
result.picks[i] = { [`week-${req.params.week}`]:req.body };
break;
} else {
console.log(chalk.red("false"));
result.picks.push({ [`week-${req.params.week}`]: picks });
break;
}
}
} else {
result.picks.push({ [`week-${req.params.week}`]: picks });
console.log(chalk.yellow('results.picks is empty'))
}
await result.save();
res.redirect("/api/dashboard");
} catch (error) {
console.log(error);
}
};
result.picks example structure
[{"week-1":
{"jojo-0":"ARI","jojo-1":"ARI","jojo-2":"ARI"}
},
{"week-2":
{"jojo-0":"ATL","jojo-1":"ATL","jojo-2":"BAL"}
},
{"week-3":
{"jojo-0":"ARI","jojo-1":"ARI","jojo-2":"ARI"}
}]
Router
router.route('/makePicks/:week')
.post(controller.makePicks);
EJS
<% const teamsArr = ['ARI', 'ATL', 'BAL', 'BUF', 'CAR', 'CHI', 'CIN', 'CLE', 'DAL', 'DEN', 'DET', 'GB', 'HOU', 'IND', 'JAX',
'KC', 'LAC', 'LAR', 'LV', 'MIA', 'MIN', 'NE', 'NO', 'NYG','NYJ', 'PHI', 'PIT', 'SEA', 'SF', 'TB', 'TEN', 'WAS' ] %>
<form class="mt-3 mb-3" method="POST" action="/api/users/makePicks/1">
<% for(i=0; i < user.bullets; i++){ %>
<div class="form-group">
<label for="<%= `${user.name}-${i}` %>">Make your pick for bullet <%= `${i}` %></label>
<select class="form-control" name="<%= `${user.name}-${i}` %>" id="<%= `${user.name}-${i}` %>">
<% teamsArr.forEach(team => { %>
<option value="<%= team %>"><%= team %></option>
<% }) %>
</select>
</div>
<% }; %>
<button type="submit" class="btn btn-primary">Save changes</button>
</form>
<form class="mt-3 mb-3" method="POST" action="/api/users/makePicks/2">
<% for(i=0; i < user.bullets; i++){ %>
<div class="form-group">
<label for="<%= `${user.name}-${i}` %>">Make your pick for bullet <%= `${i}` %></label>
<select class="form-control" name="<%= `${user.name}-${i}` %>" id="<%= `${user.name}-${i}` %>">
<% teamsArr.forEach(team => { %>
<option value="<%= team %>"><%= team %></option>
<% }) %>
</select>
</div>
<% }; %>
<button type="submit" class="btn btn-primary">Save changes</button>
</form>
you want to using $push and $set in one findByIdAndUpdate, that's impossible, I prefer use findById() and process and save() so just try
exports.makePicks = async (req, res, next) => {
const picks = req.body;
try {
//implementation business logic
let result = await User.findById(req.user._id)
if(result.picks && result.picks.length > 0){
result.picks.forEach(item =>{
if([`week-${req.params.week}`] in item){
item[`week-${req.params.week}`] = picks
}
else{
result.picks.push({ [`week-${req.params.week}`] : picks })
}
})
}
else{
result.picks.push({ [`week-${req.params.week}`] : picks })
}
await result.save()
res.redirect('/api/dashboard');
} catch (error) {
console.log(error)
}
}
note: don't use callback and async/await together
exports.makePicks = async (req, res, next) => {
const picks = req.body;
const { week } = req.params;
try {
let result = await User.findById(req.user._id);
const data = { [`week-${week}`]: picks };
const allPicks = [...result.picks];
if (allPicks.length > 0) {
// Search index of peek
const pickIndex = _.findIndex(allPicks, (pick) => {
return Object.keys(pick)[0] == `week-${week}`;
});
// If found, update it
if (pickIndex !== -1) {
console.log(chalk.green("true"));
allPicks[pickIndex] = data;
}
// Otherwise, push new pick
else {
console.log(chalk.red("false"));
allPicks.push(data);
}
} else {
allPicks.push(data);
console.log(chalk.yellow('results.picks is empty'))
}
result.picks = allPicks;
console.log('allPicks', allPicks);
await result.save();
res.redirect("/api/dashboard");
} catch (error) {
console.log(error);
}
};

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

Unable to fetch array into an EJS file

I want to display an appropriate error message that I am pushing in an array 'errorsList' and sending it to ideas/add.ejs as the context. In the EJS, i am iterating over errorsList and fetching the elements. The webpage displays an error saying errrorsList is not defined.
I have tried to pass the JSON directly instead of putting it into some variable just to check, still in that case it gives me an error as errorsList is not defined
App.js
// Process form
let errorsList = [];
if(!request.body.title) {
errorsList.push({ text: 'Please add a title' });
}
if(!request.body.details) {
errorsList.push({ text: 'Please add something' });
}
console.log('Errors List: ', errorsList);
if(errorsList.length > 0) {
const context = {
errorsList: errorsList,
title: request.body.title,
details: request.body.detail,
pageTitle: 'Ideas'
}
response.render('ideas/add', context);
}
else {
const context = {
pageTitle: 'Ideas'
};
response.render('ideas/ideas', context)
}
});```
add.ejs
<div class="container">
<% errorsList.forEach((error) => { %>
<div class="alert alert-danger">
<%= error.text %>
</div>
<% }) %>
Here i get the error as errorsList is not defined

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);
}
});

Resources