From 21fc032b27f0c9f90bf5332a5bf5174e80150223 Mon Sep 17 00:00:00 2001 From: Vektor19 Date: Mon, 29 Dec 2025 15:51:23 +0200 Subject: [PATCH 1/4] feat/247: create favorites store tool to store favorite streetcodes in local storage --- src/app/stores/favorites-store.ts | 55 +++++++++++++++++++++++++++++++ src/app/stores/root-store.ts | 5 ++- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/app/stores/favorites-store.ts diff --git a/src/app/stores/favorites-store.ts b/src/app/stores/favorites-store.ts new file mode 100644 index 0000000..100a5f1 --- /dev/null +++ b/src/app/stores/favorites-store.ts @@ -0,0 +1,55 @@ +import { makeAutoObservable } from 'mobx'; + +export default class FavoritesStore { + private static storageKey = 'streetcode_favorites'; + public favoriteIds: Set = new Set(); + + public constructor() { + makeAutoObservable(this); + this.loadFromLocalStorage(); + } + + private loadFromLocalStorage = () => { + const stored = localStorage.getItem(FavoritesStore.storageKey); + if (stored) { + this.favoriteIds = new Set(JSON.parse(stored)); + } + }; + + private saveToLocalStorage = () => { + localStorage.setItem( + FavoritesStore.storageKey, + JSON.stringify(Array.from(this.favoriteIds)) + ); + }; + + public addFavorite = (streetcodeId: number) => { + this.favoriteIds.add(streetcodeId); + this.saveToLocalStorage(); + }; + + public removeFavorite = (streetcodeId: number) => { + this.favoriteIds.delete(streetcodeId); + this.saveToLocalStorage(); + }; + + public toggleFavorite = (streetcodeId: number) => { + if (this.isFavorite(streetcodeId)) { + this.removeFavorite(streetcodeId); + } else { + this.addFavorite(streetcodeId); + } + }; + + public isFavorite = (streetcodeId: number): boolean => { + return this.favoriteIds.has(streetcodeId); + }; + + get favoritesCount() { + return this.favoriteIds.size; + } + + get favoritesList() { + return Array.from(this.favoriteIds); + } +} \ No newline at end of file diff --git a/src/app/stores/root-store.ts b/src/app/stores/root-store.ts index cead227..dad04a7 100644 --- a/src/app/stores/root-store.ts +++ b/src/app/stores/root-store.ts @@ -32,6 +32,7 @@ import StreetcodesByTagStore from './streetcodes-bytag-store'; import TeamStore from './team-store'; import ToponymStore from './toponym-store'; import UserLoginStore from './user-login-store'; +import FavoritesStore from './favorites-store'; interface Store { factsStore: FactsStore, @@ -63,6 +64,7 @@ interface Store { relatedByTag: StreetcodesByTagStore, createUpdateMediaStore: CreateUpdateMediaStore, modalStore: ModalStore, + favoritesStore: FavoritesStore, } export interface StreetcodeDataStore { @@ -101,7 +103,8 @@ export const store: Store = { streetcodeMainPageStore: new StreetcodesMainPageStore(), relatedByTag: new StreetcodesByTagStore(), createUpdateMediaStore: new CreateUpdateMediaStore(), - modalStore: new ModalStore() + modalStore: new ModalStore(), + favoritesStore: new FavoritesStore() }; export const streetcodeDataStore:StreetcodeDataStore = { streetcodeStore: new StreetcodeStore(), From 348065305ef9cbfd4b458b57d786e452b8634e9e Mon Sep 17 00:00:00 2001 From: Vektor19 Date: Mon, 29 Dec 2025 15:52:36 +0200 Subject: [PATCH 2/4] feat/247: implement favorite functionality for streetcodes with filtering options --- .../StreetcodeCatalog.component.tsx | 25 ++++++++- .../StreetcodeCatalog.styles.scss | 29 ++++++++++ .../StreetcodeCatalogItem.component.tsx | 55 ++++++++++++++----- .../StreetcodeCatalogItem.styles.scss | 27 +++++++++ 4 files changed, 118 insertions(+), 18 deletions(-) diff --git a/src/features/StreetcodeCatalogPage/StreetcodeCatalog.component.tsx b/src/features/StreetcodeCatalogPage/StreetcodeCatalog.component.tsx index e3ec535..8cf9f74 100644 --- a/src/features/StreetcodeCatalogPage/StreetcodeCatalog.component.tsx +++ b/src/features/StreetcodeCatalogPage/StreetcodeCatalog.component.tsx @@ -11,15 +11,20 @@ import { useAsync } from '@/app/common/hooks/stateful/useAsync.hook'; import StreetcodeCatalogItem from './StreetcodeCatalogItem/StreetcodeCatalogItem.component'; const StreetcodeCatalog = () => { - const { streetcodeCatalogStore } = useMobx(); + const { streetcodeCatalogStore, favoritesStore } = useMobx(); const { fetchCatalogStreetcodes, getCatalogStreetcodesArray } = streetcodeCatalogStore; const [loading, setLoading] = useState(false); const [screen, setScreen] = useState(1); + const [showOnlyFavorites, setShowOnlyFavorites] = useState(false); const handleSetNextScreen = () => { setScreen(screen + 1); }; + const filteredStreetcodes = showOnlyFavorites + ? getCatalogStreetcodesArray.filter((streetcode) => favoritesStore.isFavorite(streetcode.id)) + : getCatalogStreetcodesArray; + useAsync(async () => { const count = await StreetcodesApi.getCount(); if (count === getCatalogStreetcodesArray.length) { @@ -37,14 +42,28 @@ const StreetcodeCatalog = () => {

Стріткоди

+
+ + +
{ - getCatalogStreetcodesArray.map( + filteredStreetcodes.map( (streetcode, index) => ( ), diff --git a/src/features/StreetcodeCatalogPage/StreetcodeCatalog.styles.scss b/src/features/StreetcodeCatalogPage/StreetcodeCatalog.styles.scss index 881c602..89ae1bc 100644 --- a/src/features/StreetcodeCatalogPage/StreetcodeCatalog.styles.scss +++ b/src/features/StreetcodeCatalogPage/StreetcodeCatalog.styles.scss @@ -27,6 +27,35 @@ $loadImg: '@assets/images/catalog/loading.gif'; align-items: center; color: c.$milky-white-color; } + + .catalogFilterButtons { + @include mut.flexed($gap: pxToRem(12px)); + @include mut.rem-margined($bottom: 24px); + + .filterButton { + @include mut.rem-padded(10px, 20px, 10px, 20px); + @include mut.full-rounded(pxToRem(8px)); + @include vnd.vendored(transition, 'all 0.2s ease'); + + cursor: pointer; + font-size: pxToRem(16px); + background: c.$pure-white-color; + color: c.$lighter-black-color; + border: 1px solid c.$darker-gray-color; + font-weight: normal; + + &.active { + border: 2px solid c.$dark-red-color; + background: c.$dark-red-color; + color: c.$pure-white-color; + font-weight: bold; + } + + &:hover { + opacity: 0.9; + } + } + } .steetcodeCatalogContainer { display: grid; diff --git a/src/features/StreetcodeCatalogPage/StreetcodeCatalogItem/StreetcodeCatalogItem.component.tsx b/src/features/StreetcodeCatalogPage/StreetcodeCatalogItem/StreetcodeCatalogItem.component.tsx index a6750cf..96a0be2 100644 --- a/src/features/StreetcodeCatalogPage/StreetcodeCatalogItem/StreetcodeCatalogItem.component.tsx +++ b/src/features/StreetcodeCatalogPage/StreetcodeCatalogItem/StreetcodeCatalogItem.component.tsx @@ -4,6 +4,7 @@ import './StreetcodeCatalogItem.styles.scss'; import { observer } from 'mobx-react-lite'; import { useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; +import { HeartFilled, HeartOutlined } from '@ant-design/icons'; import useMobx from '@stores/root-store'; import useOnScreen from '@/app/common/hooks/scrolling/useOnScreen.hook'; @@ -20,7 +21,7 @@ interface Props { } const StreetcodeCatalogItem = ({ streetcode, isLast, handleNextScreen }: Props) => { - const { imagesStore: { getImage, fetchImage } } = useMobx(); + const { imagesStore: { getImage, fetchImage }, favoritesStore } = useMobx(); const elementRef = useRef(null); const classSelector = 'catalogItem'; const isOnScreen = useOnScreen(elementRef, classSelector); @@ -38,26 +39,43 @@ const StreetcodeCatalogItem = ({ streetcode, isLast, handleNextScreen }: Props) } const windowsize = useWindowSize(); + const handleFavoriteClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + favoritesStore.toggleFavorite(streetcode.id); + }; + + const isFavorited = favoritesStore.isFavorite(streetcode.id); + return ( <> {windowsize.width > 1024 && ( - toStreetcodeRedirectClickEvent(streetcode.url, 'catalog')}> -
-
-

{streetcode.title}

- { - streetcode.alias !== null ? ( -

- ({streetcode.alias}) -

- ) : undefined - } +
+ toStreetcodeRedirectClickEvent(streetcode.url, 'catalog')}> +
+
+

{streetcode.title}

+ { + streetcode.alias !== null ? ( +

+ ({streetcode.alias}) +

+ ) : undefined + } +
-
- + + +
)} {windowsize.width <= 1024 && ( -
+
@@ -71,6 +89,13 @@ const StreetcodeCatalogItem = ({ streetcode, isLast, handleNextScreen }: Props) }
+
)} diff --git a/src/features/StreetcodeCatalogPage/StreetcodeCatalogItem/StreetcodeCatalogItem.styles.scss b/src/features/StreetcodeCatalogPage/StreetcodeCatalogItem/StreetcodeCatalogItem.styles.scss index 8d26d08..cdba452 100644 --- a/src/features/StreetcodeCatalogPage/StreetcodeCatalogItem/StreetcodeCatalogItem.styles.scss +++ b/src/features/StreetcodeCatalogPage/StreetcodeCatalogItem/StreetcodeCatalogItem.styles.scss @@ -78,6 +78,33 @@ } } +.favoriteButton { + @include mut.positioned-as(absolute); + top: pxToRem(12px); + right: pxToRem(12px); + @include mut.sized(pxToRem(43px), pxToRem(43px)); + @include mut.flex-centered(); + + background: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 50%; + cursor: pointer; + font-size: pxToRem(22px); + @include vnd.vendored(transition, 'all 0.2s ease'); + box-shadow: 0 pxToRem(2px) pxToRem(8px) rgba(0, 0, 0, 0.15); + z-index: 10; + + &:hover { + @include vnd.vendored(transform, 'scale(1.1)'); + box-shadow: 0 pxToRem(4px) pxToRem(12px) rgba(0, 0, 0, 0.25); + } + + &.mobile { + @include mut.sized(pxToRem(40px), pxToRem(40px)); + font-size: pxToRem(20px); + } +} + @media screen and (max-width: 1024px) { .catalogItem { @include mut.sized($width: 165px, $height: 165px); From 9d7d59af4a42c93ae46f079db0ff54e90e22d9eb Mon Sep 17 00:00:00 2001 From: Vektor19 Date: Mon, 29 Dec 2025 22:12:15 +0200 Subject: [PATCH 3/4] chore/247: fix deploy file --- .github/workflows/azure-static-web-apps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/azure-static-web-apps.yml b/.github/workflows/azure-static-web-apps.yml index c12f6bc..3fcb026 100644 --- a/.github/workflows/azure-static-web-apps.yml +++ b/.github/workflows/azure-static-web-apps.yml @@ -24,6 +24,8 @@ jobs: - name: Build And Deploy id: builddeploy uses: Azure/static-web-apps-deploy@v1 + env: + REACT_APP_BACKEND_URL: ${{ secrets.REACT_APP_BACKEND_URL }} with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }} @@ -32,8 +34,6 @@ jobs: api_location: "" output_location: "dist" app_build_command: 'npm run build' - env: - REACT_APP_BACKEND_URL: ${{ secrets.REACT_APP_BACKEND_URL }} close_pull_request_job: if: github.event_name == 'pull_request' && github.event.action == 'closed' From 7f94ca1b3bfb86f80319133cc69c6965789b4436 Mon Sep 17 00:00:00 2001 From: Vektor19 Date: Mon, 29 Dec 2025 22:20:41 +0200 Subject: [PATCH 4/4] chore/247: add webpack DefinePlugin for environment variables --- config/webpack.plugins.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/webpack.plugins.js b/config/webpack.plugins.js index b1cc273..2b6a7b3 100644 --- a/config/webpack.plugins.js +++ b/config/webpack.plugins.js @@ -1,4 +1,5 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); +const webpack = require('webpack'); module.exports = [ new HtmlWebpackPlugin({ @@ -7,4 +8,8 @@ module.exports = [ favicon: "./public/favicon.ico", inject: true, }), + new webpack.DefinePlugin({ + 'process.env.REACT_APP_BACKEND_URL': JSON.stringify(process.env.REACT_APP_BACKEND_URL), + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + }), ].filter(Boolean); \ No newline at end of file