diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 830c650..7263bfe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,7 @@ module.exports = { + globals: { + JSX: true, + }, env: { browser: true, es2021: true, diff --git a/package.json b/package.json index 94376b5..0205b04 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,14 @@ "dependencies": { "@material-ui/core": "^4.11.3", "@material-ui/icons": "^4.11.2", + "i18next": "^19.9.1", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-i18next": "^11.8.8", "react-redux": "^7.2.2", "react-router-dom": "^5.2.0", - "redux": "^4.0.5" + "redux": "^4.0.5", + "redux-thunk": "^2.3.0" }, "devDependencies": { "@types/material-ui": "^0.21.8", @@ -53,6 +56,8 @@ "start": "webpack serve", "dev": "webpack --mode development", "build": "webpack --mode production", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { diff --git a/src/components/App.scss b/src/components/App.scss index 22b4811..0d1411d 100644 --- a/src/components/App.scss +++ b/src/components/App.scss @@ -4,7 +4,7 @@ display: flex; flex-direction: column; align-items: center; - color: darkcyan; font-size: 1.5rem; + height: 100%; } diff --git a/src/components/App.tsx b/src/components/App.tsx index ddb083a..b77bd41 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,5 +1,12 @@ -import * as React from 'react'; +import React from 'react'; +import { + BrowserRouter, + Route, + Switch, +} from 'react-router-dom'; import Button from '@material-ui/core/Button'; +import MainPage from './MainPage'; +import CountryPage from './CountryPage'; import handleLangChange from '../controller/handlers'; import rootConnector, { rootProps } from '../store/rootConnector'; import './App.scss'; @@ -10,23 +17,33 @@ const App: React.FC = (props: rootProps) => ( {' '} {props.lang} + + + + + + + + + + ); diff --git a/src/components/CountryPage/index.tsx b/src/components/CountryPage/index.tsx new file mode 100644 index 0000000..55bf104 --- /dev/null +++ b/src/components/CountryPage/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { + useTranslation, +} from 'react-i18next'; +import { + useParams, +} from 'react-router-dom'; +import rootConnector, { + rootProps, +} from '../../store/rootConnector'; +import { URLParamTypes } from '../../types'; + +const CountryPage: React.FC = () => { + const { t } = useTranslation(); + + const { countryId } = useParams(); + + // TODO + + return (
{t(`${countryId}.name`)}
); +}; + +export default rootConnector(CountryPage); diff --git a/src/components/ImagesGrid/index.tsx b/src/components/ImagesGrid/index.tsx new file mode 100644 index 0000000..fb706c8 --- /dev/null +++ b/src/components/ImagesGrid/index.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + useTranslation, +} from 'react-i18next'; +import { + createStyles, + makeStyles, +} from '@material-ui/core/styles'; +import { + GridList, + GridListTile, + GridListTileBar, +} from '@material-ui/core'; +import rootConnector, +{ + rootProps, +} from '../../store/rootConnector'; +import { Country } from '../../types'; + +const useStyles = makeStyles(() => createStyles({ + root: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-around', + overflow: 'hidden', + maxWidth: 1000, + }, + gridList: { + flexWrap: 'nowrap', + transform: 'translateZ(0)', + }, + imgFullWidth: { + position: 'relative', + top: '50%', + transform: 'translateY(-50%)', + width: '100%', + }, + title: { + // color: theme.palette.primary.light, + }, + titleBar: { + background: + 'linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 70%, rgba(0,0,0,0) 100%)', + }, +})); + +const ImagesGrid: React.FC = (props: rootProps) => { + const classes = useStyles(); + const { countries } = props; + const { t } = useTranslation(); + + return ( +
+ + {countries.map((country: Country) => ( + + + {t(`${country.id}.name`)} + + + + ))} + +
+ ); +}; + +export default rootConnector(ImagesGrid); diff --git a/src/components/MainPage/index.tsx b/src/components/MainPage/index.tsx new file mode 100644 index 0000000..662fadc --- /dev/null +++ b/src/components/MainPage/index.tsx @@ -0,0 +1,8 @@ +import React, { FC } from 'react'; + +import ImagesGrid from '../ImagesGrid'; +import rootConnector, { rootProps } from '../../store/rootConnector'; + +const MainPage: FC = () => (); + +export default rootConnector(MainPage); diff --git a/src/controller/handlers.ts b/src/controller/handlers.ts index d25a4ac..0daf016 100644 --- a/src/controller/handlers.ts +++ b/src/controller/handlers.ts @@ -1,8 +1,11 @@ -import * as React from 'react'; +import React from 'react'; import { rootProps } from '../store/rootConnector'; +import i18n from '../i18next'; const handleLangChange = (props: rootProps, event: React.SyntheticEvent) => { - props.setLang(event.currentTarget.id); + const lng = event.currentTarget.id; + props.setLang(lng); + i18n.changeLanguage(lng); }; export default handleLangChange; diff --git a/src/i18next.ts b/src/i18next.ts new file mode 100644 index 0000000..d2ad401 --- /dev/null +++ b/src/i18next.ts @@ -0,0 +1,34 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import enTranslation from './i18next/en/translation.json'; +import deTranslation from './i18next/de/translation.json'; +import ruTranslation from './i18next/ru/translation.json'; + +export const resources = { + en: { translation: enTranslation }, + de: { translation: deTranslation }, + ru: { translation: ruTranslation }, +}; + +i18n + // load translation using http -> see /public/locales + // (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) + // learn more: https://github.com/i18next/i18next-http-backend + // .use(Backend) + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languageDetector + // .use(LanguageDetector) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: 'en', + debug: true, + // ns: ['special', 'common'], + // defaultNS: 'special', + resources, + supportedLngs: ['de', 'en', 'ru'], + }); + +export default i18n; diff --git a/src/i18next/de/translation.json b/src/i18next/de/translation.json new file mode 100644 index 0000000..cdf2e66 --- /dev/null +++ b/src/i18next/de/translation.json @@ -0,0 +1,29 @@ +{ + "br": { + "name": "Brasilien" + }, + "tz": { + "name": "Tanzania" + }, + "sp": { + "name": "Spanien" + }, + "ca": { + "name": "Kanada" + }, + "in": { + "name": "Indien" + }, + "ir": { + "name": "Irland" + }, + "ae": { + "name": "Vereinigte Arabische Emirate" + }, + "nz": { + "name": "Neuseeland" + }, + "jp": { + "name": "Japan" + } +} \ No newline at end of file diff --git a/src/i18next/en/translation.json b/src/i18next/en/translation.json new file mode 100644 index 0000000..0e4e7a6 --- /dev/null +++ b/src/i18next/en/translation.json @@ -0,0 +1,29 @@ +{ + "br": { + "name": "Brazil" + }, + "tz": { + "name": "Tanzania" + }, + "sp": { + "name": "Spain" + }, + "ca": { + "name": "Canada" + }, + "in": { + "name": "India" + }, + "ir": { + "name": "Ireland" + }, + "ae": { + "name": "United Arab Emirates" + }, + "nz": { + "name": "New Zealand" + }, + "jp": { + "name": "Japan" + } +} \ No newline at end of file diff --git a/src/i18next/ru/translation.json b/src/i18next/ru/translation.json new file mode 100644 index 0000000..9e05669 --- /dev/null +++ b/src/i18next/ru/translation.json @@ -0,0 +1,29 @@ +{ + "br": { + "name": "Бразилия" + }, + "tz": { + "name": "Танзания" + }, + "sp": { + "name": "Испания" + }, + "ca": { + "name": "Канада" + }, + "in": { + "name": "Индия" + }, + "ir": { + "name": "Ирландия" + }, + "ae": { + "name": "Объединённые Арабские Эмираты" + }, + "nz": { + "name": "Новая Зеландия" + }, + "jp": { + "name": "Япония" + } +} \ No newline at end of file diff --git a/src/index.scss b/src/index.scss index 9b8e5b8..1d11bcb 100644 --- a/src/index.scss +++ b/src/index.scss @@ -2,4 +2,9 @@ body { text-align: center; font-size: 2rem; font-family: 'Roboto', sans-serif; + height: 100%; + min-height: 100vh; + box-sizing: border-box; + position: relative; + margin: 0; } diff --git a/src/index.tsx b/src/index.tsx index 843deb5..dcf0258 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,12 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import React from 'react'; +import { render } from 'react-dom'; import { Provider } from 'react-redux'; -import { createStore } from 'redux'; -import rootReducer from './store/rootReducer'; +import store from './store/store'; import App from './components/App'; +import './i18next'; import './index.scss'; -const store = createStore(rootReducer); - -ReactDOM.render( +render( , diff --git a/src/store/rootConnector.ts b/src/store/rootConnector.ts index 2c5e8de..e23e98f 100644 --- a/src/store/rootConnector.ts +++ b/src/store/rootConnector.ts @@ -6,6 +6,7 @@ export type rootProps = ConnectedProps; const mapStateToProps = (state: IAppState) => ({ lang: state.lang, + countries: state.countries, }); const mapDispatchToProps = (dispatch: Dispatch) => ({ diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 6252f04..f808bbb 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -1,12 +1,56 @@ -export interface IAppState { - lang: 'EN' | 'RU' | 'DE' -} +import { IAppState, RootReducerAction } from './types'; const initialState: IAppState = { lang: 'EN', + countries: [{ + id: 'br', + name: 'Brazil', + pictureURL: 'https://i-fakt.ru/wp-content/uploads/2013/11/braz.jpg', + }, + { + id: 'tz', + name: 'Tanzania', + pictureURL: 'https://i-fakt.ru/wp-content/uploads/2021/03/interesnye-fakty-1614802526.jpg', + }, + { + id: 'sp', + name: 'Spain', + pictureURL: 'https://i-fakt.ru/wp-content/uploads/2019/12/fakty-barselona.jpg', + }, + { + id: 'ca', + name: 'Canada', + pictureURL: 'https://i-fakt.ru/wp-content/uploads/2019/12/fakty-monreal.jpg', + }, + { + id: 'in', + name: 'India', + pictureURL: 'https://i-fakt.ru/wp-content/uploads/2019/10/fakty-india.jpg', + }, + { + id: 'ir', + name: 'Ireland', + pictureURL: 'https://i-fakt.ru/wp-content/uploads/2017/11/ireland.jpg', + }, + { + id: 'ae', + name: 'United Arab Emirates', + pictureURL: 'https://i-fakt.ru/wp-content/uploads/2015/04/oaemirat.jpg', + }, + { + id: 'nz', + name: 'New Zealand', + pictureURL: 'https://i-fakt.ru/wp-content/uploads/2013/07/skytower3.jpeg', + }, + { + id: 'jp', + name: 'Japan', + pictureURL: 'https://i-fakt.ru/1/tokio.jpg', + }, + ], }; -const rootReducer = (state: IAppState = initialState, action: any) => { +const rootReducer = (state: IAppState = initialState, action: RootReducerAction) => { switch (action.type) { case 'SET_LANG': return { ...state, lang: action.payload.lang }; diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..6a7caa5 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,19 @@ +import ReduxThunk from 'redux-thunk'; +import { + createStore, + applyMiddleware, + compose, +} from 'redux'; +import rootReducer from './rootReducer'; + +declare global { + export interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; + } +} + +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + +const store = createStore(rootReducer, composeEnhancers(applyMiddleware(ReduxThunk))); + +export default store; diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 0000000..0133296 --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,14 @@ +import { Country } from '../types'; + +export interface IAppState { + lang: 'EN' | 'RU' | 'DE', + countries: Country[], +} + +export interface RootReducerAction { + type: string, + payload: { + lang?: string, + countries?: Country[], + } +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000..40f1ea2 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,8 @@ +export interface Country { + id: string, + name: string, + pictureURL: string, +} +export interface URLParamTypes { + countryId: string, +} diff --git a/tsconfig.json b/tsconfig.json index 2783ede..1531234 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,8 @@ "moduleResolution": "node", "target": "es5", "allowJs": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true }, "include": [ "./src/**/*"