коммит на всякий случай

This commit is contained in:
Аришина) 2024-11-13 02:14:46 +04:00
parent ee17998948
commit 2fa22ec4c4
31 changed files with 471 additions and 160 deletions

2
.env
View File

@ -1,2 +1,2 @@
REACT_APP_DATABASE='http://localhost:3000/' REACT_APP_DATABASE='http://localhost:3001/'
REACT_APP_RECAPTCHA_SITE_KEY='6LcZ-kkqAAAAAFdmy2tD1gKFdjxb0D71w5VMaisr' REACT_APP_RECAPTCHA_SITE_KEY='6LcZ-kkqAAAAAFdmy2tD1gKFdjxb0D71w5VMaisr'

View File

@ -1,18 +1,6 @@
# PromoCursed # PromoCursed
## Курсовая работа
ВАЖНАЯ ИНФОРМАЦИЯ!!! ### Описание
По какой-то причине при передаче объекта песни в CurrentTrack и получение свойств объекта в этом компонента Веб-приложение для прослушивания музыки.
появляются ошибки. Их не было до какого-то момента, но я не успел к сроку выявить причины (я долго дебажил),
но результата это не принесло. До сих пор причины для меня остаются, к сожалению, загадкой...
Что было добавлено/исправлено после очной демонстрации:
1. Компоненты были раскиданы по папкам
2. Переименовал некоторые папки (например, API),
3. Добавил новые компоненты
4. Роутинг и страницы
5. Валидация форм
Должен был добавить плеер с воспроизведением песен, но из-за ошибок, про которые я написал выше, не удалось.
Пришлось жестко передать объект песни прямо в коде компонента.

View File

@ -7,7 +7,7 @@
"band_name": "Nevroz", "band_name": "Nevroz",
"albumid": "1", "albumid": "1",
"album_name": "Album 1", "album_name": "Album 1",
"source": "/src/songs/Nirvana_-_Smells_Like_Teen_Spirit_75941061.mp3", "source": "http://localhost:3001/songs_sources/nirvana.mp3",
"cover": "https://cdn1.ozone.ru/s3/multimedia-t/6893834213.jpg", "cover": "https://cdn1.ozone.ru/s3/multimedia-t/6893834213.jpg",
"playlists": [ "playlists": [
"1", "1",
@ -23,8 +23,8 @@
"band_name": "Nevroz", "band_name": "Nevroz",
"albumid": "1", "albumid": "1",
"album_name": "Album 1", "album_name": "Album 1",
"source": "/src/songs/Blur_-_Song_2_47967381.mp3", "source": "http://localhost:3001/songs_sources/blur.mp3",
"cover": "https://cdn1.ozone.ru/s3/multimedia-t/6893834213.jpg", "cover": "https://lastfm.freetls.fastly.net/i/u/ar0/101e39a435244b05cbf9d3af6ddf8c74.jpg",
"playlists": [ "playlists": [
"2" "2"
], ],
@ -38,7 +38,7 @@
"band_name": "noizemchik", "band_name": "noizemchik",
"albumid": "2", "albumid": "2",
"album_name": "Ругань из-за Стёпы", "album_name": "Ругань из-за Стёпы",
"source": "/src/songs/Noice_MC_-_Rugan_iz-za_steny_63872179.mp3", "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": [ "playlists": [
@ -49,15 +49,15 @@
}, },
{ {
"id": "4", "id": "4",
"song_name": "Crazy Frog", "song_name": "Хот вилз",
"band_id": "3", "band_id": "1",
"band_name": "DSPD", "band_name": "DSPD",
"albumid": "3", "albumid": "1",
"album_name": "Album 3", "album_name": "Album 1",
"source": "/src/songs/DSPD_-_Crazy_Frog_75801748.mp3", "source": "http://localhost:3001/songs_sources/hotweelz.mp3",
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh", "cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
"playlists": [ "playlists": [
"2", "3" "1", "2"
], ],
"genreid": "1", "genreid": "1",
"genre_name": "Rock" "genre_name": "Rock"
@ -69,8 +69,8 @@
"band_name": "DSPD", "band_name": "DSPD",
"albumid": "3", "albumid": "3",
"album_name": "Album 3", "album_name": "Album 3",
"source": "/src/songs/DSPD feat. даня хренников - Кем я стал_(audio-lord.ru).mp3", "source": "http://localhost:3001/songs_sources/kemstal.mp3",
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh", "cover": "https://avatars.yandex.net/get-music-content/6058982/7e431a83.a.23910092-1/m1000x1000?webp=false",
"playlists": [ "playlists": [
"3" "3"
], ],
@ -79,28 +79,29 @@
}, },
{ {
"id": "6", "id": "6",
"song_name": "Smells Like Poop", "song_name": "American Idiotus",
"band_id": "1", "band_id": "3",
"band_name": "Nevroz", "band_name": "Red Day",
"albumid": "1", "albumid": "3",
"album_name": "Album 1", "album_name": "Album 3",
"source": "/src/songs/Nirvana_-_Smells_Like_Teen_Spirit_75941061.mp3", "source": "http://localhost:3001/songs_sources/greenday.mp3",
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh", "cover": "https://avatars.yandex.net/get-music-content/32236/d3846188.a.1001691-1/m1000x1000?webp=false",
"playlists": [ "playlists": [
"1", "2" "2", "3"
], ],
"genreid": "1", "genreid": "1",
"genre_name": "Rock" "genre_name": "Rock"
}, },
{ {
"id": "7", "id": "7",
"song_name": "Song 3", "song_name": "Ulyanofication'",
"band_id": "1", "band_id": "1",
"band_name": "Nevroz", "band_name": "Yellow Warm Russian Tomatoes",
"albumid": "1", "albumid": "1",
"album_name": "Album 1", "album_name": "Album 1",
"source": "/src/songs/Blur_-_Song_2_47967381.mp3", "source": "http://localhost:3001/songs_sources/pepers.mp3",
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh", "cover": "https://avatars.mds.yandex.net/i?id=8d01018aa8cac0da9845314da42e050ab8e61713-12569754-images-thumbs&n=13",
"playlists": [ "playlists": [
"2" "2"
], ],
@ -109,13 +110,13 @@
}, },
{ {
"id": "8", "id": "8",
"song_name": "Ругань из-за Стёпы", "song_name": "СУМАСШЕДШИЙ ПОЕЗД!!!!",
"band_id": "2", "band_id": "2",
"band_name": "noizemchik", "band_name": "СТАРИНА ОЗЗИ ОЗБОРН (МУЗЫКА ДЬЯВОЛА)",
"albumid": "2", "albumid": "2",
"album_name": "Ругань из-за Стёпы", "album_name": "Ругань из-за Стёпы",
"source": "/src/songs/Noice_MC_-_Rugan_iz-za_steny_63872179.mp3", "source": "http://localhost:3001/songs_sources/ozzy.mp3",
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh" "cover": "https://avatars.mds.yandex.net/i?id=95adb2fda305e542621b7d9757d70ddd_l-4937470-images-thumbs&n=13"
, ,
"playlists": [ "playlists": [
"1", "3" "1", "3"
@ -125,13 +126,13 @@
}, },
{ {
"id": "9", "id": "9",
"song_name": "Crazy Frog", "song_name": "Downstairs to Heaven",
"band_id": "3", "band_id": "3",
"band_name": "DSPD", "band_name": "Эйси Диси",
"albumid": "3", "albumid": "3",
"album_name": "Album 3", "album_name": "Album 3",
"source": "/src/songs/DSPD_-_Crazy_Frog_75801748.mp3", "source": "http://localhost:3001/songs_sources/acdc.mp3",
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh", "cover": "https://avatars.mds.yandex.net/get-entity_search/509339/292583724/S600xU",
"playlists": [ "playlists": [
"2", "3" "2", "3"
], ],
@ -140,13 +141,13 @@
}, },
{ {
"id": "10", "id": "10",
"song_name": "Кем я стал", "song_name": "Ай воз мэйд фор лавин ю беби",
"band_id": "3", "band_id": "3",
"band_name": "DSPD", "band_name": "KISS",
"albumid": "3", "albumid": "3",
"album_name": "Album 3", "album_name": "Album 3",
"source": "/src/songs/DSPD feat. даня хренников - Кем я стал_(audio-lord.ru).mp3", "source": "http://localhost:3001/songs_sources/kiss.mp3",
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh", "cover": "https://avatars.dzeninfra.ru/get-zen_doc/10073791/pub_64ab1cf9ec43747a7a73a3e0_64ab1d726ba4132c8f8b37b6/scale_1200",
"playlists": [ "playlists": [
"3" "3"
], ],
@ -160,7 +161,7 @@
"band_name": "DSPD", "band_name": "DSPD",
"albumid": "3", "albumid": "3",
"album_name": "Album 3", "album_name": "Album 3",
"source": "/src/songs/DSPD feat. даня хренников - Кем я стал_(audio-lord.ru).mp3", "source": "http://localhost:3001/songs_sources/kemstal.mp3",
"cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh", "cover": "https://avatars.yandex.net/get-music-content/10129881/f3cf1afc.a.30561322-1/m1000x1000?webp=falseh",
"playlists": [ "playlists": [
"3" "3"

View File

@ -33,7 +33,7 @@
}, },
"scripts": { "scripts": {
"start": "react-dotenv && set port=5000 && react-scripts start", "start": "react-dotenv && set port=5000 && react-scripts start",
"json-server": "npx json-server data.json --port 3000", "json-server": "npx json-server data.json --port 3001 --static ./public/songs_sources",
"build": "react-dotenv && react-scripts build", "build": "react-dotenv && react-scripts build",
"test": "react-dotenv && react-scripts test", "test": "react-dotenv && react-scripts test",
"eject": "react-dotenv && react-scripts eject", "eject": "react-dotenv && react-scripts eject",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -27,7 +27,7 @@ export async function getSongs() {
export async function getSong(id: string) { export async function getSong(id: string) {
return await axios.get<IPlaylist[]>(`${localhost}songs/` + id); return await axios.get<ISong>(`${localhost}songs/` + id);
} }

View File

@ -77,7 +77,12 @@
-webkit-box-shadow: 0px -26px 19px 0px rgba(34, 60, 80, 0.2); -webkit-box-shadow: 0px -26px 19px 0px rgba(34, 60, 80, 0.2);
-moz-box-shadow: 0px -26px 19px 0px rgba(34, 60, 80, 0.2); -moz-box-shadow: 0px -26px 19px 0px rgba(34, 60, 80, 0.2);
box-shadow: 0px -26px 19px 0px rgba(34, 60, 80, 0.2); box-shadow: 0px -26px 19px 0px rgba(34, 60, 80, 0.2);
transform-origin: bottom center;
animation: fadeInAndMoveUp 0.2s ease-out forwards;
}
@keyframes fadeInAndMoveUp {
from { opacity: 0; transform: translateY(50px); }
to { opacity: 1; transform: translateY(0); }
} }
.song-cover { .song-cover {
@ -92,20 +97,40 @@
.song-table-row { .song-table-row {
margin-top: 50px !important; margin-top: 50px !important;
margin-bottom: 50px !important; margin-bottom: 50px !important;
padding: 30px !important;
} }
.song-table-row:hover { .song-table-row:hover {
outline: #9d0000 solid 3px !important; outline: #9d0000 solid 3px !important;
outline-offset: -1px !important; outline-offset: -2px !important;
color: #7c0000 !important;
background-color: #fcfcfc !important;
} }
.song-table-row-active {
outline-offset: -2px !important;
background-color: #e7e7e7 !important;
}
.ant-table-tbody {
padding: 40 !important;
}
.ant-table-tbody > tr > td {
padding: 40 !important;
}
.ant-table-cell { .ant-table-cell {
padding: 8px !important; padding: 8px !important;
} }
.ant-table-tbody > tr {
padding-top: 20px !important;
padding-bottom: 20px !important;
}
.ant-table-cell:nth-child(1) { .ant-table-cell:nth-child(1) {
padding: 2px !important; padding: 1px !important;
} }
.play-song-button { .play-song-button {
@ -142,6 +167,7 @@
color: grey; color: grey;
font-size: 30px !important; font-size: 30px !important;
padding-right: 5px; padding-right: 5px;
height: 30px;
} }
.current-track-button { .current-track-button {

View File

@ -7,21 +7,29 @@ import { Loginpage } from './pages/Loginpage';
import { Registerpage } from './pages/Registerpage'; import { Registerpage } from './pages/Registerpage';
import { Profilepage } from './pages/Profilepage'; import { Profilepage } from './pages/Profilepage';
import { Layout } from './components/layoutComponents/Layout'; import { Layout } from './components/layoutComponents/Layout';
import { CurrentSongProvider } from './contexts/SongContexts/SongContextProvider';
import { PlayingProvider } from './contexts/SongContexts/PlayingProvider';
import { VolumeProvider } from './contexts/VolumeContexts/VolumeProvider';
function App() { function App() {
return ( return (
<div className="App bg-slate-100"> <div className="App bg-slate-100">
<div id="app" className='bg-white'> <div id="app" className='bg-white'>
<Routes> <CurrentSongProvider songId=''>
<Route path='/' element={<Layout />}> <PlayingProvider isPlaying={false}>
<Route index element={<Homepage song_id='2'/>} /> <VolumeProvider volumeValue={30}>
<Route path='login' element={<Loginpage />} /> <Routes>
<Route path='profile' element={<Profilepage />} /> <Route path='/' element={<Layout />}>
</Route> <Route index element={<Homepage />} />
<Route path='login' element={<Loginpage />} />
<Route path='profile' element={<Profilepage />} />
</Route>
</Routes> </Routes>
</VolumeProvider>
</PlayingProvider>
</CurrentSongProvider>
</div> </div>

View File

@ -8,10 +8,16 @@ import { AdBlock } from './AdBlock';
import { Footer } from './Footer'; import { Footer } from './Footer';
import { Header } from './Header'; import { Header } from './Header';
import { MenuBlock } from './MenuBlock'; import { MenuBlock } from './MenuBlock';
import { useEffect, useRef } from 'react';
import { usePlayingContext } from '../../contexts/SongContexts/PlayingProvider';
export function Layout() { export function Layout() {
return ( return (
<><Header />
<>
<Header />
<div id='main-content'> <div id='main-content'>
<Outlet /> <Outlet />
</div> </div>

View File

@ -17,6 +17,7 @@ interface MenuBlockProps {
} }
export function MenuBlock({playlists, songs, albums, genres}: MenuBlockProps) { export function MenuBlock({playlists, songs, albums, genres}: MenuBlockProps) {
const newSongsTab = { const newSongsTab = {
label: 'Новинки', label: 'Новинки',
key: 'New', key: 'New',
@ -39,7 +40,8 @@ export function MenuBlock({playlists, songs, albums, genres}: MenuBlockProps) {
key: 'Recomendations', key: 'Recomendations',
children: songs? <NewSongsBlock /> : null children: songs? <NewSongsBlock /> : null
}; };
const tabs = [recTab, newSongsTab, chartTab, playlistsTab]; const tabs = [recTab, newSongsTab, chartTab];
return ( return (
<div className="flex" style={{display: 'flex', alignSelf: 'flex-start', width: 'fit-content'}}> <div className="flex" style={{display: 'flex', alignSelf: 'flex-start', width: 'fit-content'}}>

View File

@ -7,7 +7,7 @@ import { PlayCircleFilled, PlayCircleOutlined } from '@ant-design/icons';
import { getSongs } from '../../API/api'; import { getSongs } from '../../API/api';
import Title from 'antd/es/typography/Title'; import Title from 'antd/es/typography/Title';
import { GetColumns } from './Templates/songsTemplate'; import { GetColumns } from './Templates/songsTemplate';
import { getColumnsWithNumber } from './Templates/numeredSongsTemplate'; import { GetColumnsWithNumber } from './Templates/numeredSongsTemplate';
// Функция для генерации номеров строк // Функция для генерации номеров строк
const generateRowNumbers = (rows: any[]) => const generateRowNumbers = (rows: any[]) =>
@ -30,16 +30,19 @@ export function ChartSongsBlock() {
return ( return (
<div className="song-block flex border-slate-100"> <div className="song-block flex border-slate-100">
<Table <Table
title={() => <Title level={4}>Чарт</Title>} title={() => <Title style={{textAlign: 'center'}} level={4}>Чарт</Title>}
style={{ width: '100%', backgroundColor: 'transparent' }} style={{ width: '100%', backgroundColor: 'transparent', overflowX: 'auto' }}
className="songs-table menu-block" className="songs-table menu-block"
dataSource={numberedSongs} dataSource={numberedSongs}
columns={getColumnsWithNumber(numberedSongs)} columns={GetColumnsWithNumber(numberedSongs)}
showHeader={false} showHeader={false}
pagination={false} pagination={false}
rowClassName={() => 'song-table-row'} rowClassName={() => 'song-table-row chart-row'}
rowKey={(record) => record.id} rowKey={(record) => record.id}
scroll={{ x: true }}
/> />
</div> </div>
); );
}; };

View File

@ -3,16 +3,21 @@ import { render } from '@testing-library/react';
import {ISong} from '../../models/IModels'; import {ISong} from '../../models/IModels';
import axios from 'axios'; import axios from 'axios';
import { Table, Button, Empty } from 'antd'; import { Table, Button, Empty } from 'antd';
import { PlayCircleFilled, PlayCircleOutlined } from '@ant-design/icons'; import { Loading3QuartersOutlined, PlayCircleFilled, PlayCircleOutlined } from '@ant-design/icons';
import { getSongs } from '../../API/api'; import { getSongs } from '../../API/api';
import Title from 'antd/es/typography/Title'; import Title from 'antd/es/typography/Title';
import { GetColumns } from './Templates/songsTemplate'; import { GetColumns } from './Templates/songsTemplate';
import { Spin } from "antd";
import { useCurrentSongContext } from '../../contexts/SongContexts/SongContextProvider';
import { usePlayingContext } from '../../contexts/SongContexts/PlayingProvider';
export function NewSongsBlock() { export function NewSongsBlock() {
const [songs, setSongs] = useState<ISong[]>([]); const [songs, setSongs] = useState<ISong[]>([]);
const currentSongId = useCurrentSongContext();
const isPlaying = usePlayingContext();
const fetchData = async () => { const fetchData = async () => {
const response = getSongs(); const response = getSongs();
setSongs((await response).data); setSongs((await response).data);
@ -25,17 +30,20 @@ export function NewSongsBlock() {
return ( return (
<div className="song-block flex border-slate-100"> <div className="song-block flex border-slate-100">
<Table <Spin indicator={<Loading3QuartersOutlined spin style={{color: '#9d0000', fontSize: 30}} />} spinning={songs.length === 0}>
title={() => <Title level={4}>Горячие новинки</Title>} <Table
style={{width: '100%', backgroundColor: 'transparent'}} title={() => <Title level={4}>Горячие новинки</Title>}
className="songs-table menu-block" style={{width: '100%', backgroundColor: 'transparent'}}
dataSource={songs.map((s: ISong) => ({...s, play: ''}))} className="songs-table menu-block"
columns={GetColumns(songs)} dataSource={songs.map((s: ISong) => ({...s, play: ''}))}
showHeader={false} columns={GetColumns(songs)}
pagination={false} showHeader={false}
rowClassName={() => 'song-table-row'} pagination={false}
rowKey={(s: ISong) => s.id} rowClassName={(record) => isPlaying ? (record.id === currentSongId.songId ? 'song-table-row-active' : 'song-table-row') : 'song-table-row'}
/> rowKey={(s: ISong) => s.id}
rowHoverable={false}
/>
</Spin>
</div> </div>
); );
}; };

View File

@ -1,8 +1,18 @@
import { Button } from "antd"; import { Button } from "antd";
import { ISong } from "../../../models/IModels"; import { ISong } from "../../../models/IModels";
import PlayCircleFilled from "@ant-design/icons/lib/icons/PlayCircleFilled"; import { useState } from "react";
import { useCurrentSongContext } from "../../../contexts/SongContexts/SongContextProvider";
import { usePlayingContext } from "../../../contexts/SongContexts/PlayingProvider";
export function getColumnsWithNumber(songs: ISong[]) { const useColumnsWithNumber = (songs: ISong[]) => {
const {isPlaying, setIsPlaying} = usePlayingContext();
const { songId, setSongId } = useCurrentSongContext();
const handlePlayClick = (songId: string) => {
setIsPlaying(!isPlaying);
setSongId(songId);
};
return [ return [
{ {
@ -23,21 +33,23 @@ export function getColumnsWithNumber(songs: ISong[]) {
width: 100, width: 100,
render: (text: string, song: ISong) => ( render: (text: string, song: ISong) => (
<Button data-content="▷" className='play-song-button rounded' style={{background: 'transparent', display: 'contents'}}> <Button data-content={!isPlaying && song.id === songId ? '┃┃' : "▷"}
className='play-song-button rounded' style={{background: 'transparent', display: 'contents'}}
onClick={() => handlePlayClick(song.id)}>
<img <img
className="rounded " className="rounded "
src={song.cover} src={song.cover}
alt={song.song_name} alt={song.song_name}
/> />
</Button> </Button>
), ),
}, },
{ {
title: '', title: '',
dataIndex: 'song_name', dataIndex: 'song_name',
key: 'song_name', key: 'song_name',
width: 350 width: 400
}, },
{ {
title: '', title: '',
@ -49,8 +61,12 @@ export function getColumnsWithNumber(songs: ISong[]) {
title: '', title: '',
dataIndex: 'album_name', dataIndex: 'album_name',
key: 'album_name', key: 'album_name',
width: 400 width: 350
} }
] ]
} }
export function GetColumnsWithNumber(songs: ISong[]) {
return useColumnsWithNumber(songs);
}

View File

@ -1,15 +1,21 @@
import { Button } from "antd"; import { Button } from "antd";
import { ISong } from "../../../models/IModels"; import { ISong } from "../../../models/IModels";
import { useState } from "react"; import { useState } from "react";
import { PauseCircleFilled, PlayCircleFilled } from "@ant-design/icons"; import { height } from "@mui/system";
import { useCurrentSongContext } from "../../../contexts/SongContexts/SongContextProvider";
import { usePlayingContext } from "../../../contexts/SongContexts/PlayingProvider";
const useColumns = (songs: ISong[]) => { const useColumns = (songs: ISong[])=> {
const [isPlaying, setIsPlaying] = useState(false);
const handlePlayClick = () => { const {isPlaying, setIsPlaying} = usePlayingContext();
const { songId, setSongId } = useCurrentSongContext();
const handlePlayClick = (songId: string) => {
setIsPlaying(!isPlaying); setIsPlaying(!isPlaying);
setSongId(songId);
}; };
return [ return [
{ {
title: 'Play', title: 'Play',
@ -18,17 +24,19 @@ const useColumns = (songs: ISong[]) => {
width: 100, width: 100,
render: (text: string, song: ISong) => ( render: (text: string, song: ISong) => (
// Тут в дата-контент вставляется JSON коды иконок <Button
<Button data-content={isPlaying ? "\u23EF\uFE0F" : "▷"} className='play-song-button rounded' style={{background: 'transparent', display: 'contents'}} onClick={handlePlayClick}> data-content={isPlaying && song.id === songId ? '┃┃' : "▷"}
className='play-song-button rounded'
style={{background: 'transparent', display: 'contents'}}
onClick={() => handlePlayClick(song.id)}
>
<img <img
className="rounded " className="rounded"
src={song.cover} src={song.cover}
alt={song.song_name} alt={song.song_name}
/> />
</Button> </Button>
), ),
}, },
{ {
@ -56,4 +64,3 @@ const useColumns = (songs: ISong[]) => {
export function GetColumns(songs: ISong[]) { export function GetColumns(songs: ISong[]) {
return useColumns(songs); return useColumns(songs);
} }

View File

@ -1,73 +1,218 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Grid, Row, Table } from 'antd'; import { Button, Col, Grid, Menu, Row, Slider, Space, Table } from 'antd';
import { BackwardFilled, FastForwardFilled, FastBackwardFilled, PlayCircleFilled, PlayCircleOutlined, StepBackwardFilled, StepForwardFilled, HeartOutlined, ShareAltOutlined, PauseCircleFilled, PauseCircleOutlined } from '@ant-design/icons'; import { FastForwardFilled, FastBackwardFilled, PlayCircleOutlined, HeartOutlined, ShareAltOutlined, PauseCircleOutlined, SoundOutlined, MutedOutlined, SoundFilled } from '@ant-design/icons';
import { ISong, SongProps } from '../../models/IModels'; import { ISong, SongProps } from '../../models/IModels';
import styled from 'styled-components' import styled from 'styled-components'
import useSound from 'use-sound'; import { useCurrentSongContext } from '../../contexts/SongContexts/SongContextProvider';
import { usePlayingContext } from '../../contexts/SongContexts/PlayingProvider';
import Dropdown from 'antd/es/dropdown/dropdown';
import Text from 'antd/es/typography/Text';
import { useVolumeContext } from '../../contexts/VolumeContexts/VolumeProvider';
const Song = styled.div` const Song = styled.div`
align-items: center;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
width: 75%;` width: 100%;
display: flex;
justify-content: space-between;
padding: 0 0px;
box-sizing: border-box;
background-color: #fff;
@media (max-width: 768px) {
padding: 0 10px;
flex-direction: column;
}
`;
type CurrentSongProps = { type CurrentSongProps = {
song: ISong song: ISong
songs: ISong[]
} }
export function CurrentTrack({ song }: CurrentSongProps) { const SongName = styled.span`
display: inline-block;
max-width: 200px; /* Максимальная ширина */
white-space: nowrap; /* Предотвращает перенос слов на новую строку */
overflow: hidden; /* Скрывает текст, выходящий за пределы контейнера */
text-overflow: ellipsis; /* Добавляет точку-эллипсис */
padding-right: 20px; /* Добавляем небольшой отступ справа для точки */
`;
const [isPlaying, setIsPlaying] = useState(false); export function CurrentTrack({ song , songs}: CurrentSongProps) {
const soundUrl = song?.source || ''; // Пустая строка в случае отсутствия source const audioPlayer = useRef<HTMLAudioElement | null>(null);
const soundUrl = song?.source;
const Playlist = songs;
const [play, { pause, duration, sound }] = useSound(soundUrl, { volume: 0.5 }); const {isPlaying, setIsPlaying} = usePlayingContext();
const { songId, setSongId } = useCurrentSongContext();
const playingButton = () => { const { volumeValue: volume, setVolumeValue: setVolume } = useVolumeContext();
const [ elapsed, setElapsed] = useState(0);
const [ duration, setDuration ] = useState(0);
const [ muted, setMuted ] = useState(false);
useEffect(() => {
if (isPlaying) { if (isPlaying) {
pause(); audioPlayer.current?.play();
setIsPlaying(false); }
} else { else {
play(); audioPlayer.current?.pause();
setIsPlaying(true); }
}, [isPlaying]);
const togglePlay = () => {
if (audioPlayer.current && audioPlayer.current.src) {
setIsPlaying(!isPlaying);
}
};
const prevTrack = () => {
if (parseInt(songId) > 0) {
const newId = (parseInt(songId) - 1).toString();
setSongId(newId);
togglePlay();
}
};
const nextTrack = () => {
if (parseInt(songId) < songs.length - 1) {
const newId = (parseInt(songId) + 1).toString();
setSongId(newId);
togglePlay();
} }
}; };
useEffect(() => { useEffect(() => {
if (song) { if (song) {
play(); audioPlayer.current?.play();
setIsPlaying(true); setIsPlaying(true);
} }
}, [song]); }, [song]);
useEffect(() => {
if (audioPlayer.current) {
audioPlayer.current.muted = muted;
}
}, [muted]);
useEffect(() => {
if (audioPlayer.current) {
audioPlayer.current.volume = volume / 100;
if (isPlaying) {
setInterval(() => {
const duration = Math.floor(audioPlayer?.current?.duration ?? 0);
const currentTime = Math.floor(audioPlayer?.current?.currentTime ?? 0);
setDuration(duration - currentTime);
setElapsed(currentTime);
if (currentTime === duration) {
nextTrack();
}
}, 100);
}
}
}, [volume, isPlaying]);
const formatTime = (time: number) => {
if (time) {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
}
return '0:00';
};
return ( return (
<Song> <Song>
<div className="current-track"> <audio ref={audioPlayer} src={soundUrl} />
<Row className="w-full h-full bg-white opacity-90 rounded" style={{ display: 'flex', alignItems: 'center' }}> <div key={song.id} className="current-track">
<Col span={4} className="flex flex-direction-row justify-center"> <span className="w-full h-full bg-white opacity-90 rounded" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Button type="link" icon={<FastBackwardFilled className="player-button" />}></Button> <span className='inline-flex' style={{ justifyContent: 'flex-center', alignSelf: 'flex-center', width: '27%' }}>
<Button <span className="flex flex-direction-row" style={{ alignItems: 'center', paddingLeft: 10, paddingRight: 10 }}>
type="link" <Button type="link" icon={<FastBackwardFilled className="player-button" />} onClick={prevTrack}></Button>
icon={isPlaying ? <PauseCircleOutlined className="player-button" onClick={playingButton} /> : <PlayCircleOutlined className="player-button" onClick={playingButton} />}
></Button> <Button
<Button type="link" icon={<FastForwardFilled className="player-button" style={{ fontSize: 30 }} />}></Button> type="link"
</Col> icon={isPlaying ?
<Col style={{ width: 80 }}> <PauseCircleOutlined
<img src={song.cover} alt={song.cover} className="current-song-cover" /> className="player-button"
</Col> onClick={togglePlay} /> : <PlayCircleOutlined className="player-button" onClick={togglePlay} />}
<Col span={16}> />
<Row style={{ paddingLeft: 10 }}>{song.song_name}</Row>
<Row style={{ paddingLeft: 10 }}>{song.band_name}</Row> <Button type="link" icon={<FastForwardFilled className="player-button" style={{ fontSize: 30 }} />} onClick={nextTrack}></Button>
</Col> </span>
<Col>
<Button type="link" icon={<HeartOutlined className="current-track-button" />}></Button> <span style={{ width: 80, display: 'flex', alignSelf: 'flex-end' }}>
<Button type="link" icon={<ShareAltOutlined className="current-track-button" />}></Button> <img src={song.cover} alt={song.cover} className="current-song-cover" />
</Col> </span>
</Row>
<span className="flex flex-direction-column justify-center items-start" style={{ paddingRight: 5, flexDirection: 'column'}} >
<SongName style={{ paddingLeft: 10}}>{song.song_name}</SongName>
<SongName style={{ paddingLeft: 10 }}>{song.band_name}</SongName>
</span>
</span>
<span className='inline-flex' style={{ alignItems: 'center', width: '100%', justifyContent: 'stretch', flexDirection: 'row' }}>
<Text style={{ width: '5%' }}>{formatTime(elapsed)}</Text>
<Slider
max={audioPlayer.current?.duration ?? 0}
step={1}
defaultValue={0}
value={elapsed}
styles={ { track: { backgroundColor: '#ad0000' }, rail: { backgroundColor: '#e9e9e9' }, handle: { color: '#ad0000',} }}
style={{ width: '100%', justifyContent: 'flex-center', alignSelf: 'flex-center' }}
tooltip={{ open: false }}
onChange={(value) => {
if (audioPlayer.current) {
audioPlayer.current.currentTime = value;
if (value >= audioPlayer.current.duration) {
nextTrack();
}
}
}}
/>
<Text style={{ width: '5%' }}>{formatTime(audioPlayer.current?.duration ?? 0)}</Text>
</span>
<span className='inline-flex' style={{ width: '14%', paddingRight: 10, justifyContent: 'flex-end', alignItems: 'center' }}>
<Dropdown overlay={<Menu>
<Menu.Item key="Slider">
<Slider
styles={ { track: { backgroundColor: '#ad0000' }, rail: { backgroundColor: '#e9e9e9' }, handle: { color: '#ad0000',} }}
defaultValue={30} value={volume} style={{ width: 150, color: 'red' }} onChange={(value) => setVolume(value)} />
</Menu.Item>
</Menu>}
trigger={['hover']}
placement='top'>
<Button type="link" style={{ width: 30}}
icon={ muted || volume === 0 ?
<SoundFilled
style={{ color: '#ad0000'}} className="current-track-button" /> : volume <= 25 ?
<SoundFilled className="current-track-button" /> : <SoundOutlined className="current-track-button" />}
onClick={() => setMuted(!muted)}
/>
</Dropdown>
<Button type="link" icon={<HeartOutlined className="current-track-button" />}></Button>
<Button type="link" icon={<ShareAltOutlined className="current-track-button" />}></Button>
</span>
</span>
</div> </div>
</Song> </Song>
); );
} }

View File

@ -0,0 +1 @@
export {}

View File

@ -0,0 +1,32 @@
import React, { createContext, useContext, ReactNode, useState } from "react";
interface PlayingProviderProps {
children: ReactNode;
isPlaying: boolean;
}
interface PlayingContextValue {
isPlaying: boolean;
setIsPlaying: (isPlaying: boolean) => void;
}
const PlayingContext = createContext<PlayingContextValue | undefined>(undefined);
export function PlayingProvider({ children, isPlaying }: PlayingProviderProps) {
const [isPlayingSong, setIsPlaying] = useState(isPlaying);
console.log(isPlayingSong);
return (
<PlayingContext.Provider value={{ isPlaying: isPlayingSong, setIsPlaying }}>
{children}
</PlayingContext.Provider>
);
}
export const usePlayingContext = function(): PlayingContextValue {
const context = useContext(PlayingContext);
if (context === undefined) {
throw new Error("usePlayingContext must be used within a PlayingProvider");
}
return context;
};

View File

@ -0,0 +1,31 @@
import React, { createContext, useContext, ReactNode, useState } from "react";
interface CurrentSongProviderProps {
children: ReactNode;
songId: string;
}
interface SongContextValue {
songId: string;
setSongId: (songId: string) => void;
}
const CurrentSongContext = createContext<SongContextValue | undefined>(undefined);
export const CurrentSongProvider: React.FC<CurrentSongProviderProps> = ({ children, songId }) => {
const [currentSongId, setCurrentSongId] = useState(songId);
return (
<CurrentSongContext.Provider value={{ songId: currentSongId, setSongId: setCurrentSongId }}>
{children}
</CurrentSongContext.Provider>
);
};
export const useCurrentSongContext = function(): SongContextValue {
const context = useContext(CurrentSongContext);
if (context === undefined) {
throw new Error("useCurrentSongContext must be used within a CurrentSongProvider");
}
return context;
};

View File

@ -0,0 +1,31 @@
import React, { createContext, useContext, ReactNode, useState } from "react";
interface VolumeProviderProps {
children: ReactNode;
volumeValue: number;
}
interface SongContextValue {
volumeValue: number;
setVolumeValue: (value: number) => void;
}
const VolumeContext = createContext<SongContextValue | undefined>(undefined);
export const VolumeProvider: React.FC<VolumeProviderProps> = ({ children, volumeValue: value }) => {
const [volumeId, setVolumeId] = useState(value);
return (
<VolumeContext.Provider value={{ volumeValue: volumeId, setVolumeValue: setVolumeId }}>
{children}
</VolumeContext.Provider>
);
};
export const useVolumeContext = function(): SongContextValue {
const context = useContext(VolumeContext);
if (context === undefined) {
throw new Error("useVolumeContext must be used within a VolumeProvider");
}
return context;
};

View File

@ -1,13 +1,13 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { getSongs, getGenres, getPlaylists, getAds } from "../API/api"; import { getSongs, getGenres, getPlaylists, getAds, getSong } from "../API/api";
import { AdBlock } from "../components/layoutComponents/AdBlock"; import { AdBlock } from "../components/layoutComponents/AdBlock";
import { MenuBlock } from "../components/layoutComponents/MenuBlock"; import { MenuBlock } from "../components/layoutComponents/MenuBlock";
import { CurrentTrack } from "../components/songComponents/CurrentTrack"; import { CurrentTrack } from "../components/songComponents/CurrentTrack";
import { IPlaylist, ISong, IAlbum, IGenre, IAdvertisement, IBand } from "../models/IModels"; import { IPlaylist, ISong, IAlbum, IGenre, IAdvertisement, IBand } from "../models/IModels";
import { useCurrentSongContext } from "../contexts/SongContexts/SongContextProvider";
export function Homepage({song_id = "0"}) { export function Homepage() {
const [currentSong, setCurrentSong] = useState<ISong | null>(null);
const [albums, setAlbums] = useState<IAlbum[]>([]); const [albums, setAlbums] = useState<IAlbum[]>([]);
const [songs, setSongs] = useState<ISong[]>([]); const [songs, setSongs] = useState<ISong[]>([]);
@ -17,6 +17,8 @@ export function Homepage({song_id = "0"}) {
const [playlists, setPlaylist] = React.useState<IPlaylist[]>([]); const [playlists, setPlaylist] = React.useState<IPlaylist[]>([]);
const [genres, setGenres] = React.useState<IGenre[]>([]); const [genres, setGenres] = React.useState<IGenre[]>([]);
const currentSongId = useCurrentSongContext();
const fetchData = async () => { const fetchData = async () => {
const [responseSongs, responseAds, responsePlaylists, responseGenres] = await Promise.all([ const [responseSongs, responseAds, responsePlaylists, responseGenres] = await Promise.all([
getSongs(), getSongs(),
@ -35,10 +37,15 @@ export function Homepage({song_id = "0"}) {
fetchData(); fetchData();
}, []); }, []);
const currentSong = songs.find(song => song.id === currentSongId.songId);
console.log('Айди песни:');
return ( return (
<> <>
<MenuBlock playlists={[]} songs={songs} albums={albums} genres={[]}/><AdBlock ads={ads}/> <MenuBlock playlists={[]} songs={songs} albums={albums} genres={[]}/><AdBlock ads={ads}/>
{currentSong && <CurrentTrack song={currentSong} />} {currentSong && <CurrentTrack song={currentSong} songs={songs}/>}
</> </>
); );
} }

View File

@ -3,7 +3,6 @@ import { Form, Input, Button, Checkbox } from "antd";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import ReCAPTCHA from "react-google-recaptcha"; import ReCAPTCHA from "react-google-recaptcha";
import React, { useState } from "react"; import React, { useState } from "react";
import FormItem from "antd/es/form/FormItem";
export function Registerpage() { export function Registerpage() {
const [verified, setVerified] = useState(false); const [verified, setVerified] = useState(false);