├── .nvmrc
├── .npmrc
├── src
├── react-app-env.d.ts
├── components
│ ├── Content
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── EventsList
│ │ ├── constants.ts
│ │ ├── Skeleton
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ └── style.module.css
│ ├── RubricTitle
│ │ ├── Skeleton
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── EventCardMain
│ │ ├── Skeleton
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── style.module.css
│ │ └── index.tsx
│ ├── Checkbox
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── TextInput
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── MenuGeoLabel
│ │ ├── down.svg
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── SelectionList
│ │ ├── Skeleton
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── style.module.css
│ │ └── index.tsx
│ ├── ClearButton
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── DateFilter
│ │ ├── Button
│ │ │ ├── down.svg
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── Skeleton
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── GalleryModal
│ │ ├── back.svg
│ │ └── styles.module.css
│ ├── YaMapModal
│ │ ├── back.svg
│ │ ├── loader.svg
│ │ ├── Map.tsx
│ │ ├── index.tsx
│ │ └── styles.module.css
│ ├── BackwardButton
│ │ ├── backward.svg
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── YaMapWidget
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── ActionButton
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── EventCardVertical
│ │ ├── Skeleton
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── style.module.css
│ │ └── index.tsx
│ ├── CollapsedText
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── EventCard
│ │ ├── Skeleton
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── style.module.css
│ │ └── index.tsx
│ ├── StackNavigator
│ │ ├── context.ts
│ │ ├── hooks.ts
│ │ ├── Screen
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── PageHeader
│ │ ├── assets
│ │ │ ├── search.svg
│ │ │ └── menu-burger.svg
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── PriceLabel
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── AuthBox
│ │ ├── styles.module.css
│ │ ├── avatar.svg
│ │ └── index.tsx
│ ├── EventsSlider
│ │ ├── style.module.css
│ │ └── index.tsx
│ ├── LoginButton
│ │ ├── styles.module.css
│ │ ├── index.tsx
│ │ └── avatar.svg
│ ├── Skeletons
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── Image
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── MenuNavigation
│ │ ├── style.module.css
│ │ └── index.tsx
│ ├── TicketPrice
│ │ └── index.tsx
│ ├── LazyBlock
│ │ └── index.tsx
│ ├── LazyRender
│ │ └── index.tsx
│ ├── Gallery
│ │ ├── styles.module.css
│ │ └── index.tsx
│ ├── CityModal
│ │ └── styles.module.css
│ ├── MenuModal
│ │ ├── style.module.css
│ │ └── index.tsx
│ ├── CitySelect
│ │ └── styles.module.css
│ └── CityChange
│ │ └── index.tsx
├── lib
│ ├── api
│ │ ├── fragments
│ │ │ ├── metro.ts
│ │ │ ├── paging.ts
│ │ │ ├── tag-preview.ts
│ │ │ ├── city-preview.ts
│ │ │ ├── coordinates.ts
│ │ │ ├── image-size.ts
│ │ │ ├── schedule-preview.ts
│ │ │ ├── event-main-image.ts
│ │ │ ├── city.ts
│ │ │ ├── ticket.ts
│ │ │ ├── gallery-image.ts
│ │ │ ├── event-schedule-info.ts
│ │ │ ├── actual-events.ts
│ │ │ ├── actual-event.ts
│ │ │ ├── place-preview.ts
│ │ │ ├── event-preview.ts
│ │ │ ├── event.ts
│ │ │ └── suggest.ts
│ │ └── types.ts
│ ├── date.ts
│ ├── is-ios.ts
│ ├── price.ts
│ ├── checkout
│ │ └── types
│ │ │ ├── index.ts
│ │ │ ├── checkout-options.ts
│ │ │ ├── checkout-request.ts
│ │ │ └── checkout-state.ts
│ ├── maps.ts
│ ├── js-api
│ │ ├── push.ts
│ │ ├── metrika.ts
│ │ ├── utils.ts
│ │ └── autofill.ts
│ ├── error.ts
│ ├── lazy.ts
│ ├── url-builder.ts
│ ├── metrika
│ │ ├── install.js
│ │ ├── event.ts
│ │ ├── index.ts
│ │ ├── ecommerce.ts
│ │ └── types.ts
│ ├── geolocation.ts
│ ├── autofill.ts
│ ├── global.d.ts
│ ├── oauth.ts
│ ├── payment.ts
│ └── request.ts
├── assets
│ ├── login-avatar.png
│ └── login-avatar-2x.png
├── screens
│ ├── EventScreen
│ │ ├── components
│ │ │ ├── ContentBlock
│ │ │ │ ├── styles.module.css
│ │ │ │ └── index.tsx
│ │ │ ├── Title
│ │ │ │ ├── styles.module.css
│ │ │ │ └── index.tsx
│ │ │ ├── VenueInfo
│ │ │ │ ├── styles.module.css
│ │ │ │ └── index.tsx
│ │ │ ├── EventsFeed
│ │ │ │ ├── Skeleton
│ │ │ │ │ ├── styles.module.css
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── styles.module.css
│ │ │ │ └── index.tsx
│ │ │ ├── RecommendedEvents
│ │ │ │ ├── Skeleton
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── Component.tsx
│ │ │ ├── CheckoutModal
│ │ │ │ ├── components
│ │ │ │ │ ├── CheckoutTextInput
│ │ │ │ │ │ ├── styles.module.css
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── CheckoutEventInfo
│ │ │ │ │ │ ├── styles.module.css
│ │ │ │ │ │ └── index.tsx
│ │ │ │ └── styles.module.css
│ │ │ ├── Cover
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.module.css
│ │ │ └── TicketButton
│ │ │ │ └── styles.module.css
│ │ ├── styles.module.css
│ │ ├── index.tsx
│ │ └── Skeleton
│ │ │ ├── index.tsx
│ │ │ └── styles.module.css
│ ├── SearchScreen
│ │ ├── Input
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── SearchResult
│ │ │ ├── Skeleton
│ │ │ │ ├── styles.module.css
│ │ │ │ └── index.tsx
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── styles.module.css
│ │ ├── PopularResult
│ │ │ ├── Skeleton
│ │ │ │ ├── styles.module.css
│ │ │ │ └── index.tsx
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── MainScreen
│ │ ├── Skeleton
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── Title
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── SelectionScreen
│ │ ├── Skeleton
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── OrdersScreen
│ │ ├── styles.module.css
│ │ ├── Skeleton
│ │ │ └── index.tsx
│ │ ├── Order
│ │ │ ├── Skeleton
│ │ │ │ ├── styles.module.css
│ │ │ │ └── index.tsx
│ │ │ ├── styles.module.css
│ │ │ └── index.tsx
│ │ ├── Component.tsx
│ │ └── index.tsx
│ └── RubricScreen
│ │ └── index.tsx
├── redux
│ ├── actions.ts
│ ├── slices
│ │ ├── date-filter.ts
│ │ ├── menu.ts
│ │ ├── city-list.ts
│ │ └── autofill.ts
│ └── index.ts
├── hooks
│ ├── usePrevious.ts
│ ├── useMetrikaHit.ts
│ ├── useThrottleLoading.ts
│ ├── useVisibleOnce.ts
│ ├── useWatchAuth.ts
│ ├── usePaginatedList.ts
│ ├── useInfiniteScroll.ts
│ ├── useVisible.ts
│ └── useScrollEffect.ts
├── index.css
├── PolyfillApp.tsx
└── index.tsx
├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── index.html
└── api
│ └── event
│ ├── event-concert-id5.json
│ ├── event-concert-id10.json
│ └── event-concert-id6.json
├── docs
├── architecture.png
└── Architecture.md
├── .editorconfig
├── AUTHORS
├── .gitignore
├── tsconfig.json
├── README.md
└── package.json
/.nvmrc:
--------------------------------------------------------------------------------
1 | 12.13.0
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=http://registry.npmjs.org
2 | save-exact=true
3 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/components/Content/styles.module.css:
--------------------------------------------------------------------------------
1 | .content {
2 | margin: 16px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/EventsList/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_ITEMS_GAP = 18;
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandex/miniapp-example/master/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandex/miniapp-example/master/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandex/miniapp-example/master/public/logo512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow: /
4 |
--------------------------------------------------------------------------------
/src/components/EventsList/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .events {
2 | display: grid;
3 | }
4 |
--------------------------------------------------------------------------------
/docs/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandex/miniapp-example/master/docs/architecture.png
--------------------------------------------------------------------------------
/src/lib/api/fragments/metro.ts:
--------------------------------------------------------------------------------
1 | export type Metro = {
2 | name: string;
3 | colors: string[];
4 | };
5 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/paging.ts:
--------------------------------------------------------------------------------
1 | export type Paging = {
2 | offset: number;
3 | total: number;
4 | };
5 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/tag-preview.ts:
--------------------------------------------------------------------------------
1 | export type Tag = {
2 | name: string;
3 | code: string;
4 | };
5 |
--------------------------------------------------------------------------------
/src/assets/login-avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandex/miniapp-example/master/src/assets/login-avatar.png
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/ContentBlock/styles.module.css:
--------------------------------------------------------------------------------
1 | .block {
2 | padding-bottom: 24px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/login-avatar-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandex/miniapp-example/master/src/assets/login-avatar-2x.png
--------------------------------------------------------------------------------
/src/lib/api/fragments/city-preview.ts:
--------------------------------------------------------------------------------
1 | export type CityPreview = {
2 | name: string;
3 | geoid: number;
4 | };
5 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/coordinates.ts:
--------------------------------------------------------------------------------
1 | export type Coordinates = {
2 | longitude: number;
3 | latitude: number;
4 | };
5 |
--------------------------------------------------------------------------------
/src/redux/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit';
2 |
3 | export const cleanup = createAction('cleanup');
4 |
--------------------------------------------------------------------------------
/src/components/RubricTitle/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | width: 60%;
3 | height: 20px;
4 | margin: 3px 0 21px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/screens/SearchScreen/Input/styles.module.css:
--------------------------------------------------------------------------------
1 | .input {
2 | position: relative;
3 |
4 | width: 100%;
5 | height: 56px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/image-size.ts:
--------------------------------------------------------------------------------
1 | export type MediaImageSize = {
2 | url: string;
3 | width: number;
4 | height: number;
5 | };
6 |
--------------------------------------------------------------------------------
/src/components/EventCardMain/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | width: 100%;
3 | height: 144px;
4 |
5 | border-radius: 8px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/screens/SearchScreen/SearchResult/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | width: 40%;
3 | height: 20px;
4 | margin: 27px 0 19px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Checkbox/styles.module.css:
--------------------------------------------------------------------------------
1 | .checkbox {
2 | margin-right: 10px;
3 | }
4 |
5 | .label {
6 | display: flex;
7 | align-content: center;
8 | }
9 |
--------------------------------------------------------------------------------
/src/screens/MainScreen/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .content {
2 | margin: 0 16px;
3 | }
4 |
5 | .title {
6 | width: 60%;
7 | height: 32px;
8 | margin: 20px 0 16px;
9 | }
10 |
--------------------------------------------------------------------------------
/src/screens/SelectionScreen/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .content {
2 | margin: 16px;
3 | }
4 |
5 | .title {
6 | width: 60%;
7 | height: 20px;
8 | margin: 0 0 18px;
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/schedule-preview.ts:
--------------------------------------------------------------------------------
1 | export type SchedulePreview = {
2 | text: string | null;
3 | singleDate: {
4 | day: number;
5 | month: number;
6 | } | null;
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/event-main-image.ts:
--------------------------------------------------------------------------------
1 | import { MediaImageSize } from './image-size';
2 |
3 | export type TouchPrimaryImage = {
4 | bgColor: string | null;
5 | touchPrimary: MediaImageSize;
6 | };
7 |
--------------------------------------------------------------------------------
/src/lib/date.ts:
--------------------------------------------------------------------------------
1 | import { format, parse } from 'date-fns';
2 |
3 | export const dateToString = (date: Date) => format(date, 'YYYY-MM-DD').toString();
4 | export const parseDate = (date: string) => parse(date);
5 |
--------------------------------------------------------------------------------
/src/components/TextInput/styles.module.css:
--------------------------------------------------------------------------------
1 | .input {
2 | padding: 0;
3 |
4 | font-size: 15px;
5 |
6 | border-width: 0 10px;
7 | border-color: transparent;
8 | outline: none;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/MenuGeoLabel/down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/RubricTitle/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | margin: 0 0 18px;
3 |
4 | font-size: 20px;
5 | font-weight: bold;
6 | line-height: 26px;
7 |
8 | opacity: .9;
9 | color: #202020;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/SelectionList/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .block {
2 | display: grid;
3 | grid-gap: 8px;
4 | }
5 |
6 | .item {
7 | width: 100%;
8 | padding-top: 50%;
9 |
10 | border-radius: 8px;
11 | }
12 |
--------------------------------------------------------------------------------
/src/screens/MainScreen/Title/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | margin: 20px 0 16px;
3 |
4 | font-size: 32px;
5 | font-weight: bold;
6 | line-height: 40px;
7 |
8 | opacity: .9;
9 | color: #202020;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/ClearButton/styles.module.css:
--------------------------------------------------------------------------------
1 | .clear {
2 | padding: 0;
3 |
4 | font-family: 'Helvetica Neue', Arial, sans-serif;
5 |
6 | border: none;
7 | outline: none;
8 | background-color: transparent;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/DateFilter/Button/down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/Title/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | margin-top: 0;
3 | margin-bottom: 16px;
4 |
5 | font-size: 20px;
6 | font-weight: bold;
7 | line-height: 26px;
8 |
9 | color: #1a1a1a;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/GalleryModal/back.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/YaMapModal/back.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/BackwardButton/backward.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/is-ios.ts:
--------------------------------------------------------------------------------
1 | export const isIOS = (): boolean =>
2 | (/iPad|iPhone|iPod/.test(navigator.platform) ||
3 | /iPad|iPhone|iPod/.test(navigator.userAgent) ||
4 | (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
5 | !window.MSStream;
6 |
--------------------------------------------------------------------------------
/src/screens/MainScreen/Title/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | const Title: React.FC = ({ children }) => {
6 | return
{children}
;
7 | };
8 |
9 | export default Title;
10 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/city.ts:
--------------------------------------------------------------------------------
1 | export type MenuTag = {
2 | name: string;
3 | code: string;
4 | };
5 |
6 | export type City = {
7 | name: string;
8 | geoid: number;
9 | longitude: number;
10 | latitude: number;
11 | eventsMenu: MenuTag[];
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/RubricTitle/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | const RubricTitle: React.FC = ({ children }) => {
6 | return {children}
;
7 | };
8 |
9 | export default RubricTitle;
10 |
--------------------------------------------------------------------------------
/src/components/EventsList/style.module.css:
--------------------------------------------------------------------------------
1 | .list {
2 | display: grid;
3 | grid-template-columns: 1fr;
4 | }
5 |
6 | .wrapper {
7 | display: flex;
8 | justify-content: center;
9 |
10 | margin-top: 16px;
11 | }
12 |
13 | .button {
14 | width: 100%;
15 | }
16 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/Title/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | const Title: React.FC = ({ children }) => {
6 | return {children}
;
7 | };
8 |
9 | export default Title;
10 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/ticket.ts:
--------------------------------------------------------------------------------
1 | export type Currency = 'rub' | 'usd';
2 |
3 | export type Price = {
4 | currency: Currency;
5 | min: number | null;
6 | max: number | null;
7 | };
8 |
9 | export type Ticket = {
10 | id: string | null;
11 | price: Price | null;
12 | };
13 |
--------------------------------------------------------------------------------
/src/lib/price.ts:
--------------------------------------------------------------------------------
1 | import { Currency } from './api/fragments/ticket';
2 |
3 | const currencySymbol: { [key in Currency]: string } = {
4 | rub: '₽',
5 | usd: '$',
6 | };
7 |
8 | export function getCurrencySymbol(currency: Currency) {
9 | return currencySymbol[currency];
10 | }
11 |
--------------------------------------------------------------------------------
/src/screens/OrdersScreen/styles.module.css:
--------------------------------------------------------------------------------
1 | .content {
2 | padding: 0 16px;
3 | }
4 |
5 | .empty {
6 | position: absolute;
7 | top: 56px;
8 | right: 0;
9 | bottom: 0;
10 | left: 0;
11 |
12 | display: flex;
13 | }
14 |
15 | .text {
16 | margin: auto;
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/gallery-image.ts:
--------------------------------------------------------------------------------
1 | import { MediaImageSize } from './image-size';
2 |
3 | export type GalleryImage = {
4 | bgColor: string | null;
5 | thumbnail: MediaImageSize;
6 | thumbnail2x: MediaImageSize;
7 | large: MediaImageSize;
8 | large2x: MediaImageSize;
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/YaMapWidget/styles.module.css:
--------------------------------------------------------------------------------
1 | .map {
2 | position: relative;
3 |
4 | width: 100vw;
5 | height: 88px;
6 | margin-bottom: 16px;
7 | margin-left: -12px;
8 |
9 | background-color: #fef33a;
10 | background-position: 50% 50%;
11 | background-size: cover;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/MenuGeoLabel/styles.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 0 0 8px;
3 | }
4 |
5 | .select {
6 | display: inline-block;
7 |
8 | padding-right: 21px;
9 |
10 | font-size: 16px;
11 | line-height: 24px;
12 |
13 | background: #fff url('./down.svg') 100% 50% no-repeat;
14 | }
15 |
--------------------------------------------------------------------------------
/src/hooks/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react';
2 |
3 | export function usePrevious(value: T) {
4 | const previousValue = useRef();
5 |
6 | useEffect(() => {
7 | previousValue.current = value;
8 | }, [value]);
9 |
10 | return previousValue.current;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/DateFilter/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .block {
2 | display: flex;
3 | overflow: hidden;
4 |
5 | padding: 23px 14px 4px;
6 | }
7 |
8 | .filter {
9 | flex: none;
10 |
11 | width: 89px;
12 | height: 40px;
13 | margin-right: 8px;
14 |
15 | border-radius: 20px;
16 | }
17 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | line-height: 1.15;
3 | }
4 |
5 | body {
6 | overflow: hidden;
7 |
8 | margin: 0;
9 |
10 | font-family: 'Helvetica Neue', Arial, sans-serif;
11 |
12 | background: #fff;
13 | -webkit-font-smoothing: antialiased;
14 | }
15 |
16 | h2 {
17 | margin: 0;
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/event-schedule-info.ts:
--------------------------------------------------------------------------------
1 | import { PlacePreview } from './place-preview';
2 | import { SchedulePreview } from './schedule-preview';
3 |
4 | export type EventScheduleInfo = {
5 | oneOfPlaces: PlacePreview | null;
6 | placePreview: string | null;
7 | preview: SchedulePreview | null;
8 | };
9 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/VenueInfo/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | padding-bottom: 6px;
3 |
4 | font-size: 16px;
5 | font-weight: bold;
6 | line-height: 22px;
7 | }
8 |
9 | .address {
10 | font-size: 12px;
11 | line-height: 16px;
12 |
13 | color: rgba(0, 0, 0, .5);
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/RubricTitle/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Text } from '../../../components/Skeletons';
4 |
5 | import styles from './styles.module.css';
6 |
7 | const RubricTitleSkeleton: React.FC = () => ;
8 |
9 | export default RubricTitleSkeleton;
10 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/actual-events.ts:
--------------------------------------------------------------------------------
1 | import { EventData } from './event';
2 | import { Paging } from './paging';
3 | import { ActualEvent } from './actual-event';
4 |
5 | export type ActualEvents = {
6 | items: Array;
7 | paging: Paging;
8 | prefetchItems?: Array;
9 | };
10 |
--------------------------------------------------------------------------------
/src/screens/SearchScreen/SearchResult/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | margin: 24px 0 16px;
3 |
4 | font-size: 20px;
5 | font-weight: bold;
6 | line-height: 26px;
7 | }
8 |
9 | .notFound {
10 | margin: 20vh 0;
11 |
12 | font-size: 6vw;
13 | text-align: center;
14 |
15 | color: #999;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/EventCardMain/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Rect } from '../../Skeletons';
4 |
5 | import styles from './styles.module.css';
6 |
7 | const EventCardMainSkeleton: React.FC = () => {
8 | return ;
9 | };
10 |
11 | export default EventCardMainSkeleton;
12 |
--------------------------------------------------------------------------------
/src/components/ActionButton/styles.module.css:
--------------------------------------------------------------------------------
1 | .action {
2 | font-family: 'Helvetica Neue', Arial, sans-serif;
3 |
4 | border: none;
5 | border-radius: 100px;
6 | outline: none;
7 | background: #ffdb4d;
8 | }
9 |
10 | .invalid,
11 | .action[disabled] {
12 | color: rgb(128, 128, 128);
13 | background: rgb(242, 242, 242);
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/EventCardVertical/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | width: 208px;
3 | }
4 |
5 | .image {
6 | height: 144px;
7 |
8 | border-radius: 12px;
9 | }
10 |
11 | .title {
12 | height: 16px;
13 | margin-top: 12px;
14 | }
15 |
16 | .description {
17 | width: 65%;
18 | height: 12px;
19 | margin-top: 6px;
20 | }
21 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | The following authors have created the source code of "MiniApp Example" published and distributed by YANDEX LLC as the owner:
2 |
3 | Maxim Zemskov
4 | Aleksandr Bulanov
5 | Kirill Medvedev
6 | Alexander Vakhitov
7 | Alexander Belev
8 |
--------------------------------------------------------------------------------
/src/components/BackwardButton/styles.module.css:
--------------------------------------------------------------------------------
1 | .backward-icon {
2 | display: inline-block;
3 |
4 | width: 16px;
5 | height: 14px;
6 | mask-image: url('backward.svg');
7 | }
8 |
9 | .icon-white {
10 | background-color: #fff;
11 | }
12 |
13 | .icon-black {
14 | background-color: #444;
15 | }
16 |
17 | .backward-button {
18 | height: 15px;
19 | }
20 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/ContentBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | const ContentBlock: React.FC<{ className?: string }> = ({ children, className }) => {
6 | return {children}
;
7 | };
8 |
9 | export default ContentBlock;
10 |
--------------------------------------------------------------------------------
/src/screens/SearchScreen/styles.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | align-items: center;
4 |
5 | box-sizing: border-box;
6 | height: 56px;
7 | padding: 18px 16px;
8 |
9 | box-shadow: 0 0 14px 0 rgba(0, 0, 0, .25);
10 | }
11 |
12 | .content {
13 | overflow-x: hidden;
14 | overflow-y: auto;
15 |
16 | padding: 0 18px 16px;
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/checkout/types/index.ts:
--------------------------------------------------------------------------------
1 | import { YandexCheckoutRequest } from './checkout-request';
2 |
3 | declare global {
4 | interface Window {
5 | YandexCheckoutRequest?: YandexCheckoutRequest;
6 | }
7 | }
8 |
9 | export * from './checkout-request';
10 | export * from './checkout-details';
11 | export * from './checkout-options';
12 | export * from './checkout-state';
13 |
--------------------------------------------------------------------------------
/src/screens/OrdersScreen/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import OrderSkeleton from '../Order/Skeleton';
4 |
5 | import EventsListSkeleton from '../../../components/EventsList/Skeleton';
6 |
7 | const OrdersScreenSkeleton: React.FC = () => {
8 | return ;
9 | };
10 |
11 | export default OrdersScreenSkeleton;
12 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/EventsFeed/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .feed {
2 | margin-bottom: 24px;
3 | }
4 |
5 | .title {
6 | width: 80%;
7 | max-width: 275px;
8 | height: 20px;
9 | margin-bottom: 16px;
10 | }
11 |
12 | .events {
13 | display: flex;
14 | overflow: hidden;
15 | }
16 |
17 | .item {
18 | flex: none;
19 |
20 | margin-right: 12px;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Content/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | export type ContentProps = {
6 | className?: string;
7 | };
8 |
9 | const Content: React.FC = props => {
10 | return {props.children}
;
11 | };
12 |
13 | export default Content;
14 |
--------------------------------------------------------------------------------
/src/screens/SearchScreen/PopularResult/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | width: 60%;
3 | height: 18px;
4 | margin: 28px 0 12px;
5 | }
6 |
7 | .event {
8 | height: 14px;
9 | margin: 26px 0;
10 | }
11 |
12 | .event:nth-child(1n) {
13 | width: 80%;
14 | }
15 |
16 | .event:nth-child(2n) {
17 | width: 50%;
18 | }
19 |
20 | .event:nth-child(3n) {
21 | width: 60%;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/CollapsedText/styles.module.css:
--------------------------------------------------------------------------------
1 | .short {
2 | display: -webkit-box;
3 | -webkit-box-orient: vertical;
4 | overflow: hidden;
5 | }
6 |
7 | .hidden {
8 | position: absolute;
9 |
10 | display: none;
11 |
12 | pointer-events: none;
13 | }
14 |
15 | .button {
16 | height: 28px;
17 |
18 | font-size: 13px;
19 | line-height: 28px;
20 |
21 | color: #04b;
22 | }
23 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/actual-event.ts:
--------------------------------------------------------------------------------
1 | import { EventPreview } from './event-preview';
2 |
3 | export type ActualEvent = {
4 | eventPreview: EventPreview;
5 | scheduleInfo: {
6 | placesTotal: number | null;
7 | preview: {
8 | singleDate: {
9 | day: string;
10 | month: string;
11 | } | null;
12 | } | null;
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/EventCard/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | display: flex;
3 | }
4 |
5 | .image {
6 | width: 152px;
7 | height: 100px;
8 | margin-right: 18px;
9 |
10 | border-radius: 8px;
11 | }
12 |
13 | .right {
14 | flex: 1;
15 | }
16 |
17 | .title {
18 | height: 16px;
19 | margin-bottom: 12px;
20 | }
21 |
22 | .description {
23 | width: 80%;
24 | height: 12px;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/YaMapModal/loader.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/StackNavigator/context.ts:
--------------------------------------------------------------------------------
1 | import { createRef, createContext, RefObject } from 'react';
2 |
3 | type StackNavigatorContextType = {
4 | onBackward: () => void;
5 | isVisible: boolean;
6 | ref: RefObject;
7 | };
8 |
9 | export const StackNavigatorContext = createContext({
10 | onBackward: () => {},
11 | isVisible: false,
12 | ref: createRef(),
13 | });
14 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/EventsFeed/styles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | position: relative;
3 | }
4 |
5 | .button {
6 | position: absolute;
7 | top: 0;
8 | right: 0;
9 |
10 | display: block;
11 |
12 | padding: 6px 14px;
13 |
14 | font-size: 12px;
15 | line-height: 16px;
16 | text-decoration: none;
17 |
18 | color: #fff;
19 | border-radius: 20px;
20 | background: #202020;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/PageHeader/assets/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/place-preview.ts:
--------------------------------------------------------------------------------
1 | import { CityPreview } from './city-preview';
2 | import { Coordinates } from './coordinates';
3 | import { Metro } from './metro';
4 |
5 | export type PlacePreview = {
6 | id: string;
7 | title: string | null;
8 | address: string | null;
9 | type: {
10 | name: string;
11 | };
12 | city: CityPreview | null;
13 | metro: Metro[];
14 | coordinates: Coordinates | null;
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/DateFilter/styles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | overflow-x: scroll;
4 | -webkit-overflow-scrolling: touch;
5 |
6 | margin-bottom: -26px; /* Отрицательный отступ и большой паддинг снизу чтобы не обрезалась тень из-за overflow */
7 | padding: 23px 14px 30px;
8 | overscroll-behavior-x: contain;
9 | scrollbar-width: none;
10 | }
11 |
12 | .container::-webkit-scrollbar {
13 | display: none;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/EventCardVertical/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Rect, Text } from '../../Skeletons';
4 |
5 | import styles from './styles.module.css';
6 |
7 | export const EventCardVerticalSkeleton: React.FC = () => (
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # editors
26 | .idea
27 | .vscode
28 |
--------------------------------------------------------------------------------
/src/components/PriceLabel/styles.module.css:
--------------------------------------------------------------------------------
1 | .price {
2 | position: absolute;
3 | z-index: 2;
4 |
5 | height: 28px;
6 | padding: 0 10px;
7 |
8 | font-size: 12px;
9 | font-weight: bold;
10 | line-height: 28px;
11 |
12 | color: #fff;
13 | border-radius: 20px;
14 | background: #202020;
15 | }
16 |
17 | .left {
18 | bottom: 10px;
19 | left: 10px;
20 | }
21 |
22 | .right {
23 | right: 14px;
24 | bottom: 14px;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/PageHeader/assets/menu-burger.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/AuthBox/styles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: inline-flex;
3 | }
4 |
5 | .image {
6 | display: block;
7 | overflow: hidden;
8 |
9 | width: 32px;
10 | height: 32px;
11 |
12 | border-radius: 50%;
13 | }
14 |
15 | .image img {
16 | border-radius: 50%;
17 | background-color: #fff;
18 | }
19 |
20 | .username {
21 | margin-left: 9px;
22 |
23 | font-size: 16px;
24 | font-weight: bold;
25 | line-height: 32px;
26 | }
27 |
--------------------------------------------------------------------------------
/src/screens/SearchScreen/PopularResult/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | margin: 26px 0 0;
3 |
4 | font-size: 16px;
5 | font-weight: bold;
6 | line-height: 24px;
7 |
8 | color: #1a1a1a;
9 | }
10 |
11 | .events {
12 | margin: 10px 0 0;
13 | padding: 0;
14 |
15 | list-style: none;
16 | }
17 |
18 | .event {
19 | font-size: 14px;
20 | line-height: 40px;
21 | }
22 |
23 | .link {
24 | text-decoration: none;
25 |
26 | color: #000;
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/TextInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | type Props = JSX.IntrinsicElements['input'];
6 | const TextInput = forwardRef(({ className, type, ...props }, ref) => {
7 | const cls = [styles.input, className].filter(Boolean).join(' ');
8 |
9 | return ;
10 | });
11 |
12 | export default TextInput;
13 |
--------------------------------------------------------------------------------
/src/lib/maps.ts:
--------------------------------------------------------------------------------
1 | type StaticMapUrlOptions = {
2 | size: [number, number];
3 | z: number;
4 | ll?: [number, number];
5 | pt?: [number, number, 'pm2rdl'];
6 | };
7 |
8 | export function getStaticMapUrl(options: StaticMapUrlOptions) {
9 | const params = new URLSearchParams({ l: 'map' });
10 |
11 | for (const [key, value] of Object.entries(options)) {
12 | params.set(key, String(value));
13 | }
14 |
15 | return `//static-maps.yandex.ru/1.x/?${params}`;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/DateFilter/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Rect } from '../../Skeletons';
4 |
5 | import styles from './styles.module.css';
6 |
7 | const DateFilterSkeleton: React.FC = () => (
8 |
9 | {Array(2)
10 | .fill(0)
11 | .map((_, index) => (
12 |
13 | ))}
14 |
15 | );
16 |
17 | export default DateFilterSkeleton;
18 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/RecommendedEvents/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { EventsFeedSkeleton } from '../../EventsFeed/Skeleton';
4 |
5 | const RecommendedEventsSkeleton: React.FC = () => {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 | >
13 | );
14 | };
15 |
16 | export default RecommendedEventsSkeleton;
17 |
--------------------------------------------------------------------------------
/src/components/DateFilter/Button/styles.module.css:
--------------------------------------------------------------------------------
1 | .button {
2 | height: 40px;
3 | margin-right: 8px;
4 | padding: 0 20px;
5 |
6 | font-size: 14px;
7 | font-weight: 500;
8 | line-height: 40px;
9 | white-space: nowrap;
10 |
11 | border-radius: 20px;
12 | box-shadow: 0 5px 20px rgba(193, 193, 193, .4);
13 | }
14 |
15 | .button.active {
16 | color: #f00;
17 | }
18 |
19 | .button.tip {
20 | padding-right: 36px;
21 |
22 | background: url('./down.svg') 62px 19px no-repeat;
23 | }
24 |
--------------------------------------------------------------------------------
/src/hooks/useMetrikaHit.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | import { useScreenVisible } from '../components/StackNavigator';
4 | import { hit } from '../lib/metrika';
5 | import { YMetrikaVisitParams } from '../lib/metrika/types';
6 |
7 | export function useMetrikaHit(params?: YMetrikaVisitParams) {
8 | const isVisible = useScreenVisible();
9 |
10 | useEffect(() => {
11 | if (isVisible) {
12 | hit(window.location.href, { params });
13 | }
14 | }, [isVisible, params]);
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/ClearButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | export type Props = {
6 | className?: string;
7 | onClick: () => void;
8 | };
9 | const ClearButton: React.FC = ({ className, children, onClick }) => {
10 | const cls = [styles.clear, className].filter(Boolean).join(' ');
11 | return (
12 |
15 | );
16 | };
17 |
18 | export default ClearButton;
19 |
--------------------------------------------------------------------------------
/src/components/EventsSlider/style.module.css:
--------------------------------------------------------------------------------
1 | .list {
2 | display: flex;
3 | overflow-x: scroll;
4 | -webkit-overflow-scrolling: touch;
5 | flex-direction: row;
6 |
7 | scroll-snap-type: x mandatory;
8 | overscroll-behavior-x: contain;
9 | scrollbar-width: none;
10 | }
11 |
12 | .list::-webkit-scrollbar {
13 | display: none;
14 | }
15 |
16 | .item {
17 | flex: none;
18 |
19 | margin-left: 12px;
20 | scroll-snap-align: center;
21 | }
22 |
23 | .item:first-child {
24 | margin-left: 0;
25 | }
26 |
--------------------------------------------------------------------------------
/src/lib/js-api/push.ts:
--------------------------------------------------------------------------------
1 | import { callJsApi } from './utils';
2 |
3 | export type YandexPushTokenForTransaction = {
4 | jwtToken: string;
5 | pushToken: string;
6 | };
7 |
8 | export function getPushTokenForTransaction(paymentToken: string): Promise {
9 | return callJsApi({
10 | name: 'window.yandex.app.push.getPushTokenForTransaction',
11 | args: [paymentToken],
12 | scope: window?.yandex?.app?.push,
13 | method: 'getPushTokenForTransaction',
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/checkout/types/checkout-options.ts:
--------------------------------------------------------------------------------
1 | export type YandexCheckoutOptions = {
2 | // Показывается рядом с суммой заказа.
3 | // По умолчанию: домен страницы, на которой открыт чекаут.
4 | shopName?: string;
5 |
6 | // Показывается рядом названием магазина. Рекомендуемый размер 48x48.
7 | // По умолчанию: favicon страницы, на которой открыт чекаут.
8 | shopIcon?: string;
9 |
10 | // Используется в случаях, если на одном домене работает несколько разных магазинов.
11 | // По умолчанию: `/`
12 | baseUrl?: string;
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/LoginButton/styles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: inline-flex;
3 | }
4 |
5 | .container::before {
6 | display: block;
7 |
8 | width: 24px;
9 | height: 24px;
10 |
11 | content: '';
12 |
13 | background: url('avatar.svg') center center no-repeat;
14 | background-size: contain;
15 | }
16 |
17 | .button {
18 | margin-left: 9px;
19 | padding: 0;
20 |
21 | font-size: 16px;
22 | font-weight: bold;
23 | line-height: 24px;
24 |
25 | border: none;
26 | outline: none;
27 | background: none;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/PriceLabel/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Ticket } from '../../lib/api/fragments/ticket';
4 | import TicketPrice from '../TicketPrice';
5 |
6 | import styles from './styles.module.css';
7 |
8 | type Props = {
9 | ticket: Ticket;
10 | position?: 'left' | 'right';
11 | };
12 |
13 | const PriceLabel: React.FC = props => (
14 |
15 |
16 |
17 | );
18 |
19 | export default PriceLabel;
20 |
--------------------------------------------------------------------------------
/src/screens/OrdersScreen/Order/Skeleton/styles.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | display: flex;
3 | justify-content: space-between;
4 |
5 | box-sizing: border-box;
6 | height: 92px;
7 | padding: 16px;
8 | }
9 |
10 | .image {
11 | flex: 0 0 48px;
12 |
13 | height: 60px;
14 |
15 | border-radius: 8px;
16 | }
17 |
18 | .content {
19 | width: 100%;
20 | margin: 0 16px;
21 | }
22 |
23 | .right {
24 | flex: 0 0 75px;
25 | }
26 |
27 | .short {
28 | width: 60%;
29 | margin: 4px 0;
30 | }
31 |
32 | .right .short {
33 | margin-left: auto;
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | export type Props = JSX.IntrinsicElements['input'] & {
6 | label?: string;
7 | };
8 | const Checkbox: React.FC = ({ className, label, ...props }) => {
9 | const cls = [styles.checkbox, className].filter(Boolean).join(' ');
10 |
11 | return (
12 |
16 | );
17 | };
18 |
19 | export default Checkbox;
20 |
--------------------------------------------------------------------------------
/src/components/EventCard/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Rect, Text } from '../../Skeletons';
4 |
5 | import styles from './styles.module.css';
6 |
7 | const EventCardSkeleton: React.FC = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default EventCardSkeleton;
20 |
--------------------------------------------------------------------------------
/src/components/YaMapModal/Map.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { Map as YMap, Placemark, YMaps } from 'react-yandex-maps';
3 |
4 | type Props = {
5 | point: [number, number];
6 | };
7 |
8 | const Map: React.FC = ({ point }) => {
9 | const mapData = useMemo(() => ({ center: point, zoom: 15 }), [point]);
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Map;
21 |
--------------------------------------------------------------------------------
/src/lib/error.ts:
--------------------------------------------------------------------------------
1 | export enum AppErrorCode {
2 | OauthAccessDenied = 'oauth_access_denied',
3 | OauthUnauthorizedClient = 'oauth_unauthorized_client',
4 | JsApiDenied = 'js_api_denied',
5 | JsApiCancelled = 'js_api_cancelled',
6 | JsApiAlreadyShown = 'js_api_already_shown',
7 | JsApiMethodNotAvailable = 'js_api_method_not_available',
8 | }
9 |
10 | export class AppError extends Error {
11 | public code: AppErrorCode;
12 |
13 | constructor(code: AppErrorCode, message: string) {
14 | super(message);
15 |
16 | this.code = code;
17 | this.name = 'AppError';
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/SelectionList/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Rect } from '../../Skeletons';
4 |
5 | import styles from './styles.module.css';
6 |
7 | const SelectionListSkeleton: React.FC<{ className?: string }> = ({ className }) => {
8 | return (
9 |
10 | {Array(5)
11 | .fill(0)
12 | .map((_, index) => (
13 |
14 | ))}
15 |
16 | );
17 | };
18 |
19 | export default SelectionListSkeleton;
20 |
--------------------------------------------------------------------------------
/src/components/StackNavigator/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useContext, useMemo } from 'react';
2 |
3 | import { StackNavigatorContext } from './context';
4 | import { BackwardProps } from './StackNavigator';
5 |
6 | export function useBackward(params?: BackwardProps) {
7 | const onBackward = useContext(StackNavigatorContext).onBackward;
8 |
9 | return useMemo(() => onBackward.bind(null, params), [onBackward, params]);
10 | }
11 |
12 | export function useScreenVisible() {
13 | return useContext(StackNavigatorContext).isVisible;
14 | }
15 |
16 | export function useScreenRef() {
17 | return useContext(StackNavigatorContext).ref;
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react",
17 | "noFallthroughCasesInSwitch": true
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Skeletons/styles.module.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --skeleton-color: #eee;
3 | --skeleton-animation: loading 1.5s ease-in-out infinite;
4 | }
5 |
6 | .rect {
7 | background-color: var(--skeleton-color);
8 |
9 | animation: var(--skeleton-animation);
10 | }
11 |
12 | .text {
13 | height: 12px;
14 |
15 | border-radius: 26px;
16 | background-color: var(--skeleton-color);
17 |
18 | animation: var(--skeleton-animation);
19 | }
20 |
21 | @keyframes loading {
22 | 0% {
23 | opacity: 1;
24 | }
25 |
26 | 50% {
27 | opacity: .4;
28 | }
29 |
30 | 100% {
31 | opacity: 1;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/LoginButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 |
3 | import { useDispatch } from 'react-redux';
4 |
5 | import { login } from '../../redux/slices/user';
6 |
7 | import styles from './styles.module.css';
8 |
9 | const LoginButton: React.FC = () => {
10 | const dispatch = useDispatch();
11 |
12 | const onLoginClick = useCallback(() => {
13 | dispatch(login());
14 | }, [dispatch]);
15 |
16 | return (
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default LoginButton;
24 |
--------------------------------------------------------------------------------
/src/lib/js-api/metrika.ts:
--------------------------------------------------------------------------------
1 | import { AppErrorCode } from '../error';
2 | import { callJsApi } from './utils';
3 |
4 | export async function reportGoalReached(goal: string, params: object): Promise {
5 | try {
6 | await callJsApi({
7 | name: 'window.yandex.app.reportGoalReached',
8 | args: [goal, params],
9 | scope: window?.yandex?.app,
10 | method: 'reportGoalReached',
11 | });
12 | } catch (err) {
13 | const { code } = err;
14 |
15 | if (code === AppErrorCode.JsApiMethodNotAvailable) {
16 | return;
17 | }
18 |
19 | console.error(err);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Image/styles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | position: relative;
3 | }
4 |
5 | .image {
6 | width: 100%;
7 | height: 100%;
8 |
9 | color: #fff;
10 | object-fit: cover;
11 | }
12 |
13 | .stub {
14 | position: absolute;
15 | top: 0;
16 | right: 0;
17 | bottom: 0;
18 | left: 0;
19 |
20 | display: block;
21 |
22 | content: '';
23 | pointer-events: none;
24 |
25 | opacity: 1;
26 | }
27 |
28 | .stub-hidden {
29 | opacity: 0;
30 |
31 | animation: fadeIn .4s ease forwards;
32 | }
33 |
34 | @keyframes fadeIn {
35 | from {
36 | opacity: 1;
37 | }
38 |
39 | to {
40 | opacity: 0;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/MenuNavigation/style.module.css:
--------------------------------------------------------------------------------
1 | .list {
2 | margin: 0;
3 | padding: 0;
4 |
5 | list-style: none;
6 |
7 | font-size: 16px;
8 | }
9 |
10 | .item {
11 | display: flex;
12 | justify-content: stretch;
13 | align-items: stretch;
14 |
15 | height: 55px;
16 |
17 | line-height: 55px;
18 |
19 | border-top: 1px solid #efefef;
20 | }
21 |
22 | .item:first-child {
23 | border-top: none;
24 | }
25 |
26 | .orders-text {
27 | font-weight: 500;
28 | }
29 |
30 | .link {
31 | display: block;
32 |
33 | width: 100%;
34 |
35 | text-decoration: none;
36 |
37 | color: rgba(0, 0, 0, .8);
38 | }
39 |
40 | .active-item {
41 | color: #000;
42 | }
43 |
--------------------------------------------------------------------------------
/src/PolyfillApp.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | import App from './App';
4 |
5 | const polyfillPromises: Promise[] = [];
6 |
7 | if (!('IntersectionObserver' in window)) {
8 | // @ts-ignore
9 | polyfillPromises.push(import('intersection-observer'));
10 | }
11 |
12 | const PolyfillApp: React.FC = () => {
13 | const [canRender, setCanRender] = useState(Boolean(!polyfillPromises.length));
14 |
15 | useEffect(() => {
16 | if (polyfillPromises.length) {
17 | Promise.all(polyfillPromises).then(() => setCanRender(true));
18 | }
19 | }, []);
20 |
21 | return <>{canRender && }>;
22 | };
23 |
24 | export default PolyfillApp;
25 |
--------------------------------------------------------------------------------
/src/components/EventsList/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 |
3 | import { DEFAULT_ITEMS_GAP } from '../constants';
4 |
5 | import styles from './styles.module.css';
6 |
7 | type Props = {
8 | count: number;
9 | card: ComponentType;
10 | itemsGap?: number;
11 | };
12 |
13 | const EventsListSkeleton: React.FC = ({ itemsGap = DEFAULT_ITEMS_GAP, count, card: Card }) => (
14 |
15 | {Array(count)
16 | .fill(0)
17 | .map((_, index) => (
18 |
19 | ))}
20 |
21 | );
22 |
23 | export default EventsListSkeleton;
24 |
--------------------------------------------------------------------------------
/src/screens/SearchScreen/PopularResult/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Text } from '../../../../components/Skeletons';
4 |
5 | import styles from './styles.module.css';
6 |
7 | type Props = {
8 | showTitle?: boolean;
9 | };
10 |
11 | const PopularResultSkeleton: React.FC = ({ showTitle }) => (
12 | <>
13 | {showTitle && }
14 |
15 |
16 | {Array(15)
17 | .fill(0)
18 | .map((_, index) => (
19 |
20 | ))}
21 |
22 | >
23 | );
24 |
25 | export default PopularResultSkeleton;
26 |
--------------------------------------------------------------------------------
/src/components/DateFilter/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | type Props = {
6 | onClick: () => void;
7 | isActive?: boolean;
8 | tip?: boolean;
9 | };
10 |
11 | const DateFilterButton: React.FC = props => {
12 | const classNames = [styles.button];
13 |
14 | if (props.isActive) {
15 | classNames.push(styles.active);
16 | }
17 |
18 | if (props.tip) {
19 | classNames.push(styles.tip);
20 | }
21 |
22 | return (
23 |
24 | {props.children}
25 |
26 | );
27 | };
28 |
29 | export default DateFilterButton;
30 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/CheckoutModal/components/CheckoutTextInput/styles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: block;
3 | }
4 |
5 | .label {
6 | font-size: 13px;
7 | line-height: 16px;
8 |
9 | color: rgba(0, 0, 0, .4);
10 | }
11 |
12 | .input {
13 | display: block;
14 |
15 | width: 100%;
16 | height: 34px;
17 | margin: -3px 0 0;
18 |
19 | font-size: 16px;
20 | font-weight: normal;
21 | font-style: normal;
22 | line-height: 34px;
23 |
24 | color: rgba(0, 0, 0, .8);
25 | border: none;
26 | border-bottom: 1px solid rgb(207, 207, 207);
27 | background: none;
28 | }
29 |
30 | .input:-webkit-autofill {
31 | box-shadow: 0 0 0 1000px #fff inset;
32 | }
33 |
--------------------------------------------------------------------------------
/src/hooks/useThrottleLoading.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export function useThrottleLoading(isLoading: boolean, time: number) {
4 | const [isThrottleLoading, setThrottleLoading] = useState(false);
5 |
6 | // Задерживаем отрисовку на некоторое время после начала загрузки данных.
7 | // Тем самым исключим моргание между заглушкой и рендером основного компонента
8 | useEffect(() => {
9 | if (isLoading) {
10 | const newTimerId = window.setTimeout(() => setThrottleLoading(true), time);
11 |
12 | return () => clearTimeout(newTimerId);
13 | }
14 |
15 | setThrottleLoading(false);
16 | }, [isLoading, time]);
17 |
18 | return isThrottleLoading;
19 | }
20 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { HashRouter } from 'react-router-dom';
5 | import { PersistGate } from 'redux-persist/integration/react';
6 |
7 | import { initMetrika } from './lib/metrika';
8 | import store, { persistor } from './redux';
9 | import App from './PolyfillApp';
10 |
11 | import './index.css';
12 |
13 | initMetrika();
14 |
15 | ReactDOM.render(
16 |
17 |
18 |
19 |
20 |
21 |
22 | ,
23 | document.getElementById('root')
24 | );
25 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/styles.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | z-index: 2;
4 |
5 | margin-top: -16px;
6 | padding: 4px 12px 0;
7 |
8 | border-radius: 16px 16px 0 0;
9 | background-color: #fff;
10 | }
11 |
12 | .city {
13 | margin-top: 12px;
14 | padding-bottom: 4px;
15 |
16 | font-size: 12px;
17 | line-height: 16px;
18 |
19 | color: rgba(0, 0, 0, .5);
20 | }
21 |
22 | .schedule {
23 | padding-bottom: 16px;
24 |
25 | font-size: 16px;
26 | font-weight: bold;
27 | line-height: 22px;
28 | }
29 |
30 | .description {
31 | font-size: 14px;
32 | line-height: 20px;
33 | white-space: pre-wrap;
34 | }
35 |
36 | .placeholder {
37 | height: 80px;
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/TicketPrice/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Ticket } from '../../lib/api/fragments/ticket';
4 | import { getCurrencySymbol } from '../../lib/price';
5 |
6 | type Props = {
7 | ticket: Ticket;
8 | exact?: boolean;
9 | };
10 |
11 | const space = '\u00A0';
12 |
13 | const TicketPrice: React.FC = ({ ticket, exact }) => {
14 | if (!ticket.price || !ticket.price.min) {
15 | return null;
16 | }
17 |
18 | const text = [
19 | exact ? '' : `от${space}`,
20 | (ticket.price.min || 0) / 100,
21 | space,
22 | getCurrencySymbol(ticket.price.currency),
23 | ].filter(Boolean).join(space);
24 |
25 | return <>{text}>;
26 | };
27 |
28 | export default TicketPrice;
29 |
--------------------------------------------------------------------------------
/src/lib/lazy.ts:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react';
2 |
3 | type LazyDeferred = typeof lazy;
4 |
5 | // Минимальное время перед импортом, чтобы дать отрисоваться скелетону
6 | const LAZY_DEFFERED_SLEEP = 50;
7 | // Минимальное время импорта компонента
8 | const LAZY_DEFFERED_DELAY = 500;
9 |
10 | export const lazyDeferred: LazyDeferred = importCallback => {
11 | type LazyImportType = ReturnType;
12 |
13 | return lazy(() =>
14 | Promise.all([
15 | new Promise(resolve => setTimeout(() => resolve(importCallback()), LAZY_DEFFERED_SLEEP)),
16 | new Promise(resolve => setTimeout(resolve, LAZY_DEFFERED_DELAY)),
17 | ]).then(([module]) => module)
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/RecommendedEvents/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 |
3 | import { lazyDeferred } from '../../../../lib/lazy';
4 |
5 | import LazyBlock from '../../../../components/LazyBlock';
6 | import RecommendedEventsSkeleton from './Skeleton';
7 |
8 | const Component = lazyDeferred(() =>
9 | import(
10 | /* webpackChunkName: "recommended-events" */
11 | './Component'
12 | )
13 | );
14 |
15 | const RecommendedEventsLazyBlock: React.FC = () => {
16 | return (
17 | }>
18 | } />
19 |
20 | );
21 | };
22 |
23 | export default memo(RecommendedEventsLazyBlock);
24 |
--------------------------------------------------------------------------------
/src/screens/SelectionScreen/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Text } from '../../../components/Skeletons';
4 | import DateFilterSkeleton from '../../../components/DateFilter/Skeleton';
5 | import EventsListSkeleton from '../../../components/EventsList/Skeleton';
6 | import EventCardSkeleton from '../../../components/EventCard/Skeleton';
7 |
8 | import styles from './styles.module.css';
9 |
10 | const SelectionScreenSkeleton: React.FC = () => (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export default SelectionScreenSkeleton;
21 |
--------------------------------------------------------------------------------
/src/lib/js-api/utils.ts:
--------------------------------------------------------------------------------
1 | import { AppError, AppErrorCode } from '../error';
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
4 | type YandexJsApiObject = Record | undefined;
5 |
6 | export function callJsApi>({
7 | name,
8 | args,
9 | scope,
10 | method,
11 | }: {
12 | name: string;
13 | args: Parameters[K]>>;
14 | scope: T;
15 | method: K,
16 | }): ReturnType[K]>> {
17 | const func = scope ? scope![method] : null;
18 |
19 | if (!func) {
20 | throw new AppError(AppErrorCode.JsApiMethodNotAvailable, `${name} is not available in this browser version.`);
21 | }
22 |
23 | return func.apply(scope, args);
24 | }
25 |
--------------------------------------------------------------------------------
/src/redux/slices/date-filter.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 |
3 | import { dateToString } from '../../lib/date';
4 |
5 | export type DateFilter = {
6 | date: string;
7 | period: number;
8 | };
9 |
10 | const initialState: DateFilter = {
11 | date: dateToString(new Date()).toString(),
12 | period: 1,
13 | };
14 |
15 | const dateFilter = createSlice({
16 | name: 'date-filter',
17 | initialState,
18 | reducers: {
19 | setDate(state, action: PayloadAction<{ date: string; period?: number }>) {
20 | state.date = action.payload.date;
21 | state.period = action.payload.period || 1;
22 | },
23 | },
24 | });
25 |
26 | export const { setDate } = dateFilter.actions;
27 |
28 | export default dateFilter.reducer;
29 |
--------------------------------------------------------------------------------
/src/screens/SearchScreen/SearchResult/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Text } from '../../../../components/Skeletons';
4 | import EventsListSkeleton from '../../../../components/EventsList/Skeleton';
5 | import EventCardSkeleton from '../../../../components/EventCard/Skeleton';
6 |
7 | import styles from './styles.module.css';
8 |
9 | const SearchResultSkeleton: React.FC = () => (
10 | <>
11 | {Array(3)
12 | .fill(0)
13 | .map((_, index) => (
14 |
15 |
16 |
17 |
18 |
19 | ))}
20 | >
21 | );
22 |
23 | export default SearchResultSkeleton;
24 |
--------------------------------------------------------------------------------
/src/lib/url-builder.ts:
--------------------------------------------------------------------------------
1 | type QueryParams = Record;
2 |
3 | export function getMainPageUrl() {
4 | return '/';
5 | }
6 |
7 | export function getEventUrl(id: string) {
8 | return `/event/${id}`;
9 | }
10 |
11 | export function getEventGalleryUrl(id: string, query: QueryParams) {
12 | const params = query ? `?${new URLSearchParams(query)}` : '';
13 |
14 | return `/event/${id}/gallery${params}`;
15 | }
16 |
17 | export function getRubricUrl(code: string) {
18 | return `/events/${encodeURIComponent(code)}`;
19 | }
20 |
21 | export function getSelectionUrl(code: string) {
22 | return `/selection/${encodeURIComponent(code)}`;
23 | }
24 |
25 | export function getSearchUrl() {
26 | return '/search';
27 | }
28 |
29 | export function getOrdersUrl() {
30 | return '/orders';
31 | }
32 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/CheckoutModal/components/CheckoutEventInfo/styles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 |
5 | font-size: 16px;
6 | line-height: 20px;
7 | }
8 |
9 | .title {
10 | flex: 1;
11 |
12 | margin: 0 12px;
13 |
14 | color: rgba(0, 0, 0, .8);
15 | }
16 |
17 | .price {
18 | flex: none;
19 |
20 | font-size: 16px;
21 | line-height: 20px;
22 |
23 | color: rgba(0, 0, 0, .4);
24 | }
25 |
26 | .image,
27 | .preview {
28 | display: block;
29 | flex: none;
30 |
31 | width: 48px;
32 | height: 48px;
33 |
34 | border-radius: 6px;
35 | }
36 |
37 | .image {
38 | overflow: hidden;
39 |
40 | border-radius: 6px;
41 | object-fit: cover;
42 | }
43 |
44 | .preview {
45 | background: #eaeaea;
46 | }
47 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/event-preview.ts:
--------------------------------------------------------------------------------
1 | import { Ticket } from './ticket';
2 | import { Tag } from './tag-preview';
3 | import { MediaImageSize } from './image-size';
4 |
5 | export type EventPreview = {
6 | id: string;
7 | title: string;
8 | argument: string | null;
9 | type: Tag;
10 | tags: Tag[];
11 | contentRating: string | null;
12 | tickets: Ticket[] | null;
13 | userRating: {
14 | overall: {
15 | count: number;
16 | value: number;
17 | };
18 | } | null;
19 | image: {
20 | bgColor: string | null;
21 | eventListCard: MediaImageSize;
22 | eventListCard2x: MediaImageSize;
23 | actualListCard: MediaImageSize;
24 | actualListCard2x: MediaImageSize;
25 | selectionCard: MediaImageSize;
26 | } | null;
27 | };
28 |
--------------------------------------------------------------------------------
/src/lib/metrika/install.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | (function() {
3 | 'use strict';
4 |
5 | var isBeaconAvailable = Boolean(
6 | window.yandex &&
7 | window.yandex.navigator &&
8 | window.yandex.navigator.sendPersistentBeacon
9 | );
10 | var scriptName = isBeaconAvailable ? 'tag_turboapp.js' : 'tag.js';
11 |
12 | window.ym = function() {
13 | (window.ym.a = window.ym.a || []).push(arguments);
14 | }
15 | window.ym.l = Number(new Date());
16 |
17 | var metrikaScript = document.createElement('script');
18 | metrikaScript.src = 'https://mc.yandex.ru/metrika/' + scriptName;
19 | metrikaScript.async = true;
20 |
21 | var firstScript = document.getElementsByTagName('script')[0];
22 | firstScript.parentNode.insertBefore(metrikaScript, firstScript);
23 | })();
24 |
--------------------------------------------------------------------------------
/src/components/LoginButton/avatar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/AuthBox/avatar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/LazyBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, Suspense, ReactElement, ReactNode } from 'react';
2 |
3 | import LazyRender from '../LazyRender';
4 |
5 | type Props = {
6 | children: ReactNode;
7 | fetchData?: () => void;
8 | skeleton?: ReactElement;
9 | };
10 |
11 | const LazyBlock: React.FC = ({ children, skeleton, fetchData }) => {
12 | return (
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | const Inner: React.FC = ({ children, skeleton, fetchData }) => {
20 | useEffect(() => {
21 | fetchData && fetchData();
22 | }, [fetchData]);
23 |
24 | return {children};
25 | };
26 |
27 | export default LazyBlock;
28 |
--------------------------------------------------------------------------------
/src/hooks/useVisibleOnce.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect, useState } from 'react';
2 |
3 | export function useVisibleOnce(ref: RefObject, options?: IntersectionObserverInit) {
4 | // Пока не навешан обработчик на элемент, мы точно не можем сказать видим он или нет,
5 | // поэтому начальное значение undefined
6 | const [isVisible, setVisible] = useState();
7 |
8 | useEffect(() => {
9 | if (!ref.current || isVisible) {
10 | return;
11 | }
12 |
13 | const observer = new IntersectionObserver(([entry]) => {
14 | setVisible(entry.isIntersecting);
15 | }, options);
16 |
17 | observer.observe(ref.current);
18 |
19 | return () => {
20 | observer.disconnect();
21 | };
22 | }, [ref, options, isVisible]);
23 |
24 | return isVisible;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/LazyRender/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, ReactElement, useState, useEffect } from 'react';
2 |
3 | import { useVisibleOnce } from '../../hooks/useVisibleOnce';
4 |
5 | type Props = {
6 | children: ReactElement;
7 | skeleton?: ReactElement;
8 | intersectionOptions?: IntersectionObserverInit;
9 | };
10 |
11 | const LazyRender: React.FC = ({ children, skeleton, intersectionOptions }) => {
12 | const ref = useRef(null);
13 | const canRender = useVisibleOnce(ref, intersectionOptions);
14 | const [needSkeleton, showSkeleton] = useState(false);
15 |
16 | useEffect(() => {
17 | if (!canRender) {
18 | showSkeleton(true);
19 | }
20 | }, [canRender]);
21 |
22 | return {canRender ? children : needSkeleton && skeleton}
;
23 | };
24 |
25 | export default LazyRender;
26 |
--------------------------------------------------------------------------------
/src/screens/OrdersScreen/Order/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Rect, Text } from '../../../../components/Skeletons';
4 |
5 | import styles from './styles.module.css';
6 |
7 | const OrderSkeleton: React.FC = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default OrderSkeleton;
25 |
--------------------------------------------------------------------------------
/src/components/Skeletons/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | type RectProps = {
6 | className?: string;
7 | width?: number | string;
8 | height?: number | string;
9 | borderRadius?: number;
10 | };
11 |
12 | export const Rect: React.FC = ({ className, width, height }) => {
13 | return ;
14 | };
15 |
16 | type TextProps = {
17 | className?: string;
18 | width?: number | string;
19 | size?: number;
20 | };
21 |
22 | export const Text: React.FC = ({ className, width, size }) => {
23 | return ;
24 | };
25 |
26 | function mix(...classNames: Array) {
27 | return classNames.filter(Boolean).join(' ');
28 | }
29 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/EventsFeed/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Text } from '../../../../../components/Skeletons';
4 | import { EventCardVerticalSkeleton } from '../../../../../components/EventCardVertical/Skeleton';
5 |
6 | import styles from './styles.module.css';
7 |
8 | export const EventsFeedSkeleton: React.FC = () => (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/src/components/BackwardButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from '../ClearButton';
4 | import { useBackward } from '../StackNavigator/hooks';
5 |
6 | import styles from './styles.module.css';
7 |
8 | type Props = {
9 | fill?: 'white' | 'black';
10 | className?: string;
11 | };
12 |
13 | const BackwardIcon: React.FC = ({ fill = 'white' }) => {
14 | const classNames = [styles['backward-icon'], styles[`icon-${fill}`]].join(' ');
15 |
16 | return ;
17 | };
18 |
19 | const Backward: React.FC = props => {
20 | const onClick = useBackward();
21 |
22 | return (
23 |
26 | );
27 | };
28 |
29 | export default Backward;
30 |
--------------------------------------------------------------------------------
/src/components/ActionButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react';
2 |
3 | import styles from './styles.module.css';
4 |
5 | export type Props = {
6 | type?: ButtonHTMLAttributes['type'];
7 | className?: string;
8 | disabled?: boolean;
9 | formInvalid?: boolean;
10 | onClick?: () => void;
11 | children: ReactNode;
12 | };
13 | const ActionButton = forwardRef(
14 | ({ type, className, disabled, formInvalid, children, onClick }, ref) => {
15 | const cls = [styles.action, className, formInvalid && styles.invalid].filter(Boolean).join(' ');
16 | return (
17 |
20 | );
21 | }
22 | );
23 |
24 | export default ActionButton;
25 |
--------------------------------------------------------------------------------
/src/components/EventsSlider/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ActualEvent } from '../../lib/api/fragments/actual-event';
4 |
5 | import styles from './style.module.css';
6 |
7 | export type Props = {
8 | items: Array;
9 | component: React.FC<{
10 | event: ActualEvent;
11 | onClick?: () => void;
12 | className?: string;
13 | }>;
14 | };
15 |
16 | const EventsSlider: React.FC = ({ items, component: Card }) => {
17 | return (
18 |
19 | {items.map(actualEvent => {
20 | if (!actualEvent) {
21 | return null;
22 | }
23 |
24 | return ;
25 | })}
26 |
27 | );
28 | };
29 |
30 | export default EventsSlider;
31 |
--------------------------------------------------------------------------------
/src/hooks/useWatchAuth.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 |
4 | import { checkUser } from '../redux/slices/user';
5 |
6 | export function useWatchAuth() {
7 | const dispatch = useDispatch();
8 |
9 | // Проверяем авторизацию на старте приложения
10 | useEffect(() => {
11 | dispatch(checkUser());
12 | }, [dispatch]);
13 |
14 | // При переключении владок браузера пользователь мог сменить логин, поэтому проверяем авторизацию
15 | useEffect(() => {
16 | const onVisibilityChange = () => {
17 | if (document.visibilityState === 'visible') {
18 | dispatch(checkUser());
19 | }
20 | };
21 |
22 | document.addEventListener('visibilitychange', onVisibilityChange);
23 |
24 | return () => document.removeEventListener('visibilitychange', onVisibilityChange);
25 | }, [dispatch]);
26 | }
27 |
--------------------------------------------------------------------------------
/src/hooks/usePaginatedList.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo, useState } from 'react';
2 |
3 | type PaginatedListOptions = {
4 | items: T[];
5 | hasMore: boolean;
6 | loadMore: () => void;
7 | pageSize: number;
8 | };
9 |
10 | export function usePaginatedList(options: PaginatedListOptions): [T[], boolean, () => void] {
11 | const { items, hasMore, loadMore, pageSize } = options;
12 |
13 | const [limit, setLimit] = useState(pageSize);
14 | const events = useMemo(() => items.slice(0, limit), [items, limit]);
15 |
16 | const wrappedLoadMore = useCallback(() => {
17 | if (items.length > limit) {
18 | setLimit(limit + pageSize);
19 | } else {
20 | loadMore();
21 | }
22 | }, [loadMore, limit, pageSize, items]);
23 |
24 | const wrappedHasMore = hasMore || limit < items.length;
25 |
26 | return [events, wrappedHasMore, wrappedLoadMore];
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/MenuGeoLabel/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { useSelector } from 'react-redux';
3 |
4 | import { RootState } from '../../redux';
5 |
6 | import styles from './styles.module.css';
7 |
8 | const MenuGeoLabel: React.FC<{
9 | setCityModalVisibleCallback: (visible: boolean) => void;
10 | }> = ({ setCityModalVisibleCallback }) => {
11 | const currentCity = useSelector((state: RootState) => state.city.currentCity);
12 | const onCitySelectLabelClickHandle = useCallback(() => {
13 | setCityModalVisibleCallback(true);
14 | }, [setCityModalVisibleCallback]);
15 |
16 | return (
17 |
18 |
19 | {currentCity ? currentCity.name : 'Выбрать город'}
20 |
21 |
22 | );
23 | };
24 |
25 | export default MenuGeoLabel;
26 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/event.ts:
--------------------------------------------------------------------------------
1 | import { EventScheduleInfo } from './event-schedule-info';
2 | import { GalleryImage } from './gallery-image';
3 | import { TouchPrimaryImage } from './event-main-image';
4 | import { Ticket } from './ticket';
5 |
6 | export type Event = {
7 | id: string;
8 | title: string;
9 | argument: string | null;
10 | contentRating: string | null;
11 | description: string | null;
12 | tags: {
13 | name: string;
14 | code: string;
15 | }[];
16 | image: TouchPrimaryImage | null;
17 | images: Array | null;
18 | type: {
19 | name: string;
20 | };
21 | userRating: {
22 | overall: {
23 | count: number;
24 | value: number;
25 | };
26 | } | null;
27 | tickets: Array | null;
28 | };
29 |
30 | export type EventData = {
31 | event: Event;
32 | scheduleInfo: EventScheduleInfo;
33 | };
34 |
--------------------------------------------------------------------------------
/src/hooks/useInfiniteScroll.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect, useMemo } from 'react';
2 |
3 | import { useVisible } from './useVisible';
4 |
5 | type InfiniteScrollOptions = {
6 | ref: RefObject;
7 | isLoading: boolean;
8 | hasMore: boolean;
9 | loadMore: () => void;
10 | threshold?: number;
11 | };
12 |
13 | export function useInfiniteScroll({ threshold, ref, isLoading, loadMore, hasMore }: InfiniteScrollOptions) {
14 | const options = useMemo(() => {
15 | return {
16 | rootMargin: `0px 0px ${threshold || 0}px 0px`,
17 | };
18 | }, [threshold]);
19 |
20 | const needLoadMore = useVisible(ref, options) || false;
21 |
22 | useEffect(() => {
23 | if (!hasMore) return;
24 | if (isLoading) return;
25 |
26 | if (needLoadMore) {
27 | loadMore();
28 | }
29 | }, [needLoadMore, loadMore, hasMore, isLoading]);
30 | }
31 |
--------------------------------------------------------------------------------
/src/screens/MainScreen/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Text } from '../../../components/Skeletons';
4 | import DateFilterSkeleton from '../../../components/DateFilter/Skeleton';
5 | import EventsListSkeleton from '../../../components/EventsList/Skeleton';
6 | import EventCardMainSkeleton from '../../../components/EventCardMain/Skeleton';
7 | import SelectionListSkeleton from '../../../components/SelectionList/Skeleton';
8 |
9 | import styles from './styles.module.css';
10 |
11 | const MainScreenSkeleton: React.FC = () => (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
23 | export default MainScreenSkeleton;
24 |
--------------------------------------------------------------------------------
/src/components/StackNavigator/Screen/styles.module.css:
--------------------------------------------------------------------------------
1 | .screen {
2 | position: fixed;
3 | z-index: 1;
4 | top: 0;
5 | right: 0;
6 | bottom: 0;
7 | left: 0;
8 |
9 | overflow: scroll;
10 | -webkit-overflow-scrolling: touch;
11 |
12 | background: #fff;
13 |
14 | transition-timing-function: cubic-bezier(.25, .1, .25, 1);
15 | transition-duration: 300ms;
16 | transition-property: transform, filter;
17 | }
18 |
19 | .hidden {
20 | display: none;
21 | }
22 |
23 | .screenEnter {
24 | transform: translateX(100vw);
25 | }
26 |
27 | .screenEnterActive {
28 | pointer-events: none;
29 |
30 | transform: translateX(0);
31 | }
32 |
33 | .screenExit {
34 | transform: translateX(0);
35 | }
36 |
37 | .screenExitActive {
38 | pointer-events: none;
39 |
40 | transition-duration: 250ms;
41 | transform: translateX(100vw);
42 | }
43 |
44 | .offset {
45 | pointer-events: none;
46 |
47 | filter: brightness(.5);
48 |
49 | transform: translateX(-25vw);
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Gallery/styles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | scroll-snap-type: x mandatory;
4 |
5 | overflow-x: scroll;
6 | overscroll-behavior-x: contain;
7 | -webkit-overflow-scrolling: touch;
8 | scrollbar-width: none;
9 | }
10 |
11 | .container::-webkit-scrollbar {
12 | display: none;
13 | }
14 |
15 | .thumbnail {
16 | flex: none;
17 |
18 | width: 208px;
19 | margin-left: 12px;
20 |
21 | scroll-snap-align: center;
22 | }
23 |
24 | .thumbnail:first-child {
25 | margin-left: 0;
26 | }
27 |
28 | .thumbnail:last-child {
29 | padding-right: 17px;
30 | }
31 |
32 | .image {
33 | position: relative;
34 |
35 | overflow: hidden;
36 |
37 | width: 208px;
38 | height: 144px;
39 |
40 | border-radius: 12px;
41 | }
42 |
43 | .image::after {
44 | position: absolute;
45 | top: 0;
46 | right: 0;
47 | bottom: 0;
48 | left: 0;
49 |
50 | display: block;
51 |
52 | content: '';
53 |
54 | background-color: rgba(0, 0, 0, .1);
55 | }
56 |
--------------------------------------------------------------------------------
/src/lib/api/fragments/suggest.ts:
--------------------------------------------------------------------------------
1 | import { MediaImageSize } from './image-size';
2 | import { Currency } from './ticket';
3 |
4 | export type Document = {
5 | object: {
6 | objectType: 'SearchObjectEvent';
7 | id: string;
8 | title: string;
9 | argument: string | null;
10 | minPrice: {
11 | currency: Currency;
12 | value: number;
13 | } | null;
14 | type: {
15 | code: string;
16 | name: string;
17 | };
18 | image: {
19 | bgColor: string | null;
20 | eventListCard: MediaImageSize;
21 | eventListCard2x: MediaImageSize;
22 | actualListCard: MediaImageSize;
23 | actualListCard2x: MediaImageSize;
24 | selectionCard: MediaImageSize;
25 | } | null;
26 | };
27 | };
28 |
29 | export type Groups = {
30 | code: string;
31 | title: string | null;
32 | documents: Document[];
33 | };
34 |
35 | export type Suggest = {
36 | groups: Groups[];
37 | };
38 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, Suspense } from 'react';
2 | import { useDispatch } from 'react-redux';
3 |
4 | import { lazyDeferred } from '../../lib/lazy';
5 | import { loadEvent } from '../../redux/slices/event';
6 | import { loadRecommendedEvents } from '../../redux/slices/recommended-events';
7 |
8 | import EventSkeleton from './Skeleton';
9 |
10 | const Component = lazyDeferred(() =>
11 | import(
12 | /* webpackChunkName: "event-screen" */
13 | './Component'
14 | )
15 | );
16 |
17 | const EventScreen: React.FC<{ id: string }> = ({ id }) => {
18 | const dispatch = useDispatch();
19 |
20 | useEffect(() => {
21 | dispatch(loadEvent(id));
22 | dispatch(loadRecommendedEvents());
23 | }, [dispatch, id]);
24 |
25 | return (
26 | <>
27 | }>
28 | } />
29 |
30 | >
31 | );
32 | };
33 |
34 | export default EventScreen;
35 |
--------------------------------------------------------------------------------
/src/screens/EventScreen/components/VenueInfo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import YaMapWidget from '../../../../components/YaMapWidget';
4 | import { ScheduleInfo } from '../../../../redux/slices/event';
5 |
6 | import styles from './styles.module.css';
7 |
8 | type Props = {
9 | schedule: Partial;
10 | };
11 |
12 | const VenueInfo: React.FC = ({ schedule }) => {
13 | const { oneOfPlaces, placePreview } = schedule;
14 |
15 | if (!oneOfPlaces || !oneOfPlaces.coordinates) {
16 | return {placePreview}
;
17 | }
18 |
19 | return (
20 | <>
21 |
26 | {oneOfPlaces.title}
27 | {oneOfPlaces.address}
28 | >
29 | );
30 | };
31 |
32 | export default VenueInfo;
33 |
--------------------------------------------------------------------------------
/src/screens/OrdersScreen/Order/styles.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | justify-content: space-between;
4 | align-items: center;
5 |
6 | box-sizing: border-box;
7 | min-height: 92px;
8 | padding: 16px 0;
9 | }
10 |
11 | .logo-container {
12 | display: flex;
13 | flex: 0 0 48px;
14 | }
15 |
16 | .logo {
17 | overflow: hidden;
18 |
19 | height: 60px;
20 | margin: auto;
21 |
22 | border-radius: 8px;
23 | }
24 |
25 | .content {
26 | display: flex;
27 | flex-grow: 1;
28 | }
29 |
30 | .info {
31 | flex-grow: 1;
32 |
33 | margin: 0 16px;
34 | }
35 |
36 | .status-container {
37 | flex-shrink: 0;
38 |
39 | text-align: right;
40 | }
41 |
42 | .title,
43 | .cost {
44 | font-size: 16px;
45 | font-weight: 500;
46 | line-height: 20px;
47 | }
48 |
49 | .schedule,
50 | .status {
51 | margin-top: 2px;
52 |
53 | font-size: 14px;
54 | line-height: 18px;
55 |
56 | color: rgba(0, 0, 0, .4);
57 | }
58 |
59 | .info p,
60 | .status-container p {
61 | margin: 0;
62 | }
63 |
--------------------------------------------------------------------------------
/src/redux/slices/menu.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import { persistReducer } from 'redux-persist';
3 |
4 | import { getPersistConfig } from '../helpers/persist';
5 |
6 | import { MenuTag } from '../../lib/api/fragments/city';
7 |
8 | export type Menu = {
9 | visible: boolean;
10 | items: Array;
11 | activeItem: MenuTag['code'];
12 | };
13 |
14 | const initialState: Menu = {
15 | visible: false,
16 | items: [],
17 | activeItem: '',
18 | };
19 |
20 | const menu = createSlice({
21 | name: 'menu',
22 | initialState,
23 | reducers: {
24 | setItems(state, action: PayloadAction) {
25 | state.items = action.payload;
26 | },
27 | setVisible(state, action: PayloadAction) {
28 | state.visible = action.payload;
29 | },
30 | },
31 | });
32 |
33 | export const { setItems, setVisible } = menu.actions;
34 |
35 | const persistConfig = getPersistConfig