Ну вроде нормально

This commit is contained in:
Аришина) 2024-11-22 05:35:02 +04:00
parent 96f318a306
commit 027f4b4405
38 changed files with 1735 additions and 760 deletions

20
Dockerfile Normal file
View 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
View 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"]

View File

@ -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
View File

@ -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
View 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
View File

@ -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",

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

0
routes.ts Normal file
View File

View File

@ -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));
}

View File

@ -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>

View File

@ -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";

View File

@ -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>
);

View File

@ -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 />
</>
);
}

View File

@ -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) {
);
}

View File

@ -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>

View File

@ -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;
}
}
},
})}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
)
}

View File

@ -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>
)

View File

@ -1,11 +0,0 @@
import React from "react";
export interface ISongTemplateInterface {
key: number;
title: string;
artist: string;
album: string;
image: string;
duration: string;
}

View File

@ -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 ?
[

View File

@ -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>

View 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>
)
}

View 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;
};

View File

@ -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
View 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
View 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>
);
}

View File

@ -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}/>}
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>