Ну вроде нормально
This commit is contained in:
parent
96f318a306
commit
027f4b4405
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
# Используем базовый образ Node.js
|
||||
FROM node:14-alpine
|
||||
|
||||
# Устанавливаем рабочую директорию
|
||||
WORKDIR /
|
||||
|
||||
# Копируем файлы package.json и package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN npm ci
|
||||
|
||||
# Копируем остальные файлы
|
||||
COPY . .
|
||||
|
||||
# Устанавливаем TypeScript и другие зависимости
|
||||
RUN npm install -g typescript @types/node ts-node
|
||||
|
||||
# Запускаем приложение
|
||||
CMD npm run start
|
14
Dockerfile.json-server
Normal file
14
Dockerfile.json-server
Normal file
@ -0,0 +1,14 @@
|
||||
# Используем базовый образ Node.js
|
||||
FROM node:14-alpine
|
||||
|
||||
# Устанавливаем рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файл data.json
|
||||
COPY data.json .
|
||||
|
||||
# Устанавливаем json-server
|
||||
RUN npm install -g json-server
|
||||
|
||||
# Запускаем JSON Server
|
||||
CMD ["npm", "run", "json-server"]
|
28
README.md
28
README.md
@ -4,3 +4,31 @@
|
||||
### Описание
|
||||
|
||||
Веб-приложение для прослушивания музыки.
|
||||
|
||||
### Технологии
|
||||
* React
|
||||
* Typescript
|
||||
* React Router DOM
|
||||
* Ant Design
|
||||
* Styled Components
|
||||
* Tailwind CSS
|
||||
* Json Server
|
||||
|
||||
### Что было реализовано
|
||||
|
||||
* Регистрация и авторизация с сохранением данных в localStorage
|
||||
* Сайдбар
|
||||
* Прослушивание музыки
|
||||
* Поиск музыки
|
||||
* Страница группы
|
||||
* Страница альбома
|
||||
* Страница пользователя с добавленными треками
|
||||
|
||||
### Как запустить
|
||||
|
||||
`npm install`
|
||||
`npm run start` - запустить веб-приложение на порту 5000
|
||||
|
||||
### Как запустить json-server
|
||||
|
||||
`npm run json-server`
|
332
data.json
332
data.json
@ -19,8 +19,8 @@
|
||||
{
|
||||
"id": "2",
|
||||
"song_name": "Song 3",
|
||||
"band_id": "1",
|
||||
"band_name": "Nevroz",
|
||||
"band_id": "2",
|
||||
"band_name": "Blur",
|
||||
"albumid": "1",
|
||||
"album_name": "Album 1",
|
||||
"source": "http://localhost:3001/songs_sources/blur.mp3",
|
||||
@ -34,15 +34,15 @@
|
||||
{
|
||||
"id": "3",
|
||||
"song_name": "Ругань из-за Стёпы",
|
||||
"band_id": "2",
|
||||
"band_name": "noizemchik",
|
||||
"band_id": "3",
|
||||
"band_name": "noize mc",
|
||||
"albumid": "2",
|
||||
"album_name": "Ругань из-за Стёпы",
|
||||
"source": "http://localhost:3001/songs_sources/noizemc.mp3",
|
||||
"cover": "https://sun9-11.userapi.com/impg/yfCwuWXI6NkFVC2HvMlegM2qWLlenkeiiRyvNQ/jpH8m1wlLqs.jpg?size=604x604&quality=95&sign=60a1d1877b49631fb6078db88715fc88&c_uniq_tag=UKrig2vg02reyBH_ML0OKeqt5gm_2jpwEXD7NJBUXoQ&type=album"
|
||||
,
|
||||
"cover": "https://sun9-11.userapi.com/impg/yfCwuWXI6NkFVC2HvMlegM2qWLlenkeiiRyvNQ/jpH8m1wlLqs.jpg?size=604x604&quality=95&sign=60a1d1877b49631fb6078db88715fc88&c_uniq_tag=UKrig2vg02reyBH_ML0OKeqt5gm_2jpwEXD7NJBUXoQ&type=album",
|
||||
"playlists": [
|
||||
"1", "3"
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
@ -50,14 +50,15 @@
|
||||
{
|
||||
"id": "4",
|
||||
"song_name": "Хот вилз",
|
||||
"band_id": "1",
|
||||
"band_id": "5",
|
||||
"band_name": "DSPD",
|
||||
"albumid": "1",
|
||||
"album_name": "Album 1",
|
||||
"source": "http://localhost:3001/songs_sources/hotweelz.mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
|
||||
"playlists": [
|
||||
"1", "2"
|
||||
"1",
|
||||
"2"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
@ -65,7 +66,7 @@
|
||||
{
|
||||
"id": "5",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_id": "5",
|
||||
"band_name": "DSPD",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
@ -87,21 +88,21 @@
|
||||
"source": "http://localhost:3001/songs_sources/greenday.mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/32236/d3846188.a.1001691-1/m1000x1000?webp=false",
|
||||
"playlists": [
|
||||
"2", "3"
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"song_name": "Ulyanofication'",
|
||||
"song_name": "Ulyanofication",
|
||||
"band_id": "1",
|
||||
"band_name": "Yellow Warm Russian Tomatoes",
|
||||
"band_name": "RHCP",
|
||||
"albumid": "1",
|
||||
"album_name": "Album 1",
|
||||
"source": "http://localhost:3001/songs_sources/pepers.mp3",
|
||||
"cover": "https://avatars.mds.yandex.net/i?id=8d01018aa8cac0da9845314da42e050ab8e61713-12569754-images-thumbs&n=13",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/42108/916a66bd.a.21862-1/m1000x1000",
|
||||
"playlists": [
|
||||
"2"
|
||||
],
|
||||
@ -111,15 +112,15 @@
|
||||
{
|
||||
"id": "8",
|
||||
"song_name": "СУМАСШЕДШИЙ ПОЕЗД!!!!",
|
||||
"band_id": "2",
|
||||
"band_name": "СТАРИНА ОЗЗИ ОЗБОРН (МУЗЫКА ДЬЯВОЛА)",
|
||||
"band_id": "4",
|
||||
"band_name": "Ozzy Osbourne",
|
||||
"albumid": "2",
|
||||
"album_name": "Ругань из-за Стёпы",
|
||||
"source": "http://localhost:3001/songs_sources/ozzy.mp3",
|
||||
"cover": "https://avatars.mds.yandex.net/i?id=95adb2fda305e542621b7d9757d70ddd_l-4937470-images-thumbs&n=13"
|
||||
,
|
||||
"cover": "https://avatars.mds.yandex.net/i?id=95adb2fda305e542621b7d9757d70ddd_l-4937470-images-thumbs&n=13",
|
||||
"playlists": [
|
||||
"1", "3"
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
@ -132,9 +133,10 @@
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "http://localhost:3001/songs_sources/acdc.mp3",
|
||||
"cover": "https://avatars.mds.yandex.net/get-entity_search/509339/292583724/S600xU",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/13663712/7796821b.a.33691089-1/m1000x1000",
|
||||
"playlists": [
|
||||
"2", "3"
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
@ -147,7 +149,7 @@
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "http://localhost:3001/songs_sources/kiss.mp3",
|
||||
"cover": "https://avatars.dzeninfra.ru/get-zen_doc/10073791/pub_64ab1cf9ec43747a7a73a3e0_64ab1d726ba4132c8f8b37b6/scale_1200",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/2810397/594156f4.a.85617-3/m1000x1000",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
@ -156,13 +158,13 @@
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"song_name": "Танцевать (feat. мухи мрут)",
|
||||
"band_id": "5",
|
||||
"band_name": "DSPD",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "http://localhost:3001/songs_sources/kemstal.mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
|
||||
"source": "http://localhost:3001/songs_sources/dance.mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10930741/76e980d5.a.27465446-1/m1000x1000",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
@ -171,13 +173,13 @@
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_name": "DSPD",
|
||||
"song_name": "Небо падает вниз",
|
||||
"band_id": "6",
|
||||
"band_name": "нет пути",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "/src/songs/DSPD feat. даня хренников - Кем я стал_(audio-lord.ru).mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
|
||||
"source": "http://localhost:3001/songs_sources/noway.mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/8096180/0c820616.a.25211644-1/m1000x1000",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
@ -186,13 +188,13 @@
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_name": "DSPD",
|
||||
"song_name": "Гаснет пламя",
|
||||
"band_id": "7",
|
||||
"band_name": "Crazy Boy",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "/src/songs/DSPD feat. даня хренников - Кем я стал_(audio-lord.ru).mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
|
||||
"source": "http://localhost:3001/songs_sources/flame.mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/6255016/09ca7466.a.23604055-1/m1000x1000",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
@ -201,140 +203,19 @@
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_name": "DSPD",
|
||||
"song_name": "Ферстлав",
|
||||
"band_id": "7",
|
||||
"band_name": "Crazy Boy",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "/src/songs/DSPD feat. даня хренников - Кем я стал_(audio-lord.ru).mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
|
||||
"source": "http://localhost:3001/songs_sources/firstlove.mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/6214856/ae258804.a.23324210-1/m1000x1000",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_name": "DSPD",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "/src/songs/DSPD feat. даня хренников - Кем я стал_(audio-lord.ru).mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_name": "DSPD",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "/src/songs/DSPD feat. даня хренников - Кем я стал_(audio-lord.ru).mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_name": "DSPD",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "/src/songs/DSPD feat. даня хренников - Кем я стал_(audio-lord.ru).mp3",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
},
|
||||
{
|
||||
"id": "20",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_name": "DSPD",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "source_path",
|
||||
"cover": "https://sun9-11.userapi.com/impg/yfCwuWXI6NkFVC2HvMlegM2qWLlenkeiiRyvNQ/jpH8m1wlLqs.jpg?size=604x604&quality=95&sign=60a1d1877b49631fb6078db88715fc88&c_uniq_tag=UKrig2vg02reyBH_ML0OKeqt5gm_2jpwEXD7NJBUXoQ&type=album",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
},
|
||||
{
|
||||
"id": "21",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_name": "DSPD",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "source_path",
|
||||
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
},
|
||||
{
|
||||
"id": "22",
|
||||
"song_name": "Кем я стал",
|
||||
"band_id": "3",
|
||||
"band_name": "DSPD",
|
||||
"albumid": "3",
|
||||
"album_name": "Album 3",
|
||||
"source": "source_path",
|
||||
"cover": "https://sun9-11.userapi.com/impg/yfCwuWXI6NkFVC2HvMlegM2qWLlenkeiiRyvNQ/jpH8m1wlLqs.jpg?size=604x604&quality=95&sign=60a1d1877b49631fb6078db88715fc88&c_uniq_tag=UKrig2vg02reyBH_ML0OKeqt5gm_2jpwEXD7NJBUXoQ&type=album",
|
||||
"playlists": [
|
||||
"3"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
},
|
||||
{
|
||||
"id": "23",
|
||||
"song_name": "Song 3",
|
||||
"band_id": "1",
|
||||
"band_name": "Nevroz",
|
||||
"albumid": "1",
|
||||
"album_name": "Album 1",
|
||||
"source": "source_path",
|
||||
"cover": "https://cdn1.ozone.ru/s3/multimedia-t/6893834213.jpg",
|
||||
"playlists": [
|
||||
"2"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
},
|
||||
{
|
||||
"id": "24",
|
||||
"song_name": "Song 3",
|
||||
"band_id": "1",
|
||||
"band_name": "Nevroz",
|
||||
"albumid": "1",
|
||||
"album_name": "Album 1",
|
||||
"source": "source_path",
|
||||
"cover": "https://cdn1.ozone.ru/s3/multimedia-t/6893834213.jpg",
|
||||
"playlists": [
|
||||
"2"
|
||||
],
|
||||
"genreid": "1",
|
||||
"genre_name": "Rock"
|
||||
}
|
||||
|
||||
],
|
||||
"albums": [
|
||||
{
|
||||
@ -345,7 +226,8 @@
|
||||
"genres": [
|
||||
"Post-Punk",
|
||||
"Indie"
|
||||
]
|
||||
],
|
||||
"bandid": "1"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
@ -355,7 +237,8 @@
|
||||
"genres": [
|
||||
"Post-Punk",
|
||||
"Indie"
|
||||
]
|
||||
],
|
||||
"bandid": "5"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
@ -365,10 +248,10 @@
|
||||
"genres": [
|
||||
"Post-Punk",
|
||||
"Punk"
|
||||
]
|
||||
],
|
||||
"bandid": "3"
|
||||
}
|
||||
],
|
||||
|
||||
"bands": [
|
||||
{
|
||||
"id": "1",
|
||||
@ -381,11 +264,24 @@
|
||||
"Indie"
|
||||
],
|
||||
"description": "Nevroz — это коллектив, созданный в 2018 году в Москве. Группа специализируется на написании песен на русском языке и исполнении в жанре Post-Punk и Indie. В состав коллектива входят вокалистка, басист, гитарист и ударник. Несколько песен группы стали популярными в интернете, а также коллектив неоднократно выступал на различных мероприятиях в Москве.",
|
||||
"photo": "photo_path"
|
||||
"photo": "https://avatars.yandex.net/get-music-content/5496390/06f54371.p.9262/m1000x1000"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Blur",
|
||||
"city": "Лондон",
|
||||
"country": "Великобритания",
|
||||
"years": "1998-",
|
||||
"genres": [
|
||||
"Rock",
|
||||
"Punk"
|
||||
],
|
||||
"description": "Blur — это британская рок-группа, сформированная в 1988 году в Лондоне. Группа является одной из основных групп бритпоп-движения, она выпустила восемь студийных альбомов, имеет множество хитов и является одной из самых успешных британских рок-групп 1990-х годов.",
|
||||
"photo": "https://avatars.yandex.net/get-music-content/32236/7b077fe9.p.36807/m1000x1000"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "noizemchik",
|
||||
"name": "noize mc",
|
||||
"city": "Калининград",
|
||||
"country": "Россия",
|
||||
"years": "2020-",
|
||||
@ -394,27 +290,78 @@
|
||||
"Punk"
|
||||
],
|
||||
"description": "noizemchik — это коллектив, созданный в 2020 году в Калининграде. Группа специализируется на написании песен на русском языке и исполнении в жанре Rock и Панк. В состав коллектива входят вокалист, гитарист, басист и барабанщик. Несколько песен группы стали популярными в интернете, а также коллектив неоднократно выступал на различных мероприятиях в Калининграде и других городах.",
|
||||
"photo": "photo_path"
|
||||
"photo": "https://avatars.yandex.net/get-music-content/10103188/57ac2da0.p.160970/m1000x1000"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "Ozzy Osbourne",
|
||||
"city": "Москва",
|
||||
"country": "Россия",
|
||||
"years": "1980-",
|
||||
"genres": [
|
||||
"Rock",
|
||||
"Punk"
|
||||
],
|
||||
"description": "Старый добрый Оззи Озборн ОУУУУ ДААА рокенроллллл",
|
||||
"photo": "https://avatars.yandex.net/get-music-content/42108/d803cb79.p.3274/m1000x1000"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"name": "DSPD",
|
||||
"city": "Москва",
|
||||
"city": "Ульяновск",
|
||||
"country": "Россия",
|
||||
"years": "2019-",
|
||||
"genres": [
|
||||
"Rock",
|
||||
"Punk"
|
||||
],
|
||||
"description": "DSPD — это коллектив, созданный в 2019 году в Москве. Группа специализируется на написании песен на русском языке и исполнении в жанре Rock и Панк. В состав коллектива входят вокалист, гитарист, басист и барабанщик. Несколько песень группы стали популярными в интернете, а также коллектив неоднократно выступал на различных мероприятиях в Москве и других городах.",
|
||||
"photo": "photo_path"
|
||||
"description": "DSPD — это коллектив, созданный в 2019 году в Ульяновске, изначально состоящий из одного человека: 16-летнего Долгова Дмитрия. Группа специализируется на написании песен на русском языке и исполнении в жанре Рок и Поп-панк. Сейчас в состав коллектива входят вокалист (Дима) и гитарист (Даня). А еще Дима сделал этот курсач!",
|
||||
"photo": "https://sun9-12.userapi.com/impg/KdOpbLpgHbAGso6x6yYt9hcPOyNS15ru4twqZg/FXY8tAEOSwo.jpg?size=1280x1280&quality=95&sign=13979d395da57623ee370d413d24dac3&type=album"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"name": "нет пути",
|
||||
"city": "Ульяновск",
|
||||
"country": "Россия",
|
||||
"years": "2023-2023",
|
||||
"genres": [
|
||||
"Rock",
|
||||
"Punk"
|
||||
],
|
||||
"description": "нет пути - это группа, записавшая всего одну песню, а все потому, что Даня и Витя не сошлись во вкусах. Теперь Даня в группе DSPD у Димы!!!",
|
||||
"photo": "https://sun9-80.userapi.com/impg/ZUXnTxRO7KtApWirBnAxpph9zD1uF5O1ZI3FBA/uy5ZcMBRvuM.jpg?size=1000x1000&quality=95&sign=3ef9d90bc8cfb2307e7f90b820dc2cb2&type=album"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"name": "Crazy Boy",
|
||||
"city": "Ульяновск",
|
||||
"country": "Россия",
|
||||
"years": "2020-",
|
||||
"genres": [
|
||||
"Rock",
|
||||
"Punk"
|
||||
],
|
||||
"description": "Андрей Crazy Boy - молодой энтузиаст из Ульяновска, в котором так и кипит жажда деятельности. Пусть в его треках плохой текст, рифма, да и вообще всё что угодно - он делает это от души.....",
|
||||
"photo": "https://sun9-62.userapi.com/impg/3orK7BWBrArY5ogpYj6mdvaodhKnUte2o3p41A/xFTEOb8cHl4.jpg?size=2560x1440&quality=95&sign=9d4856fbdf1defa41a94207ea3985e0c&type=album"
|
||||
}
|
||||
],
|
||||
"genres": [
|
||||
{"id": "1", "name": "Post-Punk"},
|
||||
{"id": "2", "name": "Rock"},
|
||||
{"id": "3", "name": "Punk"},
|
||||
{"id": "4", "name": "Indie"}
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Post-Punk"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Rock"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "Punk"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "Indie"
|
||||
}
|
||||
],
|
||||
"playlists": [
|
||||
{
|
||||
@ -427,7 +374,6 @@
|
||||
"Post-Punk",
|
||||
"Indie"
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
@ -438,7 +384,6 @@
|
||||
"genres": [
|
||||
"Rock"
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
@ -449,7 +394,6 @@
|
||||
"genres": [
|
||||
"Indie"
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
@ -461,7 +405,6 @@
|
||||
"Post-Punk",
|
||||
"Indie"
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
@ -472,7 +415,6 @@
|
||||
"genres": [
|
||||
"Post-Punk"
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
@ -484,19 +426,32 @@
|
||||
"Rock",
|
||||
"Indie"
|
||||
]
|
||||
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Амид Девилинкович",
|
||||
"email": "email",
|
||||
"email": "email@mail.ru",
|
||||
"password": "password",
|
||||
"photo": "photo_path",
|
||||
"favorite_songs": [
|
||||
"1", "2", "3"
|
||||
]
|
||||
"photo": "https://steamuserimages-a.akamaihd.net/ugc/5072773634763560590/7D8EFFF884BAD1D365C5B05FBB77F7E1B2E1655D/?imw=512&imh=288&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true",
|
||||
"favorite_songs": []
|
||||
},
|
||||
{
|
||||
"id": "e01d",
|
||||
"name": "Аришинскаяя",
|
||||
"email": "arina.gracheva.2015@gmail.com",
|
||||
"password": "i6d-3JV-mug-iL3",
|
||||
"photo": "https://i.ytimg.com/vi/bAUDgdDX034/maxresdefault.jpg",
|
||||
"favorite_songs": []
|
||||
},
|
||||
{
|
||||
"id": "d9f6",
|
||||
"name": "Кукумбер Кукумберович",
|
||||
"email": "cucumber@gmail.com",
|
||||
"password": "cucumber",
|
||||
"photo": "https://main-cdn.sbermegamarket.ru/big2/hlr-system/-21/401/557/724/261/38/100050923600b0.png",
|
||||
"favorite_songs": []
|
||||
}
|
||||
],
|
||||
"advertisements": [
|
||||
@ -505,6 +460,5 @@
|
||||
"name": "Advert 1",
|
||||
"photo": "https://sun9-15.userapi.com/impg/5IS2PMmTs9YGUuo2CGNDm3KxKbe6T7205_PT6A/-VHlhuPWlGk.jpg?size=1640x856&quality=95&sign=77953363d2f78b014aff77f540f7bfa0&type=album"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@ -0,0 +1,15 @@
|
||||
services:
|
||||
frontend:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./node_modules:/app/node_modules
|
||||
|
||||
json-server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.json-server
|
||||
ports:
|
||||
- "3001:3001"
|
143
package-lock.json
generated
143
package-lock.json
generated
@ -22,12 +22,15 @@
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"antd": "^5.20.1",
|
||||
"axios": "^1.7.4",
|
||||
"body-parser": "^1.20.3",
|
||||
"node-fetch": "^2.6.7",
|
||||
"react": "^18.3.1",
|
||||
"react-audio-player-component": "^1.2.4",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dotenv": "^0.1.3",
|
||||
"react-easy-crop": "^5.1.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"styled-components": "^6.1.12",
|
||||
@ -48,7 +51,9 @@
|
||||
"@storybook/react-webpack5": "^8.2.9",
|
||||
"@storybook/test": "^8.2.9",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/react-avatar-editor": "^13.0.3",
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"storybook": "^8.2.9",
|
||||
@ -9512,6 +9517,16 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-avatar-editor": {
|
||||
"version": "13.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-avatar-editor/-/react-avatar-editor-13.0.3.tgz",
|
||||
"integrity": "sha512-icRAOKLKjkIsExFAiFSquztByJwpyTKEgnBRYSuLG2V81bM3LtQZi7hRS+Hr+4AXreq0yNRjVZiMOVeEeh6DLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
|
||||
@ -9531,6 +9546,16 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-helmet": {
|
||||
"version": "6.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz",
|
||||
"integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-transition-group": {
|
||||
"version": "4.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
|
||||
@ -11323,9 +11348,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
@ -11336,7 +11361,7 @@
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
@ -11382,6 +11407,21 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/bonjour-service": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz",
|
||||
@ -15791,6 +15831,39 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@ -15800,6 +15873,18 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@ -21373,6 +21458,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-wheel": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
|
||||
"integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
@ -24971,6 +25062,20 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-easy-crop": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.1.0.tgz",
|
||||
"integrity": "sha512-UsYeF/N7zoqtfOSD+2xSt1nRaoBYCI2YLkzmq+hi+aVepS4/bAMhbrLwJtDAP60jsVzWRiQCX7JG+ZtfWcHsiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"tslib": "^2.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.4.0",
|
||||
"react-dom": ">=16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-element-to-jsx-string": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz",
|
||||
@ -25000,6 +25105,12 @@
|
||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-google-recaptcha": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz",
|
||||
@ -25013,6 +25124,21 @@
|
||||
"react": ">=16.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-helmet": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
|
||||
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.1.1",
|
||||
"react-side-effect": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
@ -25142,6 +25268,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-side-effect": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
|
||||
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
|
@ -17,12 +17,15 @@
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"antd": "^5.20.1",
|
||||
"axios": "^1.7.4",
|
||||
"body-parser": "^1.20.3",
|
||||
"node-fetch": "^2.6.7",
|
||||
"react": "^18.3.1",
|
||||
"react-audio-player-component": "^1.2.4",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dotenv": "^0.1.3",
|
||||
"react-easy-crop": "^5.1.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"styled-components": "^6.1.12",
|
||||
@ -76,7 +79,9 @@
|
||||
"@storybook/react-webpack5": "^8.2.9",
|
||||
"@storybook/test": "^8.2.9",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/react-avatar-editor": "^13.0.3",
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"storybook": "^8.2.9",
|
||||
|
BIN
public/default-avatar.png
Normal file
BIN
public/default-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
BIN
public/songs_sources/dance.mp3
Normal file
BIN
public/songs_sources/dance.mp3
Normal file
Binary file not shown.
BIN
public/songs_sources/firstlove.mp3
Normal file
BIN
public/songs_sources/firstlove.mp3
Normal file
Binary file not shown.
BIN
public/songs_sources/flame.mp3
Normal file
BIN
public/songs_sources/flame.mp3
Normal file
Binary file not shown.
BIN
public/songs_sources/noway.mp3
Normal file
BIN
public/songs_sources/noway.mp3
Normal file
Binary file not shown.
BIN
public/user-avatars/default-avatar.png
Normal file
BIN
public/user-avatars/default-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
@ -1,5 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { IAdvertisement, IGenre, IPlaylist, ISong } from "../models/IModels";
|
||||
import { IAdvertisement, IGenre, IPlaylist, ISong, IAlbum } from "../models/IModels";
|
||||
|
||||
const localhost = process.env.REACT_APP_DATABASE;
|
||||
|
||||
@ -58,7 +58,67 @@ export async function getBand(id: string) {
|
||||
return await axios.get(`${localhost}bands/` + id);
|
||||
}
|
||||
|
||||
export async function getAlbumsByBand(id: string) {
|
||||
const response = await getAlbums();
|
||||
return response.data.filter((album: IAlbum) => album.bandid === id);
|
||||
}
|
||||
|
||||
export async function getSongsByBand(id: string) {
|
||||
const response = await getSongs();
|
||||
return response.data.filter((song: ISong) => song.band_id === id);
|
||||
}
|
||||
|
||||
export async function getBands() {
|
||||
|
||||
return await axios.get(`${localhost}bands/`);
|
||||
}
|
||||
|
||||
export async function getUser(id: string){
|
||||
return await axios.get( `${localhost}users/${id}`);
|
||||
}
|
||||
|
||||
export async function getUsers(){
|
||||
return await axios.get( `${localhost}users`);
|
||||
}
|
||||
|
||||
export async function getUserByEmail(email: string){
|
||||
return await axios.get( `${localhost}users?email=${email}`);
|
||||
}
|
||||
|
||||
export async function getFavoriteSongs(userid: string) {
|
||||
const response = await getUser(userid);
|
||||
const user = response.data;
|
||||
const favoriteSongs = await Promise.all(user.favorite_songs.map((songId: string) => getSong(songId)));
|
||||
return favoriteSongs.map(songResponse => songResponse.data);
|
||||
}
|
||||
|
||||
export async function createUser(user: any) {
|
||||
return await axios.post(`${localhost}users`, user);
|
||||
}
|
||||
|
||||
export async function updateUser(user: any) {
|
||||
console.log(`put ${localhost}users/${user.id}`, user);
|
||||
return await axios.put(`${localhost}users/${user.id}`, user);
|
||||
}
|
||||
|
||||
export async function addFavoriteSong(userId: string, songId: string) {
|
||||
let user = await (await getUser(userId)).data;
|
||||
user.favorite_songs.push(songId);
|
||||
return await axios.put(`${localhost}users/${userId}`, user);
|
||||
}
|
||||
|
||||
export async function removeFavoriteSong(userId: string, songId: string) {
|
||||
let user = await (await getUser(userId)).data;
|
||||
user.favorite_songs = user.favorite_songs.filter((song: string) => song !== songId);
|
||||
return await axios.put(`${localhost}users/${userId}`, user);
|
||||
}
|
||||
|
||||
export async function filterSongsByName(name: string) {
|
||||
const response = await getSongs();
|
||||
return response.data.filter((song: ISong) => song.song_name.toLowerCase().includes(name.toLowerCase()));
|
||||
}
|
||||
|
||||
export async function filterSongsByGenre(genre: string) {
|
||||
const response = await getSongs();
|
||||
return response.data.filter((song: ISong) => song.genre_name.includes(genre));
|
||||
}
|
17
src/App.tsx
17
src/App.tsx
@ -13,6 +13,9 @@ import { VolumeProvider } from './contexts/VolumeContexts/VolumeProvider';
|
||||
import { SideBlockProvider } from './contexts/SideBlockContexts/SideBlockProvider';
|
||||
import { useState } from 'react';
|
||||
import { IAdvertisement } from './models/IModels';
|
||||
import { BandPage } from './pages/BandPage';
|
||||
import { CurrentPlaylistProvider } from './contexts/SongContexts/CurrentPlaylistProvider';
|
||||
import { AlbumPage } from './pages/AlbumPage';
|
||||
|
||||
function App() {
|
||||
|
||||
@ -20,20 +23,32 @@ function App() {
|
||||
<div className="App bg-slate-100">
|
||||
<div id="app" className='bg-white'>
|
||||
<CurrentSongProvider songId=''>
|
||||
<CurrentPlaylistProvider playlist={[]}>
|
||||
<PlayingProvider isPlaying={false}>
|
||||
<VolumeProvider volumeValue={30}>
|
||||
<SideBlockProvider>
|
||||
<Routes>
|
||||
<Route path='/' element={<Layout />}>
|
||||
|
||||
<Route index element={<Homepage />} />
|
||||
<Route path='login' element={<Loginpage />} />
|
||||
<Route path='profile' element={<Profilepage />} />
|
||||
<Route path='register' element={<Registerpage />} />
|
||||
<Route path='profile' element={<Profilepage />}>
|
||||
</Route>
|
||||
<Route path='bands' element={<BandPage />} >
|
||||
<Route path=':bandId' element={<BandPage />} />
|
||||
</Route>
|
||||
<Route path='albums' element={<AlbumPage />}>
|
||||
<Route path=':albumId' element={<AlbumPage />} />
|
||||
</Route>
|
||||
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
</SideBlockProvider>
|
||||
</VolumeProvider>
|
||||
</PlayingProvider>
|
||||
</CurrentPlaylistProvider>
|
||||
</CurrentSongProvider>
|
||||
</div>
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
import React, { ReactElement, useEffect } from "react";
|
||||
import { IAdvertisement, IAlbum, IBand, ISong } from "../../models/IModels";
|
||||
import { AdCard } from "../cardComponents/AdCard";
|
||||
import { getAds } from "../../API/api";
|
||||
import { useSideBlockContext } from '../../contexts/SideBlockContexts/SideBlockProvider';
|
||||
import { InformationTemplate } from "../menuComponents/Templates/informationTemplate";
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { Avatar, Button } from 'antd';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export function Header() {
|
||||
const defaultAvatar = 'http://localhost:3001/default-avatar.png';
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 border-b bg-gradient-to-bl from-red-900 to-red-600 border-solid border-2 border-slate-100">
|
||||
<Link to='/' className="text-3xl" style={{ fontFamily: 'Permanent Marker' }}>😈 DEVIL music</Link>
|
||||
<Link to="/login" style={{ width: 30, height: 30 }}>
|
||||
<UserOutlined />
|
||||
<Link to={localStorage.getItem('user') ? '/profile' : '/login' } style={{ width: 30, height: 30 }}>
|
||||
{localStorage.getItem('user') ? <Avatar src={JSON.parse(localStorage.getItem('user') || '{}').photo || defaultAvatar} />
|
||||
: <UserOutlined /> }
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom';
|
||||
import { IPlaylist, ISong, IAlbum, IGenre, IAdvertisement } from '../../models/IModels';
|
||||
import { CurrentTrack } from '../songComponents/CurrentTrack';
|
||||
import { AdBlock } from './AdBlock';
|
||||
@ -10,17 +10,26 @@ import { Header } from './Header';
|
||||
import { MenuBlock } from './MenuBlock';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { usePlayingContext } from '../../contexts/SongContexts/PlayingProvider';
|
||||
|
||||
import { useCurrentSongContext } from '../../contexts/SongContexts/SongContextProvider';
|
||||
import { useCurrentPlaylistContext } from '../../contexts/SongContexts/CurrentPlaylistProvider';
|
||||
import { getSong } from '../../API/api';
|
||||
|
||||
export function Layout() {
|
||||
|
||||
return (
|
||||
const { songId } = useCurrentSongContext();
|
||||
const { playlist } = useCurrentPlaylistContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div id='main-content'>
|
||||
<Outlet />
|
||||
</div>
|
||||
<Footer /></>
|
||||
{songId && playlist.length > 0 && (
|
||||
<CurrentTrack song={playlist.find(s => s.id === songId) ?? playlist[0]} songs={playlist} />
|
||||
)}
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,12 @@ import { Tabs } from 'antd';
|
||||
import React from 'react';
|
||||
import { PlaylistsBlock } from '../menuComponents/PlaylistsBlock';
|
||||
import { IAlbum, IGenre, IPlaylist, ISong } from '../../models/IModels';
|
||||
import { SongsBlock } from '../menuComponents/NewSongsBlock';
|
||||
import { SongsBlock } from '../menuComponents/SongsBlock';
|
||||
import { FullPlaylistBlock } from '../menuComponents/FullPlaylistBlock';
|
||||
import { width } from '@mui/system';
|
||||
import TabPane from 'antd/es/tabs/TabPane';
|
||||
import Search from 'antd/es/input/Search';
|
||||
|
||||
|
||||
interface MenuBlockProps {
|
||||
playlists?: IPlaylist[],
|
||||
@ -20,7 +22,7 @@ export function MenuBlock({playlists, songs, albums, genres}: MenuBlockProps) {
|
||||
const newSongsTab = {
|
||||
label: 'Новинки',
|
||||
key: 'New',
|
||||
children: songs? <SongsBlock /> : null
|
||||
children: songs? <SongsBlock title='Самые горячие новинки'/> : null
|
||||
};
|
||||
const playlistsTab = {
|
||||
label: 'Плейлисты',
|
||||
@ -31,21 +33,21 @@ export function MenuBlock({playlists, songs, albums, genres}: MenuBlockProps) {
|
||||
const chartTab = {
|
||||
label: 'Чарт',
|
||||
key: 'Chart',
|
||||
children: songs? <SongsBlock charted /> : null
|
||||
children: songs? <SongsBlock title='Самые огненные треки' charted /> : null
|
||||
};
|
||||
|
||||
const recTab = {
|
||||
label: 'Рекомендации',
|
||||
key: 'Recomendations',
|
||||
children: songs? <SongsBlock /> : null
|
||||
children: songs? <SongsBlock title='Основано на ваших предпочтениях' /> : null
|
||||
};
|
||||
|
||||
const tabs = [newSongsTab, recTab, chartTab];
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex" style={{display: 'flex', alignSelf: 'flex-start', width: 'fit-content'}}>
|
||||
<Tabs
|
||||
|
||||
type="line"
|
||||
className='container flex justify-start menu-block'
|
||||
defaultActiveKey="New"
|
||||
@ -58,3 +60,4 @@ export function MenuBlock({playlists, songs, albums, genres}: MenuBlockProps) {
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IPlaylist } from "../../models/IModels";
|
||||
import { IAlbum, IPlaylist } from "../../models/IModels";
|
||||
|
||||
import { Navigation, Pagination, Scrollbar, A11y } from 'swiper/modules';
|
||||
|
||||
@ -11,8 +11,9 @@ import 'swiper/css/pagination';
|
||||
import 'swiper/css/scrollbar';
|
||||
import { table } from "console";
|
||||
import Title from "antd/es/typography/Title";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function PlaylistsBlock({playlists}: {playlists: IPlaylist[]}) {
|
||||
export function PlaylistsBlock({playlists, albums}: {playlists?: IPlaylist[], albums?: IAlbum[]}) {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto bg-slate-300" style={{height: 300, width: '75%'}}>
|
||||
@ -25,16 +26,32 @@ export function PlaylistsBlock({playlists}: {playlists: IPlaylist[]}) {
|
||||
scrollbar={{ draggable: true }}
|
||||
onSlideChange={() => console.log('slide change')}
|
||||
loop>
|
||||
{playlists.map((p: IPlaylist) => (
|
||||
{ playlists && playlists.map((p: IPlaylist) => (
|
||||
<SwiperSlide key={p.id}>
|
||||
<div id="playlists" className="flex items-center mx-auto justify-center">
|
||||
<div id="playlist">
|
||||
<img src={p.cover} alt={p.name} className="h-full w-auto rounded m-auto" />
|
||||
<Link to={`/playlists/${p.id}`}>
|
||||
<img src={p.cover} alt={p.name} className="h-full w-auto rounded m-auto" style={{ cursor: 'pointer'}} />
|
||||
</Link>
|
||||
</div>
|
||||
<Title level={5} className="text-lg text-center">{p.name}</Title>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
{ albums && albums.map((a: IAlbum) => (
|
||||
<SwiperSlide key={a.id}>
|
||||
<div id="playlists" className="flex items-center mx-auto justify-center">
|
||||
<div id="playlist">
|
||||
<Link to={`/albums/${a.id}`}>
|
||||
<img src={a.cover} alt={a.name} className="h-full w-auto rounded m-auto" style={{ cursor: 'pointer'}} />
|
||||
</Link>
|
||||
</div>
|
||||
<Title level={5} className="text-lg text-center">{a.name}</Title>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))
|
||||
|
||||
}
|
||||
</Swiper>
|
||||
|
||||
|
||||
|
@ -2,22 +2,27 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import {IAlbum, IBand, ISong} from '../../models/IModels';
|
||||
import axios from 'axios';
|
||||
import { Table, Button, Empty } from 'antd';
|
||||
import { Loading3QuartersOutlined, PlayCircleFilled, PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { getSongs, getBands, getAlbums } from '../../API/api';
|
||||
import { Table, Button, Empty, Dropdown } from 'antd';
|
||||
import { Loading3QuartersOutlined, PlayCircleFilled, PlayCircleOutlined, RadiusBottomleftOutlined } from '@ant-design/icons';
|
||||
import { getSongs, getBands, getAlbums, filterSongsByName, filterSongsByGenre } from '../../API/api';
|
||||
import Title from 'antd/es/typography/Title';
|
||||
import { GetColumns } from './Templates/songsTemplate';
|
||||
import { Spin } from "antd";
|
||||
import { useCurrentSongContext } from '../../contexts/SongContexts/SongContextProvider';
|
||||
import { usePlayingContext } from '../../contexts/SongContexts/PlayingProvider';
|
||||
import { useSideBlockContext } from '../../contexts/SideBlockContexts/SideBlockProvider';
|
||||
import { redirect, useNavigate } from 'react-router-dom';
|
||||
import Search from 'antd/es/input/Search';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
export function SongsBlock({charted = false}: {charted?: boolean}) {
|
||||
export function SongsBlock({title, trackList, charted = false}: {title: string, charted?: boolean, trackList?: ISong[]}) {
|
||||
|
||||
// Функция для генерации номеров строк
|
||||
const generateRowNumbers = (rows: any[]) =>
|
||||
rows.map((_: any, index: number) => ({ ..._, number: index + 1 }));
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [songs, setSongs] = useState<ISong[]>([]);
|
||||
const [bands, setBands] = useState<IBand[]>([]);
|
||||
const [albums, setAlbums] = useState<IAlbum[]>([]);
|
||||
@ -25,30 +30,58 @@ export function SongsBlock({charted = false}: {charted?: boolean}) {
|
||||
const currentSongId = useCurrentSongContext();
|
||||
const isPlaying = usePlayingContext();
|
||||
const { setContentObject } = useSideBlockContext();
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
|
||||
const songColumn = charted ? 2 : 1;
|
||||
const bandColumn = charted ? 3 : 2;
|
||||
const albumColumn = charted ? 4 : 3;
|
||||
|
||||
const fetchData = async () => {
|
||||
const responseSongs = getSongs();
|
||||
trackList ? setSongs(trackList) : setSongs((await getSongs()).data);
|
||||
trackList ? console.log(`trackList ${trackList.length > 0 ? 'has' : 'has no'} songs`) : console.log(`SongsBlock ${songs.length > 0 ? 'has' : 'has no'} songs`);
|
||||
const responseBands = getBands();
|
||||
const responseAlbums = getAlbums();
|
||||
|
||||
setSongs((await responseSongs).data);
|
||||
setBands((await responseBands).data);
|
||||
setAlbums((await responseAlbums).data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
setLoading(false);
|
||||
}, [trackList]);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
filterSongsByName(value)
|
||||
.then(res => setSongs(res));
|
||||
}
|
||||
|
||||
const search = <div className="flex" style={{width: '100%', display: 'flex', justifyContent: 'center', marginTop: 5}}>
|
||||
<Search placeholder="Поиск" onSearch={value => handleSearch(value)} style={{width: 200}}/>
|
||||
|
||||
</div>
|
||||
|
||||
console.log(`SongsBlock ${songs.length > 0 ? 'has' : 'has no'} songs`);
|
||||
return (
|
||||
<div className="song-block flex border-slate-100">
|
||||
|
||||
<Spin indicator={<Loading3QuartersOutlined spin style={{color: '#9d0000', fontSize: 30}} />} spinning={songs.length === 0}>
|
||||
<div className="song-block flex border-slate-100">
|
||||
<Helmet>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="msapplication-TileImage" content="/favicon.ico" />
|
||||
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||
</Helmet>
|
||||
<Spin spinning={loading} indicator={<Loading3QuartersOutlined spin style={{color: '#9d0000', fontSize: 30}} />} >
|
||||
<Table
|
||||
title={() =>
|
||||
<Title level={4}>
|
||||
{ charted ? <>Чарт: самые огненные треки</> : <>Горячие новинки </>}
|
||||
</Title>}
|
||||
<div className="flex" style={{ flexDirection: 'row', justifyContent: 'ontent-distribution'}}>
|
||||
<Title level={4} style={{width: '100%', display: 'flex', justifyContent: 'center', marginBottom: 5}}>
|
||||
{ title }
|
||||
</Title>
|
||||
{ search }
|
||||
</div>}
|
||||
style={{width: '100%', backgroundColor: 'transparent'}}
|
||||
className="songs-table menu-block"
|
||||
dataSource={
|
||||
@ -65,18 +98,18 @@ export function SongsBlock({charted = false}: {charted?: boolean}) {
|
||||
if (event.target instanceof HTMLTableCellElement && event.target.tagName === 'TD') {
|
||||
let song = songs.find((s: ISong) => s.id === record.id)
|
||||
switch (event.target.cellIndex) {
|
||||
|
||||
case 1:
|
||||
case songColumn:
|
||||
setContentObject(song);
|
||||
break;
|
||||
case 2:
|
||||
case bandColumn:
|
||||
setContentObject(bands.find((b: IBand) => b.id === song?.band_id));
|
||||
redirect(`/bands/${song?.band_id}`);
|
||||
break;
|
||||
case 3:
|
||||
case albumColumn:
|
||||
setContentObject(albums.find((a: IAlbum) => a.id === song?.albumid));
|
||||
redirect(`/albums/${song?.albumid}`);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
})}
|
@ -0,0 +1,25 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { IAlbum } from "../../../../models/IModels";
|
||||
import Text from 'antd/es/typography/Text';
|
||||
|
||||
export function AlbumInfoTemplate({Information, obj}: {Information: any, obj: IAlbum}) {
|
||||
return (
|
||||
<div className="flex container information" style={{ marginTop: 10, marginBottom: 10, display: 'flex', flexDirection: 'column', alignContent: 'left' }}>
|
||||
<Information style={{ marginLeft: 10, marginBottom: 20 }}>
|
||||
<Link to={`/albums/${obj.id}`} style={{ fontSize: 24, marginTop: 10, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{obj.name}
|
||||
</Link>
|
||||
</Information>
|
||||
<Information style={{ marginLeft: 10, overflow: 'hidden' }}>
|
||||
<Text style={{ fontSize: 20, marginTop: 10, whiteSpace: 'normal', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
Год выпуска: {obj.year}
|
||||
</Text>
|
||||
</Information>
|
||||
<Information>
|
||||
<Text style={{ fontSize: 18, marginTop: 10, whiteSpace: 'normal', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
Жанры: {obj.genres.join(', ')}
|
||||
</Text>
|
||||
</Information>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import { red } from "@mui/material/colors";
|
||||
import { IBand } from "../../../../models/IModels";
|
||||
import Text from 'antd/es/typography/Text';
|
||||
import { redirect } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function BandInfoTemplate({Information, obj}: {Information: any, obj: IBand}) {
|
||||
|
||||
return (
|
||||
<div className="flex container information" style={{ marginTop: 10, marginBottom: 10, display: 'flex', flexDirection: 'column', alignContent: 'left' }}>
|
||||
<Information style={{ marginLeft: 10, marginBottom: 20 }}>
|
||||
<Link to={`/bands/${obj.id}`} style={{ cursor: 'pointer', fontSize: 24, marginTop: 10, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{obj.name}
|
||||
</Link>
|
||||
</Information>
|
||||
<Information style={{ marginLeft: 10 }}>
|
||||
<Text style={{ marginLeft: 10, fontSize: 20, marginTop: 10, whiteSpace: 'normal', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
Информация:
|
||||
</Text>
|
||||
</Information>
|
||||
<Information>
|
||||
<Text style={{ marginLeft: 10, fontSize: 18, marginTop: 10, whiteSpace: 'normal', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{obj.city}, {obj.country}
|
||||
</Text>
|
||||
</Information>
|
||||
<Information>
|
||||
<Text style={{ marginLeft: 10, fontSize: 18, marginTop: 10, whiteSpace: 'normal', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
годы: {obj.years}
|
||||
</Text>
|
||||
</Information>
|
||||
<Information>
|
||||
<Text style={{ marginLeft: 10, fontSize: 18, marginTop: 10, whiteSpace: 'normal', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
жанры: {obj.genres.map((genre: string, index: number) => (
|
||||
<span key={index}>{genre}{index < obj.genres.length - 1 ? ', ' : ''}</span>
|
||||
))}
|
||||
</Text>
|
||||
</Information>
|
||||
<Information style={{ marginLeft: 10, marginTop: 10 }}>
|
||||
<Text style={{ marginLeft: 10, fontSize: 20, whiteSpace: 'normal', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
Описание:
|
||||
</Text>
|
||||
</Information>
|
||||
<Information>
|
||||
<Text style={{marginLeft: 10, fontSize: 18, marginTop: 10, whiteSpace: 'normal', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{obj.description}
|
||||
</Text>
|
||||
</Information>
|
||||
<Information>
|
||||
<img src={obj.photo} width={'100%'} alt={obj.name} />
|
||||
</Information>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import { PauseCircleFilled, PlayCircleFilled, HeartOutlined, ShareAltOutlined } from "@ant-design/icons";
|
||||
import { usePlayingContext } from "../../../../contexts/SongContexts/PlayingProvider";
|
||||
import { useCurrentSongContext } from "../../../../contexts/SongContexts/SongContextProvider";
|
||||
import { ISong } from "../../../../models/IModels";
|
||||
import Text from 'antd/es/typography/Text';
|
||||
|
||||
|
||||
export function SongInfoTemplate({Information, obj}: {Information: any, obj: ISong}) {
|
||||
const { isPlaying, setIsPlaying } = usePlayingContext();
|
||||
const { songId, setSongId } = useCurrentSongContext();
|
||||
|
||||
return (
|
||||
<div className="information" style={{ marginTop: 10, marginBottom: 10 }}>
|
||||
<img src={obj.cover} alt={obj.song_name} className="rounded m-auto" style={{ width: '50%' }} />
|
||||
<Information style={{ marginLeft: 70 }}>
|
||||
<Text style={{ fontSize: 18, marginTop: 10, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{obj.song_name}
|
||||
</Text>
|
||||
</Information>
|
||||
<Information style={{ marginLeft: 70 }}>
|
||||
<Text style={{ fontSize: 20, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{obj.band_name}
|
||||
</Text>
|
||||
</Information>
|
||||
{isPlaying && obj.id === songId ? (
|
||||
<PauseCircleFilled
|
||||
style={{ color: '#9d0000', fontSize: 30, marginTop: 10, marginRight: 10 }}
|
||||
onClick={() => {
|
||||
setIsPlaying(!isPlaying);
|
||||
setSongId(obj.id);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<PlayCircleFilled
|
||||
style={{ color: '#9d0000', fontSize: 30, marginTop: 10, marginRight: 10 }}
|
||||
onClick={() => {
|
||||
setIsPlaying(!isPlaying);
|
||||
setSongId(obj.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ShareAltOutlined style={{ color: '#9d0000', fontSize: 30, marginTop: 10, marginRight: 10 }} />
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
@ -2,17 +2,16 @@ import react from "react";
|
||||
import { IAlbum, IBand, ISong, IAdvertisement } from "../../../models/IModels";
|
||||
import { getSong, getAlbum, getBand } from "../../../API/api";
|
||||
import Text from 'antd/es/typography/Text';
|
||||
import { usePlayingContext } from "../../../contexts/SongContexts/PlayingProvider";
|
||||
import { useCurrentSongContext } from "../../../contexts/SongContexts/SongContextProvider";
|
||||
import { AdCard } from "../../cardComponents/AdCard";
|
||||
import { HeartOutlined, Loading3QuartersOutlined, PauseCircleFilled, PlayCircleFilled, ShareAltOutlined } from "@ant-design/icons";
|
||||
import { CloseOutlined, HeartOutlined, Loading3QuartersOutlined, PauseCircleFilled, PlayCircleFilled, ShareAltOutlined } from "@ant-design/icons";
|
||||
import { Spin, Typography } from "antd";
|
||||
import styled from "styled-components";
|
||||
import { SongInfoTemplate } from "./informationBlock/songInfoTemplate";
|
||||
import { BandInfoTemplate } from "./informationBlock/bandInfoTemplate";
|
||||
import { AlbumInfoTemplate } from "./informationBlock/albumInfoTemplate";
|
||||
|
||||
export function InformationTemplate ({obj}: {obj: ISong | IBand | IAlbum | IAdvertisement | undefined}) {
|
||||
|
||||
const { isPlaying, setIsPlaying } = usePlayingContext();
|
||||
const { songId, setSongId } = useCurrentSongContext();
|
||||
|
||||
function isAd(obj: any): obj is IAdvertisement {
|
||||
return Array.isArray(obj) && obj.every(item =>
|
||||
typeof item === 'object' &&
|
||||
@ -45,6 +44,17 @@ export function InformationTemplate ({obj}: {obj: ISong | IBand | IAlbum | IAdve
|
||||
'city' in obj;
|
||||
}
|
||||
|
||||
const Information = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
switch (typeof obj) {
|
||||
case 'object':
|
||||
return (
|
||||
@ -52,92 +62,11 @@ export function InformationTemplate ({obj}: {obj: ISong | IBand | IAlbum | IAdve
|
||||
|
||||
{isAd(obj) && Array.isArray(obj) ? obj.map((ad) => <AdCard ad={ad} key={ad.id} />) : null}
|
||||
|
||||
{isSong(obj) ?
|
||||
<div style={{marginTop: 10, marginBottom: 10}}>
|
||||
{isSong(obj) ? <SongInfoTemplate Information={Information} obj={obj} /> : null}
|
||||
|
||||
<img
|
||||
src={obj.cover} alt={obj.song_name} className="rounded m-auto" style={{ width: '50%'}}>
|
||||
</img>
|
||||
{isBand(obj) ? <BandInfoTemplate Information={Information} obj={obj} /> : null}
|
||||
|
||||
|
||||
<div style={{display: 'flex', textAlign: 'left', justifyContent: 'left', width: '100%', marginLeft: 70, overflow: 'hidden'}}>
|
||||
<Text style={{fontSize: 18, marginTop: 10, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>{obj.song_name}</Text>
|
||||
</div>
|
||||
<div style={{display: 'flex', textAlign: 'left', justifyContent: 'left', width: '100%', marginLeft: 70, overflow: 'hidden'}}>
|
||||
<Text style={{fontSize: 20, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>{obj.band_name}</Text>
|
||||
</div>
|
||||
|
||||
{isPlaying ?
|
||||
<PauseCircleFilled style={{color: '#9d0000', fontSize: 30, marginTop: 10}}
|
||||
onClick={() => {setIsPlaying(!isPlaying); setSongId(obj.id)}}>
|
||||
|
||||
</PauseCircleFilled>
|
||||
|
||||
:
|
||||
<PlayCircleFilled style={{color: '#9d0000', fontSize: 30, marginTop: 10}}
|
||||
onClick={() => {setIsPlaying(!isPlaying); setSongId(obj.id)}}>
|
||||
|
||||
</PlayCircleFilled>}
|
||||
|
||||
<HeartOutlined style={{color: '#9d0000', fontSize: 30, marginTop: 10}}></HeartOutlined>
|
||||
<ShareAltOutlined style={{color: '#9d0000', fontSize: 30, marginTop: 10}}></ShareAltOutlined>
|
||||
|
||||
</div>
|
||||
|
||||
: null}
|
||||
|
||||
|
||||
{isBand(obj) ?
|
||||
|
||||
<div style={{marginTop: 10, marginBottom: 10, display: 'flex', flexDirection: 'column', alignContent: 'left'}}>
|
||||
<div style={{display: 'flex', textAlign: 'left', justifyContent: 'left', width: '100%', marginLeft: 70, marginBottom: 20, overflow: 'hidden'}}>
|
||||
<Text style={{fontSize: 22, marginTop: 10, whiteSpace:
|
||||
'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
{obj.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{display: 'flex', textAlign: 'left', justifyContent: 'left', width: '100%', marginLeft: 70, overflow: 'hidden'}}>
|
||||
<Text style={{fontSize: 20, marginTop: 10, whiteSpace:
|
||||
'normal', overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
Информация:
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{display: 'flex', textAlign: 'left', justifyContent: 'left', width: '100%', marginLeft: 70, overflow: 'hidden'}}>
|
||||
<Text style={{fontSize: 18, marginTop: 10, marginLeft: 10, whiteSpace:
|
||||
'normal', overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
{obj.city},{obj.country}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{display: 'flex', textAlign: 'left', justifyContent: 'left', width: '100%', marginLeft: 70, overflow: 'hidden'}}>
|
||||
<Text style={{fontSize: 18, marginTop: 10, marginLeft: 10, whiteSpace:
|
||||
'normal', overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
годы: {obj.years}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{display: 'flex', textAlign: 'left', justifyContent: 'left', width: '100%', marginLeft: 70, overflow: 'hidden'}}>
|
||||
<Text style={{fontSize: 18, marginTop: 10, marginLeft: 10, whiteSpace:
|
||||
'normal', overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
жанры: {obj.genres.map((genre: string, index: number) => (
|
||||
<span key={index}>{genre}{index < obj.genres.length - 1 ? ', ' : ''}</span>
|
||||
))}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{display: 'flex', textAlign: 'left', justifyContent: 'left', width: '100%', marginLeft: 70, overflow: 'hidden'}}>
|
||||
<Text style={{fontSize: 20, marginTop: 10, whiteSpace:
|
||||
'normal', overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
Описание:
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{display: 'flex', textAlign: 'left', justifyContent: 'left', width: '100%', marginLeft: 70, overflow: 'hidden'}}>
|
||||
<Text style={{fontSize: 18, marginTop: 10, marginLeft: 10, whiteSpace:
|
||||
'normal', overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
{obj.description}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
: null}
|
||||
{isAlbum(obj) ? <Text>{obj.name}</Text> : null}
|
||||
{isAlbum(obj) ? <AlbumInfoTemplate Information={Information} obj={obj} /> : null}
|
||||
|
||||
</div>
|
||||
)
|
||||
|
@ -1,11 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export interface ISongTemplateInterface {
|
||||
key: number;
|
||||
title: string;
|
||||
artist: string;
|
||||
album: string;
|
||||
image: string;
|
||||
duration: string;
|
||||
}
|
||||
|
@ -1,21 +1,24 @@
|
||||
import { Button } from "antd";
|
||||
import { ISong } from "../../../models/IModels";
|
||||
import { IBand, ISong } from "../../../models/IModels";
|
||||
import { useState } from "react";
|
||||
import { height, style } from "@mui/system";
|
||||
import { useCurrentSongContext } from "../../../contexts/SongContexts/SongContextProvider";
|
||||
import { usePlayingContext } from "../../../contexts/SongContexts/PlayingProvider";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useCurrentPlaylistContext } from "../../../contexts/SongContexts/CurrentPlaylistProvider";
|
||||
|
||||
const useColumns = (songs: ISong[], chart: boolean = false) => {
|
||||
|
||||
const {isPlaying, setIsPlaying} = usePlayingContext();
|
||||
const { songId, setSongId } = useCurrentSongContext();
|
||||
const { playlist, setPlaylist } = useCurrentPlaylistContext();
|
||||
|
||||
const handlePlayClick = (songId: string) => {
|
||||
setIsPlaying(!isPlaying);
|
||||
setSongId(songId);
|
||||
setPlaylist(songs);
|
||||
};
|
||||
|
||||
|
||||
const columns = [
|
||||
...(chart ?
|
||||
[
|
||||
|
@ -1,13 +1,15 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Button, Col, Grid, Menu, Row, Slider, Space, Table } from 'antd';
|
||||
import { FastForwardFilled, FastBackwardFilled, PlayCircleOutlined, HeartOutlined, ShareAltOutlined, PauseCircleOutlined, SoundOutlined, MutedOutlined, SoundFilled } from '@ant-design/icons';
|
||||
import { FastForwardFilled, FastBackwardFilled, PlayCircleOutlined, HeartOutlined, ShareAltOutlined, PauseCircleOutlined, SoundOutlined, MutedOutlined, SoundFilled, HeartFilled } from '@ant-design/icons';
|
||||
import { ISong, SongProps } from '../../models/IModels';
|
||||
import styled from 'styled-components'
|
||||
import { useCurrentSongContext } from '../../contexts/SongContexts/SongContextProvider';
|
||||
import { usePlayingContext } from '../../contexts/SongContexts/PlayingProvider';
|
||||
import { useCurrentPlaylistContext } from '../../contexts/SongContexts/CurrentPlaylistProvider';
|
||||
import Dropdown from 'antd/es/dropdown/dropdown';
|
||||
import Text from 'antd/es/typography/Text';
|
||||
import { useVolumeContext } from '../../contexts/VolumeContexts/VolumeProvider';
|
||||
import { addFavoriteSong, removeFavoriteSong } from '../../API/api';
|
||||
|
||||
const Song = styled.div`
|
||||
position: fixed;
|
||||
@ -18,8 +20,6 @@ const Song = styled.div`
|
||||
padding: 0 0px;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
|
||||
|
||||
`;
|
||||
|
||||
type CurrentSongProps = {
|
||||
@ -36,6 +36,62 @@ const SongName = styled.span`
|
||||
padding-right: 20px; /* Добавляем небольшой отступ справа для точки */
|
||||
`;
|
||||
|
||||
const handleAddClick = async (songId: string) => {
|
||||
try {
|
||||
let user = localStorage.getItem('user');
|
||||
if (user) {
|
||||
let userId = JSON.parse(user).id;
|
||||
if (songId) {
|
||||
await addFavoriteSong(userId, songId);
|
||||
const favoriteSongs = JSON.parse(user).favoriteSongs || [];
|
||||
favoriteSongs.push(songId);
|
||||
localStorage.setItem('user', JSON.stringify({ ...JSON.parse(user), favoriteSongs }));
|
||||
}
|
||||
}
|
||||
else {
|
||||
alert('Сперва войдите в аккаунт!');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveClick = async (songId: string) => {
|
||||
try {
|
||||
let user = localStorage.getItem('user');
|
||||
if (user) {
|
||||
let userId = JSON.parse(user).id;
|
||||
if (songId) {
|
||||
await removeFavoriteSong(userId, songId);
|
||||
const favoriteSongs = JSON.parse(user).favoriteSongs || [];
|
||||
const index = favoriteSongs.indexOf(songId);
|
||||
if (index !== -1) {
|
||||
favoriteSongs.splice(index, 1);
|
||||
}
|
||||
localStorage.setItem('user', JSON.stringify({ ...JSON.parse(user), favoriteSongs }));
|
||||
}
|
||||
}
|
||||
else {
|
||||
alert('Сперва войдите в аккаунт!');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const wasAdded = (songId: string) => {
|
||||
let user = localStorage.getItem('user');
|
||||
if (user) {
|
||||
if (songId) {
|
||||
const favoriteSongs = JSON.parse(user).favoriteSongs;
|
||||
return Array.isArray(favoriteSongs) && favoriteSongs.includes(songId);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function CurrentTrack({ song , songs}: CurrentSongProps) {
|
||||
|
||||
const audioPlayer = useRef<HTMLAudioElement | null>(null);
|
||||
@ -201,7 +257,11 @@ export function CurrentTrack({ song , songs}: CurrentSongProps) {
|
||||
onClick={() => setMuted(!muted)}
|
||||
/>
|
||||
</Dropdown>
|
||||
<Button type="link" icon={<HeartOutlined className="current-track-button" />}></Button>
|
||||
<Button type="link" icon={ wasAdded(songId) ? <HeartFilled className="current-track-button"
|
||||
|
||||
onClick={() => handleRemoveClick(songId) } style={{ color: '#ad0000'}} /> : <HeartOutlined className="current-track-button"
|
||||
onClick={() => handleAddClick(songId)} />}></Button>
|
||||
|
||||
<Button type="link" icon={<ShareAltOutlined className="current-track-button" />}></Button>
|
||||
</span>
|
||||
|
||||
|
47
src/components/userComponents/UploadAvatar.tsx
Normal file
47
src/components/userComponents/UploadAvatar.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Avatar, Button, Form, Input } from 'antd';
|
||||
import { CloseOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { updateUser } from '../../API/api';
|
||||
import { element } from 'prop-types';
|
||||
|
||||
interface AvatarProps {
|
||||
size: number;
|
||||
src: string;
|
||||
onChange: (url: string) => void;
|
||||
}
|
||||
|
||||
interface AvatarState {
|
||||
preview: string | null;
|
||||
}
|
||||
|
||||
export function UploadAvatar({ size, src, onChange }: AvatarProps) {
|
||||
|
||||
const [ image, setImage ] = useState<AvatarState['preview']>(null);
|
||||
const [ pasting, setPasting ] = useState(false);
|
||||
const [ input, setInput ] = useState('');
|
||||
|
||||
function LoadImage(input: string) {
|
||||
onChange(input)
|
||||
setPasting(false)
|
||||
}
|
||||
|
||||
console.log(image)
|
||||
return (
|
||||
<div>
|
||||
<Avatar size={size} src={ image ? image : src} onClick={() => setPasting(true)} style={{ cursor: 'pointer' }} />
|
||||
|
||||
{ pasting ?
|
||||
<Form className='container flex'>
|
||||
<Input id='input' onChange={(e) => setInput(e.target.value)} placeholder="Вставьте URL"></Input>
|
||||
<Button onClick={() => LoadImage(input)}>Подтвердить</Button>
|
||||
<CloseOutlined onClick={() => setPasting(false)}/>
|
||||
</Form>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
)
|
||||
}
|
32
src/contexts/SongContexts/CurrentPlaylistProvider.tsx
Normal file
32
src/contexts/SongContexts/CurrentPlaylistProvider.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React, { createContext, useContext, ReactNode, useState } from "react";
|
||||
import { ISong } from "../../models/IModels";
|
||||
|
||||
interface CurrentPlaylistProviderProps {
|
||||
children: ReactNode;
|
||||
playlist: ISong[];
|
||||
}
|
||||
|
||||
interface CurrentPlaylistContextValue {
|
||||
playlist: ISong[];
|
||||
setPlaylist: (playlist: ISong[]) => void;
|
||||
}
|
||||
|
||||
const CurrentPlaylistContext = createContext<CurrentPlaylistContextValue | undefined>(undefined);
|
||||
|
||||
export function CurrentPlaylistProvider({ children, playlist }: CurrentPlaylistProviderProps) {
|
||||
const [currentPlaylist, setCurrentPlaylist] = useState(playlist);
|
||||
|
||||
return (
|
||||
<CurrentPlaylistContext.Provider value={{ playlist: currentPlaylist, setPlaylist: setCurrentPlaylist }}>
|
||||
{children}
|
||||
</CurrentPlaylistContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useCurrentPlaylistContext = function(): CurrentPlaylistContextValue {
|
||||
const context = useContext(CurrentPlaylistContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useCurrentPlaylistContext must be used within a CurrentPlaylistProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
@ -4,6 +4,7 @@ export interface IAlbum {
|
||||
cover: string;
|
||||
year: string;
|
||||
genres: string[];
|
||||
bandid: string;
|
||||
}
|
||||
|
||||
export interface IBand {
|
||||
@ -14,6 +15,7 @@ export interface IBand {
|
||||
years: string;
|
||||
genres: string[];
|
||||
description: string;
|
||||
photo: string;
|
||||
}
|
||||
|
||||
export interface IGenre {
|
||||
@ -54,3 +56,13 @@ export interface IAdvertisement {
|
||||
name: string;
|
||||
photo: string;
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
const: any;
|
||||
id: string
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
photo: string;
|
||||
favorite_songs: string[];
|
||||
}
|
141
src/pages/AlbumPage.tsx
Normal file
141
src/pages/AlbumPage.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { IAlbum, ISong } from "../models/IModels";
|
||||
import { getAlbum, getSongs } from "../API/api";
|
||||
import { useSideBlockContext } from "../contexts/SideBlockContexts/SideBlockProvider";
|
||||
import { SongsBlock } from "../components/menuComponents/SongsBlock";
|
||||
import { useCurrentPlaylistContext } from "../contexts/SongContexts/CurrentPlaylistProvider";
|
||||
import { useCurrentSongContext } from "../contexts/SongContexts/SongContextProvider";
|
||||
import styled from "styled-components";
|
||||
import { HeartOutlined, PauseCircleFilled, PlayCircleFilled, ShareAltOutlined } from "@ant-design/icons";
|
||||
|
||||
const AlbumPageStyled = styled.div`
|
||||
.album-container {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin-left: 180px;
|
||||
;
|
||||
}
|
||||
|
||||
.album-cover {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.album-info {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
margin-bottom: 40px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.album-buttons {
|
||||
margin-left: 20px;
|
||||
margin-top: 250px;
|
||||
}
|
||||
|
||||
.album-name {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.album-details {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tracklist {
|
||||
width: 75%;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export function AlbumPage() {
|
||||
const { albumId } = useParams<{ albumId: string }>();
|
||||
const [album, setAlbum] = useState<IAlbum>();
|
||||
const [songs, setSongs] = useState<ISong[]>([]);
|
||||
const { setContentObject } = useSideBlockContext();
|
||||
const [isPlaying, setIsPlaying] = React.useState(false);
|
||||
const { playlist, setPlaylist } = useCurrentPlaylistContext();
|
||||
const currentSongId = useCurrentSongContext();
|
||||
|
||||
const fetchData = async () => {
|
||||
if (albumId) {
|
||||
const responseAlbum = getAlbum(albumId);
|
||||
const responseSongs = getSongs();
|
||||
|
||||
setAlbum((await responseAlbum).data);
|
||||
setSongs((await responseSongs).data.filter((song: ISong) => song.albumid === albumId));
|
||||
}
|
||||
};
|
||||
|
||||
const isCurrentPlaylist = (playlist: ISong[]) => {
|
||||
return (
|
||||
playlist.length === songs.length &&
|
||||
playlist.every((song, index) => song.id === songs[index].id)
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSongId && songs.length > 0) {
|
||||
setIsPlaying(true);
|
||||
const song = songs.find(s => s.id === currentSongId.songId);
|
||||
if (song) {
|
||||
handlePlaySong(song);
|
||||
}
|
||||
} else {
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}, [currentSongId, songs]);
|
||||
|
||||
const handlePlaySong = (song: ISong) => {
|
||||
currentSongId.setSongId(song.id);
|
||||
setIsPlaying(true);
|
||||
setPlaylist(songs);
|
||||
};
|
||||
|
||||
const currentSong = songs.find(s => s.id === currentSongId.songId);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<AlbumPageStyled>
|
||||
{album && (
|
||||
<div className="album-container">
|
||||
<img
|
||||
src={album.cover}
|
||||
alt={album.name}
|
||||
className="album-cover"
|
||||
/>
|
||||
<div className="album-info">
|
||||
<h1 className="album-name">{album.name}</h1>
|
||||
<p className="album-details">Год: {album.year}</p>
|
||||
<p className="album-details">Жанры: {album.genres.join(", ")}</p>
|
||||
</div>
|
||||
<div className="album-buttons">
|
||||
<ShareAltOutlined style={{ color: '#9d0000', fontSize: 30, marginTop: 10, marginRight: 10 }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container mx-auto" style={{width: '75%'}}>
|
||||
<SongsBlock title="" trackList={songs} />
|
||||
</div>
|
||||
|
||||
</AlbumPageStyled>
|
||||
</div>
|
||||
);
|
||||
}
|
93
src/pages/BandPage.tsx
Normal file
93
src/pages/BandPage.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { IAlbum, IBand, ISong } from "../models/IModels";
|
||||
import { getBand, getAlbumsByBand, getSongsByBand } from "../API/api";
|
||||
import { PlaylistsBlock } from "../components/menuComponents/PlaylistsBlock";
|
||||
import { SongInfoTemplate } from "../components/menuComponents/Templates/informationBlock/songInfoTemplate";
|
||||
import { SongsBlock } from "../components/menuComponents/SongsBlock";
|
||||
import { useCurrentSongContext } from "../contexts/SongContexts/SongContextProvider";
|
||||
import { CurrentTrack } from "../components/songComponents/CurrentTrack";
|
||||
import { useCurrentPlaylistContext } from "../contexts/SongContexts/CurrentPlaylistProvider";
|
||||
|
||||
export function BandPage() {
|
||||
const { bandId } = useParams();
|
||||
|
||||
const [band, setBand] = React.useState<IBand>();
|
||||
const [albums, setAlbums] = React.useState<IAlbum[]>();
|
||||
const [songs, setSongs] = React.useState<ISong[]>([]);
|
||||
const [isPlaying, setIsPlaying] = React.useState(false);
|
||||
const { playlist, setPlaylist } = useCurrentPlaylistContext();
|
||||
const currentSongId = useCurrentSongContext();
|
||||
|
||||
const fetchData = async () => {
|
||||
if (bandId) {
|
||||
try {
|
||||
const [responseBands, responseAlbums, responseSongs] = await Promise.all([
|
||||
getBand(bandId),
|
||||
getAlbumsByBand(bandId),
|
||||
getSongsByBand(bandId)
|
||||
]);
|
||||
|
||||
setBand(responseBands.data);
|
||||
setAlbums(responseAlbums);
|
||||
setSongs(responseSongs);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSongId && songs.length > 0) {
|
||||
setIsPlaying(true);
|
||||
const song = songs.find(s => s.id === currentSongId.songId);
|
||||
if (song) {
|
||||
handlePlaySong(song);
|
||||
}
|
||||
} else {
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}, [currentSongId, songs]);
|
||||
|
||||
|
||||
|
||||
const handlePlaySong = (song: ISong) => {
|
||||
currentSongId.setSongId(song.id);
|
||||
setIsPlaying(true);
|
||||
setPlaylist(songs);
|
||||
};
|
||||
|
||||
const currentSong = songs.find(s => s.id === currentSongId.songId);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto bg-white p-8 rounded-lg shadow-md">
|
||||
{band && (
|
||||
<div className="flex justify-center mb-12">
|
||||
<div className="text-center mx-auto">
|
||||
<img src={band.photo} alt={band.name} className="rounded-full shadow-lg w-64 h-64 mb-6 mx-auto" />
|
||||
<h2 className="text-4xl font-bold mb-4">{band.name}</h2>
|
||||
<p className="text-xl text-gray-600 mb-4">Country: {band.country}</p>
|
||||
<p style={{ padding: "10px", width: '75%'}} className=" mx-auto justify-center text-gray-600 bg-slate-300 mb-6 ">{band.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="text-3xl font-bold mb-8">Альбомы</h3>
|
||||
|
||||
{albums && (
|
||||
<PlaylistsBlock albums={albums} />
|
||||
)}
|
||||
|
||||
<h3 className="text-3xl font-bold mb-8" style={{ marginTop: "60px" }}>Треки</h3>
|
||||
|
||||
<div className="container mx-auto" style={{width: '75%'}}>
|
||||
<SongsBlock title="" trackList={songs} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
@ -6,6 +6,7 @@ import { CurrentTrack } from "../components/songComponents/CurrentTrack";
|
||||
import { IPlaylist, ISong, IAlbum, IGenre, IAdvertisement, IBand } from "../models/IModels";
|
||||
import { useCurrentSongContext } from "../contexts/SongContexts/SongContextProvider";
|
||||
import { useSideBlockContext } from "../contexts/SideBlockContexts/SideBlockProvider";
|
||||
import { useCurrentPlaylistContext } from "../contexts/SongContexts/CurrentPlaylistProvider";
|
||||
|
||||
|
||||
export function Homepage() {
|
||||
@ -14,7 +15,6 @@ export function Homepage() {
|
||||
const [songs, setSongs] = useState<ISong[]>([]);
|
||||
const [ads, setAds] = useState<IAdvertisement[]>([]);
|
||||
|
||||
|
||||
const [playlists, setPlaylist] = React.useState<IPlaylist[]>([]);
|
||||
const [genres, setGenres] = React.useState<IGenre[]>([]);
|
||||
|
||||
@ -45,8 +45,6 @@ export function Homepage() {
|
||||
return (
|
||||
<>
|
||||
<MenuBlock playlists={[]} songs={songs} albums={albums} genres={[]}/><AdBlock object={contentObject}/>
|
||||
{currentSong && <CurrentTrack song={currentSong} songs={songs}/>}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,14 +1,72 @@
|
||||
import { LockOutlined, MailOutlined } from '@ant-design/icons';
|
||||
import { Form, Input, Button, Checkbox } from 'antd'
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { getUserByEmail, getUsers } from '../API/api';
|
||||
|
||||
export function Loginpage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [users, setUsers] = useState([]);
|
||||
const [user, setUser] = useState(null);
|
||||
const [ submited, setSubmited ] = useState(false);
|
||||
|
||||
const redirect = useNavigate();
|
||||
|
||||
const onFinish = async () => {
|
||||
setSubmited(true);
|
||||
let response = await findUser();
|
||||
setUser(response);
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
let response = await getUsers();
|
||||
setUsers(response.data);
|
||||
}
|
||||
|
||||
const findUser = async () => {
|
||||
try {
|
||||
let response = await getUserByEmail(email);
|
||||
console.log(response);
|
||||
if (response.data.length === 0) {
|
||||
setUser(null);
|
||||
return null;
|
||||
}
|
||||
setUser(response.data[0]);
|
||||
return response.data[0];
|
||||
} catch (error) {
|
||||
console.error("Error fetching user:", error);
|
||||
setUser(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const updateUser = useCallback(() => {
|
||||
if (user) {
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
redirect('/profile');
|
||||
} else if (submited) {
|
||||
console.error('Пользователь не найден');
|
||||
alert('Ошибка входа');
|
||||
setSubmited(false);
|
||||
redirect('/login');
|
||||
}
|
||||
}, [user, submited, redirect]);
|
||||
|
||||
useEffect(() => {
|
||||
updateUser();
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto" style={{height: '200%', width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
|
||||
<div className='login' >
|
||||
<Form>
|
||||
<p className='text-3xl' style={{ fontFamily: 'Roboto', marginTop: '100px', marginBottom: '50px'}}>Вход</p>
|
||||
<Form.Item style={{marginBottom: '50px'}}>
|
||||
<div className="container mx-auto flex flex-col justify-center items-center h-screen">
|
||||
<div className='login w-96 h-auto bg-white rounded-lg shadow-lg p-8' >
|
||||
<h2 className="text-3xl font-bold text-center">Войти</h2>
|
||||
<Form onFinish={onFinish} >
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[{ required: true, message: 'Введите вашу почту!' }, { type: 'email', message: 'Некорректный формат' }]}
|
||||
@ -18,41 +76,46 @@ export function Loginpage() {
|
||||
prefix={<MailOutlined className="site-form-item-icon" />}
|
||||
type="text"
|
||||
placeholder="Почта"
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: 'Введите пароль!!' }, { min: 8, message: 'Пароль должен быть длиной минимум 8 символов' }]}
|
||||
style={{ fontFamily: 'Roboto'}}
|
||||
rules={[
|
||||
{ required: true, message: 'Введите пароль!!' },
|
||||
{ pattern: /.{8,}/, message: 'Пароль должен содержать не менее 8 символов!' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
<Input.Password
|
||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||
type="password"
|
||||
placeholder="Пароль"
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<Button type='link' href=''>Забыли пароль?</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Form.Item name="remember" valuePropName="checked" noStyle />
|
||||
<Button type="primary" className='bg-red-800 login-button hover:bg-transparent' htmlType="submit" style={{marginRight:'10px'}}>
|
||||
Войти
|
||||
</Button>
|
||||
<Checkbox style={{marginLeft:'10px'}}>Запомнить меня</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Form.Item name="submit" noStyle>
|
||||
|
||||
</Form.Item>
|
||||
<Button type='link' href=''>Забыли пароль?</Button>
|
||||
<Form.Item name="submit" noStyle>
|
||||
<Button type="primary"
|
||||
className='bg-blue-500 hover:bg-blue-700 text-white w-full'
|
||||
htmlType="submit"
|
||||
style={{marginRight:'10px'}}>
|
||||
Войти
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Form.Item name="remember" valuePropName="checked" noStyle >
|
||||
<Checkbox style={{marginLeft:'10px'}}>Запомнить меня</Checkbox>
|
||||
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="noaccount" noStyle>
|
||||
<Link to={'../register'}>Нет аккаунта?</Link>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Link to={'../register'} className="text-blue-500 hover:text-blue-700">Нет аккаунта?</Link>
|
||||
</Form.Item>
|
||||
|
||||
</Form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,153 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { ISong, IUser } from '../models/IModels';
|
||||
import { getUser, getUserByEmail, getFavoriteSongs, updateUser } from '../API/api';
|
||||
import { Button, Card, Typography, Alert, Input } from 'antd';
|
||||
import { SongsBlock } from '../components/menuComponents/SongsBlock';
|
||||
import { UploadAvatar as Avatar } from '../components/userComponents/UploadAvatar';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const defaultAvatar = 'http://localhost:3001/default-avatar.png';
|
||||
|
||||
interface UserStorageData {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
photo: string;
|
||||
favorite_songs: string[];
|
||||
}
|
||||
|
||||
export function Profilepage() {
|
||||
const [user, setUser] = useState<IUser | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [songs, setSongs] = useState<ISong[]>([]);
|
||||
const [ editMode, setEditMode ] = useState(false);
|
||||
|
||||
const redirect = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
const storageData: UserStorageData = JSON.parse(localStorage.getItem('user') || '[]');
|
||||
|
||||
|
||||
|
||||
if (!storageData.id || !storageData.email) {
|
||||
throw new Error("Некорректные данные пользователя в localStorage");
|
||||
}
|
||||
|
||||
const response = await getUser(storageData.id);
|
||||
setUser(response.data);
|
||||
|
||||
if (response.data) {
|
||||
const responseSongs = await getFavoriteSongs(response.data.id);
|
||||
setSongs(responseSongs);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Не удалось загрузить данные пользователя');
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserData();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
updateUser(user);
|
||||
}, [user]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="container mx-auto bg-slate-300" style={{height: 300, width: '75%'}}>
|
||||
<p>Страница профиля</p>
|
||||
<div className="user-profile">
|
||||
<Alert message="Error" description={error} type="error" showIcon />
|
||||
<Button type="primary" className="exit-button" style={{ alignSelf: 'flex-end' }} onClick={() => localStorage.removeItem('user')}>
|
||||
Выйти
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <div className="user-profile"><Alert message="User not found" type="warning" showIcon /></div>;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('user');
|
||||
redirect('/login');
|
||||
}
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
if (!window.confirm("Подтвердите изменения")) {
|
||||
setEditMode(false);
|
||||
return;
|
||||
}
|
||||
const response = await updateUser(user);
|
||||
setUser(response.data);
|
||||
localStorage.setItem('user', JSON.stringify(response.data));
|
||||
setEditMode(false);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Не удалось обновить данные пользователя');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateAvatar = async (url: string) => {
|
||||
try {
|
||||
if (!window.confirm("Подтвердите изменения")) {
|
||||
return;
|
||||
}
|
||||
if (!url) {
|
||||
console.log('No file selected');
|
||||
return;
|
||||
}
|
||||
|
||||
setUser({...user, photo: url});
|
||||
localStorage.setItem('user', JSON.stringify({...user, photo: url}));
|
||||
}
|
||||
catch (err: any) {
|
||||
setError(err.message || 'Не удалось обновить аватар');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="user-profile" style={{ maxWidth: 800, margin: '0 auto', padding: 20 }}>
|
||||
<Card style={{ textAlign: 'center' }}>
|
||||
<Avatar size={100} src={user.photo || defaultAvatar} onChange={handleUpdateAvatar} />
|
||||
<div onDoubleClick={() => setEditMode(true)} style={{display: 'inline-block', width: '100%'}}>
|
||||
{editMode ? (
|
||||
<Input
|
||||
value={user.name}
|
||||
onChange={(e) => setUser({...user, name: e.target.value})}
|
||||
onBlur={() => setEditMode(false)}
|
||||
onPressEnter={
|
||||
handleUpdate
|
||||
}
|
||||
style={{width: '50%', alignSelf: 'center', justifySelf: 'center'}}
|
||||
onSubmit={handleUpdate}
|
||||
/>
|
||||
) : (
|
||||
<Title level={2}>{user.name}</Title>
|
||||
)}
|
||||
</div>
|
||||
<Text>Email: {user.email}</Text>
|
||||
</Card>
|
||||
<Card title="Любимые треки:" style={{ marginTop: 20 }}>
|
||||
{ songs.length > 0 ? <SongsBlock title="" trackList={songs} /> : <h3>У вас пока нет любимых песен :c</h3> }
|
||||
</Card>
|
||||
<div className="button-container" style={{ textAlign: 'center', marginTop: 20 }}>
|
||||
<Button type="primary" className="exit-button" onClick={() => handleLogout()}>
|
||||
Выйти
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,92 +1,121 @@
|
||||
import { MailOutlined, LockOutlined } from "@ant-design/icons";
|
||||
import { Form, Input, Button, Checkbox } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
// @ts-ignore
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import React, { useState } from "react";
|
||||
import { LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Form, Input, Button, Checkbox } from 'antd'
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { createUser, getUserByEmail } from '../API/api';
|
||||
import { useState } from 'react';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
|
||||
export function Registerpage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [password2, setPassword2] = useState('');
|
||||
const [verified, setVerified] = useState(false);
|
||||
function onChange(value: any) {
|
||||
console.log("Captcha value:", value);
|
||||
setVerified(true);
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const redirect = useNavigate();
|
||||
|
||||
const onFinish = async () => {
|
||||
if (password === password2 && verified) {
|
||||
const newUser = { name, email, password, photo: '', favorite_songs: [] };
|
||||
await createUser(newUser);
|
||||
redirect('/login');
|
||||
} else if (password !== password2) {
|
||||
alert('Пароли не совпадают');
|
||||
} else {
|
||||
alert('Подтвердите что вы не робот');
|
||||
}
|
||||
};
|
||||
|
||||
const checkEmail = async (rule: any, value: string) => {
|
||||
const response = await getUserByEmail(value);
|
||||
if (response.data.length > 0) {
|
||||
return Promise.reject('Такая почта уже зарегистрирована');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto" style={{height: '200%', width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
|
||||
<div className='login' >
|
||||
<Form>
|
||||
<p className='text-3xl' style={{ fontFamily: 'Roboto', marginTop: '100px', marginBottom: '50px'}}>Вход</p>
|
||||
<Form.Item style={{marginBottom: '50px'}}>
|
||||
<div className="container mx-auto flex flex-col justify-center items-center h-screen">
|
||||
<div className='login w-96 h-auto bg-white rounded-lg shadow-lg p-8' >
|
||||
<h2 className="text-3xl font-bold text-center">Регистрация</h2>
|
||||
<Form onFinish={onFinish} >
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: 'Введите ваше имя!' }]}
|
||||
style={{ fontFamily: 'Roboto'}}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
||||
type="text"
|
||||
placeholder="Имя"
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[{ required: true, message: 'Введите вашу почту!' }, { type: 'email', message: 'Некорректный формат' }]}
|
||||
rules={[{ required: true, message: 'Введите вашу почту!' }, { type: 'email', message: 'Некорректный формат' }, { validator: checkEmail }]}
|
||||
style={{ fontFamily: 'Roboto'}}
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined className="site-form-item-icon" />}
|
||||
type="text"
|
||||
placeholder="Почта"
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: 'Введите пароль!!' }, { min: 8, message: 'Пароль должен быть длиной минимум 8 символов' }]}
|
||||
style={{ fontFamily: 'Roboto'}}
|
||||
rules={[
|
||||
{ required: true, message: 'Введите пароль!!' },
|
||||
{ pattern: /.{8,}/, message: 'Пароль должен содержать не менее 8 символов!' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
<Input.Password
|
||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||
type="password"
|
||||
placeholder="Пароль"
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password2"
|
||||
rules={[{ required: true, message: 'Заполните поле!' }, ({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (value === getFieldValue('password')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject('Пароли должны совпадать!');
|
||||
},
|
||||
})]}
|
||||
style={{ fontFamily: 'Roboto'}}
|
||||
rules={[
|
||||
{ required: true, message: 'Введите повторно пароль!' },
|
||||
{ pattern: /.{8,}/, message: 'Пароль должен содержать не менее 8 символов!' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
<Input.Password
|
||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||
type="password"
|
||||
placeholder="Повторите пароль"
|
||||
onChange={e => setPassword2(e.target.value)}
|
||||
/>
|
||||
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<ReCAPTCHA
|
||||
sitekey={process.env.REACT_APP_RECAPTCHA_SITE_KEY as string}
|
||||
onChange={onChange}
|
||||
onChange={() => setVerified(true)}
|
||||
style={{marginTop: '10px'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Form.Item name="remember" valuePropName="checked" noStyle />
|
||||
<Button type="primary" disabled={!verified} className='bg-red-800 login-button hover:bg-transparent' htmlType="submit" style={{marginRight:'10px'} }>
|
||||
<Button type="primary"
|
||||
className='bg-red-800 hover:bg-transparent'
|
||||
htmlType="submit"
|
||||
style={{marginRight:'10px'}}
|
||||
disabled={!verified}
|
||||
>
|
||||
Регистрация
|
||||
</Button>
|
||||
<Checkbox style={{marginLeft:'10px'}}>Запомнить меня</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Form.Item name="submit" noStyle>
|
||||
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Form.Item name="noaccount" noStyle>
|
||||
<Link to={'../login'}>Уже есть аккаунт?</Link>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
</Form>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user