feat(route): add new route

add /topic/:slug route both in api as well as in view
This commit is contained in:
zyachel 2022-05-22 19:35:02 +05:30
parent 6ad2269951
commit 0a35cdaa15
10 changed files with 490 additions and 4 deletions

View file

@ -3,6 +3,7 @@
////////////////////////////////////////////////////////
import catchAsyncErrors from '../utils/catchAsyncErrors.js';
import getAnswers from '../fetchers/getAnswers.js';
import getTopic from '../fetchers/getTopic.js';
////////////////////////////////////////////////////////
// EXPORTS
@ -20,6 +21,11 @@ export const answers = catchAsyncErrors(async (req, res, next) => {
res.status(200).json({ status: 'success', data });
});
export const topic = catchAsyncErrors(async (req, res, next) => {
const data = await getTopic(req.params.slug);
res.status(200).json({ status: 'success', data });
});
export const unimplemented = (req, res, next) => {
res.status(503).json({
status: 'fail',

View file

@ -4,6 +4,7 @@
////////////////////////////////////////////////////////
import catchAsyncErrors from '../utils/catchAsyncErrors.js';
import getAnswers from '../fetchers/getAnswers.js';
import getTopic from '../fetchers/getTopic.js';
import { nonSlugRoutes } from '../utils/constants.js';
////////////////////////////////////////////////////////
@ -29,6 +30,12 @@ export const answers = catchAsyncErrors(async (req, res, next) => {
});
});
export const topic = catchAsyncErrors(async (req, res, next) => {
const topicData = await getTopic(req.params.slug);
res.status(200).render('topic', { title: topicData.name, data: topicData });
});
export const unimplemented = (req, res, next) => {
res.status(503).render('error', {
title: 'Not yet implemented',

59
fetchers/getTopic.js Normal file
View file

@ -0,0 +1,59 @@
////////////////////////////////////////////////////////
// IMPORTS
////////////////////////////////////////////////////////
import AppError from '../utils/AppError.js';
import fetcher from './fetcher.js';
////////////////////////////////////////////////////////
// FUNCTION
////////////////////////////////////////////////////////
const getTopic = async slug => {
// getting data and destructuring it in case it exists, else throwing an error
const res = await fetcher(`topic/${slug}`);
if (!Object.entries(res).length) throw new Error('no data received!');
const {
data: { topic: rawData },
} = JSON.parse(res);
if (!rawData)
throw new AppError("couldn't find such a topic. Maybe check the URL?", 400);
const data = {
tid: rawData.tid,
name: rawData.name,
url: rawData.url,
image: rawData.photoUrl,
aliases: rawData.aliases,
numFollowers: rawData.numFollowers,
numQuestions: rawData.numQuestions,
// isLocked: rawData.isLocked,
isAdult: rawData.adult,
mostViewedAuthors: rawData.mostViewedAuthors.map(author => ({
uid: author.user.uid,
name: `${author.user.names[0].givenName} ${author.user.names[0].familyName}`,
profile: author.user.profileUrl,
avatar: author.user.profileImageUrl,
isAnon: author.user.isAnon,
isVerified: author.user.isVerified,
numFollowers: author.user.followerCount,
numViews: author.numViews,
numAnswers: author.numPublicMostViewedAnswers,
credential: author.user.bestCredential?.translatedString,
})),
relatedTopics: rawData.relatedTopics.map(topic => ({
tid: topic.tid,
name: topic.name,
url: topic.url,
image: topic.photoUrl,
numFollowers: topic.numFollowers,
})),
};
return data;
};
////////////////////////////////////////////////////////
// EXPORTS
////////////////////////////////////////////////////////
export default getTopic;

View file

@ -66,6 +66,12 @@
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-clock-edit">
<path d="M21 13.1C20.9 13.1 20.7 13.2 20.6 13.3L19.6 14.3L21.7 16.4L22.7 15.4C22.9 15.2 22.9 14.8 22.7 14.6L21.4 13.3C21.3 13.2 21.2 13.1 21 13.1M19.1 14.9L13 20.9V23H15.1L21.2 16.9L19.1 14.9M11 21.9C5.9 21.4 2 17.1 2 12C2 6.5 6.5 2 12 2C17.3 2 21.6 6.1 22 11.3C21.7 11.2 21.4 11.1 21 11.1C20.2 11.1 19.6 11.5 19.2 11.9L16.5 14.6L12.5 12.2V7H11V13L15.4 15.7L11 20.1V21.9Z"></path>
</symbol>
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" id="icon-user">
<path d="M224 256c70.7 0 128-57.31 128-128s-57.3-128-128-128C153.3 0 96 57.31 96 128S153.3 256 224 256zM274.7 304H173.3C77.61 304 0 381.6 0 477.3c0 19.14 15.52 34.67 34.66 34.67h378.7C432.5 512 448 496.5 448 477.3C448 381.6 370.4 304 274.7 304z"></path>
</symbol>
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" id="icon-question">
<path d="M204.3 32.01H96c-52.94 0-96 43.06-96 96c0 17.67 14.31 31.1 32 31.1s32-14.32 32-31.1c0-17.64 14.34-32 32-32h108.3C232.8 96.01 256 119.2 256 147.8c0 19.72-10.97 37.47-30.5 47.33L127.8 252.4C117.1 258.2 112 268.7 112 280v40c0 17.67 14.31 31.99 32 31.99s32-14.32 32-31.99V298.3L256 251.3c39.47-19.75 64-59.42 64-103.5C320 83.95 268.1 32.01 204.3 32.01zM144 400c-22.09 0-40 17.91-40 40s17.91 39.1 40 39.1s40-17.9 40-39.1S166.1 400 144 400z"></path>
</symbol>
<!-- not yet used -->
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-cancel">

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before After
Before After

View file

@ -1,12 +1,17 @@
import express from 'express';
import { about, unimplemented, answers } from '../controllers/apiController.js';
import {
about,
unimplemented,
answers,
topic,
} from '../controllers/apiController.js';
const apiRouter = express.Router();
apiRouter.get('/', about);
apiRouter.get('/search', unimplemented);
apiRouter.get('/profile/:name', unimplemented);
apiRouter.get('/topic/:name', unimplemented);
apiRouter.get('/topic/:slug', topic);
apiRouter.get('/unanswered/:slug', answers);
apiRouter.get('/:slug', answers);

View file

@ -1,8 +1,9 @@
import express from 'express';
import {
about,
answers,
privacy,
answers,
topic,
unimplemented,
} from '../controllers/viewController.js';
@ -12,7 +13,7 @@ viewRouter.get('/', about);
viewRouter.get('/privacy', privacy);
viewRouter.get('/search', unimplemented);
viewRouter.get('/profile/:name', unimplemented);
viewRouter.get('/topic/:name', unimplemented);
viewRouter.get('/topic/:slug', topic);
viewRouter.get('/unanswered/:slug', answers);
viewRouter.get('/:slug', answers);

75
views/pug/topic.pug Normal file
View file

@ -0,0 +1,75 @@
extends base
mixin formatNumber(number, ...classes)
span(class=classes.join(' '))= new Intl.NumberFormat().format(number)
block content
main#main.main.topic
//- all info related to the topic
.topic__stats.topic-stats
h1.heading.heading__primary.topic__heading.topic-stats__heading= data.name
img.topic-stats__image(src=data.image alt=`image depicting ${data.name}`)
if data.aliases.length
p.topic-stats__aliases
span.topic-stats__aliases-text Also known as:&nbsp;
span.topic-stats__aliases-list= data.aliases.join(', ')
.topic-stats__metadata
p.topic-stats__metadata-item
svg.icon.topic-stats__icon: use(href='/misc/sprite.svg#icon-user')
+formatNumber(data.numFollowers, 'topic-stats__metadata-data')
span.topic-stats__metadata-text &nbsp;Followers
p.topic-stats__metadata-item
svg.icon.topic-stats__icon: use(href='/misc/sprite.svg#icon-question')
+formatNumber(data.numQuestions, 'topic-stats__metadata-data')
span.topic-stats__metadata-text &nbsp;Questions
if data.isAdult
p.topic-stats__metadata-item
svg.icon.topic-stats__icon: use(href='/misc/sprite.svg#icon-danger')
span.topic-stats__metadata-data 18+
span.topic-stats__metadata-text &nbsp;Adult Topic
a.topic__link(href='https://www.quora.com' + data.url) View on Quora
.topic__famous-authors.famous-authors
h2.heading.heading__secondary.famous-authors__heading Most viewed authors
.famous-authors__list
each author in data.mostViewedAuthors
figure.famous-authors__author
figcaption.famous-authors__author-name
if author.isAnon
span Anonymous
else
a.topic__link(href=author.profile)= author.name
if author.isVerified
svg.famous-authors__icon
title verified
use(href='/misc/sprite.svg#icon-verified')
img.famous-authors__author-image(src=author.avatar, alt=`${author.name}'s profile photo`)
if author.credential
p.famous-authors__author-credentials(aria-label=`${author.name}'s credentials`)= author.credential || ''
.famous-authors__metadata
p.famous-authors__metadata-item
svg.icon.famous-authors__icon: use(href='/misc/sprite.svg#icon-user')
+formatNumber(author.numFollowers, 'famous-authors__metadata-data')
span.famous-authors__metadata-text &nbsp;Followers
p.famous-authors__metadata-item
svg.icon.famous-authors__icon: use(href='/misc/sprite.svg#icon-eye')
+formatNumber(author.numViews, 'famous-authors__metadata-data')
span.famous-authors__metadata-text &nbsp;Views
p.famous-authors__metadata-item
svg.icon.famous-authors__icon: use(href='/misc/sprite.svg#icon-comments')
+formatNumber(author.numAnswers, 'famous-authors__metadata-data')
span.famous-authors__metadata-text &nbsp;Answers
.topic__related-topics.related-topics
h2.heading.heading__secondary.related-topics__heading Related Topics
.related-topics__list
each topic in data.relatedTopics
.related-topics__topic
a.topic__link.related-topics__topic-name(href=topic.url)= topic.name
img.related-topics__image(src=topic.image alt=`image depicting ${topic.name}`)
.related-topics__metadata
p.related-topics__metadata-item
svg.icon.related-topics__icon: use(href='/misc/sprite.svg#icon-user')
+formatNumber(topic.numFollowers, 'related-topics__metadata-data')
span.topic-stats__metadata-text &nbsp;Followers

View file

@ -31,6 +31,7 @@ $misc-vars: (
fs-400: 4rem,
fs-500: 5rem,
fs-600: 6rem,
fs-800: 8rem,
fs-1000: 10rem,
fs-1500: 15rem,
space-050: 0.5rem,

View file

@ -469,3 +469,283 @@
font-weight: 500;
}
}
////////////////////////////////////////////////////////
// TOPIC STATS
////////////////////////////////////////////////////////
.topic-stats {
display: grid;
gap: var(--space-050) var(--space-200);
grid-template-columns: auto 1fr;
grid-template-rows: repeat(3, auto);
&__heading {
grid-column: 2 / -1;
align-self: end;
line-height: 1;
@include respond-to(bp-450) {
align-self: center;
}
}
&__image {
grid-row: 1 / -1;
grid-column: 1 / span 1;
max-height: var(--fs-1000);
max-width: var(--fs-1000);
margin-block: auto;
object-fit: contain;
@include respond-to(bp-450) {
max-height: var(--fs-800);
max-width: var(--fs-800);
}
}
&__aliases {
@include respond-to(bp-450) {
grid-column: 1 / -1;
}
}
&__metadata {
display: flex;
gap: var(--space-500);
align-items: center;
flex-wrap: wrap;
@include respond-to(bp-750) {
// margin-top: var(--space-100);
grid-column: 1 / -1;
}
@include respond-to(bp-650) {
gap: var(--space-200);
}
}
&__metadata-item {
display: grid;
grid-template-columns: repeat(2, auto);
gap: 0 var(--space-050);
}
&__icon {
grid-column: 1 / span 1;
justify-self: end;
align-self: center;
height: 1.3em;
width: 1.3em;
fill: var(--clr-base-icon);
}
&__metadata-data {
grid-column: -2 / -1;
justify-self: start;
}
&__metadata-text {
grid-row: 2 / span 1;
grid-column: 1 / -1;
justify-self: center;
font-size: 0.9em;
color: var(--clr-base-text-alt-alpha);
}
@include respond-to(bp-750) {
grid-template-rows: repeat(2, auto);
row-gap: var(--space-100);
}
@include respond-to(bp-450) {
grid-template-rows: auto;
}
}
////////////////////////////////////////////////////////
// FAMOUS AUTHORS
////////////////////////////////////////////////////////
.famous-authors {
display: grid;
gap: var(--space-300);
&__list {
display: grid;
gap: var(--space-500);
}
&__author {
display: grid;
gap: var(--space-050) var(--space-100);
grid-template-columns: auto 1fr;
grid-template-rows: repeat(3, min-content);
font-size: var(--fs-160);
@include respond-to(bp-750) {
grid-template-rows: repeat(2, min-content);
}
}
&__author-name {
grid-column: 2 / -1;
line-height: 1;
align-self: center;
color: var(--clr-base-heading-alt-alpha);
font-weight: 500;
// for verified icon
display: flex;
gap: var(--space-050);
// for name linking to profile
a {
font-size: 1.05em;
color: currentColor;
}
}
&__author-credentials {
grid-column: 2 / -1;
word-break: break-word;
}
&__author-image {
margin-block: auto;
grid-row: 1 / -1;
grid-column: 1 / span 1;
max-height: var(--fs-800);
max-width: var(--fs-800);
object-fit: cover;
clip-path: circle(50% at 50% 50%);
@include respond-to(bp-750) {
max-height: var(--fs-600);
max-width: var(--fs-600);
}
}
&__metadata {
display: flex;
gap: var(--space-200);
align-items: center;
flex-wrap: wrap;
@include respond-to(bp-750) {
margin-top: var(--space-100);
grid-column: 1 / -1;
}
}
&__metadata-item {
display: grid;
grid-template-columns: repeat(2, auto);
gap: 0 var(--space-050);
}
&__icon {
grid-column: 1 / span 1;
justify-self: end;
align-self: center;
height: 1em;
width: 1em;
fill: var(--clr-base-icon);
}
&__metadata-data {
grid-column: -2 / -1;
justify-self: start;
}
&__metadata-text {
grid-row: 2 / span 1;
grid-column: 1 / -1;
justify-self: center;
line-height: 1;
font-size: 0.9em;
color: var(--clr-base-text-alt-alpha);
}
}
////////////////////////////////////////////////////////
// RELATED TOPICS
////////////////////////////////////////////////////////
.related-topics {
display: grid;
gap: var(--space-300);
&__list {
display: grid;
gap: var(--space-500);
}
&__topic {
display: grid;
gap: 0 var(--space-200);
grid-template-columns: auto 1fr;
grid-template-rows: repeat(2, min-content);
}
&__topic-name {
grid-column: 2 / -1;
justify-self: start;
font-weight: 500;
font-size: 1.05em;
}
&__image {
grid-row: 1 / -1;
max-height: var(--fs-800);
max-width: var(--fs-800);
min-height: 100%;
min-width: 100%;
object-fit: contain;
@include respond-to(bp-750) {
max-height: var(--fs-600);
max-width: var(--fs-600);
}
}
&__metadata {
grid-row: -2 / -1;
justify-self: start;
}
&__metadata-item {
display: grid;
grid-template-columns: repeat(2, auto);
gap: 0 var(--space-050);
}
&__icon {
grid-column: 1 / span 1;
justify-self: end;
align-self: center;
height: 1em;
width: 1em;
fill: var(--clr-base-icon);
}
&__metadata-data {
grid-column: -2 / -1;
justify-self: start;
}
&__metadata-text {
grid-row: 2 / span 1;
grid-column: 1 / -1;
justify-self: center;
line-height: 1;
font-size: 0.9em;
color: var(--clr-base-text-alt-alpha);
}
}

View file

@ -280,3 +280,49 @@
padding-inline: var(--space-200);
}
}
////////////////////////////////////////////////////////
// TOPIC
////////////////////////////////////////////////////////
.topic {
// justify-self: center;
padding: var(--space-800);
display: grid;
grid-template-columns: 2fr 1.2fr;
grid-template-rows: min-content 1fr;
grid-auto-flow: dense;
align-items: start;
gap: var(--space-800);
&__stats {
grid-column: 1 / -1;
}
&__heading {
font-size: var(--fs-300);
@include respond-to(bp-550) {
font-size: var(--fs-270);
}
}
&__link {
@include format-link(var(--clr-base-link), var(--clr-base-link-alt-alpha));
}
@include respond-to(bp-1200) {
display: flex;
flex-direction: column;
// gap: var(--space-800);
}
@include respond-to(bp-900) {
padding: var(--space-500);
gap: var(--space-500);
}
@include respond-to(bp-550) {
padding-inline: var(--space-200);
}
}