├── aviasales-master.zip └── aviasales-master ├── src ├── constants.js ├── components │ ├── TicketItem.vue │ ├── FilterTransfer.vue │ └── Tabs.vue ├── main.js ├── router │ └── index.js ├── views │ ├── Tickets.vue │ ├── Header.vue │ └── Content.vue ├── App.vue └── assets │ └── Logo.svg ├── babel.config.js ├── aviasales_frontend-master.zip ├── aviasales_frontend-master ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── components │ │ ├── Logo │ │ │ ├── Logo.js │ │ │ └── Logo.svg │ │ ├── TicketList │ │ │ ├── TicketList.module.scss │ │ │ └── TicketList.js │ │ ├── Alert │ │ │ ├── Alert.module.scss │ │ │ └── Alert.js │ │ ├── FilterList │ │ │ ├── FilterList.module.scss │ │ │ └── FilterList.js │ │ ├── ThemeSwitcher │ │ │ ├── ThemeSwitcher.js │ │ │ └── ThemeSwitcher.module.scss │ │ ├── Sorting │ │ │ ├── Sorting.js │ │ │ └── Sorting.module.scss │ │ ├── Checkbox │ │ │ ├── Checkbox.js │ │ │ └── Checkbox.module.scss │ │ └── Ticket │ │ │ ├── Ticket.module.scss │ │ │ └── Ticket.js │ ├── App.js │ ├── pages │ │ ├── Search.module.scss │ │ ├── testData.json │ │ └── Search.js │ ├── index.js │ ├── context │ │ └── ThemeContext │ │ │ ├── ThemeContext.js │ │ │ └── ThemeColors.js │ ├── index.scss │ └── serviceWorker.js ├── package.json ├── README.md └── server.md ├── README.md ├── package.json └── public └── index.html /aviasales-master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ooopsa27/Avia2/HEAD/aviasales-master.zip -------------------------------------------------------------------------------- /aviasales-master/src/constants.js: -------------------------------------------------------------------------------- 1 | export const getTicketsUrl = 'https://front-test.beta.aviasales.ru/tickets'; -------------------------------------------------------------------------------- /aviasales-master/src/components/TicketItem.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aviasales-master/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ooopsa27/Avia2/HEAD/aviasales-master/aviasales_frontend-master.zip -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ooopsa27/Avia2/HEAD/aviasales-master/aviasales_frontend-master/public/favicon.ico -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ooopsa27/Avia2/HEAD/aviasales-master/aviasales_frontend-master/public/logo192.png -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ooopsa27/Avia2/HEAD/aviasales-master/aviasales_frontend-master/public/logo512.png -------------------------------------------------------------------------------- /aviasales-master/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | createApp(App).use(router).mount('#app') 6 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Logo/Logo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logoSvg from "./Logo.svg"; 3 | 4 | export const Logo = () => ( 5 | <> 6 | logo 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/TicketList/TicketList.module.scss: -------------------------------------------------------------------------------- 1 | .TicketList { 2 | li { 3 | margin-bottom: 20px; 4 | 5 | &:last-child { 6 | margin-bottom: 0; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /aviasales-master/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | 3 | const routes = [] 4 | 5 | const router = createRouter({ 6 | history: createWebHistory(process.env.BASE_URL), 7 | routes 8 | }) 9 | 10 | export default router 11 | -------------------------------------------------------------------------------- /aviasales-master/src/views/Tickets.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /aviasales-master/src/views/Header.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /aviasales-master/src/components/FilterTransfer.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aviasales-master/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /aviasales-master/README.md: -------------------------------------------------------------------------------- 1 | # aviasales 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Customize configuration 19 | See [Configuration Reference](https://cli.vuejs.org/config/). 20 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Alert/Alert.module.scss: -------------------------------------------------------------------------------- 1 | .Alert { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-direction: column; 6 | 7 | span { 8 | text-align: center; 9 | } 10 | 11 | button { 12 | margin-top: 10px; 13 | color: var(--body-text); 14 | font-size: 15px; 15 | line-height: 20px; 16 | border-bottom: 1px solid; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Alert/Alert.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/accessible-emoji */ 2 | import React from "react"; 3 | import classes from "./Alert.module.scss"; 4 | 5 | export const Alert = () => { 6 | return ( 7 |
8 | 😟 Что-то пошло не так 9 | 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/FilterList/FilterList.module.scss: -------------------------------------------------------------------------------- 1 | .FilterList { 2 | min-width: 232px; 3 | background: var(--filter-background); 4 | box-shadow: 0px 2px 8px var(--box-shadow); 5 | border-radius: 5px; 6 | padding-bottom: 10px; 7 | } 8 | 9 | .Title { 10 | text-transform: uppercase; 11 | letter-spacing: 0.5px; 12 | font-style: normal; 13 | font-weight: 600; 14 | font-size: 12px; 15 | padding: 20px 20px 10px 20px; 16 | } 17 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/TicketList/TicketList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Ticket } from "../Ticket/Ticket"; 3 | import classes from "./TicketList.module.scss"; 4 | 5 | export const TicketList = ({ tickets }) => { 6 | return ( 7 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /aviasales-master/src/components/Tabs.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /aviasales-master/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aviasales", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.6.5", 11 | "vue": "^3.0.0", 12 | "vue-router": "^4.0.0-0" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "~4.5.0", 16 | "@vue/cli-plugin-router": "~4.5.0", 17 | "@vue/cli-service": "~4.5.0", 18 | "@vue/compiler-sfc": "^3.0.0" 19 | }, 20 | "browserslist": [ 21 | "> 1%", 22 | "last 2 versions", 23 | "not dead" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Search } from "./pages/Search"; 3 | import { Logo } from "./components/Logo/Logo"; 4 | import { ThemeSwitcher } from "./components/ThemeSwitcher/ThemeSwitcher"; 5 | 6 | function App() { 7 | return ( 8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 | ); 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/pages/Search.module.scss: -------------------------------------------------------------------------------- 1 | .appInformer { 2 | margin: 0 0 20px 0; 3 | 4 | @media screen and (min-width: 630px) { 5 | margin: 0 20px 0 0; 6 | } 7 | } 8 | 9 | .appContentInner { 10 | display: flex; 11 | justify-content: center; 12 | flex-direction: column; 13 | align-items: center; 14 | 15 | @media screen and (min-width: 630px) { 16 | flex-direction: row; 17 | align-items: flex-start; 18 | } 19 | } 20 | 21 | .appContent { 22 | display: flex; 23 | flex-direction: column; 24 | width: 100%; 25 | max-width: 502px; 26 | } 27 | 28 | .sorting { 29 | margin-bottom: 20px; 30 | } 31 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /aviasales-master/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.scss"; 4 | import { ThemeProvider } from "./context/ThemeContext/ThemeContext"; 5 | import App from "./App"; 6 | import * as serviceWorker from "./serviceWorker"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | 17 | // If you want your app to work offline and load faster, you can change 18 | // unregister() to register() below. Note this comes with some pitfalls. 19 | // Learn more about service workers: https://bit.ly/CRA-PWA 20 | serviceWorker.unregister(); 21 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/ThemeSwitcher/ThemeSwitcher.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import classes from "./ThemeSwitcher.module.scss"; 3 | import { ThemeContext } from "../../context/ThemeContext/ThemeContext"; 4 | 5 | export const ThemeSwitcher = () => { 6 | const { dark, toggle } = useContext(ThemeContext); 7 | 8 | return ( 9 |
10 | toggle()} 16 | /> 17 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://victoriaZubareva.github.io/aviasales_frontend", 3 | "name": "aviasales", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "gh-pages": "^2.2.0", 11 | "node-sass": "^4.13.1", 12 | "react": "^16.12.0", 13 | "react-dom": "^16.12.0", 14 | "react-scripts": "3.4.0" 15 | }, 16 | "scripts": { 17 | "predeploy": "npm run build", 18 | "deploy": "gh-pages -d build", 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Sorting/Sorting.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import classes from "./Sorting.module.scss"; 3 | 4 | export const Sorting = ({ sorts }) => { 5 | const [selectTub, setSelectTub] = useState("low price"); 6 | 7 | const paramSorting = [ 8 | { value: "Самый дешевый", id: "low price" }, 9 | { value: "Самый быстрый", id: "fastest" } 10 | ]; 11 | 12 | const checkTub = id => { 13 | if (id === selectTub) { 14 | return; 15 | } 16 | setSelectTub(id); 17 | sorts.sortTabs(id); 18 | }; 19 | 20 | return ( 21 |
22 | 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Checkbox/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import classes from "./Checkbox.module.scss"; 3 | 4 | export const Checkbox = props => { 5 | const { item, checked } = props; 6 | 7 | let [isChecked, setIsChecked] = useState(item.isChecked); 8 | 9 | // отслеживаем -- если изменился isChecked, то обновим состояние 10 | useEffect(() => { 11 | setIsChecked(props.item.isChecked); 12 | }, [props.item.isChecked]); 13 | 14 | const toggleIsChecked = () => { 15 | setIsChecked(!isChecked); 16 | checked(item.id, !isChecked); 17 | }; 18 | 19 | return ( 20 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Sorting/Sorting.module.scss: -------------------------------------------------------------------------------- 1 | .SortingTabs { 2 | height: 50px; 3 | display: flex; 4 | flex-wrap: nowrap; 5 | } 6 | 7 | .SortingTab { 8 | display: flex; 9 | cursor: pointer; 10 | letter-spacing: 0.5px; 11 | text-transform: uppercase; 12 | font-weight: 600; 13 | font-size: 12px; 14 | line-height: 20px; 15 | background: var(--sorting-tab-background); 16 | border: 1px solid var(--sorting-tab-border); 17 | flex: 1 0 auto; 18 | align-items: center; 19 | justify-content: center; 20 | 21 | &_active { 22 | background: var(--sorting-tab-background-active); 23 | color: var(--sorting-tab-text-active); 24 | border: 1px solid var(--sorting-tab-background-active); 25 | margin-right: 1px; 26 | } 27 | 28 | &:first-child { 29 | border-radius: 5px 0 0 5px; 30 | &:hover { 31 | z-index: 2; 32 | } 33 | } 34 | 35 | &:last-child { 36 | border-radius: 0 5px 5px 0; 37 | } 38 | 39 | &:not(:first-child) { 40 | margin-left: -1px; 41 | } 42 | 43 | &:hover:not(.SortingTab_active) { 44 | border: 1px solid var(--sorting-tab-border-hover); 45 | background: var(--sorting-tab-background-hover); 46 | transition: all 0.3s; 47 | color: var(--sorting-tab-text-hover); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Ticket/Ticket.module.scss: -------------------------------------------------------------------------------- 1 | .ticket { 2 | max-width: 502px; 3 | min-width: 300px; 4 | background: var(--ticket-background); 5 | box-shadow: 0px 2px 8px var(--box-shadow); 6 | border-radius: 5px; 7 | padding: 20px; 8 | } 9 | .ticketHeader { 10 | display: flex; 11 | justify-content: space-between; 12 | margin-bottom: 20px; 13 | 14 | div { 15 | flex: 2; 16 | &:last-child { 17 | flex: 1; 18 | } 19 | } 20 | } 21 | 22 | .ticketPrice { 23 | font-size: 24px; 24 | color: var(--ticket-price); 25 | font-weight: 600; 26 | align-self: center; 27 | } 28 | 29 | // body 30 | 31 | .ticketBody { 32 | display: flex; 33 | justify-content: space-between; 34 | flex-direction: column; 35 | } 36 | 37 | .bodyRow { 38 | margin-bottom: 10px; 39 | display: flex; 40 | justify-content: space-between; 41 | 42 | div { 43 | flex: 1; 44 | } 45 | 46 | &:last-child { 47 | margin-bottom: 0px; 48 | } 49 | } 50 | 51 | .titleRow { 52 | font-weight: 600; 53 | font-size: 12px; 54 | letter-spacing: 0.5px; 55 | text-transform: uppercase; 56 | color: var(--ticket-text-subtitle); 57 | display: block; 58 | line-height: 18px; 59 | } 60 | 61 | .textRow { 62 | display: block; 63 | font-weight: 600; 64 | font-size: 14px; 65 | line-height: 21px; 66 | } 67 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/context/ThemeContext/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useState } from "react"; 2 | import { darkTheme, lightTheme } from "./ThemeColors"; 3 | 4 | export const ThemeContext = React.createContext({ 5 | dark: false, 6 | toggle: () => {} 7 | }); 8 | 9 | export const ThemeProvider = ({ children }) => { 10 | // текущая тема 11 | const [dark, setDark] = useState(false); 12 | 13 | // устанавливаем стили css 14 | const applyTheme = theme => { 15 | const root = document.getElementsByTagName("html")[0]; 16 | root.style.cssText = theme.join(";"); 17 | }; 18 | 19 | // обновляем стили синхронно 20 | useLayoutEffect(() => { 21 | // устанавливаем последнюю выбранную пользователем тему 22 | const lastTheme = window.localStorage.getItem("darkTheme"); 23 | 24 | if (lastTheme === "true") { 25 | setDark(true); 26 | applyTheme(darkTheme); 27 | } else { 28 | setDark(false); 29 | applyTheme(lightTheme); 30 | } 31 | }, [dark]); 32 | 33 | const toggle = () => { 34 | setDark(!dark); 35 | window.localStorage.setItem("darkTheme", !dark); 36 | }; 37 | 38 | return ( 39 | 45 | {children} 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Checkbox/Checkbox.module.scss: -------------------------------------------------------------------------------- 1 | .Item { 2 | display: flex; 3 | height: 40px; 4 | align-items: center; 5 | cursor: pointer; 6 | 7 | &:hover { 8 | background-color: var(--filter-hover-item); 9 | transition: background-color 0.3s; 10 | } 11 | } 12 | 13 | .ItemText { 14 | font-size: 13px; 15 | line-height: 20px; 16 | } 17 | 18 | input[type="checkbox"] { 19 | display: none; 20 | pointer-events: none; 21 | } 22 | 23 | .Checkbox { 24 | width: 20px; 25 | stroke-width: 5; 26 | margin: 10px 10px 10px 20px; 27 | 28 | &__box { 29 | stroke: var(--checkbox-box); 30 | stroke-dasharray: 320; 31 | stroke-dashoffset: 0; 32 | fill: rgba(0, 0, 0, 0); 33 | } 34 | 35 | &__check { 36 | stroke: var(--checkbox-box-check); 37 | stroke-dasharray: 70; 38 | stroke-dashoffset: 70; 39 | fill: none; 40 | } 41 | } 42 | 43 | input:checked + svg { 44 | .Checkbox__box { 45 | stroke: var(--checkbox-box-check); 46 | animation: animBorder 0.3s linear forwards; 47 | } 48 | .Checkbox__check { 49 | stroke-dashoffset: 0; 50 | transition: stroke-dashoffset 0.4s linear; 51 | } 52 | } 53 | 54 | .reverse { 55 | .Checkbox__box { 56 | stroke-dashoffset: 0; 57 | transition: stroke-dashoffset 0.3s linear; 58 | } 59 | .Checkbox__check { 60 | stroke-dashoffset: 70; 61 | transition: stroke-dashoffset 0.3s linear; 62 | } 63 | } 64 | 65 | @keyframes animBorder { 66 | from { 67 | stroke-dashoffset: 320; 68 | } 69 | to { 70 | stroke-dashoffset: 0; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/README.md: -------------------------------------------------------------------------------- 1 | Что получилось: https://victoriazubareva.github.io/aviasales_frontend/ 2 | 3 | # Тестовое задание Aviasales ([frontend](https://aviasales.recruitee.com/o/frontend-developer-js-coffeescript-react%C2%A0redux--aviasalesru)) 4 | 5 | Основной frontend проект нашей команды — это страница выдачи билетов со множеством фильтров, настроек и, собственно, билетов. 6 | Проект написан на React, поэтому тестовое задание приближено к ежедневным задачам. 7 | 8 | Перед тобой упрощенный макет нашего проекта — список билетов, фильтры и сортировка. Также у нас есть небольшой сервер для тестового задания, который работает схоже с нашим основным backend движком и реализует технику long polling для передачи пачек билетов. Тебе необходимо реализовать клиент, который будет получать случайно сгенерированные билеты от сервера и отрисует интерфейс согласно макету в Figma. Достаточно будет отрендерить 5 первых билетов соотвествующих выбранным фильрам и сортировки. 9 | 10 | ## Условия 11 | 12 | - Используй React 13 | - Используй TS или JS 14 | - Работоспособность в актуальной версии Google Chrome 15 | - Остальное на твоё усмотрение 16 | 17 | ## Документация по работе с сервером: [Здесь](https://github.com/victoriaZubareva/aviasales_frontend/blob/master/server.md) 18 | 19 | ## Макет 20 | 21 | https://www.figma.com/file/4fQe1lEbo4DARjvNtaU0uJ/Aviasales-test-task 22 | 23 | Залогинься, чтобы видеть CSS-свойства элементов 24 | 25 | ![](search_preview.png?raw=true) 26 | 27 | Удачи! Если будут какие-то вопросы, пиши – добавим уточнения в репу. 28 | 29 | P.S.: Картинки авиакомпаний можешь брать с нашего CDN: `//pics.avs.io/99/36/{IATA_CODE_HERE}.png` 30 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/ThemeSwitcher/ThemeSwitcher.module.scss: -------------------------------------------------------------------------------- 1 | .Switcher { 2 | position: absolute; 3 | right: 10px; 4 | } 5 | 6 | .SwitcherInput { 7 | display: none; 8 | } 9 | 10 | .SwitcherLabel { 11 | display: inline-block; 12 | position: relative; 13 | background-color: #83d8ff; 14 | height: 30px; 15 | width: 50px; 16 | border-radius: 18px; 17 | cursor: pointer; 18 | transition: background-image 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95); 19 | background-image: url("https://www.aviasales.ru/helios-assets/day-f57cce95d54a4169fca3f8536fc8bb16.svg"); 20 | } 21 | 22 | .SwitcherToggle { 23 | display: inline-block; 24 | position: relative; 25 | z-index: 1; 26 | top: 4px; 27 | left: 4px; 28 | border-radius: 50px; 29 | transform: rotate(-45deg); 30 | transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); 31 | background-color: #ff8742; 32 | background-image: url("https://www.aviasales.ru/helios-assets/sun-85fc2c3910b661d20b962851179ad09f.svg"); 33 | width: 22px; 34 | height: 22px; 35 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.25), 36 | inset 0 0 10px 0 hsla(0, 0%, 100%, 0.5); 37 | } 38 | 39 | .SwitcherInput:checked + .SwitcherLabel { 40 | background-image: url("https://www.aviasales.ru/helios-assets/night-81c7c4b13c053594c9ac00efe3aea7bf.svg"); 41 | } 42 | 43 | .SwitcherInput:checked + .SwitcherLabel .SwitcherToggle { 44 | background-color: #eff9ff; 45 | background-image: url("https://www.aviasales.ru/helios-assets/moon-8540e558b4839650675fbb94bc126f33.svg"); 46 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.25), 47 | inset 0 -3px 6px 0 rgba(0, 0, 0, 0.15); 48 | transform: translate3d(20px, 0, 0) rotate(0); 49 | } 50 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/context/ThemeContext/ThemeColors.js: -------------------------------------------------------------------------------- 1 | export const lightTheme = [ 2 | "--background-app: #f3f7fa", 3 | "--body-text: #4A4A4A", 4 | "--box-shadow: rgba(0, 0, 0, 0.1)", 5 | "--filter-background: #fff", 6 | "--filter-hover-item: #F1FCFF", 7 | "--filter-text: #4A4A4A", 8 | "--checkbox-box: rgba(154, 187, 206, 1)", 9 | "--checkbox-box-check: #2196F3", 10 | "--sorting-tab-background: #fff", 11 | "--sorting-tab-background-active: #2196F3", 12 | "--sorting-tab-background-hover: #F1FCFF", 13 | "--sorting-tab-text: #4A4A4A", 14 | "--sorting-tab-text-active: #fff", 15 | "--sorting-tab-text-hover: #2196F3", 16 | "--sorting-tab-border: #DFE5EC", 17 | "--sorting-tab-border-hover: #2196F3", 18 | "--ticket-background: #fff", 19 | "--ticket-price: #2196F3", 20 | "--ticket-text-subtitle: #A0B0B9", 21 | "--ticket-text: #4A4A4A" 22 | ]; 23 | 24 | export const darkTheme = [ 25 | "--background-app: #05263f", 26 | "--body-text: #fff", 27 | "--box-shadow: rgba(0, 0, 0, 0.1)", 28 | "--filter-background: #1e3c52", 29 | "--filter-hover-item: #355064", 30 | "--filter-text: #fff", 31 | "--checkbox-box: rgba(154, 187, 206, 1)", 32 | "--checkbox-box-check: #2196F3", 33 | "--sorting-tab-background: #1e3c52", 34 | "--sorting-tab-background-active: #2196f3", 35 | "--sorting-tab-background-hover: #031b2c", 36 | "--sorting-tab-text: #fff", 37 | "--sorting-tab-text-active: #fff", 38 | "--sorting-tab-text-hover: #fff", 39 | "--sorting-tab-border: #000", 40 | "--sorting-tab-border-hover: #000", 41 | "--ticket-background: #1e3c52", 42 | "--ticket-price: #f57c00", 43 | "--ticket-text-subtitle: #A0B0B9", 44 | "--ticket-text: #fff" 45 | ]; 46 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/server.md: -------------------------------------------------------------------------------- 1 | # Описание взаимодействия с сервером 2 | 3 | Схема работы проста: сначала необходимо инициировать поиск на сервере и получить идентификатор поиска (`searchId`). Далее, с полученным `searchId`, ты делаешь запросы для получения неотсортированных списков билетов. Обрати внимание, что билеты прилетают пачками, которые необходимо агрегировать, фильтровать и сортировать согласно выбранным в интерфейсе параметрам. Для усложнения задачи, сервер может на один из запросов ответить ошибкой. 4 | 5 | ## Получение `searchId` 6 | 7 | Просто отправь GET-запрос на `https://front-test.beta.aviasales.ru/search` и получи его. 8 | 9 | Пример: 10 | 11 | Request: `https://front-test.beta.aviasales.ru/search` 12 | 13 | Response: `{"searchId":"4niyd"}` 14 | 15 | ## Получение пачки билетов 16 | 17 | Отправляй GET-запросы на `https://front-test.beta.aviasales.ru/tickets` и передай searchId полученный из запроса выше GET-параметром. 18 | 19 | Пример: 20 | 21 | Request: `https://front-test.beta.aviasales.ru/tickets?searchId=4niyd` 22 | 23 | Response: `{tickets: [], stop: false}` 24 | 25 | ## Обработка завершения поиска 26 | 27 | Поиск считается завершенным, когда в очередном ответе от сервера придёт значение `{stop: true}`. 28 | 29 | Пример: 30 | 31 | Request: `https://front-test.beta.aviasales.ru/tickets?searchId=4niyd` 32 | 33 | Response: `{tickets: [], stop: true}` 34 | 35 | ## Структура билета 36 | 37 | В списке `tickets` будут лежать билеты следующей структуры: 38 | 39 | ```typescript 40 | interface Ticket { 41 | // Цена в рублях 42 | price: number 43 | // Код авиакомпании (iata) 44 | carrier: string 45 | // Массив перелётов. 46 | // В тестовом задании это всегда поиск "туда-обратно" значит состоит из двух элементов 47 | segments: [ 48 | { 49 | // Код города (iata) 50 | origin: string 51 | // Код города (iata) 52 | destination: string 53 | // Дата и время вылета туда 54 | date: string 55 | // Массив кодов (iata) городов с пересадками 56 | stops: string[] 57 | // Общее время перелёта в минутах 58 | duration: number 59 | }, 60 | { 61 | // Код города (iata) 62 | origin: string 63 | // Код города (iata) 64 | destination: string 65 | // Дата и время вылета обратно 66 | date: string 67 | // Массив кодов (iata) городов с пересадками 68 | stops: string[] 69 | // Общее время перелёта в минутах 70 | duration: number 71 | } 72 | ] 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/index.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-app: #f3f7fa; 3 | --body-text: #4A4A4A; 4 | --box-shadow: rgba(0, 0, 0, 0.1); 5 | 6 | --filter-background: #fff; 7 | --filter-hover-item: #F1FCFF; 8 | --filter-text: #4A4A4A; 9 | 10 | --checkbox-box: rgba(154, 187, 206, 1); 11 | --checkbox-box-check: #2196F3; 12 | 13 | --sorting-tab-background: #fff; 14 | --sorting-tab-background-active: #2196F3; 15 | --sorting-tab-background-hover: #F1FCFF; 16 | --sorting-tab-text: #4A4A4A; 17 | --sorting-tab-text-active: #fff; 18 | --sorting-tab-text-hover: #2196F3; 19 | --sorting-tab-border: #DFE5EC; 20 | --sorting-tab-border-hover: #2196F3; 21 | 22 | --ticket-background: #fff; 23 | --ticket-price: #2196F3; 24 | --ticket-text-subtitle: #A0B0B9; 25 | --ticket-text: #4A4A4A; 26 | } 27 | 28 | *, *:before, *:after { 29 | box-sizing: border-box; 30 | } 31 | 32 | a { 33 | text-decoration: none; 34 | cursor: pointer; 35 | } 36 | 37 | button { 38 | background-color: transparent; 39 | border-width: 0; 40 | padding: 0; 41 | cursor: pointer; 42 | outline: none; 43 | } 44 | figure { 45 | margin: 0; 46 | } 47 | input::-moz-focus-inner { 48 | border: 0; 49 | padding: 0; 50 | margin: 0; 51 | } 52 | ul, 53 | ol, 54 | dd { 55 | margin: 0; 56 | padding: 0; 57 | list-style: none; 58 | } 59 | h1, 60 | h2, 61 | h3, 62 | h4, 63 | h5, 64 | h6 { 65 | margin: 0; 66 | } 67 | p { 68 | margin: 0; 69 | } 70 | cite { 71 | font-style: normal; 72 | } 73 | 74 | .app { 75 | background: var(--background-app); 76 | padding: 20px 10px; 77 | 78 | &__inner { 79 | max-width: 1280px; 80 | margin: 0 auto; 81 | position: relative; 82 | } 83 | 84 | &__section { 85 | max-width: 800px; 86 | margin: 0 auto; 87 | } 88 | 89 | @media screen and (min-width: 630px) { 90 | padding: 55px 20px; 91 | } 92 | } 93 | 94 | .navbar { 95 | display: flex; 96 | align-items: center; 97 | justify-content: center; 98 | } 99 | 100 | body { 101 | margin: 0; 102 | font-family: -apple-system, BlinkMacSystemFont, "Open Sans", "Roboto", 103 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 104 | sans-serif; 105 | -webkit-font-smoothing: antialiased; 106 | -moz-osx-font-smoothing: grayscale; 107 | color: var(--body-text); 108 | transition: all .3s ease; 109 | } 110 | 111 | section { 112 | min-height: 100vh; 113 | } 114 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/FilterList/FilterList.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Checkbox } from "../Checkbox/Checkbox"; 3 | import classes from "./FilterList.module.scss"; 4 | 5 | export const FilterList = props => { 6 | const { hiddenTickets, sortNonStop, sortStops } = props.sortTickets; 7 | 8 | const [transfers, setTransfers] = useState([ 9 | { value: "Все", id: "checkedAll", isChecked: true }, 10 | { value: "Без пересадок", id: "nonStop", isChecked: true }, 11 | { value: "1 пересадка", id: 1, isChecked: true }, 12 | { value: "2 пересадки", id: 2, isChecked: true }, 13 | { value: "3 пересадки", id: 3, isChecked: true } 14 | ]); 15 | 16 | /* универсальная функция которой будем переключать чекбоксы */ 17 | const checkID = (id, isChecked) => { 18 | if (isChecked) { 19 | const newChecked = transfers.map(item => 20 | item.id === id ? { ...item, isChecked: true } : { ...item } 21 | ); 22 | setTransfers(newChecked); 23 | } else { 24 | const newChecked = transfers.map(item => { 25 | if (item.id === id || item.id === "checkedAll") { 26 | return { ...item, isChecked: false }; 27 | } else { 28 | return { ...item }; 29 | } 30 | }); 31 | setTransfers(newChecked); 32 | } 33 | }; 34 | 35 | const changeIsChecked = (id, isChecked) => { 36 | // если выбраны "все" билеты, меняем их isChecked 37 | if (id === "checkedAll") { 38 | if (isChecked === true) { 39 | const newChecked = transfers.map(item => ({ 40 | ...item, 41 | isChecked: true 42 | })); 43 | setTransfers(newChecked); 44 | } else { 45 | const newChecked = transfers.map(item => ({ 46 | ...item, 47 | isChecked: false 48 | })); 49 | setTransfers(newChecked); 50 | } 51 | hiddenTickets(isChecked); 52 | } 53 | 54 | // если выбрано 'Без пересадок' 55 | else if (id === "nonStop") { 56 | checkID(id, isChecked); 57 | sortNonStop(isChecked); 58 | } 59 | // все остальные варианты пересадок 60 | else { 61 | checkID(id, isChecked); 62 | sortStops(id, isChecked); 63 | } 64 | }; 65 | 66 | return ( 67 |
68 |

Количество пересадок

69 | 78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/pages/testData.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "price": 76945, 4 | "carrier": "MH", 5 | "segments": [ 6 | { 7 | "origin": "MOW", 8 | "destination": "HKT", 9 | "date": "2020-03-27T00:57:00.000Z", 10 | "stops": ["AUH"], 11 | "duration": 1541 12 | }, 13 | { 14 | "origin": "MOW", 15 | "destination": "HKT", 16 | "date": "2020-04-16T15:41:00.000Z", 17 | "stops": [], 18 | "duration": 1097 19 | } 20 | ] 21 | }, 22 | { 23 | "price": 48435, 24 | "carrier": "TG", 25 | "segments": [ 26 | { 27 | "origin": "MOW", 28 | "destination": "HKT", 29 | "date": "2020-03-27T12:04:00.000Z", 30 | "stops": [], 31 | "duration": 803 32 | }, 33 | { 34 | "origin": "MOW", 35 | "destination": "HKT", 36 | "date": "2020-04-15T21:23:00.000Z", 37 | "stops": ["BKK", "AUH"], 38 | "duration": 944 39 | } 40 | ] 41 | }, 42 | { 43 | "price": 99919, 44 | "carrier": "FV", 45 | "segments": [ 46 | { 47 | "origin": "MOW", 48 | "destination": "HKT", 49 | "date": "2020-03-27T15:38:00.000Z", 50 | "stops": ["SIN", "DXB", "HKG"], 51 | "duration": 1373 52 | }, 53 | { 54 | "origin": "MOW", 55 | "destination": "HKT", 56 | "date": "2020-04-16T13:07:00.000Z", 57 | "stops": ["HKG", "AUH"], 58 | "duration": 869 59 | } 60 | ] 61 | }, 62 | { 63 | "price": 72145, 64 | "carrier": "S7", 65 | "segments": [ 66 | { 67 | "origin": "MOW", 68 | "destination": "HKT", 69 | "date": "2020-03-27T14:36:00.000Z", 70 | "stops": [], 71 | "duration": 1433 72 | }, 73 | { 74 | "origin": "MOW", 75 | "destination": "HKT", 76 | "date": "2020-04-16T04:10:00.000Z", 77 | "stops": [], 78 | "duration": 1049 79 | } 80 | ] 81 | }, 82 | { 83 | "price": 73132, 84 | "carrier": "SU", 85 | "segments": [ 86 | { 87 | "origin": "MOW", 88 | "destination": "HKT", 89 | "date": "2020-03-27T11:07:00.000Z", 90 | "stops": ["SIN", "IST", "SHA"], 91 | "duration": 1835 92 | }, 93 | { 94 | "origin": "MOW", 95 | "destination": "HKT", 96 | "date": "2020-04-16T01:03:00.000Z", 97 | "stops": ["IST", "SIN", "HKG"], 98 | "duration": 1331 99 | } 100 | ] 101 | } 102 | ] 103 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Ticket/Ticket.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classes from "./Ticket.module.scss"; 3 | 4 | export const Ticket = ({ ticket }) => { 5 | const picAvs = "http://pics.avs.io/99/36/"; 6 | 7 | // вырезаем время вылета + получаем время прилета 8 | const splitTimeOut = (str, dur) => { 9 | const [, time] = str.slice(0, -1).split("T"); 10 | const [h, m] = time.split(":"); 11 | const sum = Number(h) * 60 + Number(m) + dur; 12 | const newH = 13 | sum / 60 >= 24 ? Math.floor(sum / 60 - 24) : Math.floor(sum / 60); 14 | const newM = sum % 60; 15 | const formH = newH < 10 ? `0${newH}` : newH; 16 | const formM = newM < 10 ? `0${newM}` : newM; 17 | 18 | return `${h}:${m} - ${formH}:${formM}`; 19 | } 20 | 21 | // переводим длительность перелета(минуты) в формат 00:00 22 | const durationFunc = number => { 23 | const hours = Math.round(number / 60); 24 | const minutes = number % 60; 25 | return `${hours}ч : ${minutes}м`; 26 | } 27 | 28 | return ( 29 |
30 |
31 |
32 | {`${ticket.price.toLocaleString()} P`} 33 |
34 |
35 | {ticket.carrier} 36 |
37 |
38 |
39 | {ticket.segments.map((item, i) => ( 40 |
41 |
42 | 43 | {item.origin} – {item.destination} 44 | 45 | 46 | {splitTimeOut(item.date, item.duration)} 47 | 48 |
49 |
50 | В пути 51 | 52 | {durationFunc(item.duration)} 53 | 54 |
55 |
56 | 57 | {item.stops.length > 0 58 | ? `${item.stops.length} пересадк${ 59 | item.stops.length === 1 ? "a" : "и" 60 | }` 61 | : "Без пересадок"} 62 | 63 | {item.stops.join(", ")} 64 |
65 |
66 | ))} 67 |
68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /aviasales-master/src/views/Content.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/pages/Search.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import classes from "./Search.module.scss"; 3 | import { FilterList } from "../components/FilterList/FilterList"; 4 | import { TicketList } from "../components/TicketList/TicketList"; 5 | import { Sorting } from "../components/Sorting/Sorting"; 6 | import { Alert } from "../components/Alert/Alert"; 7 | 8 | import data from './testData.json' // тестовые данные 9 | 10 | export const Search = () => { 11 | const [tickets, setTickets] = useState([]); // неотсортированный список билетов 12 | const [hasError, setHasError] = useState(false); 13 | const [cloneTickets, setCloneTickets] = useState(null); // массив билетов который будем сортировать по пересадкам 14 | const [selectTub, setSelectTub] = useState("low price"); // таб для автоматической сортировки билетов (цена/скорость) 15 | 16 | // отсортированные по всем параметрам билеты для отрисовки 17 | const [sortTickets, setSortTickets] = useState(null); 18 | 19 | useEffect(() => { 20 | //*** закомментировала сервер aviasales, использую сохраненный json ***/ 21 | // fetch("https://front-test.beta.aviasales.ru/search") 22 | // .then(res => res.json()) 23 | // .then(({ searchId }) => 24 | // fetch( 25 | // `https://front-test.beta.aviasales.ru/tickets?searchId=${searchId}` 26 | // ) 27 | // ) 28 | // .then(res => res.json()) 29 | // .then(({ tickets }) => { 30 | // setTickets(tickets.slice(0, 5)); 31 | // setCloneTickets(tickets.slice(0, 5)); 32 | // }) 33 | // .catch(() => setHasError(true)); 34 | const tickets = data 35 | setTickets(tickets); 36 | setCloneTickets(tickets); 37 | }, []); 38 | 39 | useEffect(() => { 40 | function setSortTabs() { 41 | if (!cloneTickets) return; 42 | if (selectTub === "low price") { 43 | const newArr = [...cloneTickets].sort((a, b) => 44 | a.price > b.price ? 1 : -1 45 | ); 46 | setSortTickets(newArr); 47 | } 48 | if (selectTub === "fastest") { 49 | const newArr = [...cloneTickets].sort((a, b) => { 50 | const firstTicket = a.segments.reduce( 51 | (acc, i) => i.duration + acc, 52 | 0 53 | ); 54 | const secondTicket = b.segments.reduce( 55 | (acc, i) => i.duration + acc, 56 | 0 57 | ); 58 | 59 | if (firstTicket > secondTicket) { 60 | return 1; 61 | } else if (firstTicket < secondTicket) { 62 | return -1; 63 | } else { 64 | return 0; 65 | } 66 | }); 67 | setSortTickets(newArr); 68 | } 69 | } 70 | setSortTabs(); 71 | }, [cloneTickets, selectTub]); 72 | 73 | // скрыть/показать все билеты 74 | const hiddenTickets = isChecked => { 75 | isChecked ? setCloneTickets(tickets) : setCloneTickets([]); 76 | }; 77 | 78 | // билеты без пересадок 79 | const sortNonStop = isChecked => { 80 | if (isChecked) { 81 | const newArr = tickets.filter(item => { 82 | return item.segments.every(i => i.stops.length === 0); // every вернет true, если ВСЕ маршруты без пересадок 83 | }); 84 | setCloneTickets([...cloneTickets, ...newArr]); // вернем в state новый массив с билетами без пересадок 85 | } else { 86 | const newArr = cloneTickets.filter(item => { 87 | return item.segments.some(i => i.stops.length > 0); // оставим только те билеты, у которых хотя бы один маршрут с пересадкой 88 | }); 89 | setCloneTickets(newArr); 90 | } 91 | }; 92 | 93 | /* универсальная функция которой будем сортировать билеты 94 | в зависимости от количества пересадок */ 95 | const sortStops = (numb, isChecked) => { 96 | if (isChecked) { 97 | const newArr = tickets.filter(item => { 98 | // вернет 'true' если хотя бы один маршрут в билете будет с заданным числом пересадок 99 | const sort1 = item.segments.some(i => i.stops.length === numb); 100 | // вернет 'true' если каждый маршрут меньше или равняется заданному числу пересадок 101 | const sort2 = item.segments.every(i => i.stops.length <= numb); 102 | return sort1 && sort2; 103 | }); 104 | setCloneTickets([...cloneTickets, ...newArr]); 105 | } else { 106 | const newArr = cloneTickets.filter(item => { 107 | const sort1 = item.segments.some(i => i.stops.length > numb); 108 | const sort2 = item.segments.every(i => i.stops.length < numb); 109 | return sort1 || sort2; 110 | }); 111 | setCloneTickets(newArr); 112 | } 113 | }; 114 | 115 | // функция для изменения state 'selectTub' для сортировки по табам 116 | const sortTabs = id => { 117 | setSelectTub(id); 118 | }; 119 | 120 | return ( 121 |
122 |
123 | 124 |
125 |
126 |
127 | 128 |
129 | {sortTickets ? ( 130 | 131 | ) : hasError ? ( 132 | 133 | ) : null} 134 |
135 |
136 | ); 137 | }; 138 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /aviasales-master/src/assets/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /aviasales-master/aviasales_frontend-master/src/components/Logo/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | --------------------------------------------------------------------------------