From 81920d93c61010ad4131b2c440ff57a47536e445 Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Thu, 14 Mar 2024 16:36:29 -0400 Subject: [PATCH 1/9] feat(config): add logic to router to handle meta tags --- client/src/router.js | 65 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/client/src/router.js b/client/src/router.js index 0408e0a0..2287fb38 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -8,10 +8,16 @@ import QuickSearchPage from '../src/pages/QuickSearchPage.vue'; import ResetPassword from './pages/ResetPassword.vue'; import SearchResultPage from '../src/pages/SearchResultPage.vue'; -export const PRIVATE_ROUTES = ['/change-password']; +import acronym from 'pdap-design-system/images/acronym.svg'; const routes = [ - { path: '/', component: QuickSearchPage, name: 'QuickSearchPage' }, + { + path: '/', + component: QuickSearchPage, + name: 'QuickSearchPage', + // Use meta property on route to override meta tag defaults + // meta: { title: 'Police Data Accessibility Project - Search', metaTags: [{ property: 'og:title', title: 'Police Data Accessibility Project - Search' }] }, + }, { path: '/search/:searchTerm/:location', component: SearchResultPage, @@ -45,13 +51,66 @@ const router = createRouter({ }); router.beforeEach(async (to) => { + // Update meta tags per route + refreshMetaTagsByRoute(to); + // redirect to login page if not logged in and trying to access a restricted page const auth = useAuthStore(); - if (PRIVATE_ROUTES.includes(to.fullPath) && !auth.userId) { auth.returnUrl = to.path; router.push('/login'); } }); +// Util +export const PRIVATE_ROUTES = ['/change-password']; + +const DEFAULT_META_TAGS = new Map([ + [ + 'description', + 'Data and tools for answering questions about any police system in the United States', + ], + ['title', 'Police Data Accessibility Project'], + [ + 'og:description', + 'Data and tools for answering questions about any police system in the United States', + ], + ['og:title', 'Police Data Accessibility Project'], + ['og:type', 'website'], + ['og:site_name', 'PDAP'], + ['og:image', acronym], +]); +const META_PROPERTIES = [...DEFAULT_META_TAGS.keys(), 'og:url']; + +/** + * Adds meta tags by route + * @param {RouteLocationNormalized} to Vue router route location + */ +function refreshMetaTagsByRoute(to) { + document.title = to.meta.title ?? DEFAULT_META_TAGS.get('title'); + Array.from(document.querySelectorAll('[data-vue-router-controlled]')).map( + (el) => el.parentNode.removeChild(el), + ); + + META_PROPERTIES.filter((prop) => prop !== 'title') + .map((prop) => { + const tagInRouteMetaData = to?.meta?.metaTags?.find( + (tag) => tag.property === prop, + ); + const content = + prop === 'og:url' + ? `${import.meta.env.VITE_VUE_APP_BASE_URL}${to.fullPath}` + : tagInRouteMetaData + ? tagInRouteMetaData + : DEFAULT_META_TAGS.get(prop); + + const tag = document.createElement('meta'); + tag.setAttribute('property', prop); + tag.setAttribute('content', content); + tag.setAttribute('data-vue-router-controlled', ''); + return tag; + }) + .forEach((tag) => document.head.appendChild(tag)); +} + export default router; From 6060eae09d0c2ab4f265316cc1a6843249f93638 Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Fri, 15 Mar 2024 16:38:02 -0400 Subject: [PATCH 2/9] refactor(router): Update meta tag logic Add support for multiple matched routes --- client/src/router.js | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/client/src/router.js b/client/src/router.js index 2287fb38..e030a645 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -18,10 +18,18 @@ const routes = [ // Use meta property on route to override meta tag defaults // meta: { title: 'Police Data Accessibility Project - Search', metaTags: [{ property: 'og:title', title: 'Police Data Accessibility Project - Search' }] }, }, + { + path: '/search', + component: SearchResultPage, + name: 'SearchResultPage', + }, { path: '/search/:searchTerm/:location', component: SearchResultPage, name: 'SearchResultPage', + meta: { + metaTags: [{ property: 'og:type', content: 'test' }], + }, }, { path: '/data-sources/:id', @@ -50,7 +58,7 @@ const router = createRouter({ routes, }); -router.beforeEach(async (to) => { +router.beforeEach(async (to, _, next) => { // Update meta tags per route refreshMetaTagsByRoute(to); @@ -60,6 +68,8 @@ router.beforeEach(async (to) => { auth.returnUrl = to.path; router.push('/login'); } + + next(); }); // Util @@ -87,27 +97,36 @@ const META_PROPERTIES = [...DEFAULT_META_TAGS.keys(), 'og:url']; * @param {RouteLocationNormalized} to Vue router route location */ function refreshMetaTagsByRoute(to) { - document.title = to.meta.title ?? DEFAULT_META_TAGS.get('title'); - Array.from(document.querySelectorAll('[data-vue-router-controlled]')).map( - (el) => el.parentNode.removeChild(el), + const nearestRouteWithTitle = [...to.matched] + .reverse() + .find((r) => r.meta && r.meta.title); + + const nearestRouteWithMeta = [...to.matched] + .reverse() + .find((r) => r.meta && r.meta.metaTags); + + document.title = + nearestRouteWithTitle?.meta?.title ?? DEFAULT_META_TAGS.get('title'); + Array.from(document.querySelectorAll('[data-controlled-meta]')).map((el) => + el.parentNode.removeChild(el), ); META_PROPERTIES.filter((prop) => prop !== 'title') .map((prop) => { - const tagInRouteMetaData = to?.meta?.metaTags?.find( + const tagInRouteMetaData = nearestRouteWithMeta?.meta?.metaTags?.find( (tag) => tag.property === prop, ); const content = prop === 'og:url' ? `${import.meta.env.VITE_VUE_APP_BASE_URL}${to.fullPath}` : tagInRouteMetaData - ? tagInRouteMetaData + ? tagInRouteMetaData.content : DEFAULT_META_TAGS.get(prop); const tag = document.createElement('meta'); - tag.setAttribute('property', prop); + tag.setAttribute(prop.includes(':') ? 'property' : 'name', prop); tag.setAttribute('content', content); - tag.setAttribute('data-vue-router-controlled', ''); + tag.setAttribute('data-controlled-meta', true); return tag; }) .forEach((tag) => document.head.appendChild(tag)); From 0232de30079539e65ca9bee6c321eb694320ebc9 Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Fri, 15 Mar 2024 16:41:02 -0400 Subject: [PATCH 3/9] refactor(router): organization and comments --- client/src/router.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/src/router.js b/client/src/router.js index e030a645..377a1988 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -97,18 +97,22 @@ const META_PROPERTIES = [...DEFAULT_META_TAGS.keys(), 'og:url']; * @param {RouteLocationNormalized} to Vue router route location */ function refreshMetaTagsByRoute(to) { + // Get nearest matched route that has title / meta tag overrides const nearestRouteWithTitle = [...to.matched] .reverse() - .find((r) => r.meta && r.meta.title); + .find((route) => route?.meta?.title); const nearestRouteWithMeta = [...to.matched] .reverse() - .find((r) => r.meta && r.meta.metaTags); + .find((route) => route?.meta?.metaTags); + // Update document title document.title = nearestRouteWithTitle?.meta?.title ?? DEFAULT_META_TAGS.get('title'); - Array.from(document.querySelectorAll('[data-controlled-meta]')).map((el) => - el.parentNode.removeChild(el), + + // Update meta tags + Array.from(document.querySelectorAll('[data-controlled-meta]')).forEach( + (el) => el.parentNode.removeChild(el), ); META_PROPERTIES.filter((prop) => prop !== 'title') From 4642765a5bd80c5b632619ba3986610ac448fe19 Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Fri, 15 Mar 2024 16:47:27 -0400 Subject: [PATCH 4/9] refactor(router): miscellaneous updates --- client/src/router.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/client/src/router.js b/client/src/router.js index 377a1988..0bfa5c3b 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -27,9 +27,6 @@ const routes = [ path: '/search/:searchTerm/:location', component: SearchResultPage, name: 'SearchResultPage', - meta: { - metaTags: [{ property: 'og:type', content: 'test' }], - }, }, { path: '/data-sources/:id', @@ -120,12 +117,18 @@ function refreshMetaTagsByRoute(to) { const tagInRouteMetaData = nearestRouteWithMeta?.meta?.metaTags?.find( (tag) => tag.property === prop, ); - const content = - prop === 'og:url' - ? `${import.meta.env.VITE_VUE_APP_BASE_URL}${to.fullPath}` - : tagInRouteMetaData - ? tagInRouteMetaData.content - : DEFAULT_META_TAGS.get(prop); + + let content; + switch (true) { + case prop === 'og:url': + content = `${import.meta.env.VITE_VUE_APP_BASE_URL}${to.fullPath}`; + break; + case Boolean(tagInRouteMetaData): + content = tagInRouteMetaData.content; + break; + default: + content = DEFAULT_META_TAGS.get(prop); + } const tag = document.createElement('meta'); tag.setAttribute(prop.includes(':') ? 'property' : 'name', prop); From 2769c0029dfc5bfa4dd8b53da2786b533306057d Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Tue, 19 Mar 2024 15:49:20 -0400 Subject: [PATCH 5/9] fix(pages): update search results filtering Accommodate multiple items of each record_type --- client/src/pages/SearchResultPage.vue | 43 ++++++++++++++------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/client/src/pages/SearchResultPage.vue b/client/src/pages/SearchResultPage.vue index c4dc7ed2..f5f59279 100644 --- a/client/src/pages/SearchResultPage.vue +++ b/client/src/pages/SearchResultPage.vue @@ -42,32 +42,38 @@
- - +

{{ section.header }} - +

- -
+ class="grid pdap-grid-container-column-3 gap-4" + > + +
+ - - From 14c898991118d3bae58e675ed7c383b2729866cb Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Tue, 19 Mar 2024 15:59:36 -0400 Subject: [PATCH 6/9] refactor(pages): update SearchResultPage Get count from API results Misc template cleanup --- client/src/pages/SearchResultPage.vue | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/client/src/pages/SearchResultPage.vue b/client/src/pages/SearchResultPage.vue index f5f59279..ee6026bd 100644 --- a/client/src/pages/SearchResultPage.vue +++ b/client/src/pages/SearchResultPage.vue @@ -42,12 +42,9 @@
-
@@ -67,7 +64,7 @@ :data-source="result" />
-
+ @@ -134,7 +131,7 @@ export default { // Set data and away we go this.searchResult = resultFormatted; - this.count = Object.entries(this.searchResult).length; + this.count = res.data.count; } catch (error) { console.error(error); } finally { From 943581aad1d46e354d8effb2f359428460dab0b5 Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Tue, 19 Mar 2024 15:59:50 -0400 Subject: [PATCH 7/9] test(pages): update SearchResultPage snapshot --- .../searchResultPage.test.js.snap | 168 +++++++++++++----- 1 file changed, 121 insertions(+), 47 deletions(-) diff --git a/client/src/pages/__tests__/__snapshots__/searchResultPage.test.js.snap b/client/src/pages/__tests__/__snapshots__/searchResultPage.test.js.snap index b0190c24..f6ba6be7 100644 --- a/client/src/pages/__tests__/__snapshots__/searchResultPage.test.js.snap +++ b/client/src/pages/__tests__/__snapshots__/searchResultPage.test.js.snap @@ -4,7 +4,7 @@ exports[`SearchResultPage renders with data > Calls API and renders search resul

Data Sources Search results

-

Searching for "calls" in "Cook". Found 1 result. +

Searching for "calls" in "Cook". Found 3 results.

-
-

Police & public interactions

-
-

311 Calls for City of Chicago

- -

Record type

-
Calls for Service
-
-

Agency

-

Chicago Police Department - IL

+
+

Police & public interactions

+
+
+

Calls for Service for Cicero Police Department - IN

+ +

Record type

+
Calls for Service
+
+

Agency

+

Cicero Police Department - IN

+
+ +

Time range

+

2016–Unknown end

+

Formats available

+
    +
  • [
  • +
  • ]
  • +
+ +
+
+

Calls for Service for Chicago Police Department - IL

+ +

Record type

+
Calls for Service
+
+

Agency

+

Chicago Police Department - IL

+
+ +

Time range

+

2018–Unknown end

+

Formats available

+
    +
  • [
  • +
  • ]
  • +
+
- -

Time range

-

12/17/2018–Unknown end

-

Formats available

-
    -
  • [
  • -
  • '
  • -
  • C
  • -
  • S
  • -
  • V
  • -
  • '
  • -
  • ,
  • -
  • -
  • '
  • -
  • X
  • -
  • M
  • -
  • L
  • -
  • '
  • -
  • ,
  • -
  • -
  • '
  • -
  • R
  • -
  • D
  • -
  • F
  • -
  • '
  • -
  • ,
  • -
  • -
  • '
  • -
  • R
  • -
  • S
  • -
  • S
  • -
  • '
  • -
  • ]
  • -
- +

Time range

+

12/17/2018–Unknown end

+

Formats available

+
    +
  • [
  • +
  • '
  • +
  • C
  • +
  • S
  • +
  • V
  • +
  • '
  • +
  • ,
  • +
  • +
  • '
  • +
  • X
  • +
  • M
  • +
  • L
  • +
  • '
  • +
  • ,
  • +
  • +
  • '
  • +
  • R
  • +
  • D
  • +
  • F
  • +
  • '
  • +
  • ,
  • +
  • +
  • '
  • +
  • R
  • +
  • S
  • +
  • S
  • +
  • '
  • +
  • ]
  • +
+ +
From 5a0c1d9b9b4dfb5df984485e476ae586ae8cef88 Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Tue, 19 Mar 2024 16:24:32 -0400 Subject: [PATCH 8/9] refactor(pages): update search result page Handle UI more elegantly --- client/src/pages/SearchResultPage.vue | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/client/src/pages/SearchResultPage.vue b/client/src/pages/SearchResultPage.vue index ee6026bd..b1e2894a 100644 --- a/client/src/pages/SearchResultPage.vue +++ b/client/src/pages/SearchResultPage.vue @@ -46,19 +46,15 @@ v-for="section in uiShape" :key="section.header" data-test="search" - class="p-0 w-full" + class="mt-8 p-0 w-full" >

{{ section.header }}

-
+
{ + return [...acc, ...this.searchResult[cur.type]]; + }, []); + }, getResultsCopy() { return `${this.count} ${pluralize('result', this.count)}`; }, From 44c585c70ddce75e86d4b20db2e7ca1cd95cb8ac Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Tue, 19 Mar 2024 16:25:08 -0400 Subject: [PATCH 9/9] test(pages): update search results snapshot --- .../pages/__tests__/__snapshots__/searchResultPage.test.js.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/__tests__/__snapshots__/searchResultPage.test.js.snap b/client/src/pages/__tests__/__snapshots__/searchResultPage.test.js.snap index f6ba6be7..1c818fc2 100644 --- a/client/src/pages/__tests__/__snapshots__/searchResultPage.test.js.snap +++ b/client/src/pages/__tests__/__snapshots__/searchResultPage.test.js.snap @@ -19,7 +19,7 @@ exports[`SearchResultPage renders with data > Calls API and renders search resul

-
+

Police & public interactions