From 62b10b9dbcf81f1e1d413a2b356e13dac5ea0fd4 Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Fri, 20 Dec 2024 12:12:39 -0500 Subject: [PATCH 1/5] feat: update profile --- client/src/api/auth.js | 11 + client/src/api/search.js | 10 +- client/src/api/user.js | 12 + client/src/components/AuthWrapper.vue | 29 ++ client/src/components/SearchForm.vue | 33 +- client/src/pages/data-source/[id].vue | 21 +- client/src/pages/profile.vue | 127 ------ .../src/pages/profile/_components/APIKey.vue | 110 +++++ .../profile/_components/ThreeColumnTable.vue | 48 +++ client/src/pages/profile/index.vue | 383 ++++++++++++++++++ client/src/pages/search/results.vue | 110 +++-- client/src/pages/sign-in.vue | 4 +- client/src/pages/sign-up/index.vue | 4 +- client/src/util/DOM.js | 1 - client/src/util/constants.js | 63 +++ client/src/util/locationFormatters.js | 52 ++- 16 files changed, 796 insertions(+), 222 deletions(-) delete mode 100644 client/src/pages/profile.vue create mode 100644 client/src/pages/profile/_components/APIKey.vue create mode 100644 client/src/pages/profile/_components/ThreeColumnTable.vue create mode 100644 client/src/pages/profile/index.vue diff --git a/client/src/api/auth.js b/client/src/api/auth.js index 1c9dc071..78b9501c 100644 --- a/client/src/api/auth.js +++ b/client/src/api/auth.js @@ -155,6 +155,17 @@ export async function resetPassword(password, token) { ); } +export async function generateAPIKey() { + const auth = useAuthStore(); + + return await axios.post(`${AUTH_BASE}/${ENDPOINTS.AUTH.API_KEY}`, null, { + headers: { + ...HEADERS, + Authorization: `Bearer ${auth.$state.tokens.accessToken.value}`, + }, + }); +} + /** * @deprecated validation now done by parsing JWT directly */ diff --git a/client/src/api/search.js b/client/src/api/search.js index 7eef6574..f1ca2219 100644 --- a/client/src/api/search.js +++ b/client/src/api/search.js @@ -14,6 +14,7 @@ const HEADERS_BASIC = { }; export async function search(params) { + const authStore = useAuthStore(); const searchStore = useSearchStore(); const cached = searchStore.getSearchFromCache(JSON.stringify(params)); @@ -32,7 +33,14 @@ export async function search(params) { `${SEARCH_BASE}/${ENDPOINTS.SEARCH.RESULTS}`, { params, - headers: HEADERS_BASIC, + headers: { + ...HEADERS_BASIC, + ...(authStore.isAuthenticated() + ? { + Authorization: `Bearer ${authStore.$state.tokens.accessToken.value}`, + } + : {}), + }, }, ); diff --git a/client/src/api/user.js b/client/src/api/user.js index 3b303677..a410ef44 100644 --- a/client/src/api/user.js +++ b/client/src/api/user.js @@ -25,3 +25,15 @@ export async function changePassword(oldPassword, newPassword) { return await auth.signInWithEmail(user.email, newPassword); } + +export async function getUser() { + const auth = useAuthStore(); + const user = useUserStore(); + + return await axios.get(`${USER_BASE}/${user.id}`, { + headers: { + ...HEADERS, + Authorization: `Bearer ${auth.$state.tokens.accessToken.value}`, + }, + }); +} diff --git a/client/src/components/AuthWrapper.vue b/client/src/components/AuthWrapper.vue index 7950f80a..4e1cb347 100644 --- a/client/src/components/AuthWrapper.vue +++ b/client/src/components/AuthWrapper.vue @@ -10,6 +10,35 @@ import { useAuthStore } from '@/stores/auth'; import { useRoute, useRouter } from 'vue-router'; import { useUserStore } from '@/stores/user'; import { refreshTokens, signOut } from '@/api/auth'; +import { updateGlobalOptions, globalOptions } from 'vue3-toastify'; +import { watch, ref, onMounted } from 'vue'; + +const isHeaderVisible = ref(true); + +watch(isHeaderVisible, (visible) => { + updateGlobalOptions({ + ...globalOptions.value, + style: { + ...globalOptions.style, + top: visible ? '120px' : '20px', + }, + theme: 'auto', + }); +}); + +onMounted(() => { + const observer = new IntersectionObserver( + ([entry]) => { + isHeaderVisible.value = entry.isIntersecting; + }, + { threshold: 0 }, + ); + + const navbar = document.querySelector('.pdap-header'); + if (navbar) { + observer.observe(navbar); + } +}); const route = useRoute(); const router = useRouter(); diff --git a/client/src/components/SearchForm.vue b/client/src/components/SearchForm.vue index ad9636c2..2c90463a 100644 --- a/client/src/components/SearchForm.vue +++ b/client/src/components/SearchForm.vue @@ -82,7 +82,11 @@ import { } from 'pdap-design-system'; import TypeaheadInput from '@/components/TypeaheadInput.vue'; import { computed, onMounted, ref } from 'vue'; -import { getFullLocationText } from '@/util/locationFormatters'; +import { + getFullLocationText, + mapLocationToSearchParams, + mapSearchParamsToLocation, +} from '@/util/locationFormatters'; import _debounce from 'lodash/debounce'; import _isEqual from 'lodash/isEqual'; import { useRouter, RouterLink, useRoute } from 'vue-router'; @@ -186,17 +190,7 @@ const isButtonDisabled = computed(() => { onMounted(() => { // Set up selected state based on params if (params.state) { - const record = (({ - state_name, - county_name, - locality_name, - location_id, - }) => ({ - state: state_name, - county: county_name, - locality: locality_name, - location_id, - }))(params); + const record = mapSearchParamsToLocation(params); selectedRecord.value = record; initiallySearchedRecord.value = record; @@ -223,18 +217,9 @@ function buildParams(values) { const obj = {}; /* Handle record from typeahead input */ - const recordFilteredByParamsKeys = (({ - state_name, - county_name, - locality_name, - location_id, - }) => ({ - state: state_name, - county: county_name, - locality: locality_name, - location_id, - // If no selected record, fall back to the initial search - }))(selectedRecord.value ?? initiallySearchedRecord.value); + const recordFilteredByParamsKeys = mapLocationToSearchParams( + selectedRecord.value ?? initiallySearchedRecord.value, + ); Object.keys(recordFilteredByParamsKeys).forEach((key) => { if (recordFilteredByParamsKeys[key]) diff --git a/client/src/pages/data-source/[id].vue b/client/src/pages/data-source/[id].vue index ea887483..a72c4f26 100644 --- a/client/src/pages/data-source/[id].vue +++ b/client/src/pages/data-source/[id].vue @@ -249,12 +249,9 @@ const descriptionRef = ref(); const mainRef = ref(); const navIs = ref(''); -console.debug({ nextIdIndex, previousIdIndex }); - // Handle swipe -const { direction } = useSwipe(mainRef, { - onSwipe: (e) => { - console.debug({ e }); +const { direction, isSwiping } = useSwipe(mainRef, { + onSwipeEnd: (e) => { if (isDescendantOf(e.target, agenciesRef.value)) { e.preventDefault(); e.stopImmediatePropagation(); @@ -265,9 +262,7 @@ const { direction } = useSwipe(mainRef, { case 'left': navIs.value = 'increment'; if (typeof nextIdIndex.value === 'number' && nextIdIndex.value > -1) - router.replace( - `/data-source/${searchStore.mostRecentSearchIds[nextIdIndex.value]}`, - ); + router.replace(`/data-source/${getNext()}`); break; case 'right': navIs.value = 'decrement'; @@ -275,15 +270,19 @@ const { direction } = useSwipe(mainRef, { typeof previousIdIndex.value === 'number' && previousIdIndex.value > -1 ) - router.replace( - `/data-source/${searchStore.mostRecentSearchIds[previousIdIndex.value]}`, - ); + router.replace(`/data-source/${getPrev()}`); break; default: return; } }, }); +function getNext() { + return searchStore.mostRecentSearchIds[nextIdIndex.value]; +} +function getPrev() { + return searchStore.mostRecentSearchIds[previousIdIndex.value]; +} onMounted(() => { handleShowMoreButton(); diff --git a/client/src/pages/profile.vue b/client/src/pages/profile.vue deleted file mode 100644 index 47a19189..00000000 --- a/client/src/pages/profile.vue +++ /dev/null @@ -1,127 +0,0 @@ - - { - meta: { - auth: true - } - } - - - - - - - diff --git a/client/src/pages/profile/_components/APIKey.vue b/client/src/pages/profile/_components/APIKey.vue new file mode 100644 index 00000000..6525321e --- /dev/null +++ b/client/src/pages/profile/_components/APIKey.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/client/src/pages/profile/_components/ThreeColumnTable.vue b/client/src/pages/profile/_components/ThreeColumnTable.vue new file mode 100644 index 00000000..3a8dc27a --- /dev/null +++ b/client/src/pages/profile/_components/ThreeColumnTable.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/client/src/pages/profile/index.vue b/client/src/pages/profile/index.vue new file mode 100644 index 00000000..7d8f6183 --- /dev/null +++ b/client/src/pages/profile/index.vue @@ -0,0 +1,383 @@ + + { + meta: { + auth: true + } + } + + + + + + + + + + + diff --git a/client/src/pages/search/results.vue b/client/src/pages/search/results.vue index 3c9d2535..b45e8c69 100644 --- a/client/src/pages/search/results.vue +++ b/client/src/pages/search/results.vue @@ -9,7 +9,7 @@ >

Results - {{ searchData && 'for ' + getFullLocationText(searchData.params) }} + {{ searchData && 'for ' + getMinimalLocationText(searchData.params) }}

@@ -27,11 +27,11 @@ } " > - Follow + Follow

Sign in - to follow this search + to follow this location

@@ -39,9 +39,9 @@ v-if="isAuthenticated()" class="text-med text-neutral-500 max-w-full md:text-right" > - You are following this search.
- Review saved searches in - your profile + + Following this location
+ See your profile for more.

@@ -69,19 +69,27 @@ > {{ getAnchorLinkText(locale) }} - ({{ searchData.results[locale].count }}) + ({{ searchData?.results[locale].count }}) - - - @@ -123,7 +131,7 @@ import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic'; import { useSearchStore } from '@/stores/search'; import { NavigationResult } from 'unplugin-vue-router/runtime'; -import { onMounted, onUnmounted, onUpdated, reactive, ref } from 'vue'; +import { onMounted, onUnmounted, onUpdated, reactive, ref, watch } from 'vue'; import { ALL_LOCATION_TYPES } from '@/util/constants'; import { groupResultsByAgency, @@ -136,14 +144,12 @@ import { getMostNarrowSearchLocationWithResults, getMinimalLocationText, } from '@/util/locationFormatters'; -import _isEqual from 'lodash/isEqual'; import { DataLoaderErrorPassThrough } from '@/util/errors'; const searchStore = useSearchStore(); import { search, getFollowedSearch, followSearch } from '@/api/search'; import { getLocationDataRequests } from '@/api/locations'; +import { mapSearchParamsToLocation } from '@/util/locationFormatters'; -const query = ref(); -const data = ref(); const previousRoute = ref(); const isPreviousRouteFollowed = ref(false); @@ -152,20 +158,10 @@ export const useSearchData = defineBasicLoader( '/search/results', async (route) => { try { - const searchLocation = (({ state, county, locality, location_id }) => ({ - state_name: state, - county_name: county, - locality_name: locality, - location_id, - }))(route.query); - + const searchLocation = mapSearchParamsToLocation(route.query); const searched = getMostNarrowSearchLocationWithResults(searchLocation); - const response = - // Local caching to skip even the pinia method in case of only the hash changing while on the route. - _isEqual(searchLocation, query.value) && data.value - ? data.value - : await search(route.query); + const response = await search(route.query); // On initial fetch - get hash const hash = normalizeLocaleForHash(searched, response.data); @@ -173,9 +169,6 @@ export const useSearchData = defineBasicLoader( return new NavigationResult({ ...route, hash: `#${hash}` }); } - data.value = response; - query.value = searchLocation; - const ret = { results: groupResultsByAgency(response.data), searched, @@ -186,6 +179,7 @@ export const useSearchData = defineBasicLoader( throw new DataLoaderErrorPassThrough(error); } }, + { lazy: true }, ); export const useFollowedData = defineBasicLoader( @@ -201,6 +195,9 @@ export const useFollowedData = defineBasicLoader( throw new DataLoaderErrorPassThrough(error); } }, + { + lazy: true, + }, ); export const useRequestsData = defineBasicLoader( @@ -214,6 +211,9 @@ export const useRequestsData = defineBasicLoader( throw new DataLoaderErrorPassThrough(error); } }, + { + lazy: true, + }, ); // function isOnlyHashChanged(currentRoute, previousRoute) { @@ -239,11 +239,19 @@ import { Button } from 'pdap-design-system'; import SearchForm from '@/components/SearchForm.vue'; import SearchResults from './_components/SearchResults.vue'; import Requests from './_components/Requests.vue'; +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; +import { faUserPlus, faUserCheck } from '@fortawesome/free-solid-svg-icons'; import { toast } from 'vue3-toastify'; import { useAuthStore } from '@/stores/auth'; import { useRoute } from 'vue-router'; + const { isAuthenticated } = useAuthStore(); -const { data: searchData, isLoading, error } = useSearchData(); +const { + data: searchData, + isLoading, + error, + reload: reloadSearch, +} = useSearchData(); const { data: isFollowed, reload: reloadFollowed } = useFollowedData(); const { data: requestData, error: requestsError } = useRequestsData(); const route = useRoute(); @@ -252,17 +260,29 @@ const isSearchShown = ref(false); const dims = reactive({ width: window.innerWidth, height: window.innerHeight }); const hasDisplayedErrorByRouteParams = ref(new Map()); +watch( + () => route.query, + async () => { + try { + await reloadSearch(); + } catch (error) { + console.error('Failed to reload search:', error); + } + }, + { deep: true }, +); + // lifecycle methods onMounted(() => { if (window.innerWidth > 1280) isSearchShown.value = true; - if (searchData.value) { + if (searchData?.value) { searchStore.setMostRecentSearchIds( - getAllIdsSearched(searchData.value.results), + getAllIdsSearched(searchData?.value?.results), ); } - if (requestData.value) { + if (requestData?.value) { searchStore.setMostRecentRequestIds(requestData.value.map((req) => req.id)); } @@ -272,7 +292,7 @@ onMounted(() => { onUpdated(async () => { if (error.value) { toast.error( - `Error fetching search results for ${getMinimalLocationText(searchData.value.params)}. Please try again!`, + `Error fetching search results for ${getMinimalLocationText(searchData?.value?.params)}. Please try again!`, { autoClose: false, onClose() { @@ -281,17 +301,17 @@ onUpdated(async () => { }, ); hasDisplayedErrorByRouteParams.value.set( - JSON.stringify(searchData.value.params), + JSON.stringify(searchData?.value?.params), true, ); } - if (searchData.value) + if (searchData?.value) searchStore.setMostRecentSearchIds( - getAllIdsSearched(searchData.value.results), + getAllIdsSearched(searchData?.value?.results), ); - if (requestData.value) { + if (requestData?.value) { searchStore.setMostRecentRequestIds(requestData.value.map((req) => req.id)); } }); @@ -306,12 +326,12 @@ async function follow() { try { await followSearch(route.query.location_id); toast.success( - `Search followed for ${getMinimalLocationText(searchData.value.params)}.`, + `Search followed for ${getMinimalLocationText(searchData?.value?.params)}.`, ); await reloadFollowed(); } catch (error) { toast.error( - `Error following search for ${getMinimalLocationText(searchData.value.params)}. Please try again.`, + `Error following search for ${getMinimalLocationText(searchData?.value?.params)}. Please try again.`, ); } } diff --git a/client/src/pages/sign-in.vue b/client/src/pages/sign-in.vue index f99e0083..ea0ebe1d 100644 --- a/client/src/pages/sign-in.vue +++ b/client/src/pages/sign-in.vue @@ -117,9 +117,7 @@ export const useGithubAuth = defineBasicLoader('/sign-in', async (route) => { const tokens = await signInWithGithub(githubAccessToken); if (tokens) - return new NavigationResult( - auth.redirectTo ?? { path: '/profile', query: { linked: true } }, - ); + return new NavigationResult(auth.redirectTo ?? { path: '/profile' }); } } catch (error) { if (error.response.data.message.includes('already exists')) { diff --git a/client/src/pages/sign-up/index.vue b/client/src/pages/sign-up/index.vue index 34dd45e9..08ffba34 100644 --- a/client/src/pages/sign-up/index.vue +++ b/client/src/pages/sign-up/index.vue @@ -125,9 +125,7 @@ export const useGithubAuth = defineBasicLoader('/sign-up', async (route) => { const tokens = await signInWithGithub(githubAccessToken); if (tokens) - return new NavigationResult( - auth.redirectTo ?? { path: '/profile', query: { linked: true } }, - ); + return new NavigationResult(auth.redirectTo ?? { path: '/profile' }); } } catch (error) { if (error.response.data.message.includes('already exists')) { diff --git a/client/src/util/DOM.js b/client/src/util/DOM.js index a68251a8..2f743cb0 100644 --- a/client/src/util/DOM.js +++ b/client/src/util/DOM.js @@ -1,5 +1,4 @@ export function isDescendantOf(child, parent) { - console.debug('iDO', { child, parent }); if (!child || !parent) return false; if (child === parent) return true; return isDescendantOf(child.parentNode, parent); diff --git a/client/src/util/constants.js b/client/src/util/constants.js index 2b8f0a02..7c6a21b5 100644 --- a/client/src/util/constants.js +++ b/client/src/util/constants.js @@ -130,3 +130,66 @@ export const STATES_TO_ABBREVIATIONS = new Map([ ['Wisconsin', 'WI'], ['Wyoming', 'WY'], ]); + +export const ABBREVIATIONS_TO_STATES = new Map([ + ['AL', 'Alabama'], + ['AK', 'Alaska'], + ['AS', 'American Samoa'], + ['AZ', 'Arizona'], + ['AR', 'Arkansas'], + ['AA', 'Armed Forces Americas'], + ['AE', 'Armed Forces Europe'], + ['AP', 'Armed Forces Pacific'], + ['CA', 'California'], + ['CO', 'Colorado'], + ['CT', 'Connecticut'], + ['DE', 'Delaware'], + ['DC', 'District Of Columbia'], + ['FL', 'Florida'], + ['GA', 'Georgia'], + ['GU', 'Guam'], + ['HI', 'Hawaii'], + ['ID', 'Idaho'], + ['IL', 'Illinois'], + ['IN', 'Indiana'], + ['IA', 'Iowa'], + ['KS', 'Kansas'], + ['KY', 'Kentucky'], + ['LA', 'Louisiana'], + ['ME', 'Maine'], + ['MH', 'Marshall Islands'], + ['MD', 'Maryland'], + ['MA', 'Massachusetts'], + ['MI', 'Michigan'], + ['MN', 'Minnesota'], + ['MS', 'Mississippi'], + ['MO', 'Missouri'], + ['MT', 'Montana'], + ['NE', 'Nebraska'], + ['NV', 'Nevada'], + ['NH', 'New Hampshire'], + ['NJ', 'New Jersey'], + ['NM', 'New Mexico'], + ['NY', 'New York'], + ['NC', 'North Carolina'], + ['ND', 'North Dakota'], + ['NP', 'Northern Mariana Islands'], + ['OH', 'Ohio'], + ['OK', 'Oklahoma'], + ['OR', 'Oregon'], + ['PA', 'Pennsylvania'], + ['PR', 'Puerto Rico'], + ['RI', 'Rhode Island'], + ['SC', 'South Carolina'], + ['SD', 'South Dakota'], + ['TN', 'Tennessee'], + ['TX', 'Texas'], + ['VI', 'US Virgin Islands'], + ['UT', 'Utah'], + ['VT', 'Vermont'], + ['VA', 'Virginia'], + ['WA', 'Washington'], + ['WV', 'West Virginia'], + ['WI', 'Wisconsin'], + ['WY', 'Wyoming'], +]); diff --git a/client/src/util/locationFormatters.js b/client/src/util/locationFormatters.js index a5d8e3a5..b5a5df85 100644 --- a/client/src/util/locationFormatters.js +++ b/client/src/util/locationFormatters.js @@ -4,20 +4,31 @@ export function getFullLocationText(location) { const searched = getMostNarrowSearchLocationWithResults(location); switch (searched) { case 'locality': - return `${location.locality_name}, ${STATES_TO_ABBREVIATIONS.get(location.state_name)}`; + return `${location.locality_name}, ${location.county_name}, ${STATES_TO_ABBREVIATIONS.get(location.state_name)}`; case 'county': return `${location.county_name} ${STATES_TO_ABBREVIATIONS.get(location.state_name) === 'LA' ? 'Parish' : 'County'}, ${location.state_name}`; case 'state': return location.state_name; default: - return location.display_name; + return location.display_name ?? ''; } } export function getLocationCityState(location) { const locality = location.locality_name ?? ''; - const state = STATES_TO_ABBREVIATIONS.get(location.state_name) ?? ''; - return `${locality}${locality && state ? ', ' : ''}${state}`; + const stateAbbr = + STATES_TO_ABBREVIATIONS.get(location.state_name) ?? + // TODO: remove this once we have all the location data standardized + location.state_iso ?? + ''; + const state = location.state_name; + const displayName = location.display_name; + + if (locality && !state && !stateAbbr) return locality; + else if (locality && stateAbbr) return `${locality}, ${stateAbbr}`; + else if (locality && state) return `${locality}, ${state}`; + else if (state) return state; + else return displayName; } export function getMinimalLocationText(location) { @@ -26,7 +37,34 @@ export function getMinimalLocationText(location) { export function getMostNarrowSearchLocationWithResults(location) { if (!location) return null; - if (location?.locality_name) return 'locality'; - if (location?.county_name) return 'county'; - if (location?.state_name) return 'state'; + // Checking for string 'null' because of all the data processing that happens before we get here. + if (location?.locality_name !== 'null') return 'locality'; + if (location?.county_name !== 'null') return 'county'; + if (location?.state_name !== 'null') return 'state'; + return location?.type?.toLowerCase(); } + +// TODO: cache getLocationById function and fetch to get locations by id rather than all of this parsing. +export const mapSearchParamsToLocation = (obj) => + (({ state, county, locality, location_id, id }) => ({ + state_name: state, + state_iso: state, + county_name: county, + locality_name: locality, + id: location_id ?? id, + }))(obj); + +export const mapLocationToSearchParams = (obj) => + (({ + state_name, + state_iso, + county_name, + locality_name, + id, + location_id, + }) => ({ + state: state_name ?? state_iso, + county: county_name, + locality: locality_name, + location_id: id ?? location_id, + }))(obj); From 0541bef94a8ccfb3321c8d9223c0dee28bb0a88e Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Fri, 20 Dec 2024 13:10:55 -0500 Subject: [PATCH 2/5] fix: search not displaying results on first query --- client/src/pages/profile/index.vue | 21 +++++++++++++++++---- client/src/pages/search/results.vue | 14 +++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/client/src/pages/profile/index.vue b/client/src/pages/profile/index.vue index 7d8f6183..d89e4ede 100644 --- a/client/src/pages/profile/index.vue +++ b/client/src/pages/profile/index.vue @@ -365,16 +365,29 @@ async function unFollow(followed) { transform: translateX(-100%); background-image: linear-gradient( 90deg, - rgba(var(--color-gold-neutral-200)) 0, - rgba(var(--color-gold-neutral-300), 0.2) 20%, - rgba(var(--color-gold-neutral-400), 0.5) 60%, - rgba(var(--color-gold-neutral-400), 0) + rgba(var(--color-gold-neutral-400)) 0, + rgba(var(--color-gold-neutral-500), 0.2) 20%, + rgba(var(--color-gold-neutral-700), 0.5) 60%, + rgba(var(--color-gold-neutral-800), 0) ); animation: shimmer 2s infinite; content: ''; z-index: 999; } +/* Dark mode styles */ +@media (prefers-color-scheme: dark) { + .profile-loading::after { + background-image: linear-gradient( + 90deg, + rgba(var(--color-wine-neutral-100)) 0, + rgba(var(--color-wine-neutral-100), 0.2) 20%, + rgba(var(--color-wine-neutral-100), 0.5) 30%, + rgba(var(--color-wine-neutral-100), 0) + ); + } +} + @keyframes shimmer { 100% { transform: translateX(100%); diff --git a/client/src/pages/search/results.vue b/client/src/pages/search/results.vue index b45e8c69..d36cad74 100644 --- a/client/src/pages/search/results.vue +++ b/client/src/pages/search/results.vue @@ -179,7 +179,7 @@ export const useSearchData = defineBasicLoader( throw new DataLoaderErrorPassThrough(error); } }, - { lazy: true }, + // { lazy: true }, ); export const useFollowedData = defineBasicLoader( @@ -195,9 +195,9 @@ export const useFollowedData = defineBasicLoader( throw new DataLoaderErrorPassThrough(error); } }, - { - lazy: true, - }, + // { + // lazy: true, + // }, ); export const useRequestsData = defineBasicLoader( @@ -211,9 +211,9 @@ export const useRequestsData = defineBasicLoader( throw new DataLoaderErrorPassThrough(error); } }, - { - lazy: true, - }, + // { + // lazy: true, + // }, ); // function isOnlyHashChanged(currentRoute, previousRoute) { From 756714d2a6422fd1f3678da93fd34f39e24b8559 Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Fri, 20 Dec 2024 13:54:40 -0500 Subject: [PATCH 3/5] fix: search results lazy load --- client/src/api/search.js | 6 +- client/src/pages/search/results.vue | 58 +++++++++-------- client/src/router.js | 4 +- client/src/util/locationFormatters.js | 91 ++++++++++++++++----------- 4 files changed, 92 insertions(+), 67 deletions(-) diff --git a/client/src/api/search.js b/client/src/api/search.js index f1ca2219..07044b4d 100644 --- a/client/src/api/search.js +++ b/client/src/api/search.js @@ -94,9 +94,11 @@ export async function getFollowedSearch(location_id) { try { const response = await getFollowedSearches(); - return response.data.data.find( - ({ id: followed_id }) => Number(followed_id) === Number(location_id), + const found = response.data.data.find( + ({ location_id: followed_id }) => + Number(followed_id) === Number(location_id), ); + return found; } catch (error) { return null; } diff --git a/client/src/pages/search/results.vue b/client/src/pages/search/results.vue index d36cad74..1a7bc4d5 100644 --- a/client/src/pages/search/results.vue +++ b/client/src/pages/search/results.vue @@ -9,7 +9,11 @@ >

Results - {{ searchData && 'for ' + getMinimalLocationText(searchData.params) }} + {{ + searchData && + !isLoading && + 'for ' + getMinimalLocationText(searchData.params) + }}

@@ -160,26 +164,20 @@ export const useSearchData = defineBasicLoader( try { const searchLocation = mapSearchParamsToLocation(route.query); const searched = getMostNarrowSearchLocationWithResults(searchLocation); - const response = await search(route.query); - // On initial fetch - get hash - const hash = normalizeLocaleForHash(searched, response.data); - if (!route.hash && hash) { - return new NavigationResult({ ...route, hash: `#${hash}` }); - } - - const ret = { + return { results: groupResultsByAgency(response.data), + response: response.data, searched, params: searchLocation, + hash: normalizeLocaleForHash(searched, response.data), }; - return ret; } catch (error) { throw new DataLoaderErrorPassThrough(error); } }, - // { lazy: true }, + { lazy: true }, ); export const useFollowedData = defineBasicLoader( @@ -195,9 +193,9 @@ export const useFollowedData = defineBasicLoader( throw new DataLoaderErrorPassThrough(error); } }, - // { - // lazy: true, - // }, + { + lazy: true, + }, ); export const useRequestsData = defineBasicLoader( @@ -211,9 +209,9 @@ export const useRequestsData = defineBasicLoader( throw new DataLoaderErrorPassThrough(error); } }, - // { - // lazy: true, - // }, + { + lazy: true, + }, ); // function isOnlyHashChanged(currentRoute, previousRoute) { @@ -243,7 +241,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { faUserPlus, faUserCheck } from '@fortawesome/free-solid-svg-icons'; import { toast } from 'vue3-toastify'; import { useAuthStore } from '@/stores/auth'; -import { useRoute } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; const { isAuthenticated } = useAuthStore(); const { @@ -255,23 +253,31 @@ const { const { data: isFollowed, reload: reloadFollowed } = useFollowedData(); const { data: requestData, error: requestsError } = useRequestsData(); const route = useRoute(); +const router = useRouter(); const searchResultsRef = ref(); const isSearchShown = ref(false); const dims = reactive({ width: window.innerWidth, height: window.innerHeight }); const hasDisplayedErrorByRouteParams = ref(new Map()); +// watch(() => route.query, reloadSearch); + watch( - () => route.query, - async () => { - try { - await reloadSearch(); - } catch (error) { - console.error('Failed to reload search:', error); + () => route, + (newRoute) => { + if (newRoute.hash && !route.hash) { + const hash = `#${normalizeLocaleForHash(searchData.searched, searchData.response)}`; + router.replace({ ...route, hash }); } }, - { deep: true }, + { immediate: true, deep: true }, ); +// On initial fetch - get hash +// const hash = normalizeLocaleForHash(searched, response.data); +// if (!route.hash && hash) { +// return new NavigationResult({ ...route, hash: `#${hash}` }); +// } + // lifecycle methods onMounted(() => { if (window.innerWidth > 1280) isSearchShown.value = true; @@ -325,10 +331,10 @@ onUnmounted(() => { async function follow() { try { await followSearch(route.query.location_id); + await reloadFollowed(); toast.success( `Search followed for ${getMinimalLocationText(searchData?.value?.params)}.`, ); - await reloadFollowed(); } catch (error) { toast.error( `Error following search for ${getMinimalLocationText(searchData?.value?.params)}. Please try again.`, diff --git a/client/src/router.js b/client/src/router.js index f5865cc0..e41d648c 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -24,12 +24,12 @@ router.beforeEach(async (to, from, next) => { // redirect to login page if not logged in and trying to access a restricted page const auth = useAuthStore(); - if (to.path === '/sign-in' && from.meta.auth) { + if (to.path === '/sign-in' && from.meta?.auth) { auth.$patch({ redirectTo: from }); next(); } - if (to.meta.auth && !auth.isAuthenticated()) { + if (to.meta?.auth && !auth.isAuthenticated()) { auth.$patch({ redirectTo: to }); next({ path: '/sign-in', replace: true }); diff --git a/client/src/util/locationFormatters.js b/client/src/util/locationFormatters.js index b5a5df85..ed4c870e 100644 --- a/client/src/util/locationFormatters.js +++ b/client/src/util/locationFormatters.js @@ -15,20 +15,20 @@ export function getFullLocationText(location) { } export function getLocationCityState(location) { - const locality = location.locality_name ?? ''; - const stateAbbr = - STATES_TO_ABBREVIATIONS.get(location.state_name) ?? - // TODO: remove this once we have all the location data standardized - location.state_iso ?? - ''; + const searched = getMostNarrowSearchLocationWithResults(location); + + const locality = location.locality_name; + const stateAbbr = STATES_TO_ABBREVIATIONS.get(location.state_name); const state = location.state_name; const displayName = location.display_name; - if (locality && !state && !stateAbbr) return locality; - else if (locality && stateAbbr) return `${locality}, ${stateAbbr}`; - else if (locality && state) return `${locality}, ${state}`; - else if (state) return state; - else return displayName; + if (searched === 'locality') { + if (locality && !state && !stateAbbr) return locality; + else if (locality && stateAbbr) return `${locality}, ${stateAbbr}`; + else if (locality && state) return `${locality}, ${state}`; + } else if (searched === 'county' || searched === 'state') { + return state; + } else return displayName; } export function getMinimalLocationText(location) { @@ -37,34 +37,51 @@ export function getMinimalLocationText(location) { export function getMostNarrowSearchLocationWithResults(location) { if (!location) return null; - // Checking for string 'null' because of all the data processing that happens before we get here. - if (location?.locality_name !== 'null') return 'locality'; - if (location?.county_name !== 'null') return 'county'; - if (location?.state_name !== 'null') return 'state'; + if (location?.locality_name) return 'locality'; + if (location?.county_name) return 'county'; + if (location?.state_name) return 'state'; return location?.type?.toLowerCase(); } // TODO: cache getLocationById function and fetch to get locations by id rather than all of this parsing. -export const mapSearchParamsToLocation = (obj) => - (({ state, county, locality, location_id, id }) => ({ - state_name: state, - state_iso: state, - county_name: county, - locality_name: locality, - id: location_id ?? id, - }))(obj); +export const mapSearchParamsToLocation = (obj) => { + const { state, county, locality, location_id, id } = obj; + const mapped = {}; + + if (state) { + mapped.state_name = state; + mapped.state_iso = state; + } + if (county) { + mapped.county_name = county; + } + if (locality) { + mapped.locality_name = locality; + } + if (location_id || id) { + mapped.id = location_id ?? id; + } + + return mapped; +}; + +export const mapLocationToSearchParams = (obj) => { + const { state_name, state_iso, county_name, locality_name, id, location_id } = + obj; + const mapped = {}; + + if (state_name || state_iso) { + mapped.state = state_name ?? state_iso; + } + if (county_name) { + mapped.county = county_name; + } + if (locality_name) { + mapped.locality = locality_name; + } + if (id || location_id) { + mapped.location_id = id ?? location_id; + } -export const mapLocationToSearchParams = (obj) => - (({ - state_name, - state_iso, - county_name, - locality_name, - id, - location_id, - }) => ({ - state: state_name ?? state_iso, - county: county_name, - locality: locality_name, - location_id: id ?? location_id, - }))(obj); + return mapped; +}; From bac5080bc135814b7de1b3b9eaf924c7aec82280 Mon Sep 17 00:00:00 2001 From: Joshua Graber Date: Fri, 20 Dec 2024 14:19:34 -0500 Subject: [PATCH 4/5] fix: profile loading --- client/src/pages/profile/index.vue | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/src/pages/profile/index.vue b/client/src/pages/profile/index.vue index d89e4ede..55dcdfb7 100644 --- a/client/src/pages/profile/index.vue +++ b/client/src/pages/profile/index.vue @@ -14,7 +14,9 @@
-
+

{{ profileData?.email }}

@@ -24,7 +26,9 @@
-
+