diff --git a/README.md b/README.md index 3860a52..628e35c 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ From [their privacy policy](https://www.quora.com/about/privacy) ## To-Do -- [x] add missing routes like topics, profile, and search +- [x] add missing routes like topics and profile - [x] use redis - [x] serve images and other assets from Quetre - [x] implement a better installation method diff --git a/controllers/apiController.js b/controllers/apiController.js index 2957322..414c55c 100644 --- a/controllers/apiController.js +++ b/controllers/apiController.js @@ -7,9 +7,8 @@ import catchAsyncErrors from '../utils/catchAsyncErrors.js'; import getAnswers from '../fetchers/getAnswers.js'; import getTopic from '../fetchers/getTopic.js'; import getProfile from '../fetchers/getProfile.js'; -import getSearch from '../fetchers/getSearch.js'; import getOrSetCache from '../utils/getOrSetCache.js'; -import { answersKey, profileKey, searchKey, topicKey } from '../utils/cacheKeys.js'; +import { answersKey, profileKey, topicKey } from '../utils/cacheKeys.js'; //////////////////////////////////////////////////////// // EXPORTS @@ -18,7 +17,7 @@ export const about = (req, res, next) => { res.status(200).json({ status: 'success', message: `make a request. - available endpoints are: '/slug', '/unanswered/slug', '/topic/slug', '/profile/slug', '/search?q=query', /?q=query.`, + available endpoints are: '/slug', '/unanswered/slug', '/topic/slug', '/profile/slug'`, }); }; @@ -55,21 +54,6 @@ export const profile = catchAsyncErrors(async (req, res, next) => { res.status(200).json({ status: 'success', data }); }); -export const search = catchAsyncErrors(async (req, res, next) => { - const { - urlObj, - query: { lang }, - } = req; - - const searchText = urlObj.searchParams.get('q')?.trim(); // no search to perform if there isn't any query - let searchData = null; - - if (searchText) - searchData = await getOrSetCache(searchKey(urlObj), getSearch, urlObj.search, lang); - - res.status(200).json({ status: 'success', data: {searchData, searchText} }); -}); - export const unimplemented = (req, res, next) => { res.status(501).json({ status: 'fail', @@ -77,6 +61,14 @@ export const unimplemented = (req, res, next) => { }); }; +export const gone = (req, res, next) => { + res.status(501).json({ + status: 'fail', + message: "This route doesn't exist anymore.", + }); +}; + + export const image = catchAsyncErrors(async (req, res, next) => { const { domain, path } = req.params; if (!domain.endsWith('quoracdn.net')) { diff --git a/controllers/viewController.js b/controllers/viewController.js index 537f47b..734732f 100644 --- a/controllers/viewController.js +++ b/controllers/viewController.js @@ -7,9 +7,8 @@ import getAnswers from '../fetchers/getAnswers.js'; import getTopic from '../fetchers/getTopic.js'; import { acceptedLanguages, nonSlugRoutes } from '../utils/constants.js'; import getProfile from '../fetchers/getProfile.js'; -import getSearch from '../fetchers/getSearch.js'; import getOrSetCache from '../utils/getOrSetCache.js'; -import { answersKey, profileKey, searchKey, topicKey } from '../utils/cacheKeys.js'; +import { answersKey, profileKey, topicKey } from '../utils/cacheKeys.js'; //////////////////////////////////////////////////////// // EXPORTS @@ -101,28 +100,6 @@ export const profile = catchAsyncErrors(async (req, res, next) => { }); }); -export const search = catchAsyncErrors(async (req, res, next) => { - const { - urlObj, - query: { lang }, - } = req; - const searchText = urlObj.searchParams.get('q')?.trim(); - let searchData = null; - - if (searchText) - searchData = await getOrSetCache(searchKey(urlObj), getSearch, urlObj.search, lang); - - res.status(200).render('search', { - data: { searchData, searchText }, - meta: { - title: searchText ? `Results for '${searchText}'` : 'Search', - url: urlObj, - imageUrl: `${urlObj.origin}/icon.svg`, - description: searchText ? `results for '${searchText}'` : 'search page', - }, - }); -}); - const regex = /^https:\/\/(.{2,})\.quora\.com(\/.*)$/; // local helper constant export const redirect = (req, res, next) => { const url = req.originalUrl.replace('/redirect/', ''); // removing `/redirect/` part. @@ -158,3 +135,20 @@ export const unimplemented = (req, res, next) => { }, }); }; + +export const gone = (req, res, next) => { + const data = { + message: "This route doesn't exist anymore.", + statusCode: 410, + }; + + res.status(data.statusCode).render('error', { + data, + meta: { + title: 'Gone', + url: req.urlObj, + imageUrl: `${req.urlObj.origin}/icon.svg`, + description: data.message, + }, + }); +}; diff --git a/fetchers/getSearch.js b/fetchers/getSearch.js deleted file mode 100644 index 6c28e6c..0000000 --- a/fetchers/getSearch.js +++ /dev/null @@ -1,168 +0,0 @@ -//////////////////////////////////////////////////////// -// IMPORTS -//////////////////////////////////////////////////////// -import AppError from '../utils/AppError.js'; -import fetcher from './fetcher.js'; -import { quetrefy } from '../utils/urlModifiers.js'; - -//////////////////////////////////////////////////////// -// HELPER FUNCTIONS -//////////////////////////////////////////////////////// -const topicCleaner = topic => ({ - type: 'topic', - url: quetrefy(topic.url), - name: topic.name, - numFollowers: topic.numFollowers, - image: topic.photoUrl, - isSensitive: topic.isSensitive, -}); -const spaceCleaner = space => ({ - type: 'space', - numUsers: space.tribeUserCount, - url: quetrefy(space.url), - name: space.nameString, - description: space.descriptionString, - image: space.iconRetinaUrl, - isSensitive: space.isSensitive, -}); -const profileCleaner = profile => ({ - type: 'profile', - credential: profile.bestCredential?.translatedString, - isAnon: profile.isAnon, - name: `${profile.names[0]?.givenName} ${profile.names[0]?.familyName}`, - url: quetrefy(profile.profileUrl), - image: profile.profileImageUrl, - numFollowers: profile.followerCount, - isVerified: profile.isVerified, - isBusiness: profile.businessStatus, - isPlusUser: profile.consumerBundleActive, -}); -const questionCleaner = question => ({ - type: 'question', - text: JSON.parse(question.title).sections, - url: quetrefy(question.url), - isDeleted: question.isDeleted, - numFollowers: question.followerCount, - creationTime: question.creationTime, - numComments: question.numDisplayComments, - isSensitive: question.isSensitive, -}); -const answerCleaner = ({ question, previewAnswer: answer }) => ({ - type: 'answer', - question: { - ...questionCleaner(question), - }, - ...(answer.originalQuestionIfDifferent && { - originalQuestion: { - text: JSON.parse(answer.originalQuestionIfDifferent.question.title).sections, - url: quetrefy(answer.originalQuestionIfDifferent.question.url), - qid: answer.originalQuestionIfDifferent.question.qid, - }, - }), - isViewable: !!answer.viewerHasAccess, - text: JSON.parse(answer.content).sections, - creationTime: answer.creationTime, - updatedTime: answer.updatedTime, - numComments: answer.numDisplayComments, - numUpvotes: answer.numUpvotes, - numViews: answer.numViews, - numShares: answer.numShares, - numAnswerRequests: answer.numRequesters, - isBusinessAnswer: answer.businessAnswer, - url: quetrefy(answer.url), - isSensitive: answer.isSensitive, - author: { - uid: answer.author.uid, - isAnon: answer.author.isAnon, - image: answer.author.profileImageUrl, - isVerified: answer.author.isVerified, - isPlusUser: answer.author.consumerBundleActive, - url: quetrefy(answer.author.profileUrl), - name: `${answer.author.names[0].givenName} ${answer.author.names[0].familyName}`, - credential: answer.authorCredential?.translatedString, - }, -}); -const postCleaner = post => ({ - type: 'post', - pid: post.pid, - isViewable: post.viewerHasAccess, - url: quetrefy(post.url), - title: JSON.parse(post.title).sections, - isDeleted: post.isDeleted, - isSensitive: post.isSensitive, - text: JSON.parse(post.content).sections, - creationTime: post.creationTime, - updatedTime: post.updatedTime, - numComments: post.numDisplayComments, - numUpvotes: post.numUpvotes, - numViews: post.numViews, - numShares: post.numShares, - author: { - uid: post.author.uid, - isAnon: post.author.isAnon, - image: post.author.profileImageUrl, - isVerified: post.author.isVerified, - isPlusUser: post.author.consumerBundleActive, - url: quetrefy(post.author.profileUrl), - name: `${post.author.names[0].givenName} ${post.author.names[0].familyName}`, - credential: post.authorCredential?.translatedString, - }, - ...(post.tribeItem && { - space: { - isSensitive: post.tribeItem.tribe.isSensitive, - name: post.tribeItem.tribe.nameString, - url: quetrefy(post.tribeItem.tribe.url), - image: post.tribeItem.tribe.iconRetinaUrl, - description: post.tribeItem.descriptionString, - numFollowers: post.tribeItem.tribe.numFollowers, - }, - }), -}); - -const resultsCleaner = results => { - const cleanedResults = results.map(result => { - const resultToClean = result.node; - - if (resultToClean.topic) return topicCleaner(resultToClean.topic); - if (resultToClean.tribe) return spaceCleaner(resultToClean.tribe); - if (resultToClean.post) return postCleaner(resultToClean.post); - if (resultToClean.user) return profileCleaner(resultToClean.user); - if (resultToClean.previewAnswer) return answerCleaner(resultToClean); - if (resultToClean.question) return questionCleaner(resultToClean.question); - - return {}; - }); - - return cleanedResults; -}; - -//////////////////////////////////////////////////////// -// FUNCTION -//////////////////////////////////////////////////////// -const KEYWORD = 'searchConnection'; - -const getSearch = async (querySlug, lang) => { - const options = { keyword: KEYWORD, lang, toEncode: false }; - const res = await fetcher(`search/${querySlug}`, options); - - const { - data: { [KEYWORD]: rawData }, - } = JSON.parse(res); - - if (!rawData) - throw new AppError( - "Search couldn't be done. Recheck the URL, or resend the request if you believe the URL is correct.", - 404 - ); - - const data = { - results: resultsCleaner(rawData.edges), - }; - - return data; -}; - -//////////////////////////////////////////////////////// -// EXPORTS -//////////////////////////////////////////////////////// -export default getSearch; diff --git a/routes/apiRoutes.js b/routes/apiRoutes.js index ef2baaa..6001b31 100644 --- a/routes/apiRoutes.js +++ b/routes/apiRoutes.js @@ -6,13 +6,13 @@ import { topic, image, profile, - search, + gone, } from '../controllers/apiController.js'; const apiRouter = express.Router(); -apiRouter.get('/(|search)', search); -apiRouter.get('/about', about); +apiRouter.get('/search', gone); +apiRouter.get('/(|about)', about); apiRouter.get('/image/:domain/:path', image); apiRouter.get('/profile/:name', profile); apiRouter.get('/topic/:slug', topic); diff --git a/routes/viewRoutes.js b/routes/viewRoutes.js index 36c0cf9..e30bb9c 100644 --- a/routes/viewRoutes.js +++ b/routes/viewRoutes.js @@ -6,14 +6,14 @@ import { topic, unimplemented, profile, - search, + gone, redirect, } from '../controllers/viewController.js'; const viewRouter = express.Router(); -viewRouter.get('/(|search)', search); // search on / or /search -viewRouter.get('/about', about); +viewRouter.get('/search', gone); +viewRouter.get('/(|about)', about); viewRouter.get('/privacy', privacy); viewRouter.get('/profile/:name', profile); viewRouter.get('/topic/:slug', topic); diff --git a/utils/cacheKeys.js b/utils/cacheKeys.js index 5ec4c65..e39a5af 100644 --- a/utils/cacheKeys.js +++ b/utils/cacheKeys.js @@ -8,11 +8,6 @@ const formatSlug = (slug, charToRemove) => //////////////////////////////////////////////////////// // EXPORTS //////////////////////////////////////////////////////// -export const searchKey = urlObj => { - const slug = formatSlug(urlObj.search, '?'); - return `search:${slug}`; -}; - export const answersKey = urlObj => { const slug = formatSlug(urlObj.pathname, '/'); const lang = getLang(urlObj); diff --git a/views/pug/layout/_footer.pug b/views/pug/layout/_footer.pug index 492d347..573e6b7 100644 --- a/views/pug/layout/_footer.pug +++ b/views/pug/layout/_footer.pug @@ -11,7 +11,6 @@ footer.footer(class=`${meta.title ==='About' ? 'footer__about' : ''}`) ul.footer__nav - if (meta.title !=='About') li.footer__nav-item: a.footer__nav-link.footer__link(href="/about") About - li.footer__nav-item: a.footer__nav-link.footer__link(href="/search") Search li.footer__nav-item: a.footer__nav-link.footer__link(href="https://github.com/zyachel/quetre") Source Code li.footer__nav-item: a.footer__nav-link.footer__link(href="/privacy") Privacy li.footer__nav-item: a.footer__nav-link.footer__link(href="#") Back to top diff --git a/views/pug/layout/_header.pug b/views/pug/layout/_header.pug index 1647bc2..5b5ebf1 100644 --- a/views/pug/layout/_header.pug +++ b/views/pug/layout/_header.pug @@ -10,8 +10,6 @@ header.header(class=`${meta.title === 'About' ? 'header__about': ''}`) //- BUTTON FOR CHANGING THEME .header__misc - a.link.header__search(href="/search", aria-label='search page') - svg.icon: use(href='/misc/sprite.svg#icon-search') button.button.theme-changer.header__theme(aria-label='Change Theme') svg.icon.icon__theme.theme-changer__icon.theme-changer__icon--sun: use(href='/misc/sprite.svg#icon-sun') svg.icon.icon__theme.theme-changer__icon.theme-changer__icon--moon: use(href='/misc/sprite.svg#icon-moon') diff --git a/views/pug/pages/search.pug b/views/pug/pages/search.pug deleted file mode 100644 index 7d5e537..0000000 --- a/views/pug/pages/search.pug +++ /dev/null @@ -1,89 +0,0 @@ -//-////////////////////////////////////////////////////// -//- INCLUDES/EXTENDS -//-////////////////////////////////////////////////////// -extends ../base -include ../mixins/_formatText -include ../mixins/_utils -include ../mixins/_answer -include ../mixins/_question -include ../mixins/_post -include ../mixins/_metadata - - -//-////////////////////////////////////////////////////// -//- MAIN CONTENT -//-////////////////////////////////////////////////////// -block content - main#main(class=`main search ${data.searchData ? '' :'search--no-results'}`) - - - const typesArr = [{key: 'question', text: 'Questions'}, {key: 'answer', text: 'Answers'}, {key: 'post', text: 'Posts'}, {key: 'profile', text: 'Profiles'}, {key: 'topic', text: 'Topics'}, {key: 'tribe', text: 'Spaces'}] - const timesArr = [{key: 'hour', text: 'Hour'}, {key: 'day', text: 'Day'}, {key: 'week', text: 'Week'}, {key: 'month', text: 'Month'}, {key: 'year', text: 'Year'}] - const languagesArr = ['en','es','fr','de','it','jp','id','pt','hi','nl','da','fi','nb','sv','mr','bn','ta','ar','he','gu','kn','ml','te','po']; - - form.search__form.search-form(action="/search", method='get', autocomplete='off', name='search') - .search-form__search-container - input.search-form__searchbar(type="search", name="q", placeholder='Enter your query...', minlength='3', aria-label='search for anything') - button.search-form__button.search-form__button--reset(type="reset", aria-label='clear searchbar and filters'): svg.icon: use(href='/misc/sprite.svg#icon-cross') - button.search-form__button.search-form__button--submit(type="submit", aria-label='search'): svg.icon: use(href='/misc/sprite.svg#icon-search') - .search-form__filters-container.search-form__filters-container--type - p.search-form__filters-heading Filter by Type - each item in typesArr - .search-form__filters-group - input.search-form__radio.visually-hidden(type="radio", name="type", value=item.key, id=`type--${item.key}`) - label.search-form__label(for=`type--${item.key}`)= item.text - - .search-form__filters-container.search-form__filters-container--time - p.search-form__filters-heading Filter by Time - each item in timesArr - .search-form__filters-group - input.search-form__radio.visually-hidden(type="radio", name='time', value=item.key, id=`time--${item.key}`) - label.search-form__label(for=`time--${item.key}`)= item.text - - .search-form__filters-container.search-form__filters-container--lang - p.search-form__filters-heading Filter by Language - each lang in languagesArr - .search-form__filters-group - input.search-form__radio.visually-hidden(type="radio", name='lang', value=lang, id=`lang--${lang}`) - label.search-form__label(for=`lang--${lang}`)= lang.toUpperCase() - - //- TODO: refactor 'profile', 'topic', and 'space' into resusable mixins. - - if (data.searchData?.results) - section.search__results.search-results - h1.heading.heading__primary= `Results for '${data.searchText}'` - - if (data.searchData.results.length === 0) - p No results found for the query. Try being less specific and/or removing filters. - - else - .search-results__container - each item in data.searchData.results - .search-results__item - - if (item.type === 'answer') - +addAnswer(item, true) - - else if (item.type === 'question') - +addQuestion(item) - - else if (item.type === 'post') - +addPost(item) - - else if (item.type === 'profile') - .metadata-primary - p.metadata-primary__heading - if item.isAnon - span Anonymous - else - a.link.metadata-primary__link(href=item.url)= item.name - if item.isVerified - svg.icon.metadata-primary__icon - title verified - use(href='/misc/sprite.svg#icon-verified') - +proxifyImg(item.image)(class='metadata-primary__image', alt='', aria-hidden='true') - p.metadata-primary__misc(aria-label=`${item.name}'s credentials`)= item.credential || '' - - else if (item.type === 'topic') - .metadata-primary - a.link.metadata-primary__heading(href=item.url)= item.name - +proxifyImg(item.image)(class='metadata-primary__image', aria-hidden='true', alt='') - p.metadata-primary__misc - +formatNumber(item.numFollowers) - | Followers - - else if (item.type === 'space') - .metadata-primary.profile-spaces__list-item - +proxifyImg(item.image)(class='metadata-primary__image', alt='', aria-hidden='true') - a.link.metadata-primary__heading(href=item.url)= item.name - p.metadata-primary__misc= item.description diff --git a/views/sass/_components.scss b/views/sass/_components.scss index c41b3c7..611d2dc 100644 --- a/views/sass/_components.scss +++ b/views/sass/_components.scss @@ -536,119 +536,3 @@ gap: var(--space-800); } } - -//////////////////////////////////////////////////////// -// SEARCH PAGE COMPONENTS -//////////////////////////////////////////////////////// -/// -.search-form { - display: grid; - gap: var(--space-200); - // justify-items: center; - - &__search-container { - display: grid; - grid-auto-flow: column; - grid-auto-columns: minmax(15rem, auto) 3rem 3rem; - grid-auto-rows: 3rem; - border: 1px solid var(--clr-base-icon-alt-alpha); - border-radius: 100vmax; - overflow: hidden; - padding: var(--space-100); - gap: var(--space-050); - - &:focus-within { - background-color: var(--clr-code-bg); - color: var(--clr-code-text); - } - } - - &__searchbar { - outline: none; - border: none; - background-color: transparent; - font: inherit; - caret-color: var(--clr-base-icon); - - // fix for browsers with non-standard properties. yes, webkit and blink suck. - -webkit-appearance: none; - &::-webkit-search-cancel-button { - display: none; - } - } - - &__button { - border: none; - background: transparent; - cursor: pointer; - border-radius: 100vmax; - - &:hover { - transform: scale(1.1); - } - &--reset { - } - - &--submit { - } - } - - &__filters-container { - display: flex; - flex-wrap: wrap; - gap: 2rem 1rem; - } - - &__filters-heading { - flex-basis: 100%; - } - - &__filters-group { - position: relative; - } - - &__radio { - } - - &__label { - cursor: pointer; - background: var(--clr-code-bg); - color: var(--clr-code-text); - padding: 1rem; - border-radius: 1rem; - } - - &__radio:checked + &__label { - color: var(--clr-selection-text); - background: var(--clr-selection-bg); - } - - &__radio:focus + &__label { - @include focus-rules; - } - - @supports selector(:focus-visible) { - &__radio:focus + &__label { - outline: none; - } - - &__radio:focus-visible + &__label { - @include focus-rules; - } - } -} - -.search-results { - --img-dim: var(--fs-600); - - display: grid; - gap: var(--space-500); - - &__container { - display: grid; - gap: var(--space-500); - } - - &__item { - } -} diff --git a/views/sass/_layouts.scss b/views/sass/_layouts.scss index 828fa2e..bd77dcc 100644 --- a/views/sass/_layouts.scss +++ b/views/sass/_layouts.scss @@ -81,8 +81,7 @@ gap: var(--space-200); } - &__theme, - &__search { + &__theme { height: var(--fs-300); width: var(--fs-300); @@ -92,12 +91,6 @@ } } - // search icon viewBox is larger than content, hence the fix - &__search > svg { - height: 105%; - width: 105%; - } - &__info { display: grid; place-items: center; diff --git a/views/sass/_pages.scss b/views/sass/_pages.scss index f5b532d..7614519 100644 --- a/views/sass/_pages.scss +++ b/views/sass/_pages.scss @@ -405,54 +405,3 @@ padding-inline: var(--space-200); } } - -//////////////////////////////////////////////////////// -// SEARCH -//////////////////////////////////////////////////////// -.search { - // justify-self: center; - padding: var(--space-500) var(--space-800); - - display: grid; - grid-template-columns: 2fr 1.2fr; - gap: var(--space-500); - align-items: start; - - &--no-results { - grid-template-columns: unset; - place-content: center; - max-width: 100rem; - margin-inline: auto; - } - - &__results { - grid-column: 1 / 2; - grid-row: 1 / 2; - } - - &__form { - grid-row: 1 / 2; - - grid-column: -2 / -1; - } - - @include respond-to(bp-1200) { - padding: var(--space-500); - gap: var(--space-500); - } - @include respond-to(bp-900) { - grid-template-columns: auto; - grid-template-rows: max-content auto; - - &__results { - grid-row: 2 / 3; - } - - &__form { - grid-row: 1 / 2; - } - } - @include respond-to(bp-550) { - padding-inline: var(--space-200); - } -}