From 9ca0d262e2c893cfa7500eb59431c38ce3adc6c5 Mon Sep 17 00:00:00 2001 From: Max <133934232+Kleostro@users.noreply.github.com> Date: Tue, 14 May 2024 00:23:05 +0300 Subject: [PATCH] fix: handle popstate event --- src/app/App/model/AppModel.ts | 2 +- src/app/Router/model/RouterModel.ts | 47 ++++++++++++------- src/app/styles/fonts.scss | 16 +++---- .../Navigation/model/NavigationModel.ts | 2 +- src/pages/Blog/Post/view/PostView.ts | 13 ++--- .../Blog/PostList/model/PostListModel.ts | 9 +--- .../Blog/PostWidget/model/PostWidgetModel.ts | 9 +--- src/pages/MainPage/model/MainPageModel.ts | 11 +---- .../NotFoundPage/view/NotFoundPageView.ts | 1 + src/shared/Store/test.spec.ts | 4 +- src/shared/constants/initialState.ts | 4 +- src/shared/constants/pages.ts | 2 +- src/shared/types/validation/paths.ts | 20 ++++++++ src/widgets/Catalog/model/CatalogModel.ts | 2 +- 14 files changed, 75 insertions(+), 67 deletions(-) create mode 100644 src/shared/types/validation/paths.ts diff --git a/src/app/App/model/AppModel.ts b/src/app/App/model/AppModel.ts index 707e0cd9..cdeb1b30 100644 --- a/src/app/App/model/AppModel.ts +++ b/src/app/App/model/AppModel.ts @@ -27,7 +27,7 @@ class AppModel { }, [PAGE_ID.BLOG]: async (): Promise => { const { default: PostListModel } = await import('@/pages/Blog/PostList/model/PostListModel.ts'); - return new PostListModel(this.appView.getHTML(), this.router); + return new PostListModel(this.appView.getHTML()); }, [PAGE_ID.CART_PAGE]: async (): Promise => { const { default: CartPageModel } = await import('@/pages/CartPage/model/CartPageModel.ts'); diff --git a/src/app/Router/model/RouterModel.ts b/src/app/Router/model/RouterModel.ts index 0a277450..c7f961b5 100644 --- a/src/app/Router/model/RouterModel.ts +++ b/src/app/Router/model/RouterModel.ts @@ -1,10 +1,8 @@ import type { Page } from '@/shared/types/common.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; +import { isValidPath, isValidState } from '@/shared/types/validation/paths.ts'; -const DEFAULT_SEGMENT = import.meta.env.VITE_APP_DEFAULT_SEGMENT; -const NEXT_SEGMENT = import.meta.env.VITE_APP_NEXT_SEGMENT; -const PATH_SEGMENTS_TO_KEEP = import.meta.env.VITE_APP_PATH_SEGMENTS_TO_KEEP; const PROJECT_TITLE = import.meta.env.VITE_APP_PROJECT_TITLE; class RouterModel { @@ -12,12 +10,20 @@ class RouterModel { constructor() { document.addEventListener('DOMContentLoaded', async () => { - const currentPath = window.location.pathname - .split(DEFAULT_SEGMENT) - .slice(PATH_SEGMENTS_TO_KEEP + NEXT_SEGMENT) - .join(DEFAULT_SEGMENT); + const currentPath = window.location.pathname.slice(1) || PAGE_ID.DEFAULT_PAGE; await this.navigateTo(currentPath); }); + + window.addEventListener('popstate', async (event) => { + const currentPath: unknown = event.state; + + if (!isValidState(currentPath) || !isValidPath(currentPath.path)) { + window.location.pathname = PAGE_ID.DEFAULT_PAGE; + return; + } + + await this.handleRequest(currentPath.path); + }); } private changeAppTitle(path: string, hasRoute: boolean): void { @@ -25,24 +31,29 @@ class RouterModel { document.title = title; } - public async navigateTo(path: string): Promise { - const pathnameApp = window.location.pathname - .split(DEFAULT_SEGMENT) - .slice(NEXT_SEGMENT, PATH_SEGMENTS_TO_KEEP + NEXT_SEGMENT) - .join(DEFAULT_SEGMENT); - const url = `${pathnameApp}/${path}`; + private async handleRequest(path: string): Promise { + const hasRoute = this.routes.has(path); + this.changeAppTitle(path === PAGE_ID.DEFAULT_PAGE ? PAGE_ID.MAIN_PAGE : path, hasRoute); - const pathParts = url.split(DEFAULT_SEGMENT); - const hasRoute = this.routes.has(pathParts[1]); - this.changeAppTitle(pathParts[1], hasRoute); + if (!hasRoute) { + await this.routes.get(PAGE_ID.NOT_FOUND_PAGE)?.(); + return; + } + + await this.routes.get(path)?.(); + } + + public async navigateTo(path: string): Promise { + const hasRoute = this.routes.has(path); + this.changeAppTitle(path === PAGE_ID.DEFAULT_PAGE ? PAGE_ID.MAIN_PAGE : path, hasRoute); if (!hasRoute) { await this.routes.get(PAGE_ID.NOT_FOUND_PAGE)?.(); return; } - await this.routes.get(pathParts[1])?.(); - history.pushState(path, '', url); + await this.routes.get(path)?.(); + history.pushState({ path }, '', path); } public setRoutes(routes: Map Promise>): Map Promise> { diff --git a/src/app/styles/fonts.scss b/src/app/styles/fonts.scss index 47317453..2d2a8bfc 100644 --- a/src/app/styles/fonts.scss +++ b/src/app/styles/fonts.scss @@ -1,8 +1,8 @@ @font-face { src: local('cerapro-black'), - url('/public/fonts/cerapro-black-webfont.woff2') format('woff2'), - url('/public/fonts/cerapro-black-webfont.woff') format('woff'); + url('/fonts/cerapro-black-webfont.woff2') format('woff2'), + url('/fonts/cerapro-black-webfont.woff') format('woff'); font-family: 'Cerapro'; font-weight: 900; font-style: normal; @@ -12,8 +12,8 @@ @font-face { src: local('cerapro-bold'), - url('/public/fonts/cerapro-bold-webfont.woff2') format('woff2'), - url('/public/fonts/cerapro-bold-webfont.woff') format('woff'); + url('/fonts/cerapro-bold-webfont.woff2') format('woff2'), + url('/fonts/cerapro-bold-webfont.woff') format('woff'); font-family: 'Cerapro'; font-weight: 700; font-style: normal; @@ -23,8 +23,8 @@ @font-face { src: local('cerapro-medium'), - url('/public//fonts/cerapro-medium-webfont.woff2') format('woff2'), - url('/public/fonts/cerapro-medium-webfont.woff') format('woff'); + url('/fonts/cerapro-medium-webfont.woff2') format('woff2'), + url('/fonts/cerapro-medium-webfont.woff') format('woff'); font-family: 'Cerapro'; font-weight: 500; font-style: normal; @@ -34,8 +34,8 @@ @font-face { src: local('cerapro-regular'), - url('/public/fonts/cerapro-regular-webfont.woff2') format('woff2'), - url('/public/fonts/cerapro-regular-webfont.woff') format('woff'); + url('/fonts/cerapro-regular-webfont.woff2') format('woff2'), + url('/fonts/cerapro-regular-webfont.woff') format('woff'); font-family: 'Cerapro'; font-weight: 400; font-style: normal; diff --git a/src/entities/Navigation/model/NavigationModel.ts b/src/entities/Navigation/model/NavigationModel.ts index a448c25f..506d19f8 100644 --- a/src/entities/Navigation/model/NavigationModel.ts +++ b/src/entities/Navigation/model/NavigationModel.ts @@ -63,7 +63,7 @@ class NavigationModel { private switchLinksState(): boolean { const { currentPage } = getStore().getState(); - const currentPath = currentPage === '' ? PAGE_ID.MAIN_PAGE : currentPage; + const currentPath = currentPage === PAGE_ID.DEFAULT_PAGE ? PAGE_ID.MAIN_PAGE : currentPage; const navigationLinks = this.view.getNavigationLinks(); const currentLink = navigationLinks.get(String(currentPath)); navigationLinks.forEach((link) => link.setEnabled()); diff --git a/src/pages/Blog/Post/view/PostView.ts b/src/pages/Blog/Post/view/PostView.ts index 306251f8..5a804bdc 100644 --- a/src/pages/Blog/Post/view/PostView.ts +++ b/src/pages/Blog/Post/view/PostView.ts @@ -1,9 +1,7 @@ -import type RouterModel from '@/app/Router/model/RouterModel'; import type { Post } from '@/shared/constants/blog'; import getStore from '@/shared/Store/Store.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; -import { PAGE_ID } from '@/shared/constants/pages.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './post.module.scss'; @@ -17,16 +15,13 @@ export default class PostView { private post: Post; - private router: RouterModel; - - constructor(post: Post, postClickCallback: (post: PostView) => void, router: RouterModel) { + constructor(post: Post, postClickCallback: (post: PostView) => void) { this.post = post; - this.router = router; this.callback = postClickCallback; this.card = this.createCardHTML(); this.blogPost = this.createPostHtml(); - this.card.addEventListener('click', (event) => this.onPostClick(event)); + this.card.addEventListener('click', () => this.onPostClick()); observeStore(selectCurrentLanguage, () => this.updateLanguage()); } @@ -61,9 +56,7 @@ export default class PostView { return content; } - private async onPostClick(event: Event): Promise { - event.preventDefault(); - await this.router.navigateTo(`${PAGE_ID.BLOG}/${this.post.id}`); + private onPostClick(): void { this.callback(this); } diff --git a/src/pages/Blog/PostList/model/PostListModel.ts b/src/pages/Blog/PostList/model/PostListModel.ts index a54b91da..8d1a72ad 100644 --- a/src/pages/Blog/PostList/model/PostListModel.ts +++ b/src/pages/Blog/PostList/model/PostListModel.ts @@ -1,4 +1,3 @@ -import type RouterModel from '@/app/Router/model/RouterModel.ts'; import type { Post } from '@/shared/constants/blog.ts'; import type { Page } from '@/shared/types/common.ts'; @@ -20,15 +19,12 @@ export default class PostListModel implements Page { private posts: PostView[]; - private router: RouterModel; - private view: BlogPageView; - constructor(parent: HTMLDivElement, router: RouterModel) { + constructor(parent: HTMLDivElement) { const newPost: Post[] = postsData; this.parent = parent; - this.router = router; - this.posts = newPost.map((post) => new PostView(post, this.postClickHandler, this.router)); + this.posts = newPost.map((post) => new PostView(post, this.postClickHandler)); this.view = new BlogPageView(parent, this.posts); this.render(); this.init(); @@ -37,7 +33,6 @@ export default class PostListModel implements Page { private init(): boolean { getStore().dispatch(setCurrentPage(PAGE_ID.BLOG)); this.observeStoreLanguage(); - window.addEventListener('popstate', () => this.render()); return true; } diff --git a/src/pages/Blog/PostWidget/model/PostWidgetModel.ts b/src/pages/Blog/PostWidget/model/PostWidgetModel.ts index e186a450..529720d8 100644 --- a/src/pages/Blog/PostWidget/model/PostWidgetModel.ts +++ b/src/pages/Blog/PostWidget/model/PostWidgetModel.ts @@ -1,5 +1,3 @@ -import type RouterModel from '@/app/Router/model/RouterModel.ts'; - import PostView from '@/pages/Blog/Post/view/PostView.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; @@ -16,15 +14,12 @@ export default class PostWidgetModel { private posts: PostView[]; - private router: RouterModel; - private view: PostWidgetView; - constructor(parent: HTMLDivElement, router: RouterModel) { + constructor(parent: HTMLDivElement) { const shuffledPosts = [...postsData].sort(() => HALF_RANDOM - Math.random()); const newPost = shuffledPosts.slice(0, CART_COUNT); - this.router = router; - this.posts = newPost.map((post) => new PostView(post, this.postClickHandler, this.router)); + this.posts = newPost.map((post) => new PostView(post, this.postClickHandler)); this.view = new PostWidgetView(parent, this.posts); this.init(); } diff --git a/src/pages/MainPage/model/MainPageModel.ts b/src/pages/MainPage/model/MainPageModel.ts index 95a291b0..cc6eb7df 100644 --- a/src/pages/MainPage/model/MainPageModel.ts +++ b/src/pages/MainPage/model/MainPageModel.ts @@ -25,22 +25,13 @@ class MainPageModel implements Page { this.parent = parent; this.view = new MainPageView(this.parent); this.navigation = new NavigationModel(this.router); - this.blogWidget = new PostWidgetModel(this.view.getHTML(), router); + this.blogWidget = new PostWidgetModel(this.view.getHTML()); this.init(); } private init(): void { this.getHTML().append(this.navigation.getHTML(), this.blogWidget.getHTML()); getStore().dispatch(setCurrentPage(PAGE_ID.MAIN_PAGE)); - window.addEventListener('popstate', () => this.onHistoryChange()); - } - - private async onHistoryChange(): Promise { - const path = window.location.pathname; - - if (path === '/main') { - await this.router.navigateTo(PAGE_ID.MAIN_PAGE); - } } public getHTML(): HTMLDivElement { diff --git a/src/pages/NotFoundPage/view/NotFoundPageView.ts b/src/pages/NotFoundPage/view/NotFoundPageView.ts index 2058e14f..4ec17f5b 100644 --- a/src/pages/NotFoundPage/view/NotFoundPageView.ts +++ b/src/pages/NotFoundPage/view/NotFoundPageView.ts @@ -24,6 +24,7 @@ class NotFoundPageView { constructor(parent: HTMLDivElement) { this.parent = parent; + this.parent.innerHTML = ''; this.logo = this.createPageLogo(); this.title = this.createPageTitle(); this.description = this.createPageDescription(); diff --git a/src/shared/Store/test.spec.ts b/src/shared/Store/test.spec.ts index 44de5608..8a4f131f 100644 --- a/src/shared/Store/test.spec.ts +++ b/src/shared/Store/test.spec.ts @@ -91,7 +91,7 @@ vi.mock('./Store.ts', async (importOriginal) => { billingCountry: '', categories: [], currentLanguage: 'en', - currentPage: '', + currentPage: '/', currentUser: null, isAppThemeLight: true, isUserLoggedIn: false, @@ -169,7 +169,7 @@ describe('rootReducer', () => { billingCountry: '', categories: [], currentLanguage: 'en', - currentPage: '', + currentPage: '/', currentUser: null, isAppThemeLight: true, isUserLoggedIn: false, diff --git a/src/shared/constants/initialState.ts b/src/shared/constants/initialState.ts index ad452aad..6385433c 100644 --- a/src/shared/constants/initialState.ts +++ b/src/shared/constants/initialState.ts @@ -1,10 +1,12 @@ import type { State } from '../Store/reducer'; +import { PAGE_ID } from './pages.ts'; + const initialState: State = { billingCountry: '', categories: [], currentLanguage: 'en', - currentPage: '', + currentPage: PAGE_ID.DEFAULT_PAGE, currentUser: null, isAppThemeLight: true, isUserLoggedIn: false, diff --git a/src/shared/constants/pages.ts b/src/shared/constants/pages.ts index a5cf998e..060f087b 100644 --- a/src/shared/constants/pages.ts +++ b/src/shared/constants/pages.ts @@ -91,7 +91,7 @@ export const PAGE_ID = { BLOG: 'blog', CART_PAGE: 'cart', CATALOG_PAGE: 'catalog', - DEFAULT_PAGE: '', + DEFAULT_PAGE: '/', ITEM_PAGE: 'item', LOGIN_PAGE: 'login', MAIN_PAGE: 'main', diff --git a/src/shared/types/validation/paths.ts b/src/shared/types/validation/paths.ts new file mode 100644 index 00000000..1e5d140a --- /dev/null +++ b/src/shared/types/validation/paths.ts @@ -0,0 +1,20 @@ +import type { PageIdType } from '@/shared/constants/pages'; + +import { PAGE_ID } from '@/shared/constants/pages.ts'; + +interface PopStateEventState { + path: string; +} + +export const isValidPath = (value: unknown): value is PageIdType => + Object.values(PAGE_ID).findIndex((route) => route === value) !== -1; + +export const isValidState = (state: unknown): state is PopStateEventState => { + if (!state) { + return false; + } + if (typeof state === 'object' && 'path' in state && typeof state.path === 'string') { + return true; + } + return false; +}; diff --git a/src/widgets/Catalog/model/CatalogModel.ts b/src/widgets/Catalog/model/CatalogModel.ts index d67a2549..c16cc810 100644 --- a/src/widgets/Catalog/model/CatalogModel.ts +++ b/src/widgets/Catalog/model/CatalogModel.ts @@ -64,7 +64,7 @@ class CatalogModel { filter.push(addFilter(FilterFields.SIZE, size)); } - return { filter, limit: 100, sort: { direction: 'desc', field: 'name', locale: 'en' } }; + return { filter, limit: 100, sort: { direction: 'asc', field: 'price' } }; } private init(): void {