diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 9f6d61f..32b8191 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -15,10 +15,16 @@ jobs: with: node-version: 18 + - name: Configure git profile + run: | + git config --global user.email "amsterget@gmail.com" + git config --global user.name "amsterget" + git remote set-url origin https://${{ secrets.GIT_HUB_PERSONAL_TOKEN }}@github.com/${{ github.repository }} + - name: Install of node dependencies run: sh install-modules.sh env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GIT_HUB_PERSONAL_TOKEN }} - name: Deploy run: npm run deploy diff --git a/install-modules.sh b/install-modules.sh index 293ca8d..2bd52e8 100644 --- a/install-modules.sh +++ b/install-modules.sh @@ -1,6 +1,6 @@ source ./.env -npm config set //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} +npm config set //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} npm config set @aikray:registry=https://npm.pkg.github.com/ npm install diff --git a/package.json b/package.json index a5e6257..de8d3af 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,13 @@ }, "dependencies": { "@aikray/menu-bot-common": "^0.0.8", + "@ant-design/icons": "^5.0.1", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@fontsource/roboto": "^4.5.8", "@mui/icons-material": "^5.11.9", "@mui/material": "^5.11.9", + "antd": "^5.2.2", "classnames": "^2.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/App.css b/src/App.css index 74b5e05..3059d89 100644 --- a/src/App.css +++ b/src/App.css @@ -1,38 +1,4 @@ .App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } + height: 100%; + width: 100%; } diff --git a/src/App.tsx b/src/App.tsx index b3d3217..f58749e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,20 @@ import './App.css'; import { fetchQuery } from 'core/fetchQuery'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; -import logo from './logo.svg'; +import { InitialDataProvider } from './providers/initial-data-provider'; +import { Router } from './routes/Router'; export const App = () => { - const [appStatus, setAppStatus] = useState(''); - useEffect(() => { const getAppStatus = async () => { const response = await fetchQuery<{ status: string }>({ path: `/app/health-check`, }); - setAppStatus(response.status); + // eslint-disable-next-line no-console + console.log(response.status); }; getAppStatus(); @@ -22,21 +22,9 @@ export const App = () => { return (
-
- logo -

App Status: {appStatus}

-

- Edit src/App.tsx and save to reload. -

- - Learn React - -
+ + +
); }; diff --git a/src/core/styles/variables.css b/src/core/styles/variables.css new file mode 100644 index 0000000..172090c --- /dev/null +++ b/src/core/styles/variables.css @@ -0,0 +1,4 @@ +:root { + --color-primary: #1677ff; + --color-default: #555; +} diff --git a/src/index.css b/src/index.css index ec2585e..2e8edd8 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,18 @@ +@import "./core/styles/variables.css"; + +html { + height: 100%; + width: 100%; +} + +#root { + height: 100%; + width: 100%; +} + body { + height: 100%; + width: 100%; margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 9dfc1c0..0000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/pages/common/footer/Footer.module.css b/src/pages/common/footer/Footer.module.css new file mode 100644 index 0000000..dbcc54b --- /dev/null +++ b/src/pages/common/footer/Footer.module.css @@ -0,0 +1,22 @@ +.footer { + position: fixed; + bottom: 0; + width: 100%; + padding: 20px 0; + border-top: 1px solid var(--color-primary); +} + +.navigation { + display: flex; + align-items: center; + justify-content: center; + gap: 20px; +} + +.nav-icon { + font-size: 40px; + color: var(--color-default); +} +.nav-icon.active { + color: var(--color-primary); +} diff --git a/src/pages/common/footer/Footer.tsx b/src/pages/common/footer/Footer.tsx new file mode 100644 index 0000000..018beca --- /dev/null +++ b/src/pages/common/footer/Footer.tsx @@ -0,0 +1,37 @@ +import { ShopOutlined, UserOutlined } from '@ant-design/icons'; +import classNames from 'classnames/bind'; +import { NavLink } from 'react-router-dom'; + +import { PROFILE_PAGE, RESTAURANTS_PAGE } from '../../../routes/pages'; +import styles from './Footer.module.css'; + +const cx = classNames.bind(styles); + +interface FooterProps {} + +const navItems = [ + { + id: 'restaurants', + to: RESTAURANTS_PAGE, + icon: ShopOutlined, + }, + { + id: 'profile', + to: PROFILE_PAGE, + icon: UserOutlined, + }, +]; + +export const Footer = (_: FooterProps) => ( + +); diff --git a/src/pages/common/footer/index.ts b/src/pages/common/footer/index.ts new file mode 100644 index 0000000..ddcc5a9 --- /dev/null +++ b/src/pages/common/footer/index.ts @@ -0,0 +1 @@ +export * from './Footer'; diff --git a/src/pages/profile-page/ProfilePage.module.css b/src/pages/profile-page/ProfilePage.module.css new file mode 100644 index 0000000..7c8b01c --- /dev/null +++ b/src/pages/profile-page/ProfilePage.module.css @@ -0,0 +1,6 @@ +.profile-page { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} diff --git a/src/pages/profile-page/ProfilePage.tsx b/src/pages/profile-page/ProfilePage.tsx new file mode 100644 index 0000000..d8c314c --- /dev/null +++ b/src/pages/profile-page/ProfilePage.tsx @@ -0,0 +1,12 @@ +import classNames from 'classnames/bind'; + +import { Footer } from '../common/footer'; +import styles from './ProfilePage.module.css'; + +const cx = classNames.bind(styles); + +export const ProfilePage = () => ( +
+
+); diff --git a/src/pages/profile-page/index.ts b/src/pages/profile-page/index.ts new file mode 100644 index 0000000..654cb92 --- /dev/null +++ b/src/pages/profile-page/index.ts @@ -0,0 +1 @@ +export * from './ProfilePage'; diff --git a/src/pages/restaurants-page/RestaurantsPage.module.css b/src/pages/restaurants-page/RestaurantsPage.module.css new file mode 100644 index 0000000..5a5168c --- /dev/null +++ b/src/pages/restaurants-page/RestaurantsPage.module.css @@ -0,0 +1,16 @@ +.restaurants-page { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} + +.current-city { + display: flex; + justify-content: center; + padding: 10px 5px; +} + +.current-city a { + text-decoration: none; +} diff --git a/src/pages/restaurants-page/RestaurantsPage.tsx b/src/pages/restaurants-page/RestaurantsPage.tsx new file mode 100644 index 0000000..77eab91 --- /dev/null +++ b/src/pages/restaurants-page/RestaurantsPage.tsx @@ -0,0 +1,44 @@ +import { SearchOutlined } from '@ant-design/icons'; +import Button from 'antd/lib/button'; +import classNames from 'classnames/bind'; +import { useContext } from 'react'; +import { Link } from 'react-router-dom'; + +import { StoreContext } from '../../providers/store-provider'; +import { SEARCH_PAGE } from '../../routes/pages'; +import { Footer } from '../common/footer'; +import { + SEARCH_TYPE_CITIES, + SEARCH_TYPE_RESTAURANTS, +} from '../search-page/constants'; +import { NoCityBlock } from './no-city-block'; +import styles from './RestaurantsPage.module.css'; + +const cx = classNames.bind(styles); + +export const RestaurantsPage = () => { + const { selectedCity } = useContext(StoreContext); + + return ( +
+ {selectedCity ? ( + <> + + + {selectedCity} + + + + + + {/* TODO: Restaurants list will be here */} + + ) : ( + + )} +
+ ); +}; diff --git a/src/pages/restaurants-page/index.ts b/src/pages/restaurants-page/index.ts new file mode 100644 index 0000000..30cdea4 --- /dev/null +++ b/src/pages/restaurants-page/index.ts @@ -0,0 +1 @@ +export * from './RestaurantsPage'; diff --git a/src/pages/restaurants-page/no-city-block/NoCityBlock.module.css b/src/pages/restaurants-page/no-city-block/NoCityBlock.module.css new file mode 100644 index 0000000..1145be1 --- /dev/null +++ b/src/pages/restaurants-page/no-city-block/NoCityBlock.module.css @@ -0,0 +1,7 @@ +.no-city-block { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: calc(100% - 80px); +} diff --git a/src/pages/restaurants-page/no-city-block/NoCityBlock.tsx b/src/pages/restaurants-page/no-city-block/NoCityBlock.tsx new file mode 100644 index 0000000..6dd6350 --- /dev/null +++ b/src/pages/restaurants-page/no-city-block/NoCityBlock.tsx @@ -0,0 +1,20 @@ +import { SearchOutlined } from '@ant-design/icons'; +import Button from 'antd/lib/button'; +import classNames from 'classnames/bind'; +import { Link } from 'react-router-dom'; + +import { SEARCH_PAGE } from '../../../routes/pages'; +import { SEARCH_TYPE_CITIES } from '../../search-page/constants'; +import styles from './NoCityBlock.module.css'; + +const cx = classNames.bind(styles); + +export const NoCityBlock = () => ( +
+ + + +
+); diff --git a/src/pages/restaurants-page/no-city-block/index.ts b/src/pages/restaurants-page/no-city-block/index.ts new file mode 100644 index 0000000..05ecb86 --- /dev/null +++ b/src/pages/restaurants-page/no-city-block/index.ts @@ -0,0 +1 @@ +export * from './NoCityBlock'; diff --git a/src/pages/search-page/SearchPage.tsx b/src/pages/search-page/SearchPage.tsx new file mode 100644 index 0000000..73e0543 --- /dev/null +++ b/src/pages/search-page/SearchPage.tsx @@ -0,0 +1,26 @@ +import { useLocation } from 'react-router-dom'; + +import { SEARCH_TYPE_CITIES, SEARCH_TYPE_RESTAURANTS } from './constants'; +import { CitiesSearch, RestaurantsSearch } from './supported-searches'; + +// TODO: change to some fallback +const EmptyComponent = () =>
; + +export const SearchPage = () => { + const { state: locationState } = useLocation(); + + let SearchComponent = EmptyComponent; + + switch (locationState.type) { + case SEARCH_TYPE_CITIES: + SearchComponent = CitiesSearch; + break; + case SEARCH_TYPE_RESTAURANTS: + SearchComponent = RestaurantsSearch; + break; + default: + break; + } + + return ; +}; diff --git a/src/pages/search-page/constants.ts b/src/pages/search-page/constants.ts new file mode 100644 index 0000000..987f98f --- /dev/null +++ b/src/pages/search-page/constants.ts @@ -0,0 +1,2 @@ +export const SEARCH_TYPE_CITIES = 'cities'; +export const SEARCH_TYPE_RESTAURANTS = 'restaurants'; diff --git a/src/pages/search-page/data.ts b/src/pages/search-page/data.ts new file mode 100644 index 0000000..3a61c3c --- /dev/null +++ b/src/pages/search-page/data.ts @@ -0,0 +1,13 @@ +const MINSK = 'Minsk'; +const TBILISI = 'Tbilisi'; +const MOSCOW = 'Moscow'; +const TASHKENT = 'Tashkent'; + +export const supportedCities = [MINSK, TBILISI, MOSCOW, TASHKENT]; + +export const restaurantsByCities: { [name: string]: string[] } = { + [MINSK]: ['Vasilki', 'The View'], + [TBILISI]: ['Tiflis', 'Mimino'], + [MOSCOW]: ['Russkiy', 'Stolovaya'], + [TASHKENT]: ['Chaikhana', 'Plov!'], +}; diff --git a/src/pages/search-page/index.ts b/src/pages/search-page/index.ts new file mode 100644 index 0000000..4866fab --- /dev/null +++ b/src/pages/search-page/index.ts @@ -0,0 +1 @@ +export * from './SearchPage'; diff --git a/src/pages/search-page/search-with-list/SearchWithList.module.css b/src/pages/search-page/search-with-list/SearchWithList.module.css new file mode 100644 index 0000000..e0b2b70 --- /dev/null +++ b/src/pages/search-page/search-with-list/SearchWithList.module.css @@ -0,0 +1,9 @@ +.search-with-list { + padding: 10px; +} + +.entity-item { + display: block; + padding: 10px; + text-decoration: none; +} diff --git a/src/pages/search-page/search-with-list/SearchWithList.tsx b/src/pages/search-page/search-with-list/SearchWithList.tsx new file mode 100644 index 0000000..e6815c0 --- /dev/null +++ b/src/pages/search-page/search-with-list/SearchWithList.tsx @@ -0,0 +1,43 @@ +import { Input } from 'antd'; +import classNames from 'classnames/bind'; +import { ChangeEvent } from 'react'; +import { Link } from 'react-router-dom'; + +import styles from './SearchWithList.module.css'; + +const cx = classNames.bind(styles); + +const { Search } = Input; + +interface SearchWithListProps { + entities?: string[]; + onChange: (event: ChangeEvent) => void; + onEntityClick?: (entityName: string) => void; + entityLinkTarget: string; +} + +export const SearchWithList = ({ + entities = [], + onChange, + onEntityClick = () => {}, + entityLinkTarget, +}: SearchWithListProps) => ( +
+ + {entities.map(entityName => ( + onEntityClick(entityName)} + className={cx('entity-item')} + > + {entityName} + + ))} +
+); diff --git a/src/pages/search-page/search-with-list/index.ts b/src/pages/search-page/search-with-list/index.ts new file mode 100644 index 0000000..37461a1 --- /dev/null +++ b/src/pages/search-page/search-with-list/index.ts @@ -0,0 +1 @@ +export * from './SearchWithList'; diff --git a/src/pages/search-page/supported-searches/cities-search/CitiesSearch.tsx b/src/pages/search-page/supported-searches/cities-search/CitiesSearch.tsx new file mode 100644 index 0000000..a3b945e --- /dev/null +++ b/src/pages/search-page/supported-searches/cities-search/CitiesSearch.tsx @@ -0,0 +1,32 @@ +import { ChangeEvent, useContext, useState } from 'react'; + +import { StoreDispatchContext } from '../../../../providers/store-provider'; +import { RESTAURANTS_PAGE } from '../../../../routes/pages'; +import { supportedCities } from '../../data'; +import { SearchWithList } from '../../search-with-list'; + +export const CitiesSearch = () => { + const dispatch = useContext(StoreDispatchContext); + const [cities, setCities] = useState(supportedCities); + + const onSearchCities = (event: ChangeEvent) => { + const userInput = String(event.target.value).toLowerCase(); + const foundCities = supportedCities.filter(cityName => + cityName.toLowerCase().includes(userInput) + ); + setCities(foundCities); + }; + + const onCityClick = (cityName: string) => { + dispatch({ selectedCity: cityName }); + }; + + return ( + + ); +}; diff --git a/src/pages/search-page/supported-searches/cities-search/index.ts b/src/pages/search-page/supported-searches/cities-search/index.ts new file mode 100644 index 0000000..0c82d72 --- /dev/null +++ b/src/pages/search-page/supported-searches/cities-search/index.ts @@ -0,0 +1 @@ +export * from './CitiesSearch'; diff --git a/src/pages/search-page/supported-searches/index.ts b/src/pages/search-page/supported-searches/index.ts new file mode 100644 index 0000000..539094a --- /dev/null +++ b/src/pages/search-page/supported-searches/index.ts @@ -0,0 +1,2 @@ +export * from './cities-search'; +export * from './restaurants-search'; diff --git a/src/pages/search-page/supported-searches/restaurants-search/RestaurantsSearch.tsx b/src/pages/search-page/supported-searches/restaurants-search/RestaurantsSearch.tsx new file mode 100644 index 0000000..af5d579 --- /dev/null +++ b/src/pages/search-page/supported-searches/restaurants-search/RestaurantsSearch.tsx @@ -0,0 +1,32 @@ +import { ChangeEvent, useContext, useState } from 'react'; + +import { StoreContext } from '../../../../providers/store-provider'; +// TODO: will lead to particular restaurant page +import { RESTAURANTS_PAGE } from '../../../../routes/pages'; +import { restaurantsByCities } from '../../data'; +import { SearchWithList } from '../../search-with-list'; + +export const RestaurantsSearch = () => { + const { selectedCity } = useContext(StoreContext); + const [restaurants, setRestaurants] = useState( + restaurantsByCities[selectedCity] || [] + ); + + const onSearchCities = (event: ChangeEvent) => { + const userInput = String(event.target.value).toLowerCase(); + const restaurantsInParticularCity = restaurantsByCities[selectedCity]; + const foundRestaurants = restaurantsInParticularCity.filter( + (restaurantName: string) => + restaurantName.toLowerCase().includes(userInput) + ); + setRestaurants(foundRestaurants); + }; + + return ( + + ); +}; diff --git a/src/pages/search-page/supported-searches/restaurants-search/index.ts b/src/pages/search-page/supported-searches/restaurants-search/index.ts new file mode 100644 index 0000000..fcbe794 --- /dev/null +++ b/src/pages/search-page/supported-searches/restaurants-search/index.ts @@ -0,0 +1 @@ +export * from './RestaurantsSearch'; diff --git a/src/providers/initial-data-provider/InitialDataProvider.tsx b/src/providers/initial-data-provider/InitialDataProvider.tsx new file mode 100644 index 0000000..09e3152 --- /dev/null +++ b/src/providers/initial-data-provider/InitialDataProvider.tsx @@ -0,0 +1,34 @@ +import { ReactElement } from 'react'; + +import { IStoreContext, StoreProvider } from '../store-provider'; +import { LOCAL_STORAGE_SAVED_CITY_KEY } from './constants'; + +interface IInitialDataProviderProps { + children: ReactElement; +} + +// The component to collect main app data at the very beginning +export const InitialDataProvider = ({ + children, +}: IInitialDataProviderProps) => { + // TODO: Use city from Telegram + const extractCity = (): string => + localStorage.getItem(LOCAL_STORAGE_SAVED_CITY_KEY) as string; + + const saveCityLocally = ({ selectedCity }: Partial) => { + localStorage.setItem(LOCAL_STORAGE_SAVED_CITY_KEY, String(selectedCity)); + }; + + const initialStore = { + selectedCity: extractCity(), + }; + + return ( + + {children} + + ); +}; diff --git a/src/providers/initial-data-provider/constants.ts b/src/providers/initial-data-provider/constants.ts new file mode 100644 index 0000000..bed8290 --- /dev/null +++ b/src/providers/initial-data-provider/constants.ts @@ -0,0 +1 @@ +export const LOCAL_STORAGE_SAVED_CITY_KEY = 'savedCity'; diff --git a/src/providers/initial-data-provider/index.ts b/src/providers/initial-data-provider/index.ts new file mode 100644 index 0000000..3e3820f --- /dev/null +++ b/src/providers/initial-data-provider/index.ts @@ -0,0 +1 @@ +export * from './InitialDataProvider'; diff --git a/src/providers/store-provider/StoreProvider.tsx b/src/providers/store-provider/StoreProvider.tsx new file mode 100644 index 0000000..ac38aa1 --- /dev/null +++ b/src/providers/store-provider/StoreProvider.tsx @@ -0,0 +1,43 @@ +import { ReactElement, useCallback, useState } from 'react'; + +import { + IStoreContext, + StoreContext, + StoreDispatchContext, +} from './storeContext'; + +interface IStoreProviderProps { + children: ReactElement; + initialValue: IStoreContext; + dispatchCallback?: (storeProperties: Partial) => void; +} + +export const StoreProvider = ({ + children, + initialValue, + dispatchCallback, +}: IStoreProviderProps) => { + // Can be replaced with useReducer in future + const [store, setStore] = useState(initialValue); + + const updateStoreProperties = useCallback( + (storeProperties: Partial) => { + setStore({ + ...store, + ...storeProperties, + }); + if (dispatchCallback) { + dispatchCallback(storeProperties); + } + }, + [store, setStore, dispatchCallback] + ); + + return ( + + + {children} + + + ); +}; diff --git a/src/providers/store-provider/index.ts b/src/providers/store-provider/index.ts new file mode 100644 index 0000000..3a7902c --- /dev/null +++ b/src/providers/store-provider/index.ts @@ -0,0 +1,2 @@ +export * from './storeContext'; +export * from './StoreProvider'; diff --git a/src/providers/store-provider/storeContext.ts b/src/providers/store-provider/storeContext.ts new file mode 100644 index 0000000..01db2ab --- /dev/null +++ b/src/providers/store-provider/storeContext.ts @@ -0,0 +1,16 @@ +import { createContext } from 'react'; + +export interface IStoreContext { + selectedCity: string; +} + +export type StoreDispatchType = (storeProperty: Partial) => void; + +export const defaultStore: IStoreContext = { + selectedCity: '', +}; + +export const defaultStoreDispatch: StoreDispatchType = () => {}; + +export const StoreContext = createContext(defaultStore); +export const StoreDispatchContext = createContext(defaultStoreDispatch); diff --git a/src/routes/Router.tsx b/src/routes/Router.tsx new file mode 100644 index 0000000..091f922 --- /dev/null +++ b/src/routes/Router.tsx @@ -0,0 +1,13 @@ +import { HashRouter, Route, Routes } from 'react-router-dom'; + +import PagesConfig from './pages-config'; + +export const Router = () => ( + + + {PagesConfig.map(page => ( + } /> + ))} + + +); diff --git a/src/routes/pages-config.tsx b/src/routes/pages-config.tsx new file mode 100644 index 0000000..788e77f --- /dev/null +++ b/src/routes/pages-config.tsx @@ -0,0 +1,19 @@ +import { ProfilePage } from '../pages/profile-page'; +import { RestaurantsPage } from '../pages/restaurants-page'; +import { SearchPage } from '../pages/search-page'; +import { PROFILE_PAGE, RESTAURANTS_PAGE, SEARCH_PAGE } from './pages'; + +export default [ + { + path: RESTAURANTS_PAGE, + component: RestaurantsPage, + }, + { + path: SEARCH_PAGE, + component: SearchPage, + }, + { + path: PROFILE_PAGE, + component: ProfilePage, + }, +]; diff --git a/src/routes/pages.ts b/src/routes/pages.ts new file mode 100644 index 0000000..3c7fb2a --- /dev/null +++ b/src/routes/pages.ts @@ -0,0 +1,3 @@ +export const RESTAURANTS_PAGE = '/'; +export const SEARCH_PAGE = '/search'; +export const PROFILE_PAGE = '/profile'; diff --git a/src/setupProxy.js b/src/setupProxy.js index adaf7aa..d7ef94e 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -1,4 +1,5 @@ // this file works only for development +// eslint-disable-next-line import/no-extraneous-dependencies const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = app => {