Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
msfrms committed Mar 6, 2023
1 parent af440b3 commit a4910a3
Showing 175 changed files with 22,817 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.DS_Store
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM mhart/alpine-node:11 AS builder
WORKDIR /trach
COPY trach .
RUN yarn install
RUN yarn run build

FROM mhart/alpine-node
RUN yarn global add serve
WORKDIR /trach
COPY --from=builder /trach/build build
CMD ["serve", "-s", "build"]
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Mikhail Radaev
Copyright (c) 2020 Mikhail Radaev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# MusicAnalyticsFront
# MusicAnalyticsFront
Source code for [trach.top](http://trach.top/)
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: "3"

services:

trach:
build:
context: .
dockerfile: Dockerfile
container_name: trach
ports:
- "80:5000"
23 changes: 23 additions & 0 deletions trach/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
68 changes: 68 additions & 0 deletions trach/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template.

## Available Scripts

In the project directory, you can run:

### `yarn start`

Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.

The page will reload if you make edits.<br />
You will also see any lint errors in the console.

### `yarn test`

Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.

### `yarn build`

Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.

### `yarn eject`

**Note: this is a one-way operation. Once you `eject`, you can’t go back!**

If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.

Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.

You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.

## Learn More

You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).

To learn React, check out the [React documentation](https://reactjs.org/).

### Code Splitting

This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting

### Analyzing the Bundle Size

This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size

### Making a Progressive Web App

This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app

### Advanced Configuration

This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration

### Deployment

This section has moved here: https://facebook.github.io/create-react-app/docs/deployment

### `yarn build` fails to minify

This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
70 changes: 70 additions & 0 deletions trach/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "trach",
"version": "0.1.0",
"private": true,
"dependencies": {
"@date-io/date-fns": "1.x",
"@material-ui/core": "4.11.3",
"@material-ui/lab": "4.0.0-alpha.48",
"@material-ui/pickers": "3.2.10",
"@reduxjs/toolkit": "^1.1.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/bootstrap": "^4.3.2",
"@types/file-saver": "^2.0.1",
"@types/react-redux": "^7.1.7",
"@types/react-router-dom": "^5.1.5",
"@types/react-router-redux": "^5.0.18",
"@types/redux-persist": "^4.3.1",
"@types/redux-thunk": "^2.1.0",
"@types/styled-components": "^5.0.1",
"accounting": "^0.4.1",
"bootstrap": "^4.4.1",
"chart.js": "^2.9.3",
"cross-fetch": "^3.0.4",
"date-fns": "^2.11.1",
"dateformat": "^3.0.3",
"file-saver": "^2.0.2",
"firebase": "^8.6.8",
"jquery": "^3.4.1",
"lodash.throttle": "^4.1.1",
"moment": "^2.24.0",
"popper.js": "^1.16.1",
"react": "^16.13.1",
"react-bootstrap": "^1.0.0",
"react-content-loader": "^5.0.4",
"react-dom": "^16.13.1",
"react-media": "^1.10.0",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"react-router-redux": "^4.0.8",
"react-scripts": "3.4.1",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"styled-components": "^5.0.1",
"throttle-typescript": "^1.0.1",
"typescript": "^3.8.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file added trach/public/favicon.ico
Binary file not shown.
91 changes: 91 additions & 0 deletions trach/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<style>
input:focus, textarea:focus, select:focus, button:focus{
outline: none;
}
</style>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>TRACH</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->

<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/8.6.8/firebase-app.js"></script>

<!-- TODO: Add SDKs for Firebase products that you want to use
https://firebase.google.com/docs/web/setup#available-libraries -->
<script src="https://www.gstatic.com/firebasejs/8.6.8/firebase-analytics.js"></script>

<script>
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
var firebaseConfig = {
apiKey: "AIzaSyAVQ7XPIHI3b_q-95rWR1eVYjVnguZt9UE",
authDomain: "trach-adb98.firebaseapp.com",
projectId: "trach-adb98",
storageBucket: "trach-adb98.appspot.com",
messagingSenderId: "212941373758",
appId: "1:212941373758:web:6b0041ea189b3745316c6f",
measurementId: "G-2FBJH4GZMV"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.analytics();
</script>

<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js"
integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ"
crossorigin="anonymous"></script>

<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"
integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"
crossorigin="anonymous"></script>
</body>
</html>
Binary file added trach/public/logo192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added trach/public/logo512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions trach/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
2 changes: 2 additions & 0 deletions trach/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
19 changes: 19 additions & 0 deletions trach/src/actions/ArtistActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Artist } from "./chart/TrackActions"

export enum ArtistActionTypes {
DidLoadArtists = "ArtistActionTypes.DidLoadArtists"
}

export type DidLoadArtistsAction = {
type: ArtistActionTypes.DidLoadArtists
trackId: number
artists: Artist[]
}

export function didLoadArtists(artists: Artist[], byTrackId: number): DidLoadArtistsAction {
return {
type: ArtistActionTypes.DidLoadArtists,
trackId: byTrackId,
artists: artists
}
}
21 changes: 21 additions & 0 deletions trach/src/actions/LabelActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

export enum LabelActionTypes {
DidLoadLabels = "LabelActionTypes.DidLoadLabels"
}

export type Labels = {
trackId: number
labels: string[]
}

export type DidLoadLabelAction = {
type: LabelActionTypes.DidLoadLabels
labels: Labels
}

export function didLoadLabels(labels: Labels): DidLoadLabelAction {
return {
type: LabelActionTypes.DidLoadLabels,
labels: labels
}
}
126 changes: 126 additions & 0 deletions trach/src/actions/ReportActionCreators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Constants } from '../core/Constants'

import fetch from 'cross-fetch'

import FileSaver from 'file-saver'
import { AppState } from '../reducers/mainReducer'
import store from '../app/store'
import { PlaylistType } from '../core/MusicSection'
import { MusicCategory } from '../core/MusicCategory'
import * as firebase from '../util/firebase'

export function saveReportByTrackOrAlbumToCsvFile(id: number) {
const state: AppState = store.getState()
const url = (() => {
switch (state.selectFilter.playlistType) {
case PlaylistType.Chart:
switch (state.selectFilter.category) {
case MusicCategory.Track:
return Constants.api.track(id).charts.report
case MusicCategory.Album:
return Constants.api.album(id).charts.report
default:
return ""
}
case PlaylistType.New:
switch (state.selectFilter.category) {
case MusicCategory.Track:
return Constants.api.track(id).news.report
case MusicCategory.Album:
return Constants.api.album(id).news.report
default:
return ""
}
}
})()
const type: string = (() => {
switch (state.selectFilter.playlistType) {
case PlaylistType.Chart:
return "в_чартах"
case PlaylistType.New:
return "в_новинках"
}
})()
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'text/plain;charset=utf-8',
},
})
.then(
response => response.text(),
_ => undefined
)
.then(
text => {
const track = state.tracks.byTrackId[id].title
const artists = state.artists.byTrackId[id]
.map(id => state.artists.byArtistId[id].name)
.join(", ")
if (text !== undefined) {
var blob = new Blob([text], { type: "text/plain;charset=utf-8" })
FileSaver.saveAs(blob, `${artists}-${track}_${type}_stat.csv`)

const state: AppState = store.getState()

if (state.auth.authorized.email !== null) {
firebase.analytics.setUserId(state.auth.authorized.email)
firebase.analytics.setUserProperties({
"email": state.auth.authorized.email
})
firebase.analytics.logEvent("click_download_report")
}
}
}
)
}

export function saveReportByArtistAudienceToCsvFile(id: number) {
const state: AppState = store.getState()
const url = Constants.api.artist(id).report
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'text/plain;charset=utf-8',
},
})
.then(
response => response.text(),
_ => undefined
)
.then(
text => {
const artist = state.artists.byArtistId[id].name
if (text !== undefined) {
var blob = new Blob([text], { type: "text/plain;charset=utf-8" })
FileSaver.saveAs(blob, `${artist}_audience.csv`)
}
}
)
}

export function saveReportByExistTrackInPlaylistsToCsvFile(trackId: number) {
const state: AppState = store.getState()
const track = state.tracks.byTrackId[trackId].title
const artists = state.artists.byTrackId[trackId]
.map(id => state.artists.byArtistId[id].name)
.join(", ")
fetch(Constants.api.track(trackId).report, {
method: 'POST',
headers: {
'Content-Type': 'text/plain;charset=utf-8',
}
})
.then(
response => response.text(),
error => undefined
)
.then(
text => {
if (text !== undefined) {
var blob = new Blob([text], { type: "text/plain;charset=utf-8" })
FileSaver.saveAs(blob, `${artists}-${track}_stat.csv`)
}
}
)
}
49 changes: 49 additions & 0 deletions trach/src/actions/SearchActionCreator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import store from '../app/store'
import { searchItems, searchEmpty } from './SearchActions'
import { Constants } from '../core/Constants'

import fetch from 'cross-fetch'

export type FoundItemResponse = {
id: number
title: string
subtitle?: string
coverUrl?: string
}

export type SearchResponse = {
tracks: FoundItemResponse[]
artists: FoundItemResponse[]
albums: FoundItemResponse[]
}

export function searchActionCreator(search: string) {

const dispatch = store.dispatch

if (search.length < 2) {
dispatch(searchEmpty(search))
return
}

fetch(Constants.api.search, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
body: JSON.stringify({
query: search
})
})
.then(
response => response.json(),
error => undefined
)
.then(
json => {
const response = json as SearchResponse
if (response !== undefined)
dispatch(searchItems(search, response))
}
)
}
40 changes: 40 additions & 0 deletions trach/src/actions/SearchActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { SearchResponse } from './SearchActionCreator'

export enum SearchActionTypes {
Search = "SearchActionTypes.Search"
}

export type FoundItem = {
id: number
title: string
subtitle?: string
coverUrl?: string
}

export type SearchAction = {
type: SearchActionTypes.Search
search: string
tracks: FoundItem[]
artists: FoundItem[]
albums: FoundItem[]
}

export function searchItems(search: string, response: SearchResponse): SearchAction {
return {
type: SearchActionTypes.Search,
search: search,
tracks: response.tracks,
artists: response.artists,
albums: response.albums
}
}

export function searchEmpty(search: string): SearchAction {
return {
type: SearchActionTypes.Search,
search: search,
tracks: [],
artists: [],
albums: []
}
}
88 changes: 88 additions & 0 deletions trach/src/actions/SelectFilterActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { PlaylistType } from '../core/MusicSection'
import { MusicCategory } from '../core/MusicCategory'
import { PlaylistDateFilterTypes } from './track/PlaylistActions'

export enum SelectFilterActionTypes {
Platform = "SelectFilterActionTypes.Platform",
PlaylistType = "SelectFilterActionTypes.SelectFilterActionTypes.PlaylistType",
MusicCategory = "SelectFilterActionTypes.MusicCategory",
Date = "SelectFilterActionTypes.Date",
PlaylistCategory = "SelectFilterActionTypes.PlaylistCategory",
PlotFilter = "SelectFilterActionTypes.PlotFilter",
}

export type SelectPlatformAction = {
type: SelectFilterActionTypes.Platform
platform: string
}

export type SelectPlaylistTypeAction = {
type: SelectFilterActionTypes.PlaylistType
playlistType: PlaylistType
}

export type SelectMusicCategoryAction = {
type: SelectFilterActionTypes.MusicCategory
category: MusicCategory
}

export type SelectDateAction = {
type: SelectFilterActionTypes.Date
date: string
}

export type SelectPlaylistCategoryAction = {
type: SelectFilterActionTypes.PlaylistCategory
trackId: number
category: PlaylistDateFilterTypes
date?: string
}

export function selectPlaylistTypeAction(playlistType: PlaylistType): SelectPlaylistTypeAction {
return {
type: SelectFilterActionTypes.PlaylistType,
playlistType: playlistType
}
}

export function selectPlaylistCategory(
trackId: number,
date: string | undefined
): SelectPlaylistCategoryAction {
return {
type: SelectFilterActionTypes.PlaylistCategory,
trackId: trackId,
category: date !== undefined ? PlaylistDateFilterTypes.ForDate : PlaylistDateFilterTypes.All,
date: date
}
}

export type SelectFilterPlotAction = {
type: SelectFilterActionTypes.PlotFilter
trackId: number
section: PlaylistType
startDate?: string
endDate?: string
}

export function selectFilterPlot(
trackId: number,
section: PlaylistType,
startDate?: string,
endDate?: string
): SelectFilterPlotAction {
return {
type: SelectFilterActionTypes.PlotFilter,
trackId: trackId,
section: section,
startDate: startDate,
endDate: endDate
}
}

export type SelectFilterAction = SelectDateAction
| SelectMusicCategoryAction
| SelectPlaylistTypeAction
| SelectPlatformAction
| SelectPlaylistCategoryAction
| SelectFilterPlotAction
28 changes: 28 additions & 0 deletions trach/src/actions/album/AlbumInChartsAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AlbumsResponse } from "./AlbumInChartsActionCreators"

export enum AlbumChartsActionTypes {
DidLoadAlbums = "AlbumChartsActionTypes.DidLoadAlbums"
}

export type AlbumChart = {
id: number,
title: string,
coverUrl?: string,
bestPlatform: string,
year?: number,
otherPlatforms: string[]
}

export type AlbumChartsAction = {
type: AlbumChartsActionTypes.DidLoadAlbums
artistId: number,
albums: AlbumChart[]
}

export function didLoadAlbumsChartAction(artistId: number, response: AlbumsResponse[]): AlbumChartsAction {
return {
type: AlbumChartsActionTypes.DidLoadAlbums,
artistId: artistId,
albums: response
}
}
37 changes: 37 additions & 0 deletions trach/src/actions/album/AlbumInChartsActionCreators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import fetch from 'cross-fetch'
import store from '../../app/store'
import { Constants } from '../../core/Constants'
import * as actions from './AlbumInChartsAction'

export type AlbumsResponse = {
id: number,
title: string,
coverUrl?: string,
bestPlatform: string,
year?: number,
otherPlatforms: string[]
}

export function loadAlbumInChartsActionCreator(artistId: number) {
const dispatch = store.dispatch

fetch(Constants.api.artist(artistId).albums.chart, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
}
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as AlbumsResponse[]

if (response !== undefined)
dispatch(actions.didLoadAlbumsChartAction(artistId, response))
},
_ => {}
)
}
49 changes: 49 additions & 0 deletions trach/src/actions/album/AlbumInfoActionCreators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import store from '../../app/store'
import fetch from 'cross-fetch'
import { Constants } from '../../core/Constants'
import * as actions from '../track/TrackInfoActions'
import { ArtistResponse, PlayUrlResponse } from '../track/TrackActionCreators'

export type AlbumInfoResponse = {
id: number
title: string
coverUrl?: string
artists: ArtistResponse[]
releaseDate?: string
genre?: string
label?: string
links: PlayUrlResponse[]
}

export const loadAlbumInfoActionCreator = (albumId: number) => {
const dispatch = store.dispatch

dispatch(actions.inProgressTrackInfoAction(albumId))

fetch(Constants.api.album(albumId).info, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
}
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as AlbumInfoResponse
if (response !== undefined)
dispatch(actions.didLoadAlbumInfoAction(response))
else
dispatch(actions.failedTrackInfoAction(
albumId,
"К сожалению это пока не реализовано, мы работаем над исправлением этой проблемы")
)
},
error => dispatch(actions.failedTrackInfoAction(
albumId,
"К сожалению это пока не реализовано, мы работаем над исправлением этой проблемы")
)
)
}
52 changes: 52 additions & 0 deletions trach/src/actions/album/AlbumPlotInfoActionCreators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import store from '../../app/store'
import fetch from 'cross-fetch'
import { Constants } from '../../core/Constants'
import { AppState } from "../../reducers/mainReducer"
import { didLoadPlotInfoAction } from "../track/PlotInfoActions"
import { PlaylistType } from "../../core/MusicSection"
import { PlotResponse } from "../track/PlotInfoActionCreators"

export function loadAlbumPlotInfoActionCreator(albumId: number) {
const dispatch = store.dispatch
const state: AppState = store.getState()
const startDate = state.selectedDateByTrack.byTrackId[albumId]?.byChart.startDate
const endDate = state.selectedDateByTrack.byTrackId[albumId]?.byChart.endDate

const url: string = (() => {
switch (state.selectFilter.playlistType) {

case PlaylistType.Chart:
return Constants.api.album(albumId).charts.plot

case PlaylistType.New:
return Constants.api.album(albumId).news.plot

default:
return ""
}
})()

fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
body: JSON.stringify({
startDate: startDate,
endDate: endDate
})
})
.then(
response => response.json(),
error => undefined
)
.then(
json => {
const response = json as PlotResponse
if (response !== undefined) {
dispatch(didLoadPlotInfoAction(albumId, response))
}
},
_ => {}
)
}
93 changes: 93 additions & 0 deletions trach/src/actions/artist/ArtistInfoActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { AlbumChartsAction } from "../album/AlbumInChartsAction"
import { AchievementResponse } from "./LoadAchievementsActionCreator"
import { ArtistResponse } from "./LoadArtistInfoActionCreators"
import { TrackResponse } from "./LoadTracksInChartsActionCreator"

export enum ArtistInfoActionTypes {
DidLoadArtistInfo = "ArtistInfoActionTypes.DidLoadArtistInfo",
DidLoadTracksInPlaylists = "ArtistInfoActionTypes.DidLoadTracksInPlaylists",
DidLoadAchievements = "ArtistInfoActionTypes.DidLoadAchievements"
}

export type Achievement = {
title: string
rating?: {
value: number
max: number
}
coverUrl?: string
}

export type Link = {
title: string
url: string
}

export type Artist = {
id: number
name: string
coverUrl?: string
links: Link[]
}

export type Track = {
id: number
title: string
subtitle: string
coverUrl?: string
platforms: string[]
}

export type DidLoadArtistInfoAction = {
type: ArtistInfoActionTypes.DidLoadArtistInfo
artist: Artist
}

export function didLoadArtistInfoAction(response: ArtistResponse): DidLoadArtistInfoAction {
return {
type: ArtistInfoActionTypes.DidLoadArtistInfo,
artist: response
}
}

export type DidLoadTracksInPlaylistsAction = {
type: ArtistInfoActionTypes.DidLoadTracksInPlaylists
artistId: number
tracks: Track[]
isChart: boolean
}

export function didLoadTracksInPlaylistsAction(
artistId: number,
response: TrackResponse[],
isChart: boolean
): DidLoadTracksInPlaylistsAction {
return {
type: ArtistInfoActionTypes.DidLoadTracksInPlaylists,
artistId: artistId,
tracks: response,
isChart: isChart
}
}

export type DidLoadAchievementAction = {
type: ArtistInfoActionTypes.DidLoadAchievements
artistId: number
achievements: Achievement[]
}

export function didLoadAchievementAction(
artistId: number,
response: AchievementResponse[]
): DidLoadAchievementAction {
return {
type: ArtistInfoActionTypes.DidLoadAchievements,
artistId: artistId,
achievements: response
}
}

export type ArtistInfoAction = DidLoadArtistInfoAction
| DidLoadTracksInPlaylistsAction
| DidLoadAchievementAction
| AlbumChartsAction
21 changes: 21 additions & 0 deletions trach/src/actions/artist/ArtistMetricActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ArtistMetric } from "../../core/ArtistMetric"

export enum ArtistMetricActionTypes {
DidLoadArtistMetric = "ArtistMetricActionTypes.DidLoadArtistMetric",
}

export type DidLoadArtistMetricAction = {
type: ArtistMetricActionTypes.DidLoadArtistMetric
artistId: number
data: ArtistMetric
}

export function didLoadArtistInfoAction(response: ArtistMetric, artistId: number): DidLoadArtistMetricAction {
return {
type: ArtistMetricActionTypes.DidLoadArtistMetric,
artistId: artistId,
data: response
}
}

export type ArtistMetricAction = DidLoadArtistMetricAction
37 changes: 37 additions & 0 deletions trach/src/actions/artist/LoadAchievementsActionCreator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import fetch from 'cross-fetch'
import store from '../../app/store'
import { Constants } from '../../core/Constants'
import * as actions from './ArtistInfoActions'

export type AchievementResponse = {
title: string
rating?: {
value: number
max: number
}
coverUrl?: string
}

export function loadAchievementsActionCreator(artistId: number) {
const dispatch = store.dispatch

fetch(Constants.api.artist(artistId).achievement, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
}
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as AchievementResponse[]

if (response !== undefined)
dispatch(actions.didLoadAchievementAction(artistId, response))
},
error => {}
)
}
40 changes: 40 additions & 0 deletions trach/src/actions/artist/LoadArtistInfoActionCreators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fetch from 'cross-fetch'
import store from '../../app/store'
import { Constants } from '../../core/Constants'
import * as actions from './ArtistInfoActions'

export type LinkResponse = {
title: string
url: string
}

export type ArtistResponse = {
id: number
name: string
coverUrl?: string
links: LinkResponse[]
}

export function loadArtistInfoActionCreator(artistId: number) {
const dispatch = store.dispatch

fetch(Constants.api.artist(artistId).info, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
}
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as ArtistResponse

if (response !== undefined)
dispatch(actions.didLoadArtistInfoAction(response))
},
error => {}
)
}
53 changes: 53 additions & 0 deletions trach/src/actions/artist/LoadArtistMerticsActionCreator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import fetch from 'cross-fetch'
import store from '../../app/store'
import { Constants } from '../../core/Constants'
import { AppState } from '../../reducers/mainReducer'
import * as actions from './ArtistMetricActions'

export type TimelineResponse = {
value: number
date: string
}

export type MetricResponse = {
id: string
title: string
timelines: TimelineResponse[]
}

export type ArtistPlotResponse = {
metrics: MetricResponse[]
dates: string[]
}

export function loadArtistMerticsActionCreator(artistId: number) {
const dispatch = store.dispatch
const state: AppState = store.getState()

const startDate = state.selectedDateByTrack.byArtistId[artistId]?.startDate
const endDate = state.selectedDateByTrack.byArtistId[artistId]?.endDate

fetch(Constants.api.artist(artistId).plot, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
body: JSON.stringify({
startDate: startDate,
endDate: endDate
})
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as ArtistPlotResponse

if (response !== undefined)
dispatch(actions.didLoadArtistInfoAction(response, artistId))
},
error => {}
)
}
29 changes: 29 additions & 0 deletions trach/src/actions/artist/LoadSimilarArtistsActionCreator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import store from "../../app/store"
import { Constants } from "../../core/Constants"
import fetch from 'cross-fetch'
import { SimilarArtist } from "../../core/SimilarArtist"
import * as actions from './SimilarArtistActions'

export function loadSimilarArtistsActionCreator(artistId: number) {
const dispatch = store.dispatch

fetch(Constants.api.artist(artistId).similar, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as SimilarArtist[]

if (response !== undefined)
dispatch(actions.didDidLoadSimilarArtistAction(response, artistId))
},
error => {}
)
}
60 changes: 60 additions & 0 deletions trach/src/actions/artist/LoadTracksInChartsActionCreator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import fetch from 'cross-fetch'
import store from '../../app/store'
import { Constants } from '../../core/Constants'
import * as actions from './ArtistInfoActions'

export type TrackResponse = {
id: number
title: string
subtitle: string
coverUrl?: string
platforms: string[]
}

export function loadTracksInChartsActionCreator(artistId: number) {
const dispatch = store.dispatch

fetch(Constants.api.artist(artistId).tracks.inCharts, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
}
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as TrackResponse[]

if (response !== undefined)
dispatch(actions.didLoadTracksInPlaylistsAction(artistId, response, true))
},
error => {}
)
}

export function loadTracksInPlaylistsActionCreator(artistId: number) {
const dispatch = store.dispatch

fetch(Constants.api.artist(artistId).tracks.inPlaylists, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
}
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as TrackResponse[]

if (response !== undefined)
dispatch(actions.didLoadTracksInPlaylistsAction(artistId, response, false))
},
error => {}
)
}
24 changes: 24 additions & 0 deletions trach/src/actions/artist/SimilarArtistActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SimilarArtist } from "../../core/SimilarArtist";

export enum SimilarArtistActionTypes {
DidLoadSimilarArtists = "SimilarArtistActionTypes.DidLoadSimilarArtists",
}

export type DidLoadSimilarArtistAction = {
type: SimilarArtistActionTypes.DidLoadSimilarArtists
artistId: number
similarArtists: SimilarArtist[]
}

export function didDidLoadSimilarArtistAction(
similarArtists: SimilarArtist[],
artistId: number
): DidLoadSimilarArtistAction {
return {
type: SimilarArtistActionTypes.DidLoadSimilarArtists,
artistId: artistId,
similarArtists: similarArtists
}
}

export type SimilarArtistAction = DidLoadSimilarArtistAction
81 changes: 81 additions & 0 deletions trach/src/actions/auth/AuthActionCreators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { store } from "../../app/store"
import { HistoryRouter } from "../../core/Router"
import { AppState } from "../../reducers/mainReducer"
import { auth } from "../../util/firebase"
import { saveReportByTrackOrAlbumToCsvFile } from "../ReportActionCreators"

export type LoginChangeAction = {
type: AuthActionTypes.LoginChanged
login: string
}

export type PasswordChangeAction = {
type: AuthActionTypes.PasswordChanged
password: string
}

export type AuthorizedAction = {
type: AuthActionTypes.Authorized
email: string | null
}

export enum AuthActionTypes {
LoginChanged = "AuthActionTypes.LoginChanged",
PasswordChanged = "AuthActionTypes.PasswordChanged",
Authorized = "AuthActionTypes.IsAuthorized"
}

export function LoginChangeAction(login: string): LoginChangeAction {
return {
type: AuthActionTypes.LoginChanged,
login: login
}
}

export function PasswordChangeAction(password: string): PasswordChangeAction {
return {
type: AuthActionTypes.PasswordChanged,
password: password
}
}

export function AuthorizedAction(email: string | null): AuthorizedAction {
return {
type: AuthActionTypes.Authorized,
email: email
}
}

export function checkIsAuthorized() {
const dispatch = store.dispatch
const user = auth.currentUser

if (user !== null) {
dispatch(AuthorizedAction(user.email))
}
}

export function authorizeActionCreator(router: HistoryRouter, trackId: number) {
const dispatch = store.dispatch
const state: AppState = store.getState()

if (state.auth.isValid) {
auth
.signInWithEmailAndPassword(state.auth.login, state.auth.password)
.then(
result => {
if (result.user !== undefined && result.user !== null) {
saveReportByTrackOrAlbumToCsvFile(trackId)
dispatch(AuthorizedAction(result.user.email))
router.goBack()
}
else {
dispatch(AuthorizedAction(null))
}
},
_ => dispatch(AuthorizedAction(null))
)
}
}

export type AuthActions = LoginChangeAction | PasswordChangeAction | AuthorizedAction
139 changes: 139 additions & 0 deletions trach/src/actions/chart/LoadChartActionCreator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { PlaylistType } from '../../core/MusicSection'
import { Constants } from '../../core/Constants'
import * as Feature from '../../core/Feature'
import store from '../../app/store'

import * as tracks from './TrackActions'

import fetch from 'cross-fetch'
import { AppState } from '../../reducers/mainReducer'
import { MusicCategory } from '../../core/MusicCategory'

const DateFormat = require('dateformat')

export type ArtistResponse = {
id: number
name: string
}

export type PositionResponse = {
value: number
progress: string
shift?: number
}

export type StatsResponse = {
maxPosition: number
daysInChart: number
}

export type ChartResponse = {
position: PositionResponse
stats: StatsResponse
}

export type TrackResponse = {
id: number
title: string
artists: ArtistResponse[]
coverUrl: string
labels: string[]
chart: ChartResponse
}

export type PlatformResponse = {
id: string
title: string
}

export type PlaylistResponse = {
id: number
availablePlatforms: PlatformResponse[]
type: string
tracks: TrackResponse[]
}

export const loadTracksActionCreator = () => {

return (dispatch: (action: any) => void) => {

const nextState: AppState = store.getState()
const platform = nextState.selectFilter.platformId ?? "apple_music"
const dateString = nextState.selectFilter.date ?? DateFormat(new Date(), "dd.MM.yyyy")

if (Feature.isDisabled()) {
return
}

dispatch(tracks.loadingTracks(nextState.selectFilter.playlistType, platform, dateString))

const url: string = (() => {
switch (nextState.selectFilter.playlistType) {

case PlaylistType.Chart:
switch (nextState.selectFilter.category) {
case MusicCategory.Track:
return Constants.api.list.chart.tracks
case MusicCategory.Album:
return Constants.api.list.chart.albums
default:
return ""
}

case PlaylistType.New:
switch (nextState.selectFilter.category) {
case MusicCategory.Track:
return Constants.api.list.news.tracks
case MusicCategory.Album:
return Constants.api.list.news.albums
case MusicCategory.Artist:
return Constants.api.list.news.artists
default:
return ""
}

default:
return ""
}
})()

fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
body: JSON.stringify({
platform: platform,
date: dateString
})
})
.then(
response => response.json(),
error => undefined
)
.then(
json => {
const response = json as PlaylistResponse
if (response === undefined) {
dispatch(tracks.failedLoadedTracks(
nextState.selectFilter.playlistType,
platform,
dateString)
)
return
}
dispatch(tracks.didLoadTracks(
response,
platform,
dateString,
nextState.selectFilter.playlistType)
)
},
error => dispatch(tracks.failedLoadedTracks(
nextState.selectFilter.playlistType,
platform,
dateString)
)
)
}
}
189 changes: 189 additions & 0 deletions trach/src/actions/chart/TrackActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { TrackTypes } from '../../core/MusicCategory'
import { PlaylistType } from '../../core/MusicSection'
import { Position, TrackProgress } from '../../core/Position'
import { Stats } from '../../core/Stats'
import { Track } from '../../core/Track'
import { AlbumChartsAction } from '../album/AlbumInChartsAction'
import { DidLoadArtistInfoAction, DidLoadTracksInPlaylistsAction } from '../artist/ArtistInfoActions'
import { ArtistMetricAction } from '../artist/ArtistMetricActions'
import { DidLoadPlotInfoAction } from '../track/PlotInfoActions'
import { DidLoadTrackInfoAction } from '../track/TrackInfoActions'
import { PlaylistResponse } from './LoadChartActionCreator'

export enum TrackActionTypes {
DidLoadTracks = "TrackActionTypes.DidLoadTracks",
LoadingTracks = "TrackActionTypes.LoadingTracks",
FailedLoadedTracks = "TrackActionTypes.FailedLoadedTracks"
}

export type Artist = {
id: number
name: string
coverUrl?: string
}

export type TrackType = {
id: number
type: TrackTypes
}

export type ArtistByTrack = {
trackId: number
artists: Artist[]
}

export type LabelByTrack = {
trackId: number
labels: string[]
}

export type PositionByTrack = {
trackId: number
position: Position
}

export type StatsByTrack = {
trackId: number
stats: Stats
}

export type Platform = {
id: string
title: string
}

export type DidLoadTracksAction = {
type: TrackActionTypes.DidLoadTracks
playlistType: PlaylistType
platform: string
date: string
tracks: Track[]
labels: LabelByTrack[]
artists: ArtistByTrack[]
positions: PositionByTrack[]
stats: StatsByTrack[]
availablePlatforms: Platform[]
trackTypes: TrackType[]
}

export function didLoadTracks(
playlistResponse: PlaylistResponse,
platform: string,
date: string,
playlistType: PlaylistType
): DidLoadTracksAction {

const tracks: Track[] = []
const labels: LabelByTrack[] = []
const artists: ArtistByTrack[] = []
const positions: PositionByTrack[] = []
const stats: StatsByTrack[] = []
const types: TrackType[] = []

for (const track of playlistResponse.tracks) {
tracks.push({
id: track.id,
title: track.title,
coverUrl: track.coverUrl
})
labels.push({
trackId: track.id,
labels: track.labels
})
artists.push({
trackId: track.id,
artists: track.artists
})
positions.push({
trackId: track.id,
position: {
value: track.chart.position.value,
progress: track.chart.position.progress as TrackProgress,
shift: track.chart.position.shift
}
})
stats.push({
trackId: track.id,
stats: {
maxPosition: track.chart.stats.maxPosition,
daysInChart: track.chart.stats.daysInChart
}
})
types.push({
id: track.id,
type: playlistResponse.type as TrackTypes
})
}
return {
type: TrackActionTypes.DidLoadTracks,
playlistType: playlistType,
platform: platform,
date: date,
tracks: tracks,
labels: labels,
artists: artists,
positions: positions,
stats: stats,
availablePlatforms: playlistResponse.availablePlatforms.map(p => {
const platform: Platform = {
id: p.id,
title: p.title
}
return platform
}),
trackTypes: types
}
}

export type FailedLoadedTracks = {
type: TrackActionTypes.FailedLoadedTracks
playlistType: PlaylistType
platform: string
date: string
text: string
}

export function failedLoadedTracks(
playlistType: PlaylistType,
platform: string,
date: string,
text: string = "К сожалению это пока не реализовано, мы работаем над исправлением этой проблемы"
): FailedLoadedTracks {
return {
type: TrackActionTypes.FailedLoadedTracks,
playlistType: playlistType,
platform: platform,
date: date,
text: text
}
}

export type LoadingTracks = {
type: TrackActionTypes.LoadingTracks
playlistType: PlaylistType
platform: string
date: string
}

export function loadingTracks(
playlistType: PlaylistType,
platform: string,
date: string,
): LoadingTracks {
return {
type: TrackActionTypes.LoadingTracks,
playlistType: playlistType,
platform: platform,
date: date
}
}

export type TracksAction = DidLoadTracksAction
| FailedLoadedTracks
| LoadingTracks
| DidLoadTrackInfoAction
| DidLoadPlotInfoAction
| DidLoadArtistInfoAction
| DidLoadTracksInPlaylistsAction
| AlbumChartsAction
| ArtistMetricAction
38 changes: 38 additions & 0 deletions trach/src/actions/track/LegendActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { DidLoadArtistMetricAction } from "../artist/ArtistMetricActions"
import { DidLoadPlotInfoAction } from "./PlotInfoActions"

export enum LegendActionContext {
ForTrack = "LegendActionContext.ForTrack",
ForArtist = "LegendActionContext.ForArtist",
}

export enum LegendActionTypes {
SelectedPlatform = "LegendActionTypes.SelectedPlatform",
}

export type SelectedPlatformAction = {
type: LegendActionTypes.SelectedPlatform
context: LegendActionContext,
id: number
platformId: string
}

export function selectedTrackPlatformAction(trackId: number, platformId: string): SelectedPlatformAction {
return {
type: LegendActionTypes.SelectedPlatform,
context: LegendActionContext.ForTrack,
id: trackId,
platformId: platformId
}
}

export function selectedArtistPlatformAction(artistId: number, platformId: string): SelectedPlatformAction {
return {
type: LegendActionTypes.SelectedPlatform,
context: LegendActionContext.ForArtist,
id: artistId,
platformId: platformId
}
}

export type LegendAction = SelectedPlatformAction | DidLoadPlotInfoAction | DidLoadArtistMetricAction
90 changes: 90 additions & 0 deletions trach/src/actions/track/PlaylistActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Playlist } from '../../core/Playlist'

export enum PlaylistActionTypes {
StartLoadPlaylists = "PlaylistActionTypes.StartLoadPlaylists",
ErrorLoadPlaylists = "PlaylistActionTypes.ErrorLoadPlaylists",
FinishLoadPlaylists = "PlaylistActionTypes.FinishLoadPlaylists",
ChangeDate = "PlaylistActionTypes.ChangeDate"
}

export enum PlaylistDateFilterTypes {
All = "PlaylistDateFilter.All",
ForDate = "PlaylistDateFilter.ForDate"
}

export type PlaylistDateFilterAll = {
type: PlaylistDateFilterTypes.All
}

export type PlaylistDateFilterForDate = {
type: PlaylistDateFilterTypes.ForDate
date: string
}

export type StartLoadPlaylistsAction = {
type: PlaylistActionTypes.StartLoadPlaylists
dateFilter: PlaylistDateFilterAll | PlaylistDateFilterForDate
trackId: number
}

export type ErrorLoadPlaylistsAction = {
type: PlaylistActionTypes.ErrorLoadPlaylists
dateFilter: PlaylistDateFilterAll | PlaylistDateFilterForDate
trackId: number
error: string
}

export type FinishLoadPlaylistsAction = {
type: PlaylistActionTypes.FinishLoadPlaylists
dateFilter: PlaylistDateFilterAll | PlaylistDateFilterForDate
trackId: number
playlists: Playlist[]
}

export function finishLoadPlaylistsAction(
trackId: number,
date: string | undefined,
playlists: Playlist[]
): FinishLoadPlaylistsAction {
const filter: PlaylistDateFilterAll | PlaylistDateFilterForDate = date === undefined
? {type: PlaylistDateFilterTypes.All}
: {type: PlaylistDateFilterTypes.ForDate, date: date}
return {
type: PlaylistActionTypes.FinishLoadPlaylists,
dateFilter: filter,
trackId: trackId,
playlists: playlists
}
}

export function errorLoadPlaylistsAction(
trackId: number,
date: string | undefined,
error: string = "Нет данных, мы работаем над исправлением этой проблемы"
): ErrorLoadPlaylistsAction {
const filter: PlaylistDateFilterAll | PlaylistDateFilterForDate = date === undefined
? {type: PlaylistDateFilterTypes.All}
: {type: PlaylistDateFilterTypes.ForDate, date: date}
return {
type: PlaylistActionTypes.ErrorLoadPlaylists,
dateFilter: filter,
trackId: trackId,
error: error
}
}

export function startLoadPlaylistsAction(
trackId: number,
date: string | undefined,
): StartLoadPlaylistsAction {
const filter: PlaylistDateFilterAll | PlaylistDateFilterForDate = date === undefined
? {type: PlaylistDateFilterTypes.All}
: {type: PlaylistDateFilterTypes.ForDate, date: date}
return {
type: PlaylistActionTypes.StartLoadPlaylists,
dateFilter: filter,
trackId: trackId
}
}

export type PlaylistAction = StartLoadPlaylistsAction | ErrorLoadPlaylistsAction | FinishLoadPlaylistsAction
69 changes: 69 additions & 0 deletions trach/src/actions/track/PlotInfoActionCreators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { StatsResponse } from "../chart/LoadChartActionCreator"
import store from '../../app/store'
import fetch from 'cross-fetch'
import { Constants } from '../../core/Constants'
import { AppState } from "../../reducers/mainReducer"
import { didLoadPlotInfoAction } from "./PlotInfoActions"
import { PlaylistType } from "../../core/MusicSection"

export type TimelineResponse = {
position: number
date: string
}

export type PlatformResponse = {
id: string
title: string
stats?: StatsResponse
timelines: TimelineResponse[]
}

export type PlotResponse = {
platforms: PlatformResponse[]
dates: string[]
}

export function loadPlotInfoActionCreator(trackId: number) {
const dispatch = store.dispatch
const state: AppState = store.getState()
const startDate = state.selectedDateByTrack.byTrackId[trackId]?.byChart.startDate
const endDate = state.selectedDateByTrack.byTrackId[trackId]?.byChart.endDate

const url: string = (() => {
switch (state.selectFilter.playlistType) {

case PlaylistType.Chart:
return Constants.api.track(trackId).charts.plot

case PlaylistType.New:
return Constants.api.track(trackId).news.plot

default:
return ""
}
})()

fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
body: JSON.stringify({
startDate: startDate,
endDate: endDate
})
})
.then(
response => response.json(),
error => undefined
)
.then(
json => {
const response = json as PlotResponse
if (response !== undefined) {
dispatch(didLoadPlotInfoAction(trackId, response))
}
},
_ => {}
)
}
23 changes: 23 additions & 0 deletions trach/src/actions/track/PlotInfoActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Plot } from "../../core/Plot";
import { DidLoadArtistMetricAction } from "../artist/ArtistMetricActions";
import { PlotResponse } from "./PlotInfoActionCreators";

export enum PlotInfoActionTypes {
DidLoadPlotInfo = "PlotInfoActionTypes.DidLoadPlotInfo",
}

export type DidLoadPlotInfoAction = {
type: PlotInfoActionTypes.DidLoadPlotInfo
trackId: number
plot: Plot
}

export function didLoadPlotInfoAction(trackId: number, plot: PlotResponse): DidLoadPlotInfoAction {
return {
type: PlotInfoActionTypes.DidLoadPlotInfo,
trackId: trackId,
plot: plot
}
}

export type PlotInfoAction = DidLoadPlotInfoAction | DidLoadArtistMetricAction
67 changes: 67 additions & 0 deletions trach/src/actions/track/PlotSelectDateActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { DidLoadArtistMetricAction } from "../artist/ArtistMetricActions"
import { DidLoadPlotInfoAction } from "./PlotInfoActions"

export enum SelectedDateActionContext {
ForTrack = "SelectedDateActionContext.ForTrack",
ForArtist = "SelectedDateActionContext.ForArtist"
}

export enum SelectedDateActionTypes {
SelectedStartDate = "SelectedDateActionTypes.SelectedStartDate",
SelectedEndDate = "SelectedDateActionTypes.SelectedEndDate"
}

export type SelectedStartDateAction = {
type: SelectedDateActionTypes.SelectedStartDate
context: SelectedDateActionContext
id: number
date: string
}

export type SelectedEndDateAction = {
type: SelectedDateActionTypes.SelectedEndDate
context: SelectedDateActionContext
id: number
date: string
}

export function selectedStartDateAction(trackId: number, date: string): SelectedStartDateAction {
return {
type: SelectedDateActionTypes.SelectedStartDate,
context: SelectedDateActionContext.ForTrack,
id: trackId,
date: date
}
}

export function selectedEndDateAction(trackId: number, date: string): SelectedEndDateAction {
return {
type: SelectedDateActionTypes.SelectedEndDate,
context: SelectedDateActionContext.ForTrack,
id: trackId,
date: date
}
}

export function selectedArtistStartDateAction(artistId: number, date: string): SelectedStartDateAction {
return {
type: SelectedDateActionTypes.SelectedStartDate,
context: SelectedDateActionContext.ForArtist,
id: artistId,
date: date
}
}

export function selectedArtistEndDateAction(artistId: number, date: string): SelectedEndDateAction {
return {
type: SelectedDateActionTypes.SelectedEndDate,
context: SelectedDateActionContext.ForArtist,
id: artistId,
date: date
}
}

export type SelectedDateAction = SelectedStartDateAction
| SelectedEndDateAction
| DidLoadPlotInfoAction
| DidLoadArtistMetricAction
66 changes: 66 additions & 0 deletions trach/src/actions/track/TrackActionCreators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import store from '../../app/store'
import fetch from 'cross-fetch'
import { Constants } from '../../core/Constants'
import * as actions from './TrackInfoActions'

export type AlbumResponse = {
id: number
title: string
releaseDate?: string
}

export type PlayUrlResponse = {
url: string
platform: string
}

export type ArtistResponse = {
id: number
name: string
tracksInChart: number
tracksInPlaylists: number
}

export type TrackInfoResponse = {
id: number
title: string
coverUrl?: string
album?: AlbumResponse
artists: ArtistResponse[]
genres: string[]
labels: string[]
playUrls: PlayUrlResponse[]
}

export const loadTracksInfoActionCreator = (trackId: number) => {
const dispatch = store.dispatch

dispatch(actions.inProgressTrackInfoAction(trackId))

fetch(Constants.api.track(trackId).info, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
}
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as TrackInfoResponse
if (response !== undefined)
dispatch(actions.didLoadTrackInfoAction(response))
else
dispatch(actions.failedTrackInfoAction(
trackId,
"К сожалению это пока не реализовано, мы работаем над исправлением этой проблемы")
)
},
error => dispatch(actions.failedTrackInfoAction(
trackId,
"К сожалению это пока не реализовано, мы работаем над исправлением этой проблемы")
)
)
}
101 changes: 101 additions & 0 deletions trach/src/actions/track/TrackInfoActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Album } from "../../core/Album"
import { PlayUrl } from "../../core/Track"
import { AlbumChartsAction } from "../album/AlbumInChartsAction"
import { AlbumInfoResponse } from "../album/AlbumInfoActionCreators"
import { TrackInfoResponse } from "./TrackActionCreators"

export enum TrackInfoActionTypes {
DidLoadTrackInfo = "TrackInfoActionTypes.DidLoadTrackInfo",
InProgressTrackInfo = "TrackInfoActionTypes.InProgressTrackInfo",
FailedTrackInfo = "TrackInfoActionTypes.FailedLoadedTracks"
}

export type ArtistWithStats = {
id: number
name: string
tracksInChart: number
tracksInPlaylists: number
}

export type DidLoadTrackInfoAction = {
type: TrackInfoActionTypes.DidLoadTrackInfo
trackId: number
title: string
coverUrl?: string
album?: Album
artists: ArtistWithStats[]
genres: string[]
labels: string[]
playUrls: PlayUrl[]
}

export type InProgressTrackInfoAction = {
type: TrackInfoActionTypes.InProgressTrackInfo
trackId: number
}

export type FailedTrackInfoAction = {
type: TrackInfoActionTypes.FailedTrackInfo
trackId: number
text: string
}

export function inProgressTrackInfoAction(trackId: number): InProgressTrackInfoAction {
return {
type: TrackInfoActionTypes.InProgressTrackInfo,
trackId: trackId
}
}

export function didLoadTrackInfoAction(response: TrackInfoResponse): DidLoadTrackInfoAction {
return {
type: TrackInfoActionTypes.DidLoadTrackInfo,
trackId: response.id,
title: response.title,
coverUrl: response.coverUrl,
album: response.album,
artists: response.artists,
genres: response.genres,
labels: response.labels,
playUrls: response.playUrls
}
}

export function didLoadAlbumInfoAction(response: AlbumInfoResponse): DidLoadTrackInfoAction {
var genres: string[] = []
if (response.genre !== undefined)
genres.push(response.genre)

var labels: string[] = []
if (response.label !== undefined)
labels.push(response.label)

return {
type: TrackInfoActionTypes.DidLoadTrackInfo,
trackId: response.id,
title: response.title,
coverUrl: response.coverUrl,
album: {
id: response.id,
title: "",
releaseDate: response.releaseDate
},
artists: response.artists,
genres: genres,
labels: labels,
playUrls: response.links
}
}

export function failedTrackInfoAction(trackId: number, text: string): FailedTrackInfoAction {
return {
type: TrackInfoActionTypes.FailedTrackInfo,
trackId: trackId,
text: text
}
}

export type TrackInfoAction = DidLoadTrackInfoAction
| InProgressTrackInfoAction
| FailedTrackInfoAction
| AlbumChartsAction
55 changes: 55 additions & 0 deletions trach/src/actions/track/TrackPlaylistActionCreators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import store from '../../app/store'
import fetch from 'cross-fetch'
import { Constants } from '../../core/Constants'
import { AppState } from '../../reducers/mainReducer'
import * as actions from './PlaylistActions'

type PlaylistResponse = {
platform: string
title: string
author: string
coverUrl?: string
position: string
metrics: string[]
}

export function trackPlaylistActionCreators(trackId: number) {
const state: AppState = store.getState()
const dispatch = store.dispatch
const date = state.selectFilter.playlist[trackId]?.date ?? ""

dispatch(actions.startLoadPlaylistsAction(trackId, date))

fetch(Constants.api.track(trackId).playlists, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
body: date.length > 0 ? JSON.stringify({date: date}) : undefined
})
.then(
response => response.json(),
_ => undefined
)
.then(
json => {
const response = json as PlaylistResponse[]
dispatch(actions.finishLoadPlaylistsAction(trackId, date, response))
},
_ => {
if (date === undefined)
dispatch(actions.errorLoadPlaylistsAction(
trackId,
date,
"Нет данных за все время, мы работаем над исправлением этой проблемы")
)
else
dispatch(actions.errorLoadPlaylistsAction(
trackId,
date,
`Нет данных за ${date}, мы работаем над исправлением этой проблемы`)
)
}
)

}
37 changes: 37 additions & 0 deletions trach/src/app/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { reducer } from '../reducers/mainReducer'
import { createStore, applyMiddleware, compose } from 'redux'
//import { getDefaultMiddleware } from '@reduxjs/toolkit'
import thunkMiddleware from 'redux-thunk'

import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'

const persistConfig = {
key: 'trach:app',
storage,
whitelist: ['selectFilter'],
}

const persistedReducer = persistReducer(persistConfig, reducer)

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

// const middlewares = getDefaultMiddleware({
// immutableCheck: false,
// serializableCheck: false,
// thunk: true,
// });

const configureStore = () => {
let store = createStore(
persistedReducer,
composeEnhancers(
applyMiddleware(thunkMiddleware)
)
)
let persistor = persistStore(store)
return { store, persistor }
}
export const { store, persistor } = configureStore()

export default store
82 changes: 82 additions & 0 deletions trach/src/components/achievement/AchievementComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Image from '../core/image'
import { useStyles, FireIcon } from './AchievementComponentStyles'

import React from 'react'

import Grid from '@material-ui/core/Grid'

export enum AchievementTypeProps {
Text = "achievement_type_text",
Icon = "achievement_type_icon"
}

export type AchievementTypeText = {
type: AchievementTypeProps.Text,
value: number
max: number
}

export type AchievementTypeIcon = {
type: AchievementTypeProps.Icon,
}

export type AchievementItemProps = {
coverUrl: string
title: string
achievement: AchievementTypeText|AchievementTypeIcon
}

function AchievementItemComponent(props: AchievementItemProps) {
const classes = useStyles()
const content = () => {
switch (props.achievement.type) {
case AchievementTypeProps.Icon:
return (
<Grid item container className={classes.icon} alignItems="center" justify="center">
<FireIcon />
</Grid>
)
case AchievementTypeProps.Text:
return (
<Grid item container className={classes.icon} alignItems="center" justify="center">
<Grid className={classes.bigText} item>{props.achievement.value}</Grid>
<Grid className={classes.mediumText} item>/{props.achievement.max}</Grid>
</Grid>
)
}
}
return (
<Grid style={{marginRight: 60, marginTop: 30}} container direction="column">
<Grid item className={classes.coverContainer}>
<Image src={props.coverUrl} className={classes.coverBackground}/>
{content()}
</Grid>
<Grid style={{width: 120}} className={classes.smallText} item container>
<Grid item container alignItems="center" justify="center">{props.title}</Grid>
</Grid>
</Grid>
)
}

export type AchievementProps = {
header: string
achievements: AchievementItemProps[]
}

export default function AchievementComponent(props: AchievementProps) {
const classes = useStyles()
return (
<Grid className={classes.root} container>
<Grid className={classes.header} item>{props.header}</Grid>
<Grid item container>
{props.achievements.map(a => {
return (
<Grid item>
<AchievementItemComponent {...a}/>
</Grid>
)
})}
</Grid>
</Grid>
)
}
76 changes: 76 additions & 0 deletions trach/src/components/achievement/AchievementComponentStyles.tsx

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions trach/src/components/album/AlbumComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Image from '../core/image'
import { useStyles } from './AlbumComponentStyles'
import { HistoryRouter } from '../../core/Router'

import React from 'react'
import Grid from '@material-ui/core/Grid'
import { useHistory } from 'react-router'
import { Box } from '@material-ui/core'
import { GeneralInfoTooltipComponent } from '../infotooltip/InfoTooltipComponent'

export type AlbumProps = {
coverUrl: string
title: string
year?: string
subtitle: string
other?: string
onClick: (router: HistoryRouter) => void
}

export default function AlbumComponent(props: AlbumProps) {
const classes = useStyles()
const history = useHistory()
const Year = () => {
if (props.year !== undefined)
return (<Grid item className={classes.year}>{props.year}</Grid>)
else
return null
}
const Subtitle = () => {
if (props.other !== undefined)
return (
<GeneralInfoTooltipComponent text={props.other} showInElement={
<Grid item className={classes.subtitle}>{props.subtitle}</Grid>
} />
)
else
return (<Grid item className={classes.subtitle}>{props.subtitle}</Grid>)
}
return (
<Grid container wrap="nowrap">
<Image className={classes.image} src={props.coverUrl} />
<Grid item container direction="column" alignItems="flex-start">
<Grid item>
<Box
className={classes.title}
onClick={() => props.onClick(history)}>{props.title}
</Box>
</Grid>
<Grid item container xs direction="column" justify="flex-end">
<Year />
<Subtitle />
</Grid>
</Grid>
</Grid>
)
}
42 changes: 42 additions & 0 deletions trach/src/components/album/AlbumComponentStyles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'

export const useStyles = makeStyles((_: Theme) =>
createStyles({
title: {
'&:hover': {
textDecorationLine: 'underline',
},
cursor: 'pointer',
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 16,
textAlign: 'left',
color: '#000000',
marginTop: 5,
},
year: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 500,
fontSize: 16,
textAlign: 'left',
marginBottom: 3,
color: '#9F9F9F',
},
subtitle: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 500,
fontSize: 16,
textAlign: 'left',
color: '#9F9F9F',
marginBottom: 5
},
image: {
borderRadius: 6,
width: 120,
height: 120,
marginRight: 20,
},
}))
59 changes: 59 additions & 0 deletions trach/src/components/albumgrid/AlbumGridComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useStyles } from './AlbumGridComponentStyles'
import {
GeneralLoadDataEmptyComponent,
GeneralLoadDataEmpty,
GeneralLoadDataTypes
} from '../core/GeneralLoadDataComponent'
import AlbumComponent, { AlbumProps } from '../album/AlbumComponent'

import React from 'react'
import Grid from '@material-ui/core/Grid'

export type AlbumGridProps = {
header: string
albums: AlbumProps[]
}

export default function AlbumGridComponent(props: AlbumGridProps) {
const classes = useStyles()
const Albums = () => {
if (props.albums.length > 0) {
return (
<Grid className={classes.gridContainer} item container>
{
props.albums.map(album =>
<Grid item style={{ width: 'calc(100%/3)', minWidth: 350 }}>
<AlbumComponent {...album} />
</Grid>
)
}
</Grid>
)
}
else {
const emptyProps: GeneralLoadDataEmpty = {
type: GeneralLoadDataTypes.EmptyData,
text: 'Данные по альбомам могут отсутствовать, мы работаем над исправлением этой проблемы'
}
return (
<Grid className={classes.gridContainer} item container>
<GeneralLoadDataEmptyComponent {...emptyProps} />
</Grid>
)
}
}
const header = (() => {
if (props.albums.length > 0) {
return `${props.header} (${props.albums.length})`
}
else {
return `${props.header}`
}
})()
return (
<Grid className={classes.root} container>
<Grid className={classes.header} item>{header}</Grid>
<Albums />
</Grid>
)
}
19 changes: 19 additions & 0 deletions trach/src/components/albumgrid/AlbumGridComponentStyles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'

export const useStyles = makeStyles((_: Theme) =>
createStyles({
root: {
marginTop: 70,
},
gridContainer: {
marginTop: 30,
},
header: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 24,
textAlign: 'left',
color: '#000000',
},
}))
100 changes: 100 additions & 0 deletions trach/src/components/artistinfo/ArtistInfoComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Image from '../core/image'
import { InDevelopTooltipComponent } from '../infotooltip/InfoTooltipComponent'
import { FlatButton } from '../core/FlatButton'
import { useStyles, BookmarkIcon } from './ArtistInfoComponentStyles'

import React from 'react'
import { Box } from '@material-ui/core'
import Grid from '@material-ui/core/Grid'

export type ContactProps = {
title: string
link: string
onClick: () => void
}

export type ArtistInfoProps = {
coverUrl: string
name: string
contacts: ContactProps[]
onClickBookmark: () => void
}

function makeGroups(size: number, items: ContactProps[]): ContactProps[][] {
var links: ContactProps[][] = []

if (items.length < size) {
links.push(items)
return links
}

const count = Math.ceil(items.length / size)

for (let index = 0; index < count; index++) {
links.push(items.slice(index * size, index * size + size))
}

return links
}

export default function ArtistInfoComponent(props: ArtistInfoProps) {
const classes = useStyles()

const links = makeGroups(3, props.contacts)

var contacts: JSX.Element[] = links.map((group, idx, links) => {
return (
<Grid item container direction="row">
{
group.map((contact, index, group) => {
const isLast = idx === links.length - 1 && index === group.length - 1
if ((index + 1) % 3 === 0 || isLast)
return (
<Grid item direction="column">
<Box onClick={contact.onClick} className={classes.contact}>{contact.title}</Box>
</Grid>
)
else
return (
<Grid item direction="column">
<Box onClick={contact.onClick} className={classes.contactWithPoint}>{contact.title + ' · '}</Box>
</Grid>
)
})
}
</Grid>
)
})

return (
<Grid container wrap="nowrap">
<Image className={classes.image} src={props.coverUrl}/>
<Grid item container direction="column" alignItems="flex-start" justify="center" wrap="nowrap">
<Grid item>
<Box m={0} className={classes.name}>{props.name}</Box>
</Grid>
<Grid item xs></Grid>
<Grid item container xs direction="row" alignItems="center">
<div style={{height: 5, width: '100%'}}></div>
{contacts}
</Grid>
<Grid item xs></Grid>
<Grid
item
container
direction="row"
alignContent="flex-end"
style={{height: 20, marginTop: 13, marginLeft: -6}}>
<InDevelopTooltipComponent showInElement={
<FlatButton onClick={props.onClickBookmark}>
<Grid item container alignItems="center">
<BookmarkIcon/>
<span className={classes.button}>В закладки</span>
</Grid>
</FlatButton>
}/>
</Grid>
</Grid>
</Grid>
)
}
76 changes: 76 additions & 0 deletions trach/src/components/artistinfo/ArtistInfoComponentStyles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'

import React from 'react'

export const useStyles = makeStyles((_: Theme) =>
createStyles({
name: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 30,
textAlign: 'center',
color: '#000000',
},
button: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 16,
textAlign: 'left',
color: '#000000',
marginLeft: 10,
},
image: {
borderRadius: 3,
width: 180,
height: 180,
marginRight: 40,
},
bookmark: {
width: 39,
height: 39,
},
contact: {
'&:hover': {
textDecorationLine: 'underline',
},
cursor: 'pointer',
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 16,
textAlign: 'left',
color: '#969696',
paddingBottom: 5
},
contactWithPoint: {
'&:hover': {
textDecorationLine: 'underline',
},
cursor: 'pointer',
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 16,
textAlign: 'left',
color: '#969696',
paddingRight: 3,
paddingBottom: 5
},
}))

export const BookmarkIcon = () =>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink">
<rect width="15" height="15" fill="url(#pattern2)"/>
<defs>
<pattern id="pattern2" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlinkHref="#image2" transform="scale(0.05)"/>
</pattern>
<image
id="image2"
width="20"
height="20"
xlinkHref="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDQwOEY3NDA3QzUwMTFFQTkxMjVGRDMwRjZGMUY1NUEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDQwOEY3NDE3QzUwMTFFQTkxMjVGRDMwRjZGMUY1NUEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0NDA4RjczRTdDNTAxMUVBOTEyNUZEMzBGNkYxRjU1QSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0NDA4RjczRjdDNTAxMUVBOTEyNUZEMzBGNkYxRjU1QSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgR3pzYAAADHSURBVHjaYmRgZPjPQEXAAqVPAPFBCs2yAGJ7BrALGRnagZiBQlwBMouJgcpg1MBRA4eKgaDsmQTEEUhZlWwDHYD4AhDPBeLlQHwRKkaygZJAvBSI9wExJxD7QTEHEO+HGi6FXStq4cACxEVA/AmIvwFxAxBzIhUAnFAxkNxnIC4FYjbkwgHZQAcgvgLlbwZiZTwlizJUDUjtVSB2QjfwORD/A+K7QOxLQpHlC9UDMuMpsoHYvEcsRg4GsIGEvEcsBgcDQIABADRxLsmwxCUEAAAAAElFTkSuQmCC"/>
</defs>
</svg>
89 changes: 89 additions & 0 deletions trach/src/components/artistplot/ArtistStatsPlotComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useStyles } from './ArtistStatsPlotComponentStyles'
import LinePlotComponent from './plot/ArtistLinePlotComponent'
import { FlatButton } from '../core/FlatButton'

import React from 'react'

import Grid from '@material-ui/core/Grid'
import Box from '@material-ui/core/Box'
import InfoTooltipComponent, { TooltipItemProps } from '../infotooltip/InfoTooltipComponent'

export enum DateUnit {
Days = 'top_duration_days',
Months = 'top_duration_months'
}

export type LegendItemProps = {
title: string
color: string
description: string
isSelected: boolean
onSelected: () => void
}

export type LegendProps = {
enableItems: LegendItemProps[]
disableItems: string[]
}

export type ArtistStatsPlotProps = {
legend: LegendProps
plot: {
labels: string[]
lines: LineProps[]
}
}

export type LineProps = {
label: string,
color: string,
values: number[],
}

export default function ArtistStatsPlotComponent(props: ArtistStatsPlotProps) {
const classes = useStyles()

const tooltipLegendItemsProps = (legend: LegendItemProps): TooltipItemProps[] => {
return [
{
leftText: legend.description,
},
]
}

const enabledPlatforms = props.legend.enableItems.map(legend => (
<Grid item>
<InfoTooltipComponent
showInElement={
<FlatButton onClick={legend.onSelected}>
<Grid container>
<Box className={classes.circle} style={{background: !legend.isSelected ? 'white' : legend.color}}/>
<Box className={classes.legendText}>{legend.title}</Box>
</Grid>
</FlatButton>
}
items={tooltipLegendItemsProps(legend)}/>
</Grid>
))

const disablePlatforms = props.legend.disableItems.map(legend => (
<Grid item>
<Grid container>
<Box className={classes.circle} style={{background: '#D2D2D2'}}/>
<Box className={classes.disableLegendText}>{legend}</Box>
</Grid>
</Grid>
))

return (
<Grid className={classes.root} container direction="row">
<Grid className={classes.legend} item container>
{enabledPlatforms}
{disablePlatforms}
</Grid>
<Grid item xs>
<LinePlotComponent labels={props.plot.labels} lines={props.plot.lines}/>
</Grid>
</Grid>
)
}
37 changes: 37 additions & 0 deletions trach/src/components/artistplot/ArtistStatsPlotComponentStyles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'

export const useStyles = makeStyles((_: Theme) =>
createStyles({
circle: {
border: '1px solid #D2D2D2',
boxSizing: 'border-box',
borderRadius: 10,
width: 20,
height: 20,
},
legendText: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 16,
color: '#000000',
paddingRight: 20,
paddingLeft: 10,
},
disableLegendText: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 16,
color: '#969696',
paddingRight: 20,
paddingLeft: 10,
},
legend: {
paddingLeft: 25,
paddingBottom: 15,
},
root: {
paddingTop: 30
},
}))
121 changes: 121 additions & 0 deletions trach/src/components/artistplot/plot/ArtistLinePlotComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { PureComponent } from 'react'
import Chart from "chart.js";

Chart.Tooltip.positioners.custom = function (_, eventPosition) {
return {
x: eventPosition.x,
y: eventPosition.y
};
}
Chart.defaults.global.legend.display = false;

function formatN(n) {
const unitList = ['y', 'z', 'a', 'f', 'p', 'n', 'u', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
const zeroIndex = 8;
const nn = n.toExponential(4).split(/e/);
let u = Math.floor(+nn[1] / 3) + zeroIndex;
if (u > unitList.length - 1) {
u = unitList.length - 1;
} else
if (u < 0) {
u = 0;
}
if (u > zeroIndex)
u = u - 1
return numberWithSpaces((nn[0] * Math.pow(10, +nn[1] - (u - zeroIndex) * 3)).toFixed(0) + unitList[u]);
}

function numberWithSpaces(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}


function createDatasetsFromProps(props) {
return props.lines.map(line => {
return {
label: line.label,
backgroundColor: line.color,
borderColor: line.color,
pointRadius: 1,
lineTension: 0,
borderWidth: 2,
fill: false,
data: line.values,
}
})
}

export default class ArtistLinePlotComponent extends PureComponent {
linePlotRef = React.createRef();
chart = {};

componentDidMount() {
this.buildChart();
}

componentDidUpdate() {
this.chart.data = {
labels: this.props.labels,
datasets: createDatasetsFromProps(this.props)
}
this.chart.update()
}



buildChart = () => {
this.chart = new Chart(this.linePlotRef.current.getContext("2d"), {
type: "line",
data: {
labels: this.props.labels,
datasets: createDatasetsFromProps(this.props)
},
options: {
animation: {
duration: 0
},
title: {
display: false,
},
maintainAspectRatio: false,
scales: {
yAxes: [{
ticks: {
fontSize: 14,
userCallback: function (label, index, labels) {
return formatN(parseFloat(label))
},
}
}],
xAxes: [{
ticks: {
offset: true,
fontSize: 14,
}
}],
},
tooltips: {
mode: 'index',
position: 'custom',
callbacks: {
label: function (tooltipItem, myData) {
let label = myData.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
label += numberWithSpaces(parseFloat(tooltipItem.value));
return label;
}
}
}
}
});
}

render() {

return (
<canvas id="myChart" ref={this.linePlotRef} height="530%" />
)
}
}
82 changes: 82 additions & 0 deletions trach/src/components/artistsimilar/ArtistsSimilarComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useStyles } from './ArtistsSimilarComponentStyles'
import Image from '../core/image'

import React from 'react'

import Grid from '@material-ui/core/Grid'
import { Box } from '@material-ui/core'

export type LinkProps = {
link: string
onClick: () => void
}

export type ArtistSimilarProps = {
coverUrl: string
title: string
audience: string
platformMatchCount: string
genresMatchCount: string
router: LinkProps
isSelected: boolean
}

export type ArtistsSimilarProps = {
header: string
artists: ArtistSimilarProps[]
}

function Artist(props: ArtistSimilarProps) {
const classes = useStyles()

return (
<Grid className={props.isSelected ? classes.gridItemSelected : classes.gridItem}
container
wrap="nowrap">
<Image className={classes.cover} src={props.coverUrl} />
<Grid item container direction="column" alignItems="flex-start">
<Grid item>
<Box
className={classes.title}
onClick={() => props.router.onClick()}>
{props.title}
</Box>
</Grid>
<Grid className={classes.subtitle} item>{props.platformMatchCount}</Grid>
<Grid className={classes.subtitle} item>{props.genresMatchCount}</Grid>
<Grid className={classes.subtitle} item>{props.audience}</Grid>
</Grid>
</Grid>
)
}

export default function ArtistsSimilarComponent(props: ArtistsSimilarProps) {
const classes = useStyles()
const Tracks = () => {
return (
<Grid className={classes.gridContainer} item container>
{
props.artists.map(track =>
<Grid item style={{ width: 'calc(100%/3)', minWidth: 350 }}>
<Artist {...track} />
</Grid>
)
}
</Grid>
)
}
const header = (() => {
if (props.artists.length > 0) {
return `${props.header} (${props.artists.length})`
}
else {
return `${props.header}`
}
})()
return (
<Grid className={classes.root} container>
<Grid className={classes.header} item>{header}</Grid>
<Tracks />
</Grid>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'

export const useStyles = makeStyles((_: Theme) =>
createStyles({
root: {
marginTop: 70,
},
gridContainer: {
marginTop: 30,
},
gridItem: {
marginBottom: 30,
marginRight: 80,
},
gridItemSelected: {
marginBottom: 30,
marginRight: 80,
background: 'linear-gradient(to left, #F4F4F4 73.44%, rgba(248, 248, 248, 0) 100%)',
borderRadius: 8,
},
header: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 24,
textAlign: 'left',
color: '#000000',
},
title: {
'&:hover': {
textDecorationLine: 'underline',
color: '#000000',
},
cursor: 'pointer',
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 500,
fontSize: 16,
textAlign: 'left',
color: '#000000',
},
subtitle: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 500,
fontSize: 16,
textAlign: 'left',
marginTop: 2,
color: '#9F9F9F',
},
cover: {
width: 105,
height: 105,
borderRadius: 3,
marginRight: 20,
},
}))
59 changes: 59 additions & 0 deletions trach/src/components/auth/ReportComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import { Box, TextField, Typography } from '@material-ui/core';
import { Grid } from '@material-ui/core';

export type ReportProps = {
isOpen: boolean
track: string
isError: boolean
onClose: () => void
onAuth?: () => void
onLogin: (text: string) => void
onPassword: (text: string) => void
}

export default function ReportComponent(props: ReportProps) {
return (
<Dialog open={props.isOpen} onClose={props.onClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Отчет</DialogTitle>
<DialogContent>
<DialogContentText>
<Typography component="div">
<Box fontSize="fontSize" m={1}>Чтобы скачать отчет по треку:</Box>
<Box fontSize="h6.fontSize" m={1}>{props.track}</Box>
<Box fontSize="fontSize" m={1}>Вам нужно авторизоваться</Box>
</Typography>
<Grid direction='column'>
<Grid item>
<TextField
label="Email"
type={'email'}
fullWidth
style={{ paddingBottom: 10 }}
onChange={(event) => props.onLogin(event.target.value)} />
</Grid>
<Grid item>
<TextField
error={props.isError}
fullWidth
label={props.isError ? "Неправильный логин или пароль" : "Password"}
type={'password'}
onChange={(event) => props.onPassword(event.target.value)} />
</Grid>
</Grid>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={props.onAuth} disabled={props.onAuth === undefined} color="primary">
Войти
</Button>
</DialogActions>
</Dialog>
);
}
189 changes: 189 additions & 0 deletions trach/src/components/breadcrumbs/BreadcrumbsComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { FlatButton } from '../core/FlatButton'
import { GeneralInfoTooltipComponent } from '../infotooltip/InfoTooltipComponent'
import { useStyles, InfoIcon, DownloadIcon } from './BreadcrumbsComponentStyles'
import DatePickerComponent from '../datepicker/DatePickerComponent'

import React from 'react'

import Grid from '@material-ui/core/Grid'
import Box from '@material-ui/core/Box'
import { useHistory } from 'react-router-dom'
import { HistoryRouter } from '../../core/Router'

export enum BreadcrumbsItemType {
Text = 'item_text',
SingleDate = 'item_single_date',
RangeDate = 'item_range_date',
}

export type BreadcrumbsTextItemType = {
type: BreadcrumbsItemType.Text
title: string
}

export type BreadcrumbsSingleDateItemType = {
type: BreadcrumbsItemType.SingleDate
title: string
initialDate: Date
onSelected: (date: Date) => void
}

export type DateProps = {
value: Date
onSelected: (date: Date) => void
minDate?: Date
maxDate?: Date
}

export type BreadcrumbsRangeDateItemType = {
type: BreadcrumbsItemType.RangeDate
title: string
startDate: DateProps
endDate: DateProps
}

export enum BreadcrumbsActiveState {
Left = "left_props",
Right = "right_props"
}

export type BreadcrumbsProps = {
active: BreadcrumbsActiveState
info?: string
leftTitle?: {
title: string
onSelected: () => void
}
rightTitle: {
onSelected: () => void
isDisabled?: boolean
item: BreadcrumbsTextItemType | BreadcrumbsSingleDateItemType | BreadcrumbsRangeDateItemType
}
onClickDownload?: (router: HistoryRouter) => void
}

export default function BreadcrumbsComponent(props: BreadcrumbsProps) {
const classes = useStyles()
const leftTextStyle = props.active === BreadcrumbsActiveState.Left ? classes.activeText : classes.inactiveText
const rightTextStyle = props.active === BreadcrumbsActiveState.Right ? classes.activeText : classes.inactiveText
const RightDateNode = () => {
switch (props.rightTitle.item.type) {
case BreadcrumbsItemType.SingleDate:
return (
<Grid item>
<DatePickerComponent disabled={props.active === BreadcrumbsActiveState.Left} {...props.rightTitle.item} />
</Grid>
)

case BreadcrumbsItemType.RangeDate:
return (
<Grid item container direction="row" alignItems="center">
<Grid className={classes.leftSpace} item>
<DatePickerComponent
minDate={props.rightTitle.item.startDate.minDate}
maxDate={props.rightTitle.item.startDate.maxDate}
initialDate={props.rightTitle.item.startDate.value}
onSelected={props.rightTitle.item.startDate.onSelected} />
</Grid>
<Grid item>
<span className={classes.separatorDateText}></span>
</Grid>
<Grid item>
<DatePickerComponent
minDate={props.rightTitle.item.endDate.minDate}
maxDate={props.rightTitle.item.endDate.maxDate}
initialDate={props.rightTitle.item.endDate.value}
onSelected={props.rightTitle.item.endDate.onSelected} />
</Grid>
</Grid>
)

default:
return null
}
}
const DownloadIconNode = () => {
const history: HistoryRouter = useHistory()
if (props.onClickDownload !== undefined) {
return (
<Grid xs container item justify="flex-end">
<FlatButton className={classes.download} onClick={() => {
if (props.onClickDownload !== undefined) {
props.onClickDownload(history)
}
}}>
<DownloadIcon />
</FlatButton>
</Grid>
)
}
else {
return null
}
}
const RightItemNode = () => {
if (props.rightTitle.isDisabled !== undefined && props.rightTitle.isDisabled === true) {
return (
<GeneralInfoTooltipComponent
text="К сожалению это пока не реализовано, мы работаем над исправлением этой проблемы"
showInElement={
<FlatButton className={rightTextStyle} onClick={props.rightTitle.onSelected}>
{props.rightTitle.item.title}
</FlatButton>
} />
)
}
return (
<FlatButton className={rightTextStyle} onClick={props.rightTitle.onSelected}>
{props.rightTitle.item.title}
</FlatButton>
)
}
const LeftItemNode = () => {
if (props.leftTitle === undefined)
return null
return (
<Grid item>
<FlatButton className={leftTextStyle} onClick={props.leftTitle.onSelected}>
{props.leftTitle.title}
</FlatButton>
</Grid>
)
}
const SlashNode = () => {
if (props.leftTitle === undefined)
return null
return (
<Grid item className={classes.inactiveText}>/</Grid>
)
}
const InfoNode = () => {
if (props.info === undefined)
return null
return (
<Grid item>
<GeneralInfoTooltipComponent
text={props.info}
showInElement={
<Box className={classes.infoIcon}>
<InfoIcon />
</Box>
} />
</Grid>
)
}
return (
<Grid className={classes.root} container direction="row" alignItems="center">
<LeftItemNode />
<SlashNode />
<Grid item>
<RightItemNode />
</Grid>
<Grid item>
<RightDateNode />
</Grid>
<InfoNode />
<DownloadIconNode />
</Grid>
)
}
70 changes: 70 additions & 0 deletions trach/src/components/breadcrumbs/BreadcrumbsComponentStyles.tsx

Large diffs are not rendered by default.

63 changes: 63 additions & 0 deletions trach/src/components/choicegenres/ChoiceGenresComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useStyles } from './ChoiceGenresComponentStyles'

import React from 'react'

import Autocomplete from '@material-ui/lab/Autocomplete'
import TextField from '@material-ui/core/TextField';
import Chip from '@material-ui/core/Chip';

export type ChoiceGenresProps = {
genres: Array<string>
onSelectedGenres: (genres: Array<string>) => void
}

function GenreTextFied(params: any) {
const classes = useStyles()
const newInputProps = {
classes: {
notchedOutline: classes.notchedOutline,
},
...params.InputProps
}
return (
<TextField
className={classes.root}
id="standard-name"
placeholder="Выберите жанры"
{...params}
InputProps={{ disableUnderline: true, ...newInputProps }}
/>
)
}

export default function ChoiceGenresComponent(props: ChoiceGenresProps) {
return (
<Autocomplete<string>
multiple
freeSolo
id="genres_tags-filled"
options={props.genres}
renderTags={(value: string[], getTagProps) =>
value.map((option: string, index: number) =>
(<Chip
variant="outlined"
label={option}
style={{
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 16,
textAlign: 'left',
color: '#000000',
height:30
}}
{...getTagProps({ index })}
/>
))}
renderInput={(params) => GenreTextFied(params)}
onChange={(_: any, newValue: string[]) => {
props.onSelectedGenres(newValue)
}}
/>
)
}
Loading

0 comments on commit a4910a3

Please sign in to comment.